This commit is contained in:
2025-12-11 21:06:25 +01:00
parent c49ea2598d
commit ec3ddc3a5c
227 changed files with 3496 additions and 2083 deletions

View File

@@ -39,8 +39,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
}
throw reason;
};
const anyProcess = process as any;
anyProcess.on('unhandledRejection', unhandledRejectionHandler);
process.on('unhandledRejection', unhandledRejectionHandler);
});
afterEach(async () => {
@@ -54,8 +53,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
afterAll(() => {
if (unhandledRejectionHandler) {
const anyProcess = process as any;
anyProcess.removeListener('unhandledRejection', unhandledRejectionHandler);
process.removeListener('unhandledRejection', unhandledRejectionHandler);
unhandledRejectionHandler = null;
}
});
@@ -177,12 +175,19 @@ describe('Browser Mode Integration - GREEN Phase', () => {
it('should log browser mode configuration with NODE_ENV source in production', async () => {
process.env.NODE_ENV = 'production';
const logSpy: Array<{ level: string; message: string; context?: any }> = [];
const mockLogger = {
debug: (msg: string, ctx?: any) => logSpy.push({ level: 'debug', message: msg, context: ctx }),
info: (msg: string, ctx?: any) => logSpy.push({ level: 'info', message: msg, context: ctx }),
warn: (msg: string, ctx?: any) => logSpy.push({ level: 'warn', message: msg, context: ctx }),
error: (msg: string, ctx?: any) => logSpy.push({ level: 'error', message: msg, context: ctx }),
const logSpy: Array<{ level: string; message: string; context?: Record<string, unknown> }> = [];
type LoggerLike = {
debug: (msg: string, ctx?: Record<string, unknown>) => void;
info: (msg: string, ctx?: Record<string, unknown>) => void;
warn: (msg: string, ctx?: Record<string, unknown>) => void;
error: (msg: string, ctx?: Record<string, unknown>) => void;
child: () => LoggerLike;
};
const mockLogger: LoggerLike = {
debug: (msg: string, ctx?: Record<string, unknown>) => logSpy.push({ level: 'debug', message: msg, context: ctx }),
info: (msg: string, ctx?: Record<string, unknown>) => logSpy.push({ level: 'info', message: msg, context: ctx }),
warn: (msg: string, ctx?: Record<string, unknown>) => logSpy.push({ level: 'warn', message: msg, context: ctx }),
error: (msg: string, ctx?: Record<string, unknown>) => logSpy.push({ level: 'error', message: msg, context: ctx }),
child: () => mockLogger,
};
@@ -192,7 +197,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
adapter = new PlaywrightAutomationAdapter(
{ mode: 'mock' },
mockLogger as any
mockLogger
);
await adapter.connect();
@@ -250,19 +255,23 @@ describe('Browser Mode Integration - GREEN Phase', () => {
loader.setDevelopmentMode('headed');
// Capture launch options
const launches: Array<{ type: string; opts?: any; userDataDir?: string }> = [];
type LaunchOptions = { headless?: boolean; [key: string]: unknown };
const launches: Array<{ type: string; opts?: LaunchOptions; userDataDir?: string }> = [];
const mockLauncher = {
launch: async (opts: any) => {
launch: async (opts: LaunchOptions) => {
launches.push({ type: 'launch', opts });
return {
newContext: async () => ({ newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }), close: async () => {} }),
newContext: async () => ({
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
newContextSync: () => {},
};
},
launchPersistentContext: async (userDataDir: string, opts: any) => {
launchPersistentContext: async (userDataDir: string, opts: LaunchOptions) => {
launches.push({ type: 'launchPersistent', userDataDir, opts });
return {
pages: () => [{ setDefaultTimeout: () => {}, close: async () => {} }],
@@ -273,9 +282,12 @@ describe('Browser Mode Integration - GREEN Phase', () => {
};
// Inject test launcher
(PlaywrightAutomationAdapter as any).testLauncher = mockLauncher;
const AdapterWithTestLauncher = PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: typeof mockLauncher;
};
AdapterWithTestLauncher.testLauncher = mockLauncher;
adapter = new PlaywrightAutomationAdapter({ mode: 'mock' }, undefined as any, loader as any);
adapter = new PlaywrightAutomationAdapter({ mode: 'mock' }, undefined, loader);
// First connect => loader says headed => headless should be false
const r1 = await adapter.connect();
@@ -296,7 +308,7 @@ describe('Browser Mode Integration - GREEN Phase', () => {
expect(secondLaunch!.opts.headless).toBe(true);
// Cleanup test hook
(PlaywrightAutomationAdapter as any).testLauncher = undefined;
AdapterWithTestLauncher.testLauncher = undefined;
await adapter.disconnect();
});
});

View File

@@ -3,12 +3,14 @@ import { PlaywrightAutomationAdapter } from 'packages/automation/infrastructure/
describe('CarsFlow integration', () => {
test('adapter emits panel-attached then action-started then action-complete for performAddCar', async () => {
const adapter = new PlaywrightAutomationAdapter({} as any)
const received: any[] = []
adapter.onLifecycle?.((e: any) => { received.push(e) })
const adapter = new PlaywrightAutomationAdapter({})
const received: Array<{ type: string }> = []
adapter.onLifecycle?.((e) => {
received.push({ type: (e as { type: string }).type })
})
// Use mock page fixture: minimal object with required methods
const mockPage: any = {
const mockPage = {
waitForSelector: async () => {},
evaluate: async () => {},
waitForTimeout: async () => {},
@@ -20,11 +22,13 @@ describe('CarsFlow integration', () => {
await adapter.attachPanel(mockPage, 'add-car')
// simulate complete event via internal lifecycle emitter
await (adapter as any).emitLifecycle({
type: 'action-complete',
actionId: 'add-car',
timestamp: Date.now(),
} as any)
await (adapter as unknown as { emitLifecycle: (ev: { type: string; actionId: string; timestamp: number }) => Promise<void> }).emitLifecycle(
{
type: 'action-complete',
actionId: 'add-car',
timestamp: Date.now(),
},
)
const types = received.map(r => r.type)
expect(types.indexOf('panel-attached')).toBeGreaterThanOrEqual(0)

View File

@@ -40,7 +40,13 @@ describe('Overlay lifecycle (integration)', () => {
it('emits modal-opened and confirms after action-started in sane order', async () => {
const lifecycleEmitter = new TestLifecycleEmitter();
const publisher = new RecordingPublisher();
const logger = console as any;
type LoggerLike = {
debug: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
};
const logger = console as unknown as LoggerLike;
const service = new OverlaySyncService({
lifecycleEmitter,
@@ -85,7 +91,13 @@ describe('Overlay lifecycle (integration)', () => {
it('emits panel-missing when cancelAction is called', async () => {
const lifecycleEmitter = new TestLifecycleEmitter();
const publisher = new RecordingPublisher();
const logger = console as any;
type LoggerLike = {
debug: (...args: unknown[]) => void;
info: (...args: unknown[]) => void;
warn: (...args: unknown[]) => void;
error: (...args: unknown[]) => void;
};
const logger = console as unknown as LoggerLike;
const service = new OverlaySyncService({
lifecycleEmitter,

View File

@@ -10,11 +10,13 @@ describe('companion start automation - browser mode refresh wiring', () => {
beforeEach(() => {
process.env = { ...originalEnv, NODE_ENV: 'development' };
originalTestLauncher = (PlaywrightAutomationAdapter as any).testLauncher;
originalTestLauncher = (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: unknown;
}).testLauncher;
const mockLauncher = {
launch: async (_opts: any) => ({
launch: async (_opts: unknown) => ({
newContext: async () => ({
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
@@ -22,14 +24,16 @@ describe('companion start automation - browser mode refresh wiring', () => {
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
launchPersistentContext: async (_userDataDir: string, _opts: any) => ({
launchPersistentContext: async (_userDataDir: string, _opts: unknown) => ({
pages: () => [{ setDefaultTimeout: () => {}, close: async () => {} }],
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
};
(PlaywrightAutomationAdapter as any).testLauncher = mockLauncher;
(PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: typeof mockLauncher;
}).testLauncher = mockLauncher;
DIContainer.resetInstance();
});
@@ -38,7 +42,9 @@ describe('companion start automation - browser mode refresh wiring', () => {
const container = DIContainer.getInstance();
await container.shutdown();
DIContainer.resetInstance();
(PlaywrightAutomationAdapter as any).testLauncher = originalTestLauncher;
(PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: unknown;
}).testLauncher = originalTestLauncher;
process.env = originalEnv;
});
@@ -49,8 +55,8 @@ describe('companion start automation - browser mode refresh wiring', () => {
expect(loader.getDevelopmentMode()).toBe('headed');
const preStart = container.getStartAutomationUseCase();
const preEngine: any = container.getAutomationEngine();
const preAutomation = container.getBrowserAutomation() as any;
const preEngine = container.getAutomationEngine();
const preAutomation = container.getBrowserAutomation();
expect(preAutomation).toBe(preEngine.browserAutomation);
@@ -58,8 +64,8 @@ describe('companion start automation - browser mode refresh wiring', () => {
container.refreshBrowserAutomation();
const postStart = container.getStartAutomationUseCase();
const postEngine: any = container.getAutomationEngine();
const postAutomation = container.getBrowserAutomation() as any;
const postEngine = container.getAutomationEngine();
const postAutomation = container.getBrowserAutomation();
expect(postAutomation).toBe(postEngine.browserAutomation);
expect(postAutomation).not.toBe(preAutomation);
@@ -78,7 +84,7 @@ describe('companion start automation - browser mode refresh wiring', () => {
await postEngine.executeStep(StepId.create(1), config);
const sessionRepository: any = container.getSessionRepository();
const sessionRepository = container.getSessionRepository();
const session = await sessionRepository.findById(dto.sessionId);
expect(session).toBeDefined();
@@ -90,8 +96,9 @@ describe('companion start automation - browser mode refresh wiring', () => {
expect(errorMessage).not.toContain('Browser not connected');
}
const automationFromConnection = container.getBrowserAutomation() as any;
const automationFromEngine = (container.getAutomationEngine() as any).browserAutomation;
const automationFromConnection = container.getBrowserAutomation();
const automationFromEngine = (container.getAutomationEngine() as { browserAutomation: unknown })
.browserAutomation;
expect(automationFromConnection).toBe(automationFromEngine);
expect(automationFromConnection).toBe(postAutomation);

View File

@@ -10,11 +10,13 @@ describe('companion start automation - browser not connected at step 1', () => {
beforeEach(() => {
process.env = { ...originalEnv, NODE_ENV: 'production' };
originalTestLauncher = (PlaywrightAutomationAdapter as any).testLauncher;
originalTestLauncher = (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: unknown;
}).testLauncher;
const mockLauncher = {
launch: async (_opts: any) => ({
launch: async (_opts: unknown) => ({
newContext: async () => ({
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
@@ -22,14 +24,16 @@ describe('companion start automation - browser not connected at step 1', () => {
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
launchPersistentContext: async (_userDataDir: string, _opts: any) => ({
launchPersistentContext: async (_userDataDir: string, _opts: unknown) => ({
pages: () => [{ setDefaultTimeout: () => {}, close: async () => {} }],
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
};
(PlaywrightAutomationAdapter as any).testLauncher = mockLauncher;
(PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: typeof mockLauncher;
}).testLauncher = mockLauncher;
DIContainer.resetInstance();
});
@@ -38,22 +42,24 @@ describe('companion start automation - browser not connected at step 1', () => {
const container = DIContainer.getInstance();
await container.shutdown();
DIContainer.resetInstance();
(PlaywrightAutomationAdapter as any).testLauncher = originalTestLauncher;
(PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: unknown;
}).testLauncher = originalTestLauncher;
process.env = originalEnv;
});
it('marks the session as FAILED with Step 1 (LOGIN) browser-not-connected error', async () => {
const container = DIContainer.getInstance();
const startAutomationUseCase = container.getStartAutomationUseCase();
const sessionRepository: any = container.getSessionRepository();
const sessionRepository = container.getSessionRepository();
const automationEngine = container.getAutomationEngine();
const connectionResult = await container.initializeBrowserConnection();
expect(connectionResult.success).toBe(true);
const browserAutomation = container.getBrowserAutomation() as any;
if (browserAutomation.disconnect) {
await browserAutomation.disconnect();
const browserAutomation = container.getBrowserAutomation();
if (typeof (browserAutomation as { disconnect?: () => Promise<void> }).disconnect === 'function') {
await (browserAutomation as { disconnect: () => Promise<void> }).disconnect();
}
const config: HostedSessionConfig = {
@@ -77,10 +83,10 @@ describe('companion start automation - browser not connected at step 1', () => {
});
async function waitForFailedSession(
sessionRepository: { findById: (id: string) => Promise<any> },
sessionRepository: { findById: (id: string) => Promise<{ state?: { value?: string }; errorMessage?: unknown } | null> },
sessionId: string,
timeoutMs = 5000,
): Promise<any> {
): Promise<{ state?: { value?: string }; errorMessage?: unknown } | null> {
const start = Date.now();
let last: any = null;

View File

@@ -9,8 +9,10 @@ describe('companion start automation - browser connection failure before steps',
beforeEach(() => {
process.env = { ...originalEnv, NODE_ENV: 'production' };
originalTestLauncher = (PlaywrightAutomationAdapter as any).testLauncher;
originalTestLauncher = (PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: unknown;
}).testLauncher;
const failingLauncher = {
launch: async () => {
@@ -21,7 +23,9 @@ describe('companion start automation - browser connection failure before steps',
},
};
(PlaywrightAutomationAdapter as any).testLauncher = failingLauncher;
(PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: typeof failingLauncher;
}).testLauncher = failingLauncher;
DIContainer.resetInstance();
});
@@ -30,21 +34,26 @@ describe('companion start automation - browser connection failure before steps',
const container = DIContainer.getInstance();
await container.shutdown();
DIContainer.resetInstance();
(PlaywrightAutomationAdapter as any).testLauncher = originalTestLauncher;
(PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
testLauncher?: unknown;
}).testLauncher = originalTestLauncher;
process.env = originalEnv;
});
it('fails browser connection and aborts before executing step 1', async () => {
const container = DIContainer.getInstance();
const startAutomationUseCase = container.getStartAutomationUseCase();
const sessionRepository: any = container.getSessionRepository();
const sessionRepository = container.getSessionRepository();
const automationEngine = container.getAutomationEngine();
const connectionResult = await container.initializeBrowserConnection();
expect(connectionResult.success).toBe(false);
expect(connectionResult.error).toBeDefined();
const executeStepSpy = vi.spyOn(automationEngine, 'executeStep' as any);
const executeStepSpy = vi.spyOn(
automationEngine,
'executeStep' as keyof typeof automationEngine,
);
const config: HostedSessionConfig = {
sessionName: 'Companion integration connection failure',
@@ -78,12 +87,17 @@ describe('companion start automation - browser connection failure before steps',
it('treats successful adapter connect without a page as connection failure', async () => {
const container = DIContainer.getInstance();
const browserAutomation = container.getBrowserAutomation();
expect(browserAutomation).toBeInstanceOf(PlaywrightAutomationAdapter);
const originalConnect = (PlaywrightAutomationAdapter as any).prototype.connect;
(PlaywrightAutomationAdapter as any).prototype.connect = async function () {
const AdapterWithPrototype = PlaywrightAutomationAdapter as typeof PlaywrightAutomationAdapter & {
prototype: {
connect: () => Promise<{ success: boolean; error?: string }>;
};
};
const originalConnect = AdapterWithPrototype.prototype.connect;
AdapterWithPrototype.prototype.connect = async function () {
return { success: true };
};
@@ -93,7 +107,7 @@ describe('companion start automation - browser connection failure before steps',
expect(connectionResult.error).toBeDefined();
expect(String(connectionResult.error).toLowerCase()).toContain('browser');
} finally {
(PlaywrightAutomationAdapter as any).prototype.connect = originalConnect;
AdapterWithPrototype.prototype.connect = originalConnect;
}
});
});

View File

@@ -49,9 +49,9 @@ describe('renderer overlay lifecycle integration', () => {
const emitter = new MockAutomationLifecycleEmitter();
const publisher = new RecordingPublisher();
const svc = new OverlaySyncService({
lifecycleEmitter: emitter as any,
publisher: publisher as any,
logger: console as any,
lifecycleEmitter: emitter,
publisher,
logger: console,
defaultTimeoutMs: 2_000,
});
@@ -111,9 +111,9 @@ describe('renderer overlay lifecycle integration', () => {
const emitter = new MockAutomationLifecycleEmitter();
const publisher = new RecordingPublisher();
const svc = new OverlaySyncService({
lifecycleEmitter: emitter as any,
publisher: publisher as any,
logger: console as any,
lifecycleEmitter: emitter,
publisher,
logger: console,
defaultTimeoutMs: 200,
});

View File

@@ -5,8 +5,12 @@ import { OverlaySyncService } from 'packages/automation/application/services/Ove
describe('renderer overlay integration', () => {
test('renderer shows confirmed only after main acks confirmed', async () => {
const emitter = new MockAutomationLifecycleEmitter()
const publisher = { publish: async () => {} }
const svc = new OverlaySyncService({ lifecycleEmitter: emitter as any, publisher: publisher as any, logger: console as any })
const publisher: { publish: (event: unknown) => Promise<void> } = { publish: async () => {} }
const svc = new OverlaySyncService({
lifecycleEmitter: emitter,
publisher,
logger: console,
})
// simulate renderer request
const promise = svc.startAction({ id: 'add-car', label: 'Adding...' })