This commit is contained in:
2025-12-04 11:54:42 +01:00
parent 9d5caa87f3
commit b7d5551ea7
223 changed files with 5473 additions and 885 deletions

View File

@@ -1,11 +1,11 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import {
FixtureServer,
PlaywrightAutomationAdapter,
} from 'packages/automation-infrastructure/adapters/automation';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation-infrastructure/adapters/logging/PinoLogAdapter';
} from 'packages/automation/infrastructure/adapters/automation';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
describe('Real Playwright hosted-session smoke (fixtures, steps 27)', () => {
let server: FixtureServer;

View File

@@ -1,8 +1,8 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { DIContainer } from '../../../apps/companion/main/di-container';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import type { HostedSessionConfig } from 'packages/automation-domain/entities/HostedSessionConfig';
import { PlaywrightAutomationAdapter } from 'packages/automation-infrastructure/adapters/automation';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { PlaywrightAutomationAdapter } from 'packages/automation/infrastructure/adapters/automation';
describe('Companion UI - hosted workflow via fixture-backed real stack', () => {
let container: DIContainer;

View File

@@ -1,13 +1,13 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import {
PlaywrightAutomationAdapter,
} from 'packages/automation-infrastructure/adapters/automation';
} from 'packages/automation/infrastructure/adapters/automation';
import {
IRACING_SELECTORS,
IRACING_TIMEOUTS,
} from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation-infrastructure/adapters/logging/PinoLogAdapter';
} from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
const shouldRun = process.env.HOSTED_REAL_E2E === '1';
const describeMaybe = shouldRun ? describe : describe.skip;

View File

@@ -1,13 +1,13 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import {
PlaywrightAutomationAdapter,
} from 'packages/automation-infrastructure/adapters/automation';
} from 'packages/automation/infrastructure/adapters/automation';
import {
IRACING_SELECTORS,
IRACING_TIMEOUTS,
} from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation-infrastructure/adapters/logging/PinoLogAdapter';
} from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
const shouldRun = process.env.HOSTED_REAL_E2E === '1';

View File

@@ -1,15 +1,15 @@
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { promises as fs } from 'fs';
import path from 'path';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import {
PlaywrightAutomationAdapter,
} from 'packages/automation-infrastructure/adapters/automation';
} from 'packages/automation/infrastructure/adapters/automation';
import {
IRACING_SELECTORS,
IRACING_TIMEOUTS,
} from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation-infrastructure/adapters/logging/PinoLogAdapter';
} from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
const shouldRun = process.env.HOSTED_REAL_E2E === '1';
const describeMaybe = shouldRun ? describe : describe.skip;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 2 create race', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 3 race information', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 4 server details', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 5 set admins', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 6 admins', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 7 time limits', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 8 cars', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 13 track options', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 14 time of day', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
describe('Step 15 weather', () => {
let harness: StepHarness;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import type { StepHarness } from '../support/StepHarness';
import { createStepHarness } from '../support/StepHarness';
import { CheckoutConfirmation } from 'packages/automation-domain/value-objects/CheckoutConfirmation';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
describe('Step 17 team driving', () => {
let harness: StepHarness;

View File

@@ -1,6 +1,6 @@
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import type { PlaywrightAutomationAdapter } from 'packages/automation-infrastructure/adapters/automation';
import type { AutomationResult } from 'packages/automation-application/ports/AutomationResults';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { PlaywrightAutomationAdapter } from 'packages/automation/infrastructure/adapters/automation';
import type { AutomationResult } from 'packages/automation/application/ports/AutomationResults';
export function assertAutoNavigationConfig(config: Record<string, unknown>): void {
if ((config as any).__skipFixtureNavigation) {

View File

@@ -1,10 +1,10 @@
import type { AutomationResult } from 'packages/automation-application/ports/AutomationResults';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import type { AutomationResult } from 'packages/automation/application/ports/AutomationResults';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'packages/automation-infrastructure/adapters/automation';
import { PinoLogAdapter } from 'packages/automation-infrastructure/adapters/logging/PinoLogAdapter';
} from 'packages/automation/infrastructure/adapters/automation';
import { PinoLogAdapter } from 'packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
export interface StepHarness {
server: FixtureServer;

View File

@@ -2,9 +2,9 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'packages/automation-infrastructure/adapters/automation';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { PinoLogAdapter } from 'packages/automation-infrastructure/adapters/logging/PinoLogAdapter';
} from 'packages/automation/infrastructure/adapters/automation';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { PinoLogAdapter } from 'packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
import { executeStepWithAutoNavigationGuard } from '../support/AutoNavGuard';
describe('Hosted validator guards (fixture-backed, real stack)', () => {

View File

@@ -2,10 +2,10 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'packages/automation-infrastructure/adapters/automation';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { PinoLogAdapter } from 'packages/automation-infrastructure/adapters/logging/PinoLogAdapter';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
} from 'packages/automation/infrastructure/adapters/automation';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { PinoLogAdapter } from 'packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
import { executeStepWithAutoNavigationGuard } from '../support/AutoNavGuard';
describe('Workflow hosted session autonav slice (fixture-backed, real stack)', () => {

View File

@@ -2,12 +2,12 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'packages/automation-infrastructure/adapters/automation';
import { InMemorySessionRepository } from 'packages/automation-infrastructure/repositories/InMemorySessionRepository';
import { AutomationEngineAdapter } from 'packages/automation-infrastructure/adapters/automation/engine/AutomationEngineAdapter';
import { StartAutomationSessionUseCase } from 'packages/automation-application/use-cases/StartAutomationSessionUseCase';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { PinoLogAdapter } from 'packages/automation-infrastructure/adapters/logging/PinoLogAdapter';
} from 'packages/automation/infrastructure/adapters/automation';
import { InMemorySessionRepository } from 'packages/automation/infrastructure/repositories/InMemorySessionRepository';
import { AutomationEngineAdapter } from 'packages/automation/infrastructure/adapters/automation/engine/AutomationEngineAdapter';
import { StartAutomationSessionUseCase } from 'packages/automation/application/use-cases/StartAutomationSessionUseCase';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { PinoLogAdapter } from 'packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
describe('Workflow hosted session end-to-end (fixture-backed, real stack)', () => {
let server: FixtureServer;

View File

@@ -2,10 +2,10 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import {
PlaywrightAutomationAdapter,
FixtureServer,
} from 'packages/automation-infrastructure/adapters/automation';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { IRACING_SELECTORS } from 'packages/automation-infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation-infrastructure/adapters/logging/PinoLogAdapter';
} from 'packages/automation/infrastructure/adapters/automation';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { IRACING_SELECTORS } from 'packages/automation/infrastructure/adapters/automation/dom/IRacingSelectors';
import { PinoLogAdapter } from 'packages/automation/infrastructure/adapters/logging/PinoLogAdapter';
describe('Workflow steps 79 cars flow (fixture-backed, real stack)', () => {
let adapter: PlaywrightAutomationAdapter;

View File

@@ -77,7 +77,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
process.env.NODE_ENV = 'production';
const { PlaywrightAutomationAdapter } = await import(
'packages/automation-infrastructure/adapters/automation'
'packages/automation/infrastructure/adapters/automation'
);
adapter = new PlaywrightAutomationAdapter({
@@ -96,7 +96,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
process.env.NODE_ENV = 'test';
const { PlaywrightAutomationAdapter } = await import(
'packages/automation-infrastructure/adapters/automation'
'packages/automation/infrastructure/adapters/automation'
);
adapter = new PlaywrightAutomationAdapter({
@@ -115,7 +115,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
delete process.env.NODE_ENV;
const { PlaywrightAutomationAdapter } = await import(
'packages/automation-infrastructure/adapters/automation'
'packages/automation/infrastructure/adapters/automation'
);
adapter = new PlaywrightAutomationAdapter({
@@ -139,7 +139,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
process.env.NODE_ENV = 'production';
const { PlaywrightAutomationAdapter } = await import(
'packages/automation-infrastructure/adapters/automation'
'packages/automation/infrastructure/adapters/automation'
);
adapter = new PlaywrightAutomationAdapter({
@@ -155,7 +155,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
process.env.NODE_ENV = 'test';
const { PlaywrightAutomationAdapter } = await import(
'packages/automation-infrastructure/adapters/automation'
'packages/automation/infrastructure/adapters/automation'
);
adapter = new PlaywrightAutomationAdapter({
@@ -187,7 +187,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
};
const { PlaywrightAutomationAdapter } = await import(
'packages/automation-infrastructure/adapters/automation'
'packages/automation/infrastructure/adapters/automation'
);
adapter = new PlaywrightAutomationAdapter(
@@ -213,7 +213,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
process.env.NODE_ENV = 'production';
const { PlaywrightAutomationAdapter } = await import(
'packages/automation-infrastructure/adapters/automation'
'packages/automation/infrastructure/adapters/automation'
);
const userDataDir = path.join(process.cwd(), 'test-browser-data');
@@ -239,10 +239,10 @@ describe('Browser Mode Integration - GREEN Phase', () => {
it('reads mode from injected loader and passes headless flag to launcher accordingly', async () => {
process.env.NODE_ENV = 'development';
const { PlaywrightAutomationAdapter } = await import(
'packages/automation-infrastructure/adapters/automation'
'packages/automation/infrastructure/adapters/automation'
);
const { BrowserModeConfigLoader } = await import(
'../../../packages/automation-infrastructure/config/BrowserModeConfig'
'../../../packages/automation/infrastructure/config/BrowserModeConfig'
);
// Create loader and set to headed

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { Result } from '../../../packages/shared/result/Result';
import { CheckoutPriceExtractor } from '../../../packages/automation-infrastructure/adapters/automation/CheckoutPriceExtractor';
import { CheckoutStateEnum } from '../../../packages/automation-domain/value-objects/CheckoutState';
import { CheckoutPriceExtractor } from '../../../packages/automation/infrastructure/adapters/automation/CheckoutPriceExtractor';
import { CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
/**
* CheckoutPriceExtractor Integration Tests - GREEN PHASE

View File

@@ -3,7 +3,7 @@
*/
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { FixtureServer, getAllStepFixtureMappings, PlaywrightAutomationAdapter } from 'packages/automation-infrastructure/adapters/automation';
import { FixtureServer, getAllStepFixtureMappings, PlaywrightAutomationAdapter } from 'packages/automation/infrastructure/adapters/automation';
declare const getComputedStyle: any;
declare const document: any;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { InMemorySessionRepository } from '../../../packages/automation-infrastructure/repositories/InMemorySessionRepository';
import { AutomationSession } from '../../../packages/automation-domain/entities/AutomationSession';
import { StepId } from '../../../packages/automation-domain/value-objects/StepId';
import { InMemorySessionRepository } from '../../../packages/automation/infrastructure/repositories/InMemorySessionRepository';
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
describe('InMemorySessionRepository Integration Tests', () => {
let repository: InMemorySessionRepository;

View File

@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { MockBrowserAutomationAdapter } from 'packages/automation-infrastructure/adapters/automation';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { MockBrowserAutomationAdapter } from 'packages/automation/infrastructure/adapters/automation';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
describe('MockBrowserAutomationAdapter Integration Tests', () => {
let adapter: MockBrowserAutomationAdapter;

View File

@@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest'
import { PlaywrightAutomationAdapter } from 'packages/automation-infrastructure/adapters/automation'
import { PlaywrightAutomationAdapter } from 'packages/automation/infrastructure/adapters/automation'
describe('CarsFlow integration', () => {
test('adapter emits panel-attached then action-started then action-complete for performAddCar', async () => {

View File

@@ -1,14 +1,14 @@
import { describe, it, expect } from 'vitest';
import { OverlaySyncService } from 'packages/automation-application/services/OverlaySyncService';
import type { AutomationEvent } from 'packages/automation-application/ports/IAutomationEventPublisher';
import { OverlaySyncService } from 'packages/automation/application/services/OverlaySyncService';
import type { AutomationEvent } from 'packages/automation/application/ports/IAutomationEventPublisher';
import type {
IAutomationLifecycleEmitter,
LifecycleCallback,
} from 'packages/automation-infrastructure/adapters/IAutomationLifecycleEmitter';
} from 'packages/automation/infrastructure/adapters/IAutomationLifecycleEmitter';
import type {
OverlayAction,
ActionAck,
} from 'packages/automation-application/ports/IOverlaySyncPort';
} from 'packages/automation/application/ports/IOverlaySyncPort';
class TestLifecycleEmitter implements IAutomationLifecycleEmitter {
private callbacks: Set<LifecycleCallback> = new Set();

View File

@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { PageStateValidator } from 'packages/automation-domain/services/PageStateValidator';
import { StepTransitionValidator } from 'packages/automation-domain/services/StepTransitionValidator';
import { StepId } from 'packages/automation-domain/value-objects/StepId';
import { SessionState } from 'packages/automation-domain/value-objects/SessionState';
import { PageStateValidator } from '@gridpilot/automation/domain/services/PageStateValidator';
import { StepTransitionValidator } from '@gridpilot/automation/domain/services/StepTransitionValidator';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { SessionState } from '@gridpilot/automation/domain/value-objects/SessionState';
describe('Validator conformance (integration)', () => {
describe('PageStateValidator with hosted-session selectors', () => {

View File

@@ -1,8 +1,8 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { DIContainer } from '../../../..//apps/companion/main/di-container';
import type { HostedSessionConfig } from '../../../..//packages/automation-domain/entities/HostedSessionConfig';
import { StepId } from '../../../..//packages/automation-domain/value-objects/StepId';
import { PlaywrightAutomationAdapter } from '../../../..//packages/automation-infrastructure/adapters/automation';
import type { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { PlaywrightAutomationAdapter } from '../../../..//packages/automation/infrastructure/adapters/automation';
describe('companion start automation - browser mode refresh wiring', () => {
const originalEnv = { ...process.env };

View File

@@ -1,8 +1,8 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { DIContainer } from '../../../..//apps/companion/main/di-container';
import type { HostedSessionConfig } from '../../../..//packages/automation-domain/entities/HostedSessionConfig';
import { StepId } from '../../../..//packages/automation-domain/value-objects/StepId';
import { PlaywrightAutomationAdapter } from '../../../..//packages/automation-infrastructure/adapters/automation';
import type { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { PlaywrightAutomationAdapter } from '../../../..//packages/automation/infrastructure/adapters/automation';
describe('companion start automation - browser not connected at step 1', () => {
const originalEnv = { ...process.env };

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { DIContainer } from '../../../..//apps/companion/main/di-container';
import type { HostedSessionConfig } from '../../../..//packages/automation-domain/entities/HostedSessionConfig';
import { PlaywrightAutomationAdapter } from '../../../..//packages/automation-infrastructure/adapters/automation';
import type { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { PlaywrightAutomationAdapter } from '../../../..//packages/automation/infrastructure/adapters/automation';
describe('companion start automation - browser connection failure before steps', () => {
const originalEnv = { ...process.env };

View File

@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { DIContainer } from '../../../..//apps/companion/main/di-container';
import type { HostedSessionConfig } from '../../../..//packages/automation-domain/entities/HostedSessionConfig';
import { StepId } from '../../../..//packages/automation-domain/value-objects/StepId';
import type { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
describe('companion start automation - happy path', () => {
const originalEnv = { ...process.env };

View File

@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { MockAutomationLifecycleEmitter } from '../../../mocks/MockAutomationLifecycleEmitter';
import { OverlaySyncService } from 'packages/automation-application/services/OverlaySyncService';
import type { AutomationEvent } from 'packages/automation-application/ports/IAutomationEventPublisher';
import type { OverlayAction } from 'packages/automation-application/ports/IOverlaySyncPort';
import { OverlaySyncService } from 'packages/automation/application/services/OverlaySyncService';
import type { AutomationEvent } from 'packages/automation/application/ports/IAutomationEventPublisher';
import type { OverlayAction } from 'packages/automation/application/ports/IOverlaySyncPort';
type RendererOverlayState =
| { status: 'idle' }

View File

@@ -1,6 +1,6 @@
import { describe, expect, test } from 'vitest'
import { MockAutomationLifecycleEmitter } from '../../../mocks/MockAutomationLifecycleEmitter'
import { OverlaySyncService } from 'packages/automation-application/services/OverlaySyncService'
import { OverlaySyncService } from 'packages/automation/application/services/OverlaySyncService'
describe('renderer overlay integration', () => {
test('renderer shows confirmed only after main acks confirmed', async () => {

View File

@@ -1,13 +1,13 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { DIContainer } from '../../apps/companion/main/di-container';
import { StartAutomationSessionUseCase } from '../../packages/automation-application/use-cases/StartAutomationSessionUseCase';
import { CheckAuthenticationUseCase } from '../../packages/automation-application/use-cases/CheckAuthenticationUseCase';
import { InitiateLoginUseCase } from '../../packages/automation-application/use-cases/InitiateLoginUseCase';
import { ClearSessionUseCase } from '../../packages/automation-application/use-cases/ClearSessionUseCase';
import { ConfirmCheckoutUseCase } from '../../packages/automation-application/use-cases/ConfirmCheckoutUseCase';
import { PlaywrightAutomationAdapter } from 'packages/automation-infrastructure/adapters/automation';
import { InMemorySessionRepository } from '../../packages/automation-infrastructure/repositories/InMemorySessionRepository';
import { NoOpLogAdapter } from '../../packages/automation-infrastructure/adapters/logging/NoOpLogAdapter';
import { StartAutomationSessionUseCase } from '../../packages/automation/application/use-cases/StartAutomationSessionUseCase';
import { CheckAuthenticationUseCase } from '../../packages/automation/application/use-cases/CheckAuthenticationUseCase';
import { InitiateLoginUseCase } from '../../packages/automation/application/use-cases/InitiateLoginUseCase';
import { ClearSessionUseCase } from '../../packages/automation/application/use-cases/ClearSessionUseCase';
import { ConfirmCheckoutUseCase } from '../../packages/automation/application/use-cases/ConfirmCheckoutUseCase';
import { PlaywrightAutomationAdapter } from 'packages/automation/infrastructure/adapters/automation';
import { InMemorySessionRepository } from '../../packages/automation/infrastructure/repositories/InMemorySessionRepository';
import { NoOpLogAdapter } from '../../packages/automation/infrastructure/adapters/logging/NoOpLogAdapter';
// Mock Electron's app module
vi.mock('electron', () => ({

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest';
import { PlaywrightAutomationAdapter, FixtureServer } from 'packages/automation-infrastructure/adapters/automation';
import { PlaywrightAutomationAdapter, FixtureServer } from 'packages/automation/infrastructure/adapters/automation';
describe('Playwright Adapter Smoke Tests', () => {
let adapter: PlaywrightAutomationAdapter | undefined;

View File

@@ -1,8 +1,8 @@
import { describe, it, expect } from 'vitest';
import { Result } from '@/packages/shared/result/Result';
import { CheckoutConfirmation } from '@/packages/automation-domain/value-objects/CheckoutConfirmation';
import { CheckoutPrice } from '@/packages/automation-domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@/packages/automation-domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
/**
* Contract tests for ICheckoutConfirmationPort

View File

@@ -1,8 +1,8 @@
import { describe, expect, test } from 'vitest'
import { OverlayAction, ActionAck } from '../../../../packages/automation-application/ports/IOverlaySyncPort'
import { IAutomationEventPublisher, AutomationEvent } from '../../../../packages/automation-application/ports/IAutomationEventPublisher'
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../../packages/automation-infrastructure/adapters/IAutomationLifecycleEmitter'
import { OverlaySyncService } from '../../../../packages/automation-application/services/OverlaySyncService'
import { OverlayAction, ActionAck } from '../../../../packages/automation/application/ports/IOverlaySyncPort'
import { IAutomationEventPublisher, AutomationEvent } from '../../../../packages/automation/application/ports/IAutomationEventPublisher'
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../../packages/automation/infrastructure/adapters/IAutomationLifecycleEmitter'
import { OverlaySyncService } from '../../../../packages/automation/application/services/OverlaySyncService'
class MockLifecycleEmitter implements IAutomationLifecycleEmitter {
private callbacks: Set<LifecycleCallback> = new Set()

View File

@@ -1,7 +1,7 @@
import { describe, expect, test } from 'vitest'
import { OverlayAction } from '../../../../packages/automation-application/ports/IOverlaySyncPort'
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../../packages/automation-infrastructure/adapters/IAutomationLifecycleEmitter'
import { OverlaySyncService } from '../../../../packages/automation-application/services/OverlaySyncService'
import { OverlayAction } from '../../../../packages/automation/application/ports/IOverlaySyncPort'
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../../../packages/automation/infrastructure/adapters/IAutomationLifecycleEmitter'
import { OverlaySyncService } from '../../../../packages/automation/application/services/OverlaySyncService'
class MockLifecycleEmitter implements IAutomationLifecycleEmitter {
private callbacks: Set<LifecycleCallback> = new Set()

View File

@@ -1,9 +1,9 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { CheckAuthenticationUseCase } from '../../../../packages/automation-application/use-cases/CheckAuthenticationUseCase';
import { AuthenticationState } from '../../../../packages/automation-domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '../../../../packages/automation-domain/value-objects/BrowserAuthenticationState';
import { CheckAuthenticationUseCase } from '../../../../packages/automation/application/use-cases/CheckAuthenticationUseCase';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { Result } from '../../../../packages/shared/result/Result';
import type { IAuthenticationService } from '../../../../packages/automation-application/ports/IAuthenticationService';
import type { IAuthenticationService } from '../../../../packages/automation/application/ports/IAuthenticationService';
interface ISessionValidator {
validateSession(): Promise<Result<boolean>>;

View File

@@ -1,10 +1,10 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CompleteRaceCreationUseCase } from '@/packages/automation-application/use-cases/CompleteRaceCreationUseCase';
import { Result } from '@/packages/shared/result/Result';
import { RaceCreationResult } from '@/packages/automation-domain/value-objects/RaceCreationResult';
import { CheckoutPrice } from '@/packages/automation-domain/value-objects/CheckoutPrice';
import type { ICheckoutService } from '@/packages/automation-application/ports/ICheckoutService';
import { CheckoutState } from '@/packages/automation-domain/value-objects/CheckoutState';
import { CompleteRaceCreationUseCase } from '../../../../packages/automation/application/use-cases/CompleteRaceCreationUseCase';
import { Result } from '../../../../packages/shared/result/Result';
import { RaceCreationResult } from '@gridpilot/automation/domain/value-objects/RaceCreationResult';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import type { ICheckoutService } from '../../../../packages/automation/application/ports/ICheckoutService';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
describe('CompleteRaceCreationUseCase', () => {
let mockCheckoutService: ICheckoutService;

View File

@@ -1,11 +1,11 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { ConfirmCheckoutUseCase } from '@/packages/automation-application/use-cases/ConfirmCheckoutUseCase';
import { ConfirmCheckoutUseCase } from '@/packages/automation/application/use-cases/ConfirmCheckoutUseCase';
import { Result } from '@/packages/shared/result/Result';
import { CheckoutPrice } from '@/packages/automation-domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@/packages/automation-domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@/packages/automation-domain/value-objects/CheckoutConfirmation';
import type { ICheckoutService } from '@/packages/automation-application/ports/ICheckoutService';
import type { ICheckoutConfirmationPort } from '@/packages/automation-application/ports/ICheckoutConfirmationPort';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
import type { ICheckoutService } from '@/packages/automation/application/ports/ICheckoutService';
import type { ICheckoutConfirmationPort } from '@/packages/automation/application/ports/ICheckoutConfirmationPort';
describe('ConfirmCheckoutUseCase - Enhanced with Confirmation Port', () => {
let mockCheckoutService: ICheckoutService;

View File

@@ -1,11 +1,11 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { Result } from '../../../../packages/shared/result/Result';
import { ConfirmCheckoutUseCase } from '../../../../packages/automation-application/use-cases/ConfirmCheckoutUseCase';
import { ICheckoutService, CheckoutInfo } from '../../../../packages/automation-application/ports/ICheckoutService';
import { ICheckoutConfirmationPort } from '../../../../packages/automation-application/ports/ICheckoutConfirmationPort';
import { CheckoutPrice } from '../../../../packages/automation-domain/value-objects/CheckoutPrice';
import { CheckoutState, CheckoutStateEnum } from '../../../../packages/automation-domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '../../../../packages/automation-domain/value-objects/CheckoutConfirmation';
import { ConfirmCheckoutUseCase } from '../../../../packages/automation/application/use-cases/ConfirmCheckoutUseCase';
import { ICheckoutService, CheckoutInfo } from '../../../../packages/automation/application/ports/ICheckoutService';
import { ICheckoutConfirmationPort } from '../../../../packages/automation/application/ports/ICheckoutConfirmationPort';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState, CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
/**
* ConfirmCheckoutUseCase - GREEN PHASE

View File

@@ -1,9 +1,9 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { StartAutomationSessionUseCase } from '../../../../packages/automation-application/use-cases/StartAutomationSessionUseCase';
import { IAutomationEngine } from '../../../../packages/automation-application/ports/IAutomationEngine';
import { IScreenAutomation } from '../../../../packages/automation-application/ports/IScreenAutomation';
import { ISessionRepository } from '../../../../packages/automation-application/ports/ISessionRepository';
import { AutomationSession } from '../../../../packages/automation-domain/entities/AutomationSession';
import { StartAutomationSessionUseCase } from '../../../../packages/automation/application/use-cases/StartAutomationSessionUseCase';
import { IAutomationEngine } from '../../../../packages/automation/application/ports/IAutomationEngine';
import { IScreenAutomation } from '../../../../packages/automation/application/ports/IScreenAutomation';
import { ISessionRepository } from '../../../../packages/automation/application/ports/ISessionRepository';
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
describe('StartAutomationSessionUseCase', () => {
let mockAutomationEngine: {

View File

@@ -1,9 +1,9 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { VerifyAuthenticatedPageUseCase } from '../../../../packages/automation-application/use-cases/VerifyAuthenticatedPageUseCase';
import { IAuthenticationService } from '../../../../packages/automation-application/ports/IAuthenticationService';
import { VerifyAuthenticatedPageUseCase } from '../../../../packages/automation/application/use-cases/VerifyAuthenticatedPageUseCase';
import { IAuthenticationService } from '../../../../packages/automation/application/ports/IAuthenticationService';
import { Result } from '../../../../packages/shared/result/Result';
import { BrowserAuthenticationState } from '../../../../packages/automation-domain/value-objects/BrowserAuthenticationState';
import { AuthenticationState } from '../../../../packages/automation-domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
describe('VerifyAuthenticatedPageUseCase', () => {
let useCase: VerifyAuthenticatedPageUseCase;

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { AutomationSession } from '../../../../packages/automation-domain/entities/AutomationSession';
import { StepId } from '../../../../packages/automation-domain/value-objects/StepId';
import { SessionState } from '../../../../packages/automation-domain/value-objects/SessionState';
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { SessionState } from '@gridpilot/automation/domain/value-objects/SessionState';
describe('AutomationSession Entity', () => {
describe('create', () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { PageStateValidator } from '../../../../packages/automation-domain/services/PageStateValidator';
import { PageStateValidator } from '@gridpilot/automation/domain/services/PageStateValidator';
describe('PageStateValidator', () => {
const validator = new PageStateValidator();

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { StepTransitionValidator } from '../../../../packages/automation-domain/services/StepTransitionValidator';
import { StepId } from '../../../../packages/automation-domain/value-objects/StepId';
import { SessionState } from '../../../../packages/automation-domain/value-objects/SessionState';
import { StepTransitionValidator } from '@gridpilot/automation/domain/services/StepTransitionValidator';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { SessionState } from '@gridpilot/automation/domain/value-objects/SessionState';
describe('StepTransitionValidator Service', () => {
describe('canTransition', () => {

View File

@@ -1,6 +1,6 @@
import { describe, test, expect } from 'vitest';
import { BrowserAuthenticationState } from '../../../../packages/automation-domain/value-objects/BrowserAuthenticationState';
import { AuthenticationState } from '../../../../packages/automation-domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
describe('BrowserAuthenticationState', () => {
describe('isFullyAuthenticated()', () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { CheckoutConfirmation } from '../../../../packages/automation-domain/value-objects/CheckoutConfirmation';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
describe('CheckoutConfirmation Value Object', () => {
describe('create', () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { CheckoutPrice } from '../../../../packages/automation-domain/value-objects/CheckoutPrice';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
/**
* CheckoutPrice Value Object - GREEN PHASE

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { CheckoutState, CheckoutStateEnum } from '../../../../packages/automation-domain/value-objects/CheckoutState';
import { CheckoutState, CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
/**
* CheckoutState Value Object - GREEN PHASE

View File

@@ -1,5 +1,5 @@
import { describe, test, expect } from 'vitest';
import { CookieConfiguration } from '../../../../packages/automation-domain/value-objects/CookieConfiguration';
import { CookieConfiguration } from '@gridpilot/automation/domain/value-objects/CookieConfiguration';
describe('CookieConfiguration', () => {
const validTargetUrl = 'https://members-ng.iracing.com/jjwtauth/success';

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { RaceCreationResult } from '../../../../packages/automation-domain/value-objects/RaceCreationResult';
import { RaceCreationResult } from '@gridpilot/automation/domain/value-objects/RaceCreationResult';
describe('RaceCreationResult Value Object', () => {
describe('create', () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { SessionLifetime } from '../../../../packages/automation-domain/value-objects/SessionLifetime';
import { SessionLifetime } from '@gridpilot/automation/domain/value-objects/SessionLifetime';
describe('SessionLifetime Value Object', () => {
describe('Construction', () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { SessionState } from '../../../../packages/automation-domain/value-objects/SessionState';
import { SessionState } from '@gridpilot/automation/domain/value-objects/SessionState';
describe('SessionState Value Object', () => {
describe('create', () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { StepId } from '../../../../packages/automation-domain/value-objects/StepId';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
describe('StepId Value Object', () => {
describe('create', () => {

View File

@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { validateEmail, isDisposableEmail } from '@gridpilot/identity/domain/value-objects/EmailAddress';
describe('identity-domain email validation', () => {
it('accepts a valid email and normalizes it', () => {
const result = validateEmail(' USER@example.com ');
expect(result.success).toBe(true);
expect(result.email).toBe('user@example.com');
expect(result.error).toBeUndefined();
});
it('rejects an invalid email format', () => {
const result = validateEmail('not-an-email');
expect(result.success).toBe(false);
expect(result.email).toBeUndefined();
expect(result.error).toBeTypeOf('string');
});
it('rejects an email that is too short', () => {
const result = validateEmail('a@b');
expect(result.success).toBe(false);
expect(result.error).toContain('too short');
});
it('detects disposable email domains', () => {
expect(isDisposableEmail('foo@tempmail.com')).toBe(true);
expect(isDisposableEmail('bar@mailinator.com')).toBe(true);
expect(isDisposableEmail('user@example.com')).toBe(false);
});
});

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { loadAutomationConfig, getAutomationMode, AutomationMode } from '../../../packages/automation-infrastructure/config/AutomationConfig';
import { loadAutomationConfig, getAutomationMode, AutomationMode } from '../../../packages/automation/infrastructure/config/AutomationConfig';
describe('AutomationConfig', () => {
const originalEnv = process.env;

View File

@@ -1,6 +1,6 @@
import { describe, test, expect, beforeEach, vi } from 'vitest';
import type { Page } from 'playwright';
import { AuthenticationGuard } from 'packages/automation-infrastructure/adapters/automation/auth/AuthenticationGuard';
import { AuthenticationGuard } from 'packages/automation/infrastructure/adapters/automation/auth/AuthenticationGuard';
describe('AuthenticationGuard', () => {
let mockPage: Page;

View File

@@ -9,9 +9,9 @@ vi.mock('electron', () => ({
},
}));
import { ElectronCheckoutConfirmationAdapter } from '@/packages/automation-infrastructure/adapters/ipc/ElectronCheckoutConfirmationAdapter';
import { CheckoutPrice } from '@/packages/automation-domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@/packages/automation-domain/value-objects/CheckoutState';
import { ElectronCheckoutConfirmationAdapter } from '@/packages/automation/infrastructure/adapters/ipc/ElectronCheckoutConfirmationAdapter';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { ipcMain } from 'electron';
describe('ElectronCheckoutConfirmationAdapter', () => {

View File

@@ -1,11 +1,11 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Page, BrowserContext } from 'playwright';
import { PlaywrightAuthSessionService } from '../../../../packages/automation-infrastructure/adapters/automation/auth/PlaywrightAuthSessionService';
import type { PlaywrightBrowserSession } from '../../../../packages/automation-infrastructure/adapters/automation/core/PlaywrightBrowserSession';
import type { SessionCookieStore } from '../../../../packages/automation-infrastructure/adapters/automation/auth/SessionCookieStore';
import type { IPlaywrightAuthFlow } from '../../../../packages/automation-infrastructure/adapters/automation/auth/PlaywrightAuthFlow';
import type { ILogger } from '../../../../packages/automation-application/ports/ILogger';
import { AuthenticationState } from '../../../../packages/automation-domain/value-objects/AuthenticationState';
import { PlaywrightAuthSessionService } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService';
import type { PlaywrightBrowserSession } from '../../../../packages/automation/infrastructure/adapters/automation/core/PlaywrightBrowserSession';
import type { SessionCookieStore } from '../../../../packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore';
import type { IPlaywrightAuthFlow } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthFlow';
import type { ILogger } from '../../../../packages/automation/application/ports/ILogger';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { Result } from '../../../../packages/shared/result/Result';
describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', () => {

View File

@@ -1,13 +1,13 @@
import { describe, it, expect, vi } from 'vitest';
import type { Page, Locator } from 'playwright';
import { PlaywrightAuthSessionService } from '../../../../packages/automation-infrastructure/adapters/automation/auth/PlaywrightAuthSessionService';
import { AuthenticationState } from '../../../../packages/automation-domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '../../../../packages/automation-domain/value-objects/BrowserAuthenticationState';
import type { ILogger } from '../../../../packages/automation-application/ports/ILogger';
import { PlaywrightAuthSessionService } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthSessionService';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import type { ILogger } from '../../../../packages/automation/application/ports/ILogger';
import type { Result } from '../../../../packages/shared/result/Result';
import type { PlaywrightBrowserSession } from '../../../../packages/automation-infrastructure/adapters/automation/core/PlaywrightBrowserSession';
import type { SessionCookieStore } from '../../../../packages/automation-infrastructure/adapters/automation/auth/SessionCookieStore';
import type { IPlaywrightAuthFlow } from '../../../../packages/automation-infrastructure/adapters/automation/auth/PlaywrightAuthFlow';
import type { PlaywrightBrowserSession } from '../../../../packages/automation/infrastructure/adapters/automation/core/PlaywrightBrowserSession';
import type { SessionCookieStore } from '../../../../packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore';
import type { IPlaywrightAuthFlow } from '../../../../packages/automation/infrastructure/adapters/automation/auth/PlaywrightAuthFlow';
describe('PlaywrightAuthSessionService.verifyPageAuthentication', () => {
function createService(deps: {

View File

@@ -1,5 +1,5 @@
import { describe, test, expect, beforeEach } from 'vitest';
import { SessionCookieStore } from 'packages/automation-infrastructure/adapters/automation/auth/SessionCookieStore';
import { SessionCookieStore } from 'packages/automation/infrastructure/adapters/automation/auth/SessionCookieStore';
import type { Cookie } from 'playwright';
describe('SessionCookieStore - Cookie Validation', () => {

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { BrowserModeConfigLoader } from '../../../../packages/automation-infrastructure/config/BrowserModeConfig';
import { BrowserModeConfigLoader } from '../../../../packages/automation/infrastructure/config/BrowserModeConfig';
/**
* Unit tests for BrowserModeConfig - GREEN PHASE

View File

@@ -0,0 +1,125 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { JoinLeagueUseCase } from '@gridpilot/racing/application/use-cases/JoinLeagueUseCase';
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
import type {
LeagueMembership,
MembershipRole,
MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership';
class InMemoryLeagueMembershipRepository implements ILeagueMembershipRepository {
private memberships: LeagueMembership[] = [];
async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
return (
this.memberships.find(
(m) => m.leagueId === leagueId && m.driverId === driverId,
) || null
);
}
async getActiveMembershipForDriver(driverId: string): Promise<LeagueMembership | null> {
return (
this.memberships.find(
(m) => m.driverId === driverId && m.status === 'active',
) || null
);
}
async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
return this.memberships.filter(
(m) => m.leagueId === leagueId && m.status === 'active',
);
}
async getTeamMembers(leagueId: string): Promise<LeagueMembership[]> {
return this.memberships.filter(
(m) => m.leagueId === leagueId && m.status === 'active',
);
}
async saveMembership(membership: LeagueMembership): Promise<LeagueMembership> {
const existingIndex = this.memberships.findIndex(
(m) => m.leagueId === membership.leagueId && m.driverId === membership.driverId,
);
if (existingIndex >= 0) {
this.memberships[existingIndex] = membership;
} else {
this.memberships.push(membership);
}
return membership;
}
async removeMembership(leagueId: string, driverId: string): Promise<void> {
this.memberships = this.memberships.filter(
(m) => !(m.leagueId === leagueId && m.driverId === driverId),
);
}
async getJoinRequests(): Promise<never> {
throw new Error('Not implemented for this test');
}
async saveJoinRequest(): Promise<never> {
throw new Error('Not implemented for this test');
}
async removeJoinRequest(): Promise<never> {
throw new Error('Not implemented for this test');
}
seedMembership(membership: LeagueMembership): void {
this.memberships.push(membership);
}
getAllMemberships(): LeagueMembership[] {
return [...this.memberships];
}
}
describe('Membership use-cases', () => {
describe('JoinLeagueUseCase', () => {
let repository: InMemoryLeagueMembershipRepository;
let useCase: JoinLeagueUseCase;
beforeEach(() => {
repository = new InMemoryLeagueMembershipRepository();
useCase = new JoinLeagueUseCase(repository);
});
it('creates an active member when driver has no membership', async () => {
const leagueId = 'league-1';
const driverId = 'driver-1';
await useCase.execute({ leagueId, driverId });
const membership = await repository.getMembership(leagueId, driverId);
expect(membership).not.toBeNull();
expect(membership?.leagueId).toBe(leagueId);
expect(membership?.driverId).toBe(driverId);
expect(membership?.role as MembershipRole).toBe('member');
expect(membership?.status as MembershipStatus).toBe('active');
expect(membership?.joinedAt).toBeInstanceOf(Date);
});
it('throws when driver already has membership for league', async () => {
const leagueId = 'league-1';
const driverId = 'driver-1';
repository.seedMembership({
leagueId,
driverId,
role: 'member',
status: 'active',
joinedAt: new Date('2024-01-01'),
});
await expect(
useCase.execute({ leagueId, driverId }),
).rejects.toThrow('Already a member or have a pending request');
});
});
});

View File

@@ -0,0 +1,503 @@
import { describe, it, expect, beforeEach } from 'vitest';
import type { IRaceRegistrationRepository } from '@gridpilot/racing/domain/repositories/IRaceRegistrationRepository';
import type { ILeagueMembershipRepository } from '@gridpilot/racing/domain/repositories/ILeagueMembershipRepository';
import type { ITeamRepository } from '@gridpilot/racing/domain/repositories/ITeamRepository';
import type { ITeamMembershipRepository } from '@gridpilot/racing/domain/repositories/ITeamMembershipRepository';
import type { RaceRegistration } from '@gridpilot/racing/domain/entities/RaceRegistration';
import type {
LeagueMembership,
MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership';
import type {
Team,
TeamMembership,
TeamMembershipStatus,
TeamRole,
TeamJoinRequest,
} from '@gridpilot/racing/domain/entities/Team';
import { RegisterForRaceUseCase } from '@gridpilot/racing/application/use-cases/RegisterForRaceUseCase';
import { WithdrawFromRaceUseCase } from '@gridpilot/racing/application/use-cases/WithdrawFromRaceUseCase';
import {
IsDriverRegisteredForRaceQuery,
GetRaceRegistrationsQuery,
} from '@gridpilot/racing/application/use-cases/RaceRegistrationQueries';
import {
CreateTeamUseCase,
JoinTeamUseCase,
LeaveTeamUseCase,
ApproveTeamJoinRequestUseCase,
RejectTeamJoinRequestUseCase,
UpdateTeamUseCase,
GetAllTeamsQuery,
GetTeamDetailsQuery,
GetTeamMembersQuery,
GetTeamJoinRequestsQuery,
GetDriverTeamQuery,
} from '@gridpilot/racing/application/use-cases/TeamUseCases';
/**
* Simple in-memory fakes mirroring current alpha behavior.
*/
class InMemoryRaceRegistrationRepository implements IRaceRegistrationRepository {
private registrations = new Map<string, Set<string>>(); // raceId -> driverIds
async isRegistered(raceId: string, driverId: string): Promise<boolean> {
const set = this.registrations.get(raceId);
return set ? set.has(driverId) : false;
}
async getRegisteredDrivers(raceId: string): Promise<string[]> {
const set = this.registrations.get(raceId);
return set ? Array.from(set) : [];
}
async getRegistrationCount(raceId: string): Promise<number> {
const set = this.registrations.get(raceId);
return set ? set.size : 0;
}
async register(registration: RaceRegistration): Promise<void> {
if (!this.registrations.has(registration.raceId)) {
this.registrations.set(registration.raceId, new Set());
}
this.registrations.get(registration.raceId)!.add(registration.driverId);
}
async withdraw(raceId: string, driverId: string): Promise<void> {
const set = this.registrations.get(raceId);
if (!set || !set.has(driverId)) {
throw new Error('Not registered for this race');
}
set.delete(driverId);
if (set.size === 0) {
this.registrations.delete(raceId);
}
}
async getDriverRegistrations(driverId: string): Promise<string[]> {
const result: string[] = [];
for (const [raceId, set] of this.registrations.entries()) {
if (set.has(driverId)) {
result.push(raceId);
}
}
return result;
}
async clearRaceRegistrations(raceId: string): Promise<void> {
this.registrations.delete(raceId);
}
}
class InMemoryLeagueMembershipRepositoryForRegistrations implements ILeagueMembershipRepository {
private memberships: LeagueMembership[] = [];
async getMembership(leagueId: string, driverId: string): Promise<LeagueMembership | null> {
return (
this.memberships.find(
(m) => m.leagueId === leagueId && m.driverId === driverId,
) || null
);
}
async getLeagueMembers(leagueId: string): Promise<LeagueMembership[]> {
return this.memberships.filter(
(m) => m.leagueId === leagueId && m.status === 'active',
);
}
async getJoinRequests(): Promise<never> {
throw new Error('Not needed for registration tests');
}
async saveMembership(membership: LeagueMembership): Promise<LeagueMembership> {
this.memberships.push(membership);
return membership;
}
async removeMembership(): Promise<void> {
throw new Error('Not needed for registration tests');
}
async saveJoinRequest(): Promise<never> {
throw new Error('Not needed for registration tests');
}
async removeJoinRequest(): Promise<never> {
throw new Error('Not needed for registration tests');
}
seedActiveMembership(leagueId: string, driverId: string): void {
this.memberships.push({
leagueId,
driverId,
role: 'member',
status: 'active' as MembershipStatus,
joinedAt: new Date('2024-01-01'),
});
}
}
class InMemoryTeamRepository implements ITeamRepository {
private teams: Team[] = [];
async findById(id: string): Promise<Team | null> {
return this.teams.find((t) => t.id === id) || null;
}
async findAll(): Promise<Team[]> {
return [...this.teams];
}
async findByLeagueId(leagueId: string): Promise<Team[]> {
return this.teams.filter((t) => t.leagues.includes(leagueId));
}
async create(team: Team): Promise<Team> {
this.teams.push(team);
return team;
}
async update(team: Team): Promise<Team> {
const index = this.teams.findIndex((t) => t.id === team.id);
if (index >= 0) {
this.teams[index] = team;
} else {
this.teams.push(team);
}
return team;
}
async delete(id: string): Promise<void> {
this.teams = this.teams.filter((t) => t.id !== id);
}
async exists(id: string): Promise<boolean> {
return this.teams.some((t) => t.id === id);
}
seedTeam(team: Team): void {
this.teams.push(team);
}
}
class InMemoryTeamMembershipRepository implements ITeamMembershipRepository {
private memberships: TeamMembership[] = [];
private joinRequests: TeamJoinRequest[] = [];
async getMembership(teamId: string, driverId: string): Promise<TeamMembership | null> {
return (
this.memberships.find(
(m) => m.teamId === teamId && m.driverId === driverId,
) || null
);
}
async getActiveMembershipForDriver(driverId: string): Promise<TeamMembership | null> {
return (
this.memberships.find(
(m) => m.driverId === driverId && m.status === 'active',
) || null
);
}
async getTeamMembers(teamId: string): Promise<TeamMembership[]> {
return this.memberships.filter(
(m) => m.teamId === teamId && m.status === 'active',
);
}
async saveMembership(membership: TeamMembership): Promise<TeamMembership> {
const index = this.memberships.findIndex(
(m) => m.teamId === membership.teamId && m.driverId === membership.driverId,
);
if (index >= 0) {
this.memberships[index] = membership;
} else {
this.memberships.push(membership);
}
return membership;
}
async removeMembership(teamId: string, driverId: string): Promise<void> {
this.memberships = this.memberships.filter(
(m) => !(m.teamId === teamId && m.driverId === driverId),
);
}
async getJoinRequests(teamId: string): Promise<TeamJoinRequest[]> {
// For these tests we ignore teamId and return all,
// allowing use-cases to look up by request ID only.
return [...this.joinRequests];
}
async saveJoinRequest(request: TeamJoinRequest): Promise<TeamJoinRequest> {
const index = this.joinRequests.findIndex((r) => r.id === request.id);
if (index >= 0) {
this.joinRequests[index] = request;
} else {
this.joinRequests.push(request);
}
return request;
}
async removeJoinRequest(requestId: string): Promise<void> {
this.joinRequests = this.joinRequests.filter((r) => r.id !== requestId);
}
seedMembership(membership: TeamMembership): void {
this.memberships.push(membership);
}
seedJoinRequest(request: TeamJoinRequest): void {
this.joinRequests.push(request);
}
getAllMemberships(): TeamMembership[] {
return [...this.memberships];
}
getAllJoinRequests(): TeamJoinRequest[] {
return [...this.joinRequests];
}
}
describe('Racing application use-cases - registrations', () => {
let registrationRepo: InMemoryRaceRegistrationRepository;
let membershipRepo: InMemoryLeagueMembershipRepositoryForRegistrations;
let registerForRace: RegisterForRaceUseCase;
let withdrawFromRace: WithdrawFromRaceUseCase;
let isDriverRegistered: IsDriverRegisteredForRaceQuery;
let getRaceRegistrations: GetRaceRegistrationsQuery;
beforeEach(() => {
registrationRepo = new InMemoryRaceRegistrationRepository();
membershipRepo = new InMemoryLeagueMembershipRepositoryForRegistrations();
registerForRace = new RegisterForRaceUseCase(registrationRepo, membershipRepo);
withdrawFromRace = new WithdrawFromRaceUseCase(registrationRepo);
isDriverRegistered = new IsDriverRegisteredForRaceQuery(registrationRepo);
getRaceRegistrations = new GetRaceRegistrationsQuery(registrationRepo);
});
it('registers an active league member for a race and tracks registration', async () => {
const raceId = 'race-1';
const leagueId = 'league-1';
const driverId = 'driver-1';
membershipRepo.seedActiveMembership(leagueId, driverId);
await registerForRace.execute({ raceId, leagueId, driverId });
expect(await isDriverRegistered.execute({ raceId, driverId })).toBe(true);
const registeredDrivers = await getRaceRegistrations.execute({ raceId });
expect(registeredDrivers).toContain(driverId);
});
it('throws when registering a non-member for a race', async () => {
const raceId = 'race-1';
const leagueId = 'league-1';
const driverId = 'driver-1';
await expect(
registerForRace.execute({ raceId, leagueId, driverId }),
).rejects.toThrow('Must be an active league member to register for races');
});
it('withdraws a registration and reflects state in queries', async () => {
const raceId = 'race-1';
const leagueId = 'league-1';
const driverId = 'driver-1';
membershipRepo.seedActiveMembership(leagueId, driverId);
await registerForRace.execute({ raceId, leagueId, driverId });
await withdrawFromRace.execute({ raceId, driverId });
expect(await isDriverRegistered.execute({ raceId, driverId })).toBe(false);
expect(await getRaceRegistrations.execute({ raceId })).toEqual([]);
});
});
describe('Racing application use-cases - teams', () => {
let teamRepo: InMemoryTeamRepository;
let membershipRepo: InMemoryTeamMembershipRepository;
let createTeam: CreateTeamUseCase;
let joinTeam: JoinTeamUseCase;
let leaveTeam: LeaveTeamUseCase;
let approveJoin: ApproveTeamJoinRequestUseCase;
let rejectJoin: RejectTeamJoinRequestUseCase;
let updateTeamUseCase: UpdateTeamUseCase;
let getAllTeamsQuery: GetAllTeamsQuery;
let getTeamDetailsQuery: GetTeamDetailsQuery;
let getTeamMembersQuery: GetTeamMembersQuery;
let getTeamJoinRequestsQuery: GetTeamJoinRequestsQuery;
let getDriverTeamQuery: GetDriverTeamQuery;
beforeEach(() => {
teamRepo = new InMemoryTeamRepository();
membershipRepo = new InMemoryTeamMembershipRepository();
createTeam = new CreateTeamUseCase(teamRepo, membershipRepo);
joinTeam = new JoinTeamUseCase(teamRepo, membershipRepo);
leaveTeam = new LeaveTeamUseCase(membershipRepo);
approveJoin = new ApproveTeamJoinRequestUseCase(membershipRepo);
rejectJoin = new RejectTeamJoinRequestUseCase(membershipRepo);
updateTeamUseCase = new UpdateTeamUseCase(teamRepo, membershipRepo);
getAllTeamsQuery = new GetAllTeamsQuery(teamRepo);
getTeamDetailsQuery = new GetTeamDetailsQuery(teamRepo, membershipRepo);
getTeamMembersQuery = new GetTeamMembersQuery(membershipRepo);
getTeamJoinRequestsQuery = new GetTeamJoinRequestsQuery(membershipRepo);
getDriverTeamQuery = new GetDriverTeamQuery(teamRepo, membershipRepo);
});
it('creates a team and assigns creator as active owner', async () => {
const ownerId = 'driver-1';
const result = await createTeam.execute({
name: 'Apex Racing',
tag: 'APEX',
description: 'Professional GT3 racing',
ownerId,
leagues: ['league-1'],
});
expect(result.team.id).toBeDefined();
expect(result.team.ownerId).toBe(ownerId);
const membership = await membershipRepo.getActiveMembershipForDriver(ownerId);
expect(membership?.teamId).toBe(result.team.id);
expect(membership?.role as TeamRole).toBe('owner');
expect(membership?.status as TeamMembershipStatus).toBe('active');
});
it('prevents driver from joining multiple teams and mirrors legacy error message', async () => {
const ownerId = 'driver-1';
const otherTeamId = 'team-2';
// Seed an existing active membership
membershipRepo.seedMembership({
teamId: otherTeamId,
driverId: ownerId,
role: 'driver',
status: 'active',
joinedAt: new Date('2024-02-01'),
});
await expect(
joinTeam.execute({ teamId: 'team-1', driverId: ownerId }),
).rejects.toThrow('Driver already belongs to a team');
});
it('approves a join request and moves it into active membership', async () => {
const teamId = 'team-1';
const driverId = 'driver-2';
const request: TeamJoinRequest = {
id: 'req-1',
teamId,
driverId,
requestedAt: new Date('2024-03-01'),
message: 'Let me in',
};
membershipRepo.seedJoinRequest(request);
await approveJoin.execute({ requestId: request.id });
const membership = await membershipRepo.getMembership(teamId, driverId);
expect(membership).not.toBeNull();
expect(membership?.status as TeamMembershipStatus).toBe('active');
const remainingRequests = await membershipRepo.getJoinRequests(teamId);
expect(remainingRequests.find((r) => r.id === request.id)).toBeUndefined();
});
it('rejects a join request and removes it', async () => {
const teamId = 'team-1';
const driverId = 'driver-2';
const request: TeamJoinRequest = {
id: 'req-2',
teamId,
driverId,
requestedAt: new Date('2024-03-02'),
message: 'Please?',
};
membershipRepo.seedJoinRequest(request);
await rejectJoin.execute({ requestId: request.id });
const remainingRequests = await membershipRepo.getJoinRequests(teamId);
expect(remainingRequests.find((r) => r.id === request.id)).toBeUndefined();
});
it('updates team details when performed by owner or manager and reflects in queries', async () => {
const ownerId = 'driver-1';
const created = await createTeam.execute({
name: 'Original Name',
tag: 'ORIG',
description: 'Original description',
ownerId,
leagues: [],
});
await updateTeamUseCase.execute({
teamId: created.team.id,
updates: { name: 'Updated Name', description: 'Updated description' },
updatedBy: ownerId,
});
const teamDetails = await getTeamDetailsQuery.execute({
teamId: created.team.id,
driverId: ownerId,
});
expect(teamDetails.team.name).toBe('Updated Name');
expect(teamDetails.team.description).toBe('Updated description');
});
it('returns driver team via query matching legacy getDriverTeam behavior', async () => {
const ownerId = 'driver-1';
const { team } = await createTeam.execute({
name: 'Apex Racing',
tag: 'APEX',
description: 'Professional GT3 racing',
ownerId,
leagues: [],
});
const result = await getDriverTeamQuery.execute({ driverId: ownerId });
expect(result).not.toBeNull();
expect(result?.team.id).toBe(team.id);
expect(result?.membership.driverId).toBe(ownerId);
});
it('lists all teams and members via queries after multiple operations', async () => {
const ownerId = 'driver-1';
const otherDriverId = 'driver-2';
const { team } = await createTeam.execute({
name: 'Apex Racing',
tag: 'APEX',
description: 'Professional GT3 racing',
ownerId,
leagues: [],
});
await joinTeam.execute({ teamId: team.id, driverId: otherDriverId });
const teams = await getAllTeamsQuery.execute();
expect(teams.length).toBe(1);
const members = await getTeamMembersQuery.execute({ teamId: team.id });
const memberIds = members.map((m) => m.driverId).sort();
expect(memberIds).toEqual([ownerId, otherDriverId].sort());
});
});

View File

@@ -0,0 +1,281 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
const repoRoot = path.resolve(__dirname, '../../../..');
const packagesRoot = path.join(repoRoot, 'packages');
type PackageKind =
| 'racing-domain'
| 'racing-application'
| 'racing-infrastructure'
| 'racing-demo-infrastructure'
| 'other';
interface TsFile {
filePath: string;
kind: PackageKind;
}
function classifyFile(filePath: string): PackageKind {
const normalized = filePath.replace(/\\/g, '/');
// Bounded-context domain lives under packages/racing/domain
if (normalized.includes('/packages/racing/domain/')) {
return 'racing-domain';
}
if (normalized.includes('/packages/racing-application/')) {
return 'racing-application';
}
if (normalized.includes('/packages/racing-infrastructure/')) {
return 'racing-infrastructure';
}
if (normalized.includes('/packages/racing-demo-infrastructure/')) {
return 'racing-demo-infrastructure';
}
return 'other';
}
function collectTsFiles(dir: string): TsFile[] {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const files: TsFile[] = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...collectTsFiles(fullPath));
} else if (entry.isFile()) {
if (
entry.name.endsWith('.ts') ||
entry.name.endsWith('.tsx')
) {
const kind = classifyFile(fullPath);
if (kind !== 'other') {
files.push({ filePath: fullPath, kind });
}
}
}
}
return files;
}
interface ImportViolation {
file: string;
line: number;
moduleSpecifier: string;
reason: string;
}
function extractImportModule(line: string): string | null {
const trimmed = line.trim();
if (!trimmed.startsWith('import')) return null;
// Handle: import ... from 'x';
const fromMatch = trimmed.match(/from\s+['"](.*)['"]/);
if (fromMatch) {
return fromMatch[1];
}
// Handle: import 'x';
const sideEffectMatch = trimmed.match(/^import\s+['"](.*)['"]\s*;?$/);
if (sideEffectMatch) {
return sideEffectMatch[1];
}
return null;
}
describe('Package dependency structure for racing slice', () => {
const tsFiles = collectTsFiles(packagesRoot);
it('enforces import boundaries for racing-domain', () => {
const violations: ImportViolation[] = [];
const forbiddenPrefixes = [
'@gridpilot/racing-application',
'@gridpilot/racing-infrastructure',
'@gridpilot/racing-demo-infrastructure',
'apps/',
'@/',
'react',
'next',
'electron',
];
for (const { filePath, kind } of tsFiles) {
if (kind !== 'racing-domain') continue;
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
lines.forEach((line, index) => {
const moduleSpecifier = extractImportModule(line);
if (!moduleSpecifier) return;
for (const prefix of forbiddenPrefixes) {
if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing-domain must not depend on application, infrastructure, apps, or UI frameworks',
});
}
}
});
}
if (violations.length > 0) {
const message =
'Found forbidden imports in racing domain layer (packages/racing/domain):\n' +
violations
.map(
(v) =>
`- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`,
)
.join('\n');
expect(message).toBe('');
} else {
expect(violations).toEqual([]);
}
});
it('enforces import boundaries for racing-application', () => {
const violations: ImportViolation[] = [];
const forbiddenPrefixes = [
'@gridpilot/racing-infrastructure',
'@gridpilot/racing-demo-infrastructure',
'apps/',
'@/',
];
const allowedPrefixes = [
'@gridpilot/racing',
'@gridpilot/shared-result',
'@gridpilot/identity',
];
for (const { filePath, kind } of tsFiles) {
if (kind !== 'racing-application') continue;
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
lines.forEach((line, index) => {
const moduleSpecifier = extractImportModule(line);
if (!moduleSpecifier) return;
for (const prefix of forbiddenPrefixes) {
if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing-application must not depend on infrastructure or apps',
});
}
}
if (moduleSpecifier.startsWith('@gridpilot/')) {
const isAllowed = allowedPrefixes.some((prefix) =>
moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix),
);
if (!isAllowed) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing-application should only depend on domain, shared-result, or other domain packages',
});
}
}
});
}
if (violations.length > 0) {
const message =
'Found forbidden imports in packages/racing-application:\n' +
violations
.map(
(v) =>
`- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`,
)
.join('\n');
expect(message).toBe('');
} else {
expect(violations).toEqual([]);
}
});
it('enforces import boundaries for racing infrastructure packages', () => {
const violations: ImportViolation[] = [];
const forbiddenPrefixes = ['apps/', '@/'];
const allowedPrefixes = [
'@gridpilot/racing',
'@gridpilot/shared-result',
'@gridpilot/demo-support',
'@gridpilot/social',
];
for (const { filePath, kind } of tsFiles) {
if (
kind !== 'racing-infrastructure' &&
kind !== 'racing-demo-infrastructure'
) {
continue;
}
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split(/\r?\n/);
lines.forEach((line, index) => {
const moduleSpecifier = extractImportModule(line);
if (!moduleSpecifier) return;
for (const prefix of forbiddenPrefixes) {
if (moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix)) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing infrastructure must not depend on apps or @/ aliases',
});
}
}
if (moduleSpecifier.startsWith('@gridpilot/')) {
const isAllowed = allowedPrefixes.some((prefix) =>
moduleSpecifier === prefix || moduleSpecifier.startsWith(prefix),
);
if (!isAllowed) {
violations.push({
file: filePath,
line: index + 1,
moduleSpecifier,
reason: 'racing infrastructure should depend only on domain, shared-result, or demo-support',
});
}
}
});
}
if (violations.length > 0) {
const message =
'Found forbidden imports in racing infrastructure packages:\n' +
violations
.map(
(v) =>
`- ${v.file}:${v.line} :: import '${v.moduleSpecifier}' // ${v.reason}`,
)
.join('\n');
expect(message).toBe('');
} else {
expect(violations).toEqual([]);
}
});
});

View File

@@ -0,0 +1,53 @@
import { describe, it, expect } from 'vitest';
import React from 'react';
import { render, screen } from '@testing-library/react';
vi.mock('next/navigation', () => ({
usePathname: () => '/',
}));
vi.mock('next/link', () => {
const ActualLink = ({ href, children, ...rest }: any) => (
<a href={href} {...rest}>
{children}
</a>
);
return { default: ActualLink };
});
import { AlphaNav } from '../../../apps/website/components/alpha/AlphaNav';
describe('AlphaNav', () => {
it('hides Dashboard link and shows login when unauthenticated', () => {
render(<AlphaNav isAuthenticated={false} />);
const dashboardLinks = screen.queryAllByText('Dashboard');
expect(dashboardLinks.length).toBe(0);
const homeLink = screen.getByText('Home');
expect(homeLink).toBeInTheDocument();
const login = screen.getByText('Authenticate with iRacing');
expect(login).toBeInTheDocument();
expect((login as HTMLAnchorElement).getAttribute('href')).toContain(
'/auth/iracing/start?returnTo=/dashboard',
);
});
it('shows Dashboard link, hides Home, and logout control when authenticated', () => {
render(<AlphaNav isAuthenticated />);
const dashboard = screen.getByText('Dashboard');
expect(dashboard).toBeInTheDocument();
expect((dashboard as HTMLAnchorElement).getAttribute('href')).toBe('/dashboard');
const homeLink = screen.queryByText('Home');
expect(homeLink).toBeNull();
const login = screen.queryByText('Authenticate with iRacing');
expect(login).toBeNull();
const logout = screen.getByText('Logout');
expect(logout).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
/**
* Auth + caching behavior for RootLayout and Dashboard.
*
* These tests assert that:
* - RootLayout is marked dynamic so it re-evaluates cookies per request.
* - DashboardPage is also dynamic (no static caching of auth state).
*/
describe('RootLayout auth caching behavior', () => {
it('is configured as dynamic to avoid static auth caching', async () => {
const layoutModule = await import('../../../../apps/website/app/layout');
// Next.js dynamic routing flag
const dynamic = (layoutModule as any).dynamic;
expect(dynamic).toBe('force-dynamic');
});
});
describe('Dashboard auth caching behavior', () => {
it('is configured as dynamic to evaluate auth per request', async () => {
const dashboardModule = await import('../../../../apps/website/app/dashboard/page');
const dynamic = (dashboardModule as any).dynamic;
expect(dynamic).toBe('force-dynamic');
});
});

View File

@@ -0,0 +1,63 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
const cookieStore = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
};
vi.mock('next/headers', () => ({
cookies: () => cookieStore,
}));
import { InMemoryAuthService } from '../../../../apps/website/lib/auth/InMemoryAuthService';
describe('InMemoryAuthService', () => {
beforeEach(() => {
cookieStore.get.mockReset();
cookieStore.set.mockReset();
cookieStore.delete.mockReset();
});
it('startIracingAuthRedirect returns redirectUrl with returnTo and state without touching cookies', async () => {
const service = new InMemoryAuthService();
const { redirectUrl, state } = await service.startIracingAuthRedirect('some');
expect(typeof state).toBe('string');
expect(state.length).toBeGreaterThan(0);
const url = new URL(redirectUrl, 'http://localhost');
expect(url.pathname).toBe('/auth/iracing/callback');
expect(url.searchParams.get('returnTo')).toBe('some');
expect(url.searchParams.get('state')).toBe(state);
expect(url.searchParams.get('code')).toBeTruthy();
expect(cookieStore.get).not.toHaveBeenCalled();
expect(cookieStore.set).not.toHaveBeenCalled();
expect(cookieStore.delete).not.toHaveBeenCalled();
});
it('loginWithIracingCallback returns deterministic demo session', async () => {
const service = new InMemoryAuthService();
const session = await service.loginWithIracingCallback({
code: 'dummy-code',
state: 'any-state',
});
expect(session.user.id).toBe('demo-user');
expect(session.user.primaryDriverId).toBeDefined();
expect(session.user.primaryDriverId).not.toBe('');
});
it('logout does not attempt to modify cookies directly', async () => {
const service = new InMemoryAuthService();
await service.logout();
expect(cookieStore.get).not.toHaveBeenCalled();
expect(cookieStore.set).not.toHaveBeenCalled();
expect(cookieStore.delete).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,17 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
describe('IracingAuthPage imports', () => {
it('does not import cookies or getAuthService', () => {
const filePath = path.resolve(
__dirname,
'../../../../apps/website/app/auth/iracing/page.tsx',
);
const source = fs.readFileSync(filePath, 'utf-8');
expect(source.includes("from 'next/headers'")).toBe(false);
expect(source.includes('cookies(')).toBe(false);
expect(source.includes('getAuthService')).toBe(false);
});
});

View File

@@ -0,0 +1,81 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
const cookieStore = {
get: vi.fn(),
set: vi.fn(),
delete: vi.fn(),
};
vi.mock('next/headers', () => {
return {
cookies: () => cookieStore,
};
});
import { GET as startGet } from '../../../../apps/website/app/auth/iracing/start/route';
import { GET as callbackGet } from '../../../../apps/website/app/auth/iracing/callback/route';
import { POST as logoutPost } from '../../../../apps/website/app/auth/logout/route';
describe('iRacing auth route handlers', () => {
beforeEach(() => {
cookieStore.get.mockReset();
cookieStore.set.mockReset();
cookieStore.delete.mockReset();
});
it('start route redirects to auth URL and sets state cookie', async () => {
const req = new Request('http://localhost/auth/iracing/start?returnTo=/dashboard');
const res = await startGet(req as any);
expect(res.status).toBe(307);
const location = res.headers.get('location') ?? '';
expect(location).toContain('/auth/iracing/callback');
expect(location).toContain('returnTo=%2Fdashboard');
expect(location).toMatch(/state=/);
expect(cookieStore.set).toHaveBeenCalled();
const [name] = cookieStore.set.mock.calls[0];
expect(name).toBe('gp_demo_auth_state');
});
it('callback route creates session cookie and redirects to returnTo', async () => {
cookieStore.get.mockImplementation((name: string) => {
if (name === 'gp_demo_auth_state') {
return { value: 'valid-state' };
}
return undefined;
});
const req = new Request(
'http://localhost/auth/iracing/callback?code=demo-code&state=valid-state&returnTo=/dashboard',
);
const res = await callbackGet(req as any);
expect(res.status).toBe(307);
const location = res.headers.get('location');
expect(location).toBe('http://localhost/dashboard');
expect(cookieStore.set).toHaveBeenCalled();
const [sessionName, sessionValue] = cookieStore.set.mock.calls[0];
expect(sessionName).toBe('gp_demo_session');
expect(typeof sessionValue).toBe('string');
expect(cookieStore.delete).toHaveBeenCalledWith('gp_demo_auth_state');
});
it('logout route deletes session cookie and redirects home using request origin', async () => {
const req = new Request('http://example.com/auth/logout', {
method: 'POST',
});
const res = await logoutPost(req as any);
expect(res.status).toBe(307);
const location = res.headers.get('location');
expect(location).toBe('http://example.com/');
expect(cookieStore.delete).toHaveBeenCalledWith('gp_demo_session');
});
});

View File

@@ -0,0 +1,30 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
const alphaDir = path.resolve(__dirname, '../../../../apps/website/components/alpha');
const metaAllowlist = new Set([
'FeatureLimitationTooltip.tsx',
'CompanionInstructions.tsx',
'CompanionStatus.tsx',
'AlphaBanner.tsx',
'AlphaFooter.tsx',
'AlphaNav.tsx',
]);
describe('Alpha components structure', () => {
it('contains only alpha chrome and meta components', () => {
const entries = fs.readdirSync(alphaDir);
const tsxFiles = entries.filter((file) => file.endsWith('.tsx'));
const violations = tsxFiles.filter((file) => {
if (metaAllowlist.has(file)) {
return false;
}
return !file.startsWith('Alpha');
});
expect(violations).toEqual([]);
});
});

View File

@@ -0,0 +1,81 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
const websiteRoot = path.resolve(__dirname, '../../../../apps/website');
const forbiddenImportPrefixes = [
"@/lib/demo-data",
"@/lib/inmemory",
"@/lib/social",
"@/lib/email-validation",
"@/lib/membership-data",
"@/lib/registration-data",
"@/lib/team-data",
];
function collectTsFiles(dir: string): string[] {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Skip Next.js build output if present
if (entry.name === '.next') continue;
files.push(...collectTsFiles(fullPath));
} else if (entry.isFile()) {
if (
entry.name.endsWith('.ts') ||
entry.name.endsWith('.tsx')
) {
files.push(fullPath);
}
}
}
return files;
}
describe('Website import boundaries', () => {
it('does not import forbidden website lib modules directly', () => {
const files = collectTsFiles(websiteRoot);
const violations: { file: string; line: number; content: string }[] = [];
for (const file of files) {
const content = fs.readFileSync(file, 'utf8');
const lines = content.split(/\r?\n/);
lines.forEach((line, index) => {
const trimmed = line.trim();
if (!trimmed.startsWith('import')) return;
for (const prefix of forbiddenImportPrefixes) {
if (trimmed.includes(`"${prefix}`) || trimmed.includes(`'${prefix}`)) {
violations.push({
file,
line: index + 1,
content: line,
});
}
}
});
}
if (violations.length > 0) {
const message =
'Found forbidden imports in apps/website:\n' +
violations
.map(
(v) =>
`- ${v.file}:${v.line} :: ${v.content.trim()}`,
)
.join('\n');
// Fail with detailed message so we can iterate while RED
expect(message).toBe(''); // Intentionally impossible when violations exist
} else {
expect(violations).toEqual([]);
}
});
});