move automation out of core

This commit is contained in:
2025-12-16 14:31:43 +01:00
parent 29dc11deb9
commit 29410708c8
145 changed files with 378 additions and 1532 deletions

View File

@@ -1,6 +1,6 @@
import { AuthenticationState } from '../../domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState';
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
/**
* Port for authentication services implementing zero-knowledge login.

View File

@@ -2,7 +2,7 @@ export type AutomationEvent = {
actionId?: string
type: 'panel-attached'|'modal-opened'|'action-started'|'action-complete'|'action-failed'|'panel-missing'
timestamp: number
payload?: any
payload?: unknown
}
export interface AutomationEventPublisherPort {

View File

@@ -1,4 +1,4 @@
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import { CheckoutConfirmation } from '../../domain/value-objects/CheckoutConfirmation';
import type { CheckoutConfirmationRequestDTO } from '../dto/CheckoutConfirmationRequestDTO';

View File

@@ -1,4 +1,4 @@
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import type { CheckoutInfoDTO } from '../dto/CheckoutInfoDTO';
export interface CheckoutServicePort {

View File

@@ -2,7 +2,7 @@ export type AutomationEvent = {
actionId?: string
type: 'panel-attached'|'modal-opened'|'action-started'|'action-complete'|'action-failed'|'panel-missing'
timestamp: number
payload?: any
payload?: unknown
}
export interface IAutomationEventPublisher {

View File

@@ -1,3 +1,4 @@
import { SessionStateValue } from '@/automation/domain/value-objects/SessionState';
import { AutomationSession } from '../../domain/entities/AutomationSession';

View File

@@ -1,4 +1,4 @@
import type { Result } from '../../../shared/result/Result';
import type { Result } from '@gridpilot/shared/result/Result';
export interface SessionValidatorPort {
validateSession(): Promise<Result<boolean>>;

View File

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

View File

@@ -3,6 +3,8 @@ import { AutomationEventPublisherPort, AutomationEvent } from '../ports/Automati
import { AutomationLifecycleEmitterPort, LifecycleCallback } from '../ports/AutomationLifecycleEmitterPort';
import { LoggerPort } from '../ports/LoggerPort';
import type { IAsyncApplicationService } from '@core/shared/application';
import { OverlayAction, OverlaySyncPort } from '../ports/OverlaySyncPort';
import { ActionAck } from '../ports/IOverlaySyncPort';
type ConstructorArgs = {
lifecycleEmitter: AutomationLifecycleEmitterPort

View File

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

View File

@@ -1,6 +1,6 @@
import { AuthenticationState } from '../../domain/value-objects/AuthenticationState';
import type { Logger } from '@core/shared/application';
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import { SessionLifetime } from '../../domain/value-objects/SessionLifetime';
import type { SessionValidatorPort } from '../ports/SessionValidatorPort';
@@ -38,7 +38,7 @@ export class CheckAuthenticationUseCase {
this.logger.debug('Performing file-based authentication check.');
const fileResult = await this.authService.checkSession();
if (fileResult.isErr()) {
this.logger.error('File-based authentication check failed.', { error: fileResult.unwrapErr() });
this.logger.error('File-based authentication check failed.', fileResult.unwrapErr());
return fileResult;
}
this.logger.info('File-based authentication check succeeded.');
@@ -66,7 +66,7 @@ export class CheckAuthenticationUseCase {
}
this.logger.debug('Session is not expired.');
} catch (error) {
this.logger.error('Invalid expiry date encountered, treating session as expired.', { expiry, error });
this.logger.error('Invalid expiry date encountered, treating session as expired.', error as Error, { expiry });
// Invalid expiry date, treat as expired for safety
return Result.ok(AuthenticationState.EXPIRED);
}
@@ -112,7 +112,7 @@ export class CheckAuthenticationUseCase {
this.logger.info(`CheckAuthenticationUseCase completed successfully with state: ${fileState}`);
return Result.ok(fileState);
} catch (error) {
this.logger.error('An unexpected error occurred during authentication check.', { error });
this.logger.error('An unexpected error occurred during authentication check.', error as Error);
throw error;
}
}

View File

@@ -0,0 +1,106 @@
import { vi, Mock } from 'vitest';
import { ClearSessionUseCase } from './ClearSessionUseCase';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import type { Logger } from '@core/shared/application';
import { Result } from '@gridpilot/shared/result/Result';
describe('ClearSessionUseCase', () => {
let useCase: ClearSessionUseCase;
let authService: AuthenticationServicePort;
let logger: Logger;
beforeEach(() => {
const mockAuthService = {
clearSession: vi.fn(),
checkSession: vi.fn(),
initiateLogin: vi.fn(),
getState: vi.fn(),
validateServerSide: vi.fn(),
refreshSession: vi.fn(),
getSessionExpiry: vi.fn(),
verifyPageAuthentication: vi.fn(),
};
const mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
authService = mockAuthService as unknown as AuthenticationServicePort;
logger = mockLogger as Logger;
useCase = new ClearSessionUseCase(authService, logger);
});
describe('execute', () => {
it('should clear session successfully and return ok result', async () => {
const successResult = Result.ok<void>(undefined);
(authService.clearSession as Mock).mockResolvedValue(successResult);
const result = await useCase.execute();
expect(authService.clearSession).toHaveBeenCalledTimes(1);
expect(logger.debug).toHaveBeenCalledWith('Attempting to clear user session.', {
useCase: 'ClearSessionUseCase'
});
expect(logger.info).toHaveBeenCalledWith('User session cleared successfully.', {
useCase: 'ClearSessionUseCase'
});
expect(result.isOk()).toBe(true);
});
it('should handle clearSession failure and return err result', async () => {
const error = new Error('Clear session failed');
const failureResult = Result.err<void>(error);
(authService.clearSession as Mock).mockResolvedValue(failureResult);
const result = await useCase.execute();
expect(authService.clearSession).toHaveBeenCalledTimes(1);
expect(logger.debug).toHaveBeenCalledWith('Attempting to clear user session.', {
useCase: 'ClearSessionUseCase'
});
expect(logger.warn).toHaveBeenCalledWith('Failed to clear user session.', {
useCase: 'ClearSessionUseCase',
error: error,
});
expect(result.isErr()).toBe(true);
expect(result.error).toBe(error);
});
it('should handle unexpected errors and return err result with Error', async () => {
const thrownError = new Error('Unexpected error');
(authService.clearSession as Mock).mockRejectedValue(thrownError);
const result = await useCase.execute();
expect(authService.clearSession).toHaveBeenCalledTimes(1);
expect(logger.debug).toHaveBeenCalledWith('Attempting to clear user session.', {
useCase: 'ClearSessionUseCase'
});
expect(logger.error).toHaveBeenCalledWith('Error clearing user session.', thrownError, {
useCase: 'ClearSessionUseCase'
});
expect(result.isErr()).toBe(true);
expect(result.error).toBeInstanceOf(Error);
expect(result.error?.message).toBe('Unexpected error');
});
it('should handle non-Error thrown values and convert to Error', async () => {
const thrownValue = 'String error';
(authService.clearSession as Mock).mockRejectedValue(thrownValue);
const result = await useCase.execute();
expect(authService.clearSession).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith('Error clearing user session.', expect.any(Error), {
useCase: 'ClearSessionUseCase'
});
expect(result.isErr()).toBe(true);
expect(result.error).toBeInstanceOf(Error);
expect(result.error?.message).toBe('String error');
});
});
});

View File

@@ -1,4 +1,4 @@
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import type { Logger } from '@core/shared/application';
@@ -26,7 +26,7 @@ export class ClearSessionUseCase {
try {
const result = await this.authService.clearSession();
if (result.isSuccess) {
if (result.isOk()) {
this.logger.info('User session cleared successfully.', {
useCase: 'ClearSessionUseCase'
});
@@ -38,10 +38,11 @@ export class ClearSessionUseCase {
}
return result;
} catch (error) {
this.logger.error('Error clearing user session.', error, {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error('Error clearing user session.', err, {
useCase: 'ClearSessionUseCase'
});
return Result.fail(error.message);
return Result.err(err);
}
}
}

View File

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

View File

@@ -1,4 +1,4 @@
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import { RaceCreationResult } from '../../domain/value-objects/RaceCreationResult';
import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
import type { Logger } from '@core/shared/application';

View File

@@ -1,12 +1,12 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { Result } from '@core/shared/result/Result';
import { ConfirmCheckoutUseCase } from '@core/automation/application/use-cases/ConfirmCheckoutUseCase';
import type { CheckoutServicePort } from '@core/automation/application/ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '@core/automation/application/ports/CheckoutConfirmationPort';
import type { CheckoutInfoDTO } from '@core/automation/application/dto/CheckoutInfoDTO';
import { CheckoutPrice } from '@core/automation/domain/value-objects/CheckoutPrice';
import { CheckoutConfirmation } from '@core/automation/domain/value-objects/CheckoutConfirmation';
import { ConfirmCheckoutUseCase } from 'apps/companion/main/automation/application/use-cases/ConfirmCheckoutUseCase';
import type { CheckoutServicePort } from 'apps/companion/main/automation/application/ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from 'apps/companion/main/automation/application/ports/CheckoutConfirmationPort';
import { CheckoutPrice } from 'apps/companion/main/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from 'apps/companion/main/automation/domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from 'apps/companion/main/automation/domain/value-objects/CheckoutConfirmation';
import type { Logger } from '@core/shared/application';
/**
* ConfirmCheckoutUseCase - GREEN PHASE
@@ -23,6 +23,7 @@ describe('ConfirmCheckoutUseCase', () => {
let mockConfirmationPort: {
requestCheckoutConfirmation: Mock;
};
let mockLogger: Logger;
let mockPrice: CheckoutPrice;
beforeEach(() => {
@@ -30,11 +31,21 @@ describe('ConfirmCheckoutUseCase', () => {
extractCheckoutInfo: vi.fn(),
proceedWithCheckout: vi.fn(),
};
mockConfirmationPort = {
requestCheckoutConfirmation: vi.fn(),
};
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
fatal: vi.fn(),
child: vi.fn(() => mockLogger),
flush: vi.fn(),
} as Logger;
mockPrice = {
getAmount: vi.fn(() => 0.50),
toDisplayString: vi.fn(() => '$0.50'),
@@ -46,7 +57,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should extract price, get user confirmation, and proceed with checkout', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -75,7 +87,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should include price in confirmation message', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -102,7 +115,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should abort checkout when user cancels confirmation', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -126,7 +140,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should not proceed with checkout after cancellation', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -150,7 +165,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should return error when checkout state is INSUFFICIENT_FUNDS', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -172,7 +188,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should not ask for confirmation when funds are insufficient', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -193,7 +210,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should return error when price cannot be extracted', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -215,7 +233,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should return error when extraction service fails', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -239,7 +258,8 @@ describe('ConfirmCheckoutUseCase', () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -272,7 +292,8 @@ describe('ConfirmCheckoutUseCase', () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -297,7 +318,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('should return error when proceedWithCheckout fails', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -325,7 +347,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('Given checkout price $0.50 and READY state, When user confirms, Then checkout proceeds', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -348,7 +371,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('Given checkout price $0.50, When user cancels, Then checkout is aborted', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -371,7 +395,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('Given INSUFFICIENT_FUNDS state, When executing, Then error is returned', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(
@@ -390,7 +415,8 @@ describe('ConfirmCheckoutUseCase', () => {
it('Given price extraction failure, When executing, Then error is returned', async () => {
const useCase = new ConfirmCheckoutUseCase(
mockCheckoutService as unknown as CheckoutServicePort,
mockConfirmationPort as unknown as CheckoutConfirmationPort
mockConfirmationPort as unknown as CheckoutConfirmationPort,
mockLogger
);
mockCheckoutService.extractCheckoutInfo.mockResolvedValue(

View File

@@ -1,7 +1,8 @@
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import type { Logger } from '@core/shared/application';
import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '../ports/CheckoutConfirmationPort';
import { CheckoutStateEnum } from '../../domain/value-objects/CheckoutState';
interface SessionMetadata {
@@ -25,7 +26,7 @@ export class ConfirmCheckoutUseCase {
const infoResult = await this.checkoutService.extractCheckoutInfo();
if (infoResult.isErr()) {
this.logger.error('Failed to extract checkout info', { error: infoResult.unwrapErr() });
this.logger.error('Failed to extract checkout info', infoResult.unwrapErr());
return Result.err(infoResult.unwrapErr());
}
@@ -58,7 +59,7 @@ export class ConfirmCheckoutUseCase {
});
if (confirmationResult.isErr()) {
this.logger.error('Checkout confirmation failed', { error: confirmationResult.unwrapErr() });
this.logger.error('Checkout confirmation failed', confirmationResult.unwrapErr());
return Result.err(confirmationResult.unwrapErr());
}
@@ -81,7 +82,7 @@ export class ConfirmCheckoutUseCase {
if (checkoutResult.isOk()) {
this.logger.info('Checkout process completed successfully.');
} else {
this.logger.error('Checkout process failed', { error: checkoutResult.unwrapErr() });
this.logger.error('Checkout process failed', checkoutResult.unwrapErr());
}
return checkoutResult;
}

View File

@@ -1,4 +1,4 @@
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import type { Logger } from '@core/shared/application/Logger';
@@ -32,8 +32,9 @@ export class InitiateLoginUseCase {
}
return result;
} catch (error) {
this.logger.error('Error initiating login flow.', error);
return Result.fail(error.message || 'Unknown error during login initiation.');
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error('Error initiating login flow.', err);
return Result.err(err);
}
}
}

View File

@@ -1,9 +1,10 @@
import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
import { StartAutomationSessionUseCase } from '@core/automation/application/use-cases/StartAutomationSessionUseCase';
import { AutomationEnginePort as IAutomationEngine } from '@core/automation/application/ports/AutomationEnginePort';
import { IBrowserAutomation as IScreenAutomation } from '@core/automation/application/ports/ScreenAutomationPort';
import { SessionRepositoryPort as ISessionRepository } from '@core/automation/application/ports/SessionRepositoryPort';
import { AutomationSession } from '@core/automation/domain/entities/AutomationSession';
import { StartAutomationSessionUseCase } from 'apps/companion/main/automation/application/use-cases/StartAutomationSessionUseCase';
import { AutomationEnginePort as IAutomationEngine } from 'apps/companion/main/automation/application/ports/AutomationEnginePort';
import { IBrowserAutomation as IScreenAutomation } from 'apps/companion/main/automation/application/ports/ScreenAutomationPort';
import { SessionRepositoryPort as ISessionRepository } from 'apps/companion/main/automation/application/ports/SessionRepositoryPort';
import type { Logger } from '@core/shared/application';
import { AutomationSession } from 'apps/companion/main/automation/domain/entities/AutomationSession';
describe('StartAutomationSessionUseCase', () => {
let mockAutomationEngine: {
@@ -23,6 +24,12 @@ describe('StartAutomationSessionUseCase', () => {
update: Mock;
delete: Mock;
};
let mockLogger: {
debug: Mock;
info: Mock;
warn: Mock;
error: Mock;
};
let useCase: StartAutomationSessionUseCase;
beforeEach(() => {
@@ -46,10 +53,18 @@ describe('StartAutomationSessionUseCase', () => {
delete: vi.fn(),
};
mockLogger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
useCase = new StartAutomationSessionUseCase(
mockAutomationEngine as unknown as IAutomationEngine,
mockBrowserAutomation as unknown as IScreenAutomation,
mockSessionRepository as unknown as ISessionRepository
mockSessionRepository as unknown as ISessionRepository,
mockLogger as unknown as Logger
);
});

View File

@@ -25,7 +25,6 @@ export class StartAutomationSessionUseCase
const validationResult = await this.automationEngine.validateConfiguration(config);
if (!validationResult.isValid) {
this.logger.warn('Automation session configuration validation failed', { config, error: validationResult.error });
this.logger.error('Automation session configuration validation failed', { config, error: validationResult.error });
throw new Error(validationResult.error);
}
this.logger.debug('Automation session configuration validated successfully.');

View File

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

View File

@@ -1,5 +1,5 @@
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState';
import type { Logger } from '@core/shared/application';
@@ -29,7 +29,7 @@ export class VerifyAuthenticatedPageUseCase {
return Result.ok<BrowserAuthenticationState>(browserState);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error(`Page verification failed unexpectedly: ${message}`, error);
this.logger.error(`Page verification failed unexpectedly: ${message}`, error instanceof Error ? error : undefined);
return Result.err<BrowserAuthenticationState>(new Error(`Page verification failed: ${message}`));
}
}

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { AutomationSession } from '@core/automation/domain/entities/AutomationSession';
import { StepId } from '@core/automation/domain/value-objects/StepId';
import { AutomationSession } from 'apps/companion/main/automation/domain/entities/AutomationSession';
import { StepId } from 'apps/companion/main/automation/domain/value-objects/StepId';
describe('AutomationSession Entity', () => {
describe('create', () => {

View File

@@ -4,6 +4,7 @@ import { StepId } from '../value-objects/StepId';
import type { HostedSessionConfig } from '../types/HostedSessionConfig';
import { AutomationDomainError } from '../errors/AutomationDomainError';
import { SessionState } from '../value-objects/SessionState';
export class AutomationSession implements IEntity<string> {
private readonly _id: string;

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { PageStateValidator } from '@core/automation/domain/services/PageStateValidator';
import { PageStateValidator } from 'apps/companion/main/automation/domain/services/PageStateValidator';
describe('PageStateValidator', () => {
const validator = new PageStateValidator();
@@ -7,7 +7,7 @@ describe('PageStateValidator', () => {
describe('validateState', () => {
it('should return valid when all required selectors are present', () => {
// Arrange
const actualState = (_selector: string) => {
const actualState = (selector: string) => {
return ['#add-car-button', '#cars-list'].includes(selector);
};
@@ -27,7 +27,7 @@ describe('PageStateValidator', () => {
it('should return invalid when required selectors are missing', () => {
// Arrange
const actualState = (_selector: string) => {
const actualState = (selector: string) => {
return selector === '#add-car-button'; // Only one of two selectors present
};
@@ -48,7 +48,7 @@ describe('PageStateValidator', () => {
it('should return invalid when forbidden selectors are present', () => {
// Arrange
const actualState = (_selector: string) => {
const actualState = (selector: string) => {
return ['#add-car-button', '#set-track'].includes(selector);
};
@@ -70,7 +70,7 @@ describe('PageStateValidator', () => {
it('should handle empty forbidden selectors array', () => {
// Arrange
const actualState = (_selector: string) => {
const actualState = (selector: string) => {
return selector === '#add-car-button';
};
@@ -89,7 +89,7 @@ describe('PageStateValidator', () => {
it('should handle undefined forbidden selectors', () => {
// Arrange
const actualState = (_selector: string) => {
const actualState = (selector: string) => {
return selector === '#add-car-button';
};
@@ -108,7 +108,7 @@ describe('PageStateValidator', () => {
it('should return error result when actualState function throws', () => {
// Arrange
const actualState = (_selector: string) => {
const actualState = () => {
throw new Error('Selector evaluation failed');
};
@@ -144,7 +144,7 @@ describe('PageStateValidator', () => {
it('should validate complex state with both required and forbidden selectors', () => {
// Arrange - Simulate being on Cars page but Track page elements leaked through
const actualState = (_selector: string) => {
const actualState = (selector: string) => {
const presentSelectors = ['#add-car-button', '#cars-list', '#set-track'];
return presentSelectors.includes(selector);
};

View File

@@ -1,5 +1,5 @@
import type { IDomainValidationService } from '@core/shared/domain';
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
/**
* Configuration for page state validation.
@@ -183,7 +183,7 @@ export class PageStateValidator
}
// Check required selectors are present (with fallbacks for real mode)
const missingSelectors = requiredSelectors.filter(_selector => {
const missingSelectors = requiredSelectors.filter(selector => {
if (realMode) {
const relatedSelectors = selectorsToCheck.filter(s =>
s.includes(expectedStep) ||

View File

@@ -1,7 +1,8 @@
import { StepId } from '../value-objects/StepId';
import type { IDomainValidationService } from '@core/shared/domain';
import { Result } from '../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import { SessionState } from '../value-objects/SessionState';
export interface ValidationResult {
isValid: boolean;
@@ -90,13 +91,11 @@ export class StepTransitionValidator
}
static validateModalStepTransition(
currentStep: StepId,
nextStep: StepId
): ValidationResult {
return { isValid: true };
}
static shouldStopAtStep18(_nextStep: StepId): boolean {
static shouldStopAtStep18(nextStep: StepId): boolean {
return nextStep.isFinalStep();
}

View File

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

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { CheckoutConfirmation } from '@core/automation/domain/value-objects/CheckoutConfirmation';
import { CheckoutConfirmation } from 'apps/companion/main/automation/domain/value-objects/CheckoutConfirmation';
describe('CheckoutConfirmation Value Object', () => {
describe('create', () => {
@@ -19,7 +19,8 @@ describe('CheckoutConfirmation Value Object', () => {
});
it('should throw error for invalid decision', () => {
expect(() => CheckoutConfirmation.create('invalid' as unknown)).toThrow(
// @ts-expect-error Testing invalid input
expect(() => CheckoutConfirmation.create('invalid')).toThrow(
'Invalid checkout confirmation decision',
);
});

View File

@@ -24,7 +24,7 @@ export class CheckoutConfirmation {
return CheckoutConfirmation.create('confirmed');
}
static cancelled(__reason?: string): CheckoutConfirmation {
static cancelled(): CheckoutConfirmation {
return CheckoutConfirmation.create('cancelled');
}

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { CheckoutPrice } from '@core/automation/domain/value-objects/CheckoutPrice';
import { CheckoutPrice } from 'apps/companion/main/automation/domain/value-objects/CheckoutPrice';
/**
* CheckoutPrice Value Object - GREEN PHASE

View File

@@ -1,4 +1,5 @@
import { describe, it, expect } from 'vitest';
import { CheckoutState, CheckoutStateEnum } from './CheckoutState';
/**

View File

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

View File

@@ -16,7 +16,7 @@ export class CookieConfiguration {
this.cookie = cookie;
try {
this.targetUrl = new URL(targetUrl);
} catch (error) {
} catch {
throw new Error(`Invalid target URL: ${targetUrl}`);
}

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
import { RaceCreationResult } from '@core/automation/domain/value-objects/RaceCreationResult';
import { RaceCreationResult } from 'apps/companion/main/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 '@core/automation/domain/value-objects/SessionLifetime';
import { SessionLifetime } from 'apps/companion/main/automation/domain/value-objects/SessionLifetime';
describe('SessionLifetime Value Object', () => {
describe('Construction', () => {

View File

@@ -1,4 +1,6 @@
import { describe, it, expect } from 'vitest';
import { SessionState } from './SessionState';
import type { SessionStateValue } from './SessionState';
describe('SessionState Value Object', () => {
@@ -44,11 +46,11 @@ describe('SessionState Value Object', () => {
});
it('should throw error for invalid state', () => {
expect(() => SessionState.create('INVALID' as unknown)).toThrow('Invalid session state');
expect(() => SessionState.create('INVALID' as SessionStateValue)).toThrow('Invalid session state');
});
it('should throw error for empty string', () => {
expect(() => SessionState.create('' as unknown)).toThrow('Invalid session state');
expect(() => SessionState.create('' as SessionStateValue)).toThrow('Invalid session state');
});
});

View File

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

View File

@@ -10,8 +10,8 @@ vi.mock('electron', () => ({
}));
import { ElectronCheckoutConfirmationAdapter } from '@core/automation/infrastructure//ipc/ElectronCheckoutConfirmationAdapter';
import { CheckoutPrice } from '@core/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@core/automation/domain/value-objects/CheckoutState';
import { CheckoutPrice } from 'apps/companion/main/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from 'apps/companion/main/automation/domain/value-objects/CheckoutState';
import { ipcMain } from 'electron';
describe('ElectronCheckoutConfirmationAdapter', () => {

View File

@@ -1,4 +1,4 @@
import { AutomationEvent } from '@core/automation/application/ports/AutomationEventPublisherPort';
import { AutomationEvent } from 'apps/companion/main/automation/application/ports/AutomationEventPublisherPort';
export type LifecycleCallback = (event: AutomationEvent) => Promise<void> | void;

View File

@@ -1,4 +1,4 @@
import { Result } from '../../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import { CheckoutPrice } from '../../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../../domain/value-objects/CheckoutState';
import type { CheckoutInfoDTO } from '../../../application/dto/CheckoutInfoDTO';

View File

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

View File

@@ -1,5 +1,5 @@
import { Page } from 'playwright';
import { LoggerPort } from '@core/automation/application/ports/LoggerPort';
import { LoggerPort } from 'apps/companion/main/automation/application/ports/LoggerPort';
export class AuthenticationGuard {
constructor(

View File

@@ -1,5 +1,5 @@
import type { Page } from 'playwright';
import type { LoggerPort } from '@core/automation/application/ports/LoggerPort';
import type { LoggerPort } from 'apps/companion/main/automation/application/ports/LoggerPort';
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
import { IRACING_URLS, IRACING_SELECTORS, IRACING_TIMEOUTS } from '../dom/IRacingSelectors';
import { AuthenticationGuard } from './AuthenticationGuard';

View File

@@ -1,12 +1,11 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { Page, BrowserContext } from 'playwright';
import { PlaywrightAuthSessionService } from '@core/automation/infrastructure//automation/auth/PlaywrightAuthSessionService';
import type { PlaywrightBrowserSession } from '@core/automation/infrastructure//automation/core/PlaywrightBrowserSession';
import type { SessionCookieStore } from '@core/automation/infrastructure//automation/auth/SessionCookieStore';
import type { IPlaywrightAuthFlow } from '@core/automation/infrastructure//automation/auth/PlaywrightAuthFlow';
import type { LoggerPort as Logger } from '@core/automation/application/ports/LoggerPort';
import { AuthenticationState } from '@core/automation/domain/value-objects/AuthenticationState';
import { Result } from '@core/shared/result/Result';
import type { LoggerPort as Logger } from 'apps/companion/main/automation/application/ports/LoggerPort';
import { AuthenticationState } from 'apps/companion/main/automation/domain/value-objects/AuthenticationState';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
import { PlaywrightAuthSessionService } from './PlaywrightAuthSessionService';
import { SessionCookieStore } from './SessionCookieStore';
describe('PlaywrightAuthSessionService.initiateLogin browser mode behaviour', () => {
const originalEnv = { ...process.env };

View File

@@ -5,7 +5,7 @@ import type { AuthenticationServicePort } from '../../../../application/ports/Au
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
import { Result } from '../../../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { SessionCookieStore } from './SessionCookieStore';
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';

View File

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

View File

@@ -1,6 +1,6 @@
import { describe, test, expect, beforeEach } from 'vitest';
import { SessionCookieStore } from '@core/automation/infrastructure//automation/auth/SessionCookieStore';
import type { Cookie } from 'playwright';
import { SessionCookieStore } from './SessionCookieStore';
const logger = console as unknown;

View File

@@ -2,7 +2,7 @@ import * as fs from 'fs/promises';
import * as path from 'path';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { CookieConfiguration } from '../../../../domain/value-objects/CookieConfiguration';
import { Result } from '../../../../../shared/result/Result';
import { Result } from '@gridpilot/shared/result/Result';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
interface Cookie {

View File

@@ -16,14 +16,12 @@ import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO'
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import { Result } from '../../../../../shared/result/Result';
import { IRACING_SELECTORS, IRACING_URLS, IRACING_TIMEOUTS, ALL_BLOCKED_SELECTORS, BLOCKED_KEYWORDS } from '../dom/IRacingSelectors';
import { Result } from '@gridpilot/shared/result/Result';
import { IRACING_SELECTORS, IRACING_URLS, IRACING_TIMEOUTS, BLOCKED_KEYWORDS } from '../dom/IRacingSelectors';
import { SessionCookieStore } from '../auth/SessionCookieStore';
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
import { BrowserModeConfigLoader, BrowserMode } from '../../../config/BrowserModeConfig';
import { PageStateValidator, PageStateValidation, PageStateValidationResult } from '@core/automation/domain/services/PageStateValidator';
import { PageStateValidator, PageStateValidation, PageStateValidationResult } from 'apps/companion/main/automation/domain/services/PageStateValidator';
import { IRacingDomNavigator } from '../dom/IRacingDomNavigator';
import { SafeClickService } from '../dom/SafeClickService';
import { IRacingDomInteractor } from '../dom/IRacingDomInteractor';

View File

@@ -4,8 +4,7 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import * as fs from 'fs';
import * as path from 'path';
import type { LoggerPort } from '@core/automation/application/ports/LoggerPort';
import { BrowserModeConfigLoader, BrowserMode } from '../../../config/BrowserModeConfig';
import type { LoggerPort } from 'apps/companion/main/automation/application/ports/LoggerPort';
import type { PlaywrightConfig } from './PlaywrightAutomationAdapter';
import { PlaywrightAutomationAdapter } from './PlaywrightAutomationAdapter';

View File

@@ -18,7 +18,7 @@ import type {
PageStateValidation,
PageStateValidationResult,
} from '../../../../domain/services/PageStateValidator';
import type { Result } from '../../../../../shared/result/Result';
import type { Result } from '@gridpilot/shared/result/Result';
interface WizardStepOrchestratorDeps {
config: Required<PlaywrightConfig>;
@@ -167,7 +167,7 @@ export class WizardStepOrchestrator {
await this.navigator.waitForWizardStep(stepName);
}
private async checkWizardDismissed(_currentStep: number): Promise<void> {
private async checkWizardDismissed(currentStep: number): Promise<void> {
await this.navigator.checkWizardDismissed(currentStep);
}

View File

@@ -1,5 +1,5 @@
import type { Page } from 'playwright';
import type { LoggerPort } from '@core/automation/application/ports/LoggerPort';
import type { LoggerPort } from 'apps/companion/main/automation/application/ports/LoggerPort';
import { IRACING_SELECTORS, BLOCKED_KEYWORDS } from './IRacingSelectors';
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';

Some files were not shown because too many files have changed in this diff Show More