This commit is contained in:
2025-12-01 00:48:34 +01:00
parent 645f537895
commit e7ada8aa23
24 changed files with 866 additions and 438 deletions

View File

@@ -0,0 +1,99 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { DIContainer } from '../../../..//apps/companion/main/di-container';
import type { HostedSessionConfig } from '../../../..//packages/domain/entities/HostedSessionConfig';
import { StepId } from '../../../..//packages/domain/value-objects/StepId';
import { PlaywrightAutomationAdapter } from '../../../..//packages/infrastructure/adapters/automation';
describe('companion start automation - browser mode refresh wiring', () => {
const originalEnv = { ...process.env };
let originalTestLauncher: unknown;
beforeEach(() => {
process.env = { ...originalEnv, NODE_ENV: 'development' };
originalTestLauncher = (PlaywrightAutomationAdapter as any).testLauncher;
const mockLauncher = {
launch: async (_opts: any) => ({
newContext: async () => ({
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
launchPersistentContext: async (_userDataDir: string, _opts: any) => ({
pages: () => [{ setDefaultTimeout: () => {}, close: async () => {} }],
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
};
(PlaywrightAutomationAdapter as any).testLauncher = mockLauncher;
DIContainer.resetInstance();
});
afterEach(async () => {
const container = DIContainer.getInstance();
await container.shutdown();
DIContainer.resetInstance();
(PlaywrightAutomationAdapter as any).testLauncher = originalTestLauncher;
process.env = originalEnv;
});
it('uses refreshed browser automation for connection and step execution after mode change', async () => {
const container = DIContainer.getInstance();
const loader = container.getBrowserModeConfigLoader();
expect(loader.getDevelopmentMode()).toBe('headed');
const preStart = container.getStartAutomationUseCase();
const preEngine: any = container.getAutomationEngine();
const preAutomation = container.getBrowserAutomation() as any;
expect(preAutomation).toBe(preEngine.browserAutomation);
loader.setDevelopmentMode('headless');
container.refreshBrowserAutomation();
const postStart = container.getStartAutomationUseCase();
const postEngine: any = container.getAutomationEngine();
const postAutomation = container.getBrowserAutomation() as any;
expect(postAutomation).toBe(postEngine.browserAutomation);
expect(postAutomation).not.toBe(preAutomation);
expect(postStart).not.toBe(preStart);
const connectionResult = await container.initializeBrowserConnection();
expect(connectionResult.success).toBe(true);
const config: HostedSessionConfig = {
sessionName: 'Companion browser-mode refresh wiring',
trackId: 'test-track',
carIds: ['car-1'],
};
const dto = await postStart.execute(config);
await postEngine.executeStep(StepId.create(1), config);
const sessionRepository: any = container.getSessionRepository();
const session = await sessionRepository.findById(dto.sessionId);
expect(session).toBeDefined();
const state = session!.state.value as string;
const errorMessage = session!.errorMessage as string | undefined;
if (errorMessage) {
expect(errorMessage).not.toContain('Browser not connected');
}
const automationFromConnection = container.getBrowserAutomation() as any;
const automationFromEngine = (container.getAutomationEngine() as any).browserAutomation;
expect(automationFromConnection).toBe(automationFromEngine);
expect(automationFromConnection).toBe(postAutomation);
});
});

View File

@@ -0,0 +1,98 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { DIContainer } from '../../../..//apps/companion/main/di-container';
import type { HostedSessionConfig } from '../../../..//packages/domain/entities/HostedSessionConfig';
import { StepId } from '../../../..//packages/domain/value-objects/StepId';
import { PlaywrightAutomationAdapter } from '../../../..//packages/infrastructure/adapters/automation';
describe('companion start automation - browser not connected at step 1', () => {
const originalEnv = { ...process.env };
let originalTestLauncher: unknown;
beforeEach(() => {
process.env = { ...originalEnv, NODE_ENV: 'production' };
originalTestLauncher = (PlaywrightAutomationAdapter as any).testLauncher;
const mockLauncher = {
launch: async (_opts: any) => ({
newContext: async () => ({
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
launchPersistentContext: async (_userDataDir: string, _opts: any) => ({
pages: () => [{ setDefaultTimeout: () => {}, close: async () => {} }],
newPage: async () => ({ setDefaultTimeout: () => {}, close: async () => {} }),
close: async () => {},
}),
};
(PlaywrightAutomationAdapter as any).testLauncher = mockLauncher;
DIContainer.resetInstance();
});
afterEach(async () => {
const container = DIContainer.getInstance();
await container.shutdown();
DIContainer.resetInstance();
(PlaywrightAutomationAdapter as any).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 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 config: HostedSessionConfig = {
sessionName: 'Companion integration browser-not-connected',
trackId: 'test-track',
carIds: ['car-1'],
};
const dto = await startAutomationUseCase.execute(config);
await automationEngine.executeStep(StepId.create(1), config);
const session = await waitForFailedSession(sessionRepository, dto.sessionId);
expect(session).toBeDefined();
expect(session.state.value).toBe('FAILED');
const error = session.errorMessage as string | undefined;
expect(error).toBeDefined();
expect(error).toContain('Step 1 (LOGIN)');
expect(error).toContain('Browser not connected');
});
});
async function waitForFailedSession(
sessionRepository: { findById: (id: string) => Promise<any> },
sessionId: string,
timeoutMs = 5000,
): Promise<any> {
const start = Date.now();
let last: any = null;
// eslint-disable-next-line no-constant-condition
while (true) {
last = await sessionRepository.findById(sessionId);
if (last && last.state && last.state.value === 'FAILED') {
return last;
}
if (Date.now() - start >= timeoutMs) {
return last;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
}

View File

@@ -0,0 +1,99 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { DIContainer } from '../../../..//apps/companion/main/di-container';
import type { HostedSessionConfig } from '../../../..//packages/domain/entities/HostedSessionConfig';
import { PlaywrightAutomationAdapter } from '../../../..//packages/infrastructure/adapters/automation';
describe('companion start automation - browser connection failure before steps', () => {
const originalEnv = { ...process.env };
let originalTestLauncher: unknown;
beforeEach(() => {
process.env = { ...originalEnv, NODE_ENV: 'production' };
originalTestLauncher = (PlaywrightAutomationAdapter as any).testLauncher;
const failingLauncher = {
launch: async () => {
throw new Error('Simulated browser launch failure');
},
launchPersistentContext: async () => {
throw new Error('Simulated persistent context failure');
},
};
(PlaywrightAutomationAdapter as any).testLauncher = failingLauncher;
DIContainer.resetInstance();
});
afterEach(async () => {
const container = DIContainer.getInstance();
await container.shutdown();
DIContainer.resetInstance();
(PlaywrightAutomationAdapter as any).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 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 config: HostedSessionConfig = {
sessionName: 'Companion integration connection failure',
trackId: 'test-track',
carIds: ['car-1'],
};
let sessionId: string | null = null;
try {
const dto = await startAutomationUseCase.execute(config);
sessionId = dto.sessionId;
} catch (error) {
expect((error as Error).message).toBeDefined();
}
expect(executeStepSpy).not.toHaveBeenCalled();
if (sessionId) {
const session = await sessionRepository.findById(sessionId);
if (session) {
const message = session.errorMessage as string | undefined;
if (message) {
expect(message).not.toContain('Step 1 (LOGIN) failed: Browser not connected');
expect(message.toLowerCase()).toContain('browser');
}
}
}
});
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 () {
return { success: true };
};
try {
const connectionResult = await container.initializeBrowserConnection();
expect(connectionResult.success).toBe(false);
expect(connectionResult.error).toBeDefined();
expect(String(connectionResult.error).toLowerCase()).toContain('browser');
} finally {
(PlaywrightAutomationAdapter as any).prototype.connect = originalConnect;
}
});
});

View File

@@ -0,0 +1,54 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { DIContainer } from '../../../..//apps/companion/main/di-container';
import type { HostedSessionConfig } from '../../../..//packages/domain/entities/HostedSessionConfig';
import { StepId } from '../../../..//packages/domain/value-objects/StepId';
describe('companion start automation - happy path', () => {
const originalEnv = { ...process.env };
beforeEach(() => {
process.env = { ...originalEnv, NODE_ENV: 'test' };
DIContainer.resetInstance();
});
afterEach(async () => {
const container = DIContainer.getInstance();
await container.shutdown();
DIContainer.resetInstance();
process.env = originalEnv;
});
it('creates a non-failed session and does not report browser-not-connected', async () => {
const container = DIContainer.getInstance();
const startAutomationUseCase = container.getStartAutomationUseCase();
const sessionRepository = container.getSessionRepository();
const automationEngine = container.getAutomationEngine();
const connectionResult = await container.initializeBrowserConnection();
expect(connectionResult.success).toBe(true);
const config: HostedSessionConfig = {
sessionName: 'Companion integration happy path',
trackId: 'test-track',
carIds: ['car-1'],
};
const dto = await startAutomationUseCase.execute(config);
const sessionBefore = await sessionRepository.findById(dto.sessionId);
expect(sessionBefore).toBeDefined();
await automationEngine.executeStep(StepId.create(1), config);
const session = await sessionRepository.findById(dto.sessionId);
expect(session).toBeDefined();
const state = session!.state.value as string;
expect(state).not.toBe('FAILED');
const errorMessage = session!.errorMessage as string | undefined;
if (errorMessage) {
expect(errorMessage).not.toContain('Browser not connected');
}
});
});