This commit is contained in:
2025-12-04 15:15:24 +01:00
parent b7d5551ea7
commit c698a0b893
119 changed files with 1167 additions and 2652 deletions

View File

@@ -0,0 +1,4 @@
export interface AutomationEngineValidationResultDTO {
isValid: boolean;
error?: string;
}

View File

@@ -0,0 +1,5 @@
export interface AutomationResultDTO {
success: boolean;
error?: string;
metadata?: Record<string, unknown>;
}

View File

@@ -0,0 +1,13 @@
import { CheckoutPrice } from '../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../domain/value-objects/CheckoutState';
export interface CheckoutConfirmationRequestDTO {
price: CheckoutPrice;
state: CheckoutState;
sessionMetadata: {
sessionName: string;
trackId: string;
carIds: string[];
};
timeoutMs: number;
}

View File

@@ -0,0 +1,8 @@
import { CheckoutPrice } from '../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../domain/value-objects/CheckoutState';
export interface CheckoutInfoDTO {
price: CheckoutPrice | null;
state: CheckoutState;
buttonHtml: string;
}

View File

@@ -0,0 +1,5 @@
import type { AutomationResultDTO } from './AutomationResultDTO';
export interface ClickResultDTO extends AutomationResultDTO {
target: string;
}

View File

@@ -0,0 +1,6 @@
import type { AutomationResultDTO } from './AutomationResultDTO';
export interface FormFillResultDTO extends AutomationResultDTO {
fieldName: string;
valueSet: string;
}

View File

@@ -0,0 +1,6 @@
import type { AutomationResultDTO } from './AutomationResultDTO';
export interface ModalResultDTO extends AutomationResultDTO {
stepId: number;
action: string;
}

View File

@@ -0,0 +1,6 @@
import type { AutomationResultDTO } from './AutomationResultDTO';
export interface NavigationResultDTO extends AutomationResultDTO {
url: string;
loadTime: number;
}

View File

@@ -0,0 +1,11 @@
import type { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
export interface SessionDTO {
sessionId: string;
state: string;
currentStep: number;
config: HostedSessionConfig;
startedAt?: Date;
completedAt?: Date;
errorMessage?: string;
}

View File

@@ -0,0 +1,7 @@
import type { AutomationResultDTO } from './AutomationResultDTO';
export interface WaitResultDTO extends AutomationResultDTO {
target: string;
waitedMs: number;
found: boolean;
}

View File

@@ -1,6 +1,6 @@
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { Result } from '../../shared/result/Result';
import { AuthenticationState } from '../../domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState';
import { Result } from '../../../shared/result/Result';
/**
* Port for authentication services implementing zero-knowledge login.
@@ -10,7 +10,7 @@ import { Result } from '../../shared/result/Result';
* the user logs in directly with iRacing. GridPilot only observes
* URL changes to detect successful authentication.
*/
export interface IAuthenticationService {
export interface AuthenticationServicePort {
/**
* Check if user has a valid session without prompting login.
* Navigates to a protected iRacing page and checks for login redirects.

View File

@@ -0,0 +1,9 @@
import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
import { StepId } from '../../domain/value-objects/StepId';
import type { AutomationEngineValidationResultDTO } from '../dto/AutomationEngineValidationResultDTO';
export interface AutomationEnginePort {
validateConfiguration(config: HostedSessionConfig): Promise<AutomationEngineValidationResultDTO>;
executeStep(stepId: StepId, config: HostedSessionConfig): Promise<void>;
stopAutomation(): void;
}

View File

@@ -5,6 +5,6 @@ export type AutomationEvent = {
payload?: any
}
export interface IAutomationEventPublisher {
export interface AutomationEventPublisherPort {
publish(event: AutomationEvent): Promise<void>
}

View File

@@ -1,30 +0,0 @@
export interface AutomationResult {
success: boolean;
error?: string;
metadata?: Record<string, unknown>;
}
export interface NavigationResult extends AutomationResult {
url: string;
loadTime: number;
}
export interface FormFillResult extends AutomationResult {
fieldName: string;
valueSet: string;
}
export interface ClickResult extends AutomationResult {
target: string;
}
export interface WaitResult extends AutomationResult {
target: string;
waitedMs: number;
found: boolean;
}
export interface ModalResult extends AutomationResult {
stepId: number;
action: string;
}

View File

@@ -0,0 +1,9 @@
import { Result } from '../../../shared/result/Result';
import { CheckoutConfirmation } from '../../domain/value-objects/CheckoutConfirmation';
import type { CheckoutConfirmationRequestDTO } from '../dto/CheckoutConfirmationRequestDTO';
export interface CheckoutConfirmationPort {
requestCheckoutConfirmation(
request: CheckoutConfirmationRequestDTO
): Promise<Result<CheckoutConfirmation>>;
}

View File

@@ -0,0 +1,7 @@
import { Result } from '../../../shared/result/Result';
import type { CheckoutInfoDTO } from '../dto/CheckoutInfoDTO';
export interface CheckoutServicePort {
extractCheckoutInfo(): Promise<Result<CheckoutInfoDTO>>;
proceedWithCheckout(): Promise<Result<void>>;
}

View File

@@ -1,13 +0,0 @@
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
export interface ValidationResult {
isValid: boolean;
error?: string;
}
export interface IAutomationEngine {
validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult>;
executeStep(stepId: StepId, config: HostedSessionConfig): Promise<void>;
stopAutomation(): void;
}

View File

@@ -1,21 +0,0 @@
import { Result } from '../../shared/result/Result';
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';
export interface CheckoutConfirmationRequest {
price: CheckoutPrice;
state: CheckoutState;
sessionMetadata: {
sessionName: string;
trackId: string;
carIds: string[];
};
timeoutMs: number;
}
export interface ICheckoutConfirmationPort {
requestCheckoutConfirmation(
request: CheckoutConfirmationRequest
): Promise<Result<CheckoutConfirmation>>;
}

View File

@@ -1,14 +0,0 @@
import { Result } from '../../shared/result/Result';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
export interface CheckoutInfo {
price: CheckoutPrice | null;
state: CheckoutState;
buttonHtml: string;
}
export interface ICheckoutService {
extractCheckoutInfo(): Promise<Result<CheckoutInfo>>;
proceedWithCheckout(): Promise<Result<void>>;
}

View File

@@ -1,35 +0,0 @@
/**
* Log levels in order of severity (lowest to highest)
*/
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';
/**
* Contextual metadata attached to log entries
*/
export interface LogContext {
/** Unique session identifier for correlation */
sessionId?: string;
/** Current automation step (1-18) */
stepId?: number;
/** Step name for human readability */
stepName?: string;
/** Adapter or component name */
adapter?: string;
/** Operation duration in milliseconds */
durationMs?: number;
/** Additional arbitrary metadata */
[key: string]: unknown;
}
/**
* ILogger - Port interface for application-layer logging.
*/
export interface ILogger {
debug(message: string, context?: LogContext): void;
info(message: string, context?: LogContext): void;
warn(message: string, context?: LogContext): void;
error(message: string, error?: Error, context?: LogContext): void;
fatal(message: string, error?: Error, context?: LogContext): void;
child(context: LogContext): ILogger;
flush(): Promise<void>;
}

View File

@@ -0,0 +1,17 @@
/**
* Contextual metadata attached to log entries
*/
export interface LogContext {
/** Unique session identifier for correlation */
sessionId?: string;
/** Current automation step (1-18) */
stepId?: number;
/** Step name for human readability */
stepName?: string;
/** Adapter or component name */
adapter?: string;
/** Operation duration in milliseconds */
durationMs?: number;
/** Additional arbitrary metadata */
[key: string]: unknown;
}

View File

@@ -0,0 +1,4 @@
/**
* Log levels in order of severity (lowest to highest)
*/
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal';

View File

@@ -0,0 +1,15 @@
import type { LogLevel } from './LoggerLogLevel';
import type { LogContext } from './LoggerContext';
/**
* LoggerPort - Port interface for application-layer logging.
*/
export interface LoggerPort {
debug(message: string, context?: LogContext): void;
info(message: string, context?: LogContext): void;
warn(message: string, context?: LogContext): void;
error(message: string, error?: Error, context?: LogContext): void;
fatal(message: string, error?: Error, context?: LogContext): void;
child(context: LogContext): LoggerPort;
flush(): Promise<void>;
}

View File

@@ -1,7 +1,7 @@
export type OverlayAction = { id: string; label: string; meta?: Record<string, unknown>; timeoutMs?: number }
export type ActionAck = { id: string; status: 'confirmed' | 'tentative' | 'failed'; reason?: string }
export interface IOverlaySyncPort {
export interface OverlaySyncPort {
startAction(action: OverlayAction): Promise<ActionAck>
cancelAction(actionId: string): Promise<void>
}

View File

@@ -1,12 +1,10 @@
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import {
NavigationResult,
FormFillResult,
ClickResult,
WaitResult,
ModalResult,
AutomationResult,
} from './AutomationResults';
import { StepId } from '../../domain/value-objects/StepId';
import type { NavigationResultDTO } from '../dto/NavigationResultDTO';
import type { ClickResultDTO } from '../dto/ClickResultDTO';
import type { WaitResultDTO } from '../dto/WaitResultDTO';
import type { ModalResultDTO } from '../dto/ModalResultDTO';
import type { AutomationResultDTO } from '../dto/AutomationResultDTO';
import type { FormFillResultDTO } from '../dto/FormFillResultDTO';
/**
* Browser automation interface for Playwright-based automation.
@@ -19,38 +17,38 @@ export interface IBrowserAutomation {
/**
* Navigate to a URL.
*/
navigateToPage(url: string): Promise<NavigationResult>;
navigateToPage(url: string): Promise<NavigationResultDTO>;
/**
* Fill a form field by name or selector.
*/
fillFormField(fieldName: string, value: string): Promise<FormFillResult>;
fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO>;
/**
* Click an element by selector or action name.
*/
clickElement(target: string): Promise<ClickResult>;
clickElement(target: string): Promise<ClickResultDTO>;
/**
* Wait for an element to appear.
*/
waitForElement(target: string, maxWaitMs?: number): Promise<WaitResult>;
waitForElement(target: string, maxWaitMs?: number): Promise<WaitResultDTO>;
/**
* Handle modal dialogs.
*/
handleModal(stepId: StepId, action: string): Promise<ModalResult>;
handleModal(stepId: StepId, action: string): Promise<ModalResultDTO>;
/**
* Execute a complete workflow step.
*/
executeStep?(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult>;
executeStep?(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO>;
/**
* Initialize the browser connection.
* Returns an AutomationResult indicating success or failure.
*/
connect?(): Promise<AutomationResult>;
connect?(): Promise<AutomationResultDTO>;
/**
* Clean up browser resources.
@@ -62,9 +60,3 @@ export interface IBrowserAutomation {
*/
isConnected?(): boolean;
}
/**
* @deprecated Use IBrowserAutomation directly. IScreenAutomation was for OS-level
* automation which has been removed in favor of browser-only automation.
*/
export type IScreenAutomation = IBrowserAutomation;

View File

@@ -1,7 +1,7 @@
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
import { SessionStateValue } from '@gridpilot/automation/domain/value-objects/SessionState';
import { AutomationSession } from '../../domain/entities/AutomationSession';
import { SessionStateValue } from '../../domain/value-objects/SessionState';
export interface ISessionRepository {
export interface SessionRepositoryPort {
save(session: AutomationSession): Promise<void>;
findById(id: string): Promise<AutomationSession | null>;
update(session: AutomationSession): Promise<void>;

View File

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

View File

@@ -1,22 +1,22 @@
import { IOverlaySyncPort, OverlayAction, ActionAck } from '../ports/IOverlaySyncPort';
import { IAutomationEventPublisher, AutomationEvent } from '../ports/IAutomationEventPublisher';
import { OverlaySyncPort, OverlayAction, ActionAck } from '../ports/OverlaySyncPort';
import { AutomationEventPublisherPort, AutomationEvent } from '../ports/AutomationEventPublisherPort';
import { IAutomationLifecycleEmitter, LifecycleCallback } from '../../infrastructure/adapters/IAutomationLifecycleEmitter';
import { ILogger } from '../ports/ILogger';
import { LoggerPort } from '../ports/LoggerPort';
type ConstructorArgs = {
lifecycleEmitter: IAutomationLifecycleEmitter
publisher: IAutomationEventPublisher
logger: ILogger
publisher: AutomationEventPublisherPort
logger: LoggerPort
initialPanelWaitMs?: number
maxPanelRetries?: number
backoffFactor?: number
defaultTimeoutMs?: number
}
export class OverlaySyncService implements IOverlaySyncPort {
export class OverlaySyncService implements OverlaySyncPort {
private lifecycleEmitter: IAutomationLifecycleEmitter
private publisher: IAutomationEventPublisher
private logger: ILogger
private publisher: AutomationEventPublisherPort
private logger: LoggerPort
private initialPanelWaitMs: number
private maxPanelRetries: number
private backoffFactor: number

View File

@@ -1,14 +1,8 @@
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { Result } from '../../shared/result/Result';
import type { IAuthenticationService } from '../ports/IAuthenticationService';
import { SessionLifetime } from '@gridpilot/automation/domain/value-objects/SessionLifetime';
/**
* Port for optional server-side session validation.
*/
export interface ISessionValidator {
validateSession(): Promise<Result<boolean>>;
}
import { AuthenticationState } from '../../domain/value-objects/AuthenticationState';
import { Result } from '../../../shared/result/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import { SessionLifetime } from '../../domain/value-objects/SessionLifetime';
import type { SessionValidatorPort } from '../ports/SessionValidatorPort';
/**
* Use case for checking if the user has a valid iRacing session.
@@ -22,8 +16,8 @@ export interface ISessionValidator {
*/
export class CheckAuthenticationUseCase {
constructor(
private readonly authService: IAuthenticationService,
private readonly sessionValidator?: ISessionValidator
private readonly authService: AuthenticationServicePort,
private readonly sessionValidator?: SessionValidatorPort
) {}
/**

View File

@@ -1,5 +1,5 @@
import { Result } from '../../shared/result/Result';
import type { IAuthenticationService } from '../ports/IAuthenticationService';
import { Result } from '../../../shared/result/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
/**
* Use case for clearing the user's session (logout).
@@ -8,7 +8,7 @@ import type { IAuthenticationService } from '../ports/IAuthenticationService';
* the user out. The next automation attempt will require re-authentication.
*/
export class ClearSessionUseCase {
constructor(private readonly authService: IAuthenticationService) {}
constructor(private readonly authService: AuthenticationServicePort) {}
/**
* Execute the session clearing.

View File

@@ -1,9 +1,9 @@
import { Result } from '../../shared/result/Result';
import { RaceCreationResult } from '@gridpilot/automation/domain/value-objects/RaceCreationResult';
import type { ICheckoutService } from '../ports/ICheckoutService';
import { Result } from '../../../shared/result/Result';
import { RaceCreationResult } from '../../domain/value-objects/RaceCreationResult';
import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
export class CompleteRaceCreationUseCase {
constructor(private readonly checkoutService: ICheckoutService) {}
constructor(private readonly checkoutService: CheckoutServicePort) {}
async execute(sessionId: string): Promise<Result<RaceCreationResult>> {
if (!sessionId || sessionId.trim() === '') {

View File

@@ -1,7 +1,7 @@
import { Result } from '../../shared/result/Result';
import { ICheckoutService } from '../ports/ICheckoutService';
import { ICheckoutConfirmationPort } from '../ports/ICheckoutConfirmationPort';
import { CheckoutStateEnum } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { Result } from '../../../shared/result/Result';
import type { CheckoutServicePort } from '../ports/CheckoutServicePort';
import type { CheckoutConfirmationPort } from '../ports/CheckoutConfirmationPort';
import { CheckoutStateEnum } from '../../domain/value-objects/CheckoutState';
interface SessionMetadata {
sessionName: string;
@@ -13,8 +13,8 @@ export class ConfirmCheckoutUseCase {
private static readonly DEFAULT_TIMEOUT_MS = 30000;
constructor(
private readonly checkoutService: ICheckoutService,
private readonly confirmationPort: ICheckoutConfirmationPort
private readonly checkoutService: CheckoutServicePort,
private readonly confirmationPort: CheckoutConfirmationPort
) {}
async execute(sessionMetadata?: SessionMetadata): Promise<Result<void>> {

View File

@@ -1,5 +1,5 @@
import { Result } from '../../shared/result/Result';
import type { IAuthenticationService } from '../ports/IAuthenticationService';
import { Result } from '../../../shared/result/Result';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
/**
* Use case for initiating the manual login flow.
@@ -9,7 +9,7 @@ import type { IAuthenticationService } from '../ports/IAuthenticationService';
* indicating successful login.
*/
export class InitiateLoginUseCase {
constructor(private readonly authService: IAuthenticationService) {}
constructor(private readonly authService: AuthenticationServicePort) {}
/**
* Execute the login flow.

View File

@@ -1,24 +1,15 @@
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { IAutomationEngine } from '../ports/IAutomationEngine';
import type { IBrowserAutomation } from '../ports/IScreenAutomation';
import { ISessionRepository } from '../ports/ISessionRepository';
export interface SessionDTO {
sessionId: string;
state: string;
currentStep: number;
config: HostedSessionConfig;
startedAt?: Date;
completedAt?: Date;
errorMessage?: string;
}
import { AutomationSession } from '../../domain/entities/AutomationSession';
import { HostedSessionConfig } from '../../domain/entities/HostedSessionConfig';
import { AutomationEnginePort } from '../ports/AutomationEnginePort';
import type { IBrowserAutomation } from '../ports/ScreenAutomationPort';
import { SessionRepositoryPort } from '../ports/SessionRepositoryPort';
import type { SessionDTO } from '../dto/SessionDTO';
export class StartAutomationSessionUseCase {
constructor(
private readonly automationEngine: IAutomationEngine,
private readonly automationEngine: AutomationEnginePort,
private readonly browserAutomation: IBrowserAutomation,
private readonly sessionRepository: ISessionRepository
private readonly sessionRepository: SessionRepositoryPort
) {}
async execute(config: HostedSessionConfig): Promise<SessionDTO> {

View File

@@ -1,6 +1,6 @@
import { IAuthenticationService } from '../ports/IAuthenticationService';
import { Result } from '../../shared/result/Result';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import type { AuthenticationServicePort } from '../ports/AuthenticationServicePort';
import { Result } from '../../../shared/result/Result';
import { BrowserAuthenticationState } from '../../domain/value-objects/BrowserAuthenticationState';
/**
* Use case for verifying browser shows authenticated page state.
@@ -8,7 +8,7 @@ import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-o
*/
export class VerifyAuthenticatedPageUseCase {
constructor(
private readonly authService: IAuthenticationService
private readonly authService: AuthenticationServicePort
) {}
async execute(): Promise<Result<BrowserAuthenticationState>> {

View File

@@ -1,4 +1,4 @@
import { Result } from '../shared/Result';
import { Result } from '../../../shared/result/Result';
/**
* Configuration for page state validation.

View File

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

View File

@@ -1,7 +1,7 @@
import { Result } from '../../../shared/result/Result';
import { CheckoutPrice } from '@gridpilot/automation/domain/value-objects/CheckoutPrice';
import { CheckoutState } from '@gridpilot/automation/domain/value-objects/CheckoutState';
import { CheckoutInfo } from '../../../application/ports/ICheckoutService';
import { Result } from '../../../../shared/result/Result';
import { CheckoutPrice } from '../../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../../domain/value-objects/CheckoutState';
import type { CheckoutInfoDTO } from '../../../application/dto/CheckoutInfoDTO';
import { IRACING_SELECTORS } from './dom/IRacingSelectors';
interface Page {
@@ -22,7 +22,7 @@ export class CheckoutPriceExtractor {
constructor(private readonly page: Page) {}
async extractCheckoutInfo(): Promise<Result<CheckoutInfo>> {
async extractCheckoutInfo(): Promise<Result<CheckoutInfoDTO>> {
try {
// Prefer the explicit pill element which contains the price
const pillLocator = this.page.locator('.label-pill, .label-inverse');

View File

@@ -1,10 +1,10 @@
import { Page } from 'playwright';
import { ILogger } from '../../../../application/ports/ILogger';
import { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
export class AuthenticationGuard {
constructor(
private readonly page: Page,
private readonly logger?: ILogger
private readonly logger?: LoggerPort
) {}
async checkForLoginUI(): Promise<boolean> {

View File

@@ -1,11 +1,11 @@
import type { Page } from 'playwright';
import type { ILogger } from '../../../../application/ports/ILogger';
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
import { IRACING_URLS, IRACING_SELECTORS, IRACING_TIMEOUTS } from '../dom/IRacingSelectors';
import { AuthenticationGuard } from './AuthenticationGuard';
export class IRacingPlaywrightAuthFlow implements IPlaywrightAuthFlow {
constructor(private readonly logger?: ILogger) {}
constructor(private readonly logger?: LoggerPort) {}
getLoginUrl(): string {
return IRACING_URLS.login;

View File

@@ -1,11 +1,11 @@
import * as fs from 'fs';
import type { BrowserContext, Page } from 'playwright';
import type { IAuthenticationService } from '../../../../application/ports/IAuthenticationService';
import type { ILogger } from '../../../../application/ports/ILogger';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
import { Result } from '../../../../shared/result/Result';
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
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 { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { SessionCookieStore } from './SessionCookieStore';
import type { IPlaywrightAuthFlow } from './PlaywrightAuthFlow';
@@ -26,11 +26,11 @@ interface PlaywrightAuthSessionConfig {
* - Cookie persistence via SessionCookieStore
* - Exposing the IAuthenticationService port for application layer
*/
export class PlaywrightAuthSessionService implements IAuthenticationService {
export class PlaywrightAuthSessionService implements AuthenticationServicePort {
private readonly browserSession: PlaywrightBrowserSession;
private readonly cookieStore: SessionCookieStore;
private readonly authFlow: IPlaywrightAuthFlow;
private readonly logger?: ILogger;
private readonly logger?: LoggerPort;
private readonly navigationTimeoutMs: number;
private readonly loginWaitTimeoutMs: number;
@@ -41,7 +41,7 @@ export class PlaywrightAuthSessionService implements IAuthenticationService {
browserSession: PlaywrightBrowserSession,
cookieStore: SessionCookieStore,
authFlow: IPlaywrightAuthFlow,
logger?: ILogger,
logger?: LoggerPort,
config?: PlaywrightAuthSessionConfig,
) {
this.browserSession = browserSession;

View File

@@ -1,9 +1,9 @@
import * as fs from 'fs/promises';
import * as path from 'path';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { CookieConfiguration } from '@gridpilot/automation/domain/value-objects/CookieConfiguration';
import { Result } from '../../../../shared/result/Result';
import type { ILogger } from '../../../../application/ports/ILogger';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { CookieConfiguration } from '../../../../domain/value-objects/CookieConfiguration';
import { Result } from '../../../../../shared/result/Result';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
interface Cookie {
name: string;
@@ -43,9 +43,9 @@ const EXPIRY_BUFFER_SECONDS = 300;
export class SessionCookieStore {
private readonly storagePath: string;
private logger?: ILogger;
constructor(userDataDir: string, logger?: ILogger) {
private logger?: LoggerPort;
constructor(userDataDir: string, logger?: LoggerPort) {
this.storagePath = path.join(userDataDir, 'session-state.json');
this.logger = logger;
}

View File

@@ -1,24 +1,22 @@
import type { Browser, Page, BrowserContext } from 'playwright';
import * as fs from 'fs';
import * as path from 'path';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import { AuthenticationState } from '@gridpilot/automation/domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '@gridpilot/automation/domain/value-objects/BrowserAuthenticationState';
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 { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
import type {
NavigationResult,
FormFillResult,
ClickResult,
WaitResult,
ModalResult,
AutomationResult,
} from '../../../../application/ports/AutomationResults';
import type { IAuthenticationService } from '../../../../application/ports/IAuthenticationService';
import type { ILogger } from '../../../../application/ports/ILogger';
import { Result } from '../../../../shared/result/Result';
import { StepId } from '../../../../domain/value-objects/StepId';
import { AuthenticationState } from '../../../../domain/value-objects/AuthenticationState';
import { BrowserAuthenticationState } from '../../../../domain/value-objects/BrowserAuthenticationState';
import { CheckoutPrice } from '../../../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../../../domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
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 { SessionCookieStore } from '../auth/SessionCookieStore';
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
@@ -421,7 +419,7 @@ export interface PlaywrightConfig {
userDataDir?: string;
}
export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthenticationService {
export class PlaywrightAutomationAdapter implements IBrowserAutomation, AuthenticationServicePort {
private browser: Browser | null = null;
private persistentContext: BrowserContext | null = null;
private context: BrowserContext | null = null;
@@ -430,7 +428,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
private browserSession: PlaywrightBrowserSession;
private connected = false;
private isConnecting = false;
private logger?: ILogger;
private logger?: LoggerPort;
private cookieStore: SessionCookieStore;
private authService: PlaywrightAuthSessionService;
private overlayInjected = false;
@@ -450,7 +448,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
private domInteractor!: IRacingDomInteractor;
private readonly stepOrchestrator: WizardStepOrchestrator;
constructor(config: PlaywrightConfig = {}, logger?: ILogger, browserModeLoader?: BrowserModeConfigLoader) {
constructor(config: PlaywrightConfig = {}, logger?: LoggerPort, browserModeLoader?: BrowserModeConfigLoader) {
this.config = {
headless: true,
timeout: 10000,
@@ -623,7 +621,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
this.connected = this.browserSession.isConnected();
}
async connect(forceHeaded: boolean = false): Promise<AutomationResult> {
async connect(forceHeaded: boolean = false): Promise<AutomationResultDTO> {
const result = await this.browserSession.connect(forceHeaded);
if (!result.success) {
return { success: false, error: result.error };
@@ -701,7 +699,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return this.connected && this.page !== null;
}
async navigateToPage(url: string): Promise<NavigationResult> {
async navigateToPage(url: string): Promise<NavigationResultDTO> {
const result = await this.navigator.navigateToPage(url);
if (result.success) {
// Reset overlay state after successful navigation (page context changed)
@@ -710,7 +708,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return result;
}
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
async fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO> {
return this.domInteractor.fillFormField(fieldName, value);
}
@@ -727,7 +725,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return fieldMap[fieldName] || IRACING_SELECTORS.fields.textInput;
}
async clickElement(target: string): Promise<ClickResult> {
async clickElement(target: string): Promise<ClickResultDTO> {
return this.domInteractor.clickElement(target);
}
@@ -749,15 +747,15 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return actionMap[action] || `button:has-text("${action}")`;
}
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResult> {
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResultDTO> {
return this.navigator.waitForElement(target, maxWaitMs);
}
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
async handleModal(stepId: StepId, action: string): Promise<ModalResultDTO> {
return this.domInteractor.handleModal(stepId, action);
}
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult> {
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO> {
const stepNumber = stepId.value;
const skipFixtureNavigation =
(config as any).__skipFixtureNavigation === true;
@@ -1989,7 +1987,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
* First checks if user is already authenticated - if so, navigates directly to hosted sessions.
* Otherwise navigates to login page and waits for user to complete manual login.
*/
private async handleLogin(): Promise<AutomationResult> {
private async handleLogin(): Promise<AutomationResultDTO> {
try {
if (this.config.baseUrl && !this.config.baseUrl.includes('members.iracing.com')) {
this.log('info', 'Fixture baseUrl detected, treating session as authenticated for Step 1', {
@@ -2120,7 +2118,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
* Tries the primary selector first, then falls back to alternative selectors.
* This is needed because iRacing's form structure can vary slightly.
*/
private async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResult> {
private async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResultDTO> {
if (!this.page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
}
@@ -2224,7 +2222,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
}
}
async clickAction(action: string): Promise<ClickResult> {
async clickAction(action: string): Promise<ClickResultDTO> {
if (!this.page) {
return { success: false, target: action, error: 'Browser not connected' };
}
@@ -2253,7 +2251,7 @@ export class PlaywrightAutomationAdapter implements IBrowserAutomation, IAuthent
return { success: true, target: selector };
}
async fillField(fieldName: string, value: string): Promise<FormFillResult> {
async fillField(fieldName: string, value: string): Promise<FormFillResultDTO> {
if (!this.page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
}

View File

@@ -4,7 +4,7 @@ import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import * as fs from 'fs';
import * as path from 'path';
import type { ILogger } from '../../../../application/ports/ILogger';
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import { BrowserModeConfigLoader, BrowserMode } from '../../../config/BrowserModeConfig';
import { getAutomationMode } from '../../../config/AutomationConfig';
import type { PlaywrightConfig } from './PlaywrightAutomationAdapter';
@@ -27,7 +27,7 @@ export class PlaywrightBrowserSession {
constructor(
private readonly config: Required<PlaywrightConfig>,
private readonly logger?: ILogger,
private readonly logger?: LoggerPort,
browserModeLoader?: BrowserModeConfigLoader,
) {
const automationMode = getAutomationMode();

View File

@@ -1,15 +1,13 @@
import type { Page } from 'playwright';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type {
AutomationResult,
ClickResult,
FormFillResult,
} from '../../../../application/ports/AutomationResults';
import type { IAuthenticationService } from '../../../../application/ports/IAuthenticationService';
import type { ILogger } from '../../../../application/ports/ILogger';
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 { StepId } from '../../../../domain/value-objects/StepId';
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
import type { AuthenticationServicePort } from '../../../../application/ports/AuthenticationServicePort';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import { CheckoutPrice } from '../../../../domain/value-objects/CheckoutPrice';
import { CheckoutState } from '../../../../domain/value-objects/CheckoutState';
import { CheckoutConfirmation } from '../../../../domain/value-objects/CheckoutConfirmation';
import type { PlaywrightConfig } from './PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from './PlaywrightBrowserSession';
import { IRacingDomNavigator } from '../dom/IRacingDomNavigator';
@@ -19,16 +17,16 @@ import { getFixtureForStep } from '../engine/FixtureServer';
import type {
PageStateValidation,
PageStateValidationResult,
} from '@gridpilot/automation/domain/services/PageStateValidator';
import type { Result } from '../../../../shared/result/Result';
} from '../../../../domain/services/PageStateValidator';
import type { Result } from '../../../../../shared/result/Result';
interface WizardStepOrchestratorDeps {
config: Required<PlaywrightConfig>;
browserSession: PlaywrightBrowserSession;
navigator: IRacingDomNavigator;
interactor: IRacingDomInteractor;
authService: IAuthenticationService;
logger?: ILogger;
authService: AuthenticationServicePort;
logger?: LoggerPort;
totalSteps: number;
getCheckoutConfirmationCallback: () =>
| ((
@@ -56,7 +54,7 @@ interface WizardStepOrchestratorDeps {
dismissDatetimePickers(): Promise<void>;
};
helpers: {
handleLogin(): Promise<AutomationResult>;
handleLogin(): Promise<AutomationResultDTO>;
validatePageState(
validation: PageStateValidation,
): Promise<Result<PageStateValidationResult, Error>>;
@@ -69,8 +67,8 @@ export class WizardStepOrchestrator {
private readonly browserSession: PlaywrightBrowserSession;
private readonly navigator: IRacingDomNavigator;
private readonly interactor: IRacingDomInteractor;
private readonly authService: IAuthenticationService;
private readonly logger?: ILogger;
private readonly authService: AuthenticationServicePort;
private readonly logger?: LoggerPort;
private readonly totalSteps: number;
private readonly getCheckoutConfirmationCallbackInternal: WizardStepOrchestratorDeps['getCheckoutConfirmationCallback'];
private readonly overlay: WizardStepOrchestratorDeps['overlay'];
@@ -139,7 +137,7 @@ export class WizardStepOrchestrator {
await this.guards.dismissModals();
}
private async handleLogin(): Promise<AutomationResult> {
private async handleLogin(): Promise<AutomationResultDTO> {
return this.helpers.handleLogin();
}
@@ -147,14 +145,14 @@ export class WizardStepOrchestrator {
await this.navigator.waitForStep(stepNumber);
}
private async clickAction(action: string): Promise<ClickResult> {
private async clickAction(action: string): Promise<ClickResultDTO> {
return this.interactor.clickAction(action);
}
private async fillFieldWithFallback(
fieldName: string,
value: string,
): Promise<FormFillResult> {
): Promise<FormFillResultDTO> {
return this.interactor.fillFieldWithFallback(fieldName, value);
}
@@ -200,7 +198,7 @@ export class WizardStepOrchestrator {
private async fillField(
fieldName: string,
value: string,
): Promise<FormFillResult> {
): Promise<FormFillResultDTO> {
return this.interactor.fillField(fieldName, value);
}
@@ -266,7 +264,7 @@ export class WizardStepOrchestrator {
async executeStep(
stepId: StepId,
config: Record<string, unknown>,
): Promise<AutomationResult> {
): Promise<AutomationResultDTO> {
if (!this.page) {
return { success: false, error: 'Browser not connected' };
}

View File

@@ -1,11 +1,9 @@
import type { Page } from 'playwright';
import type { ILogger } from '../../../../application/ports/ILogger';
import type {
FormFillResult,
ClickResult,
ModalResult,
} from '../../../../application/ports/AutomationResults';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { IRACING_SELECTORS, IRACING_TIMEOUTS } from './IRacingSelectors';
@@ -17,7 +15,7 @@ export class IRacingDomInteractor {
private readonly config: Required<PlaywrightConfig>,
private readonly browserSession: PlaywrightBrowserSession,
private readonly safeClickService: SafeClickService,
private readonly logger?: ILogger,
private readonly logger?: LoggerPort,
) {}
private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void {
@@ -42,7 +40,7 @@ export class IRacingDomInteractor {
// ===== Public port-facing operations =====
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
async fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
@@ -104,7 +102,7 @@ export class IRacingDomInteractor {
}
}
async clickElement(target: string): Promise<ClickResult> {
async clickElement(target: string): Promise<ClickResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, target, error: 'Browser not connected' };
@@ -124,7 +122,7 @@ export class IRacingDomInteractor {
}
}
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
async handleModal(stepId: StepId, action: string): Promise<ModalResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, stepId: stepId.value, action, error: 'Browser not connected' };
@@ -156,7 +154,7 @@ export class IRacingDomInteractor {
// ===== Public interaction helpers used by adapter steps =====
async fillField(fieldName: string, value: string): Promise<FormFillResult> {
async fillField(fieldName: string, value: string): Promise<FormFillResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
@@ -208,7 +206,7 @@ export class IRacingDomInteractor {
}
}
async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResult> {
async fillFieldWithFallback(fieldName: string, value: string): Promise<FormFillResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, fieldName, valueSet: value, error: 'Browser not connected' };
@@ -249,7 +247,7 @@ export class IRacingDomInteractor {
}
}
async clickAction(action: string): Promise<ClickResult> {
async clickAction(action: string): Promise<ClickResultDTO> {
const page = this.browserSession.getPage();
if (!page) {
return { success: false, target: action, error: 'Browser not connected' };

View File

@@ -1,6 +1,7 @@
import type { Page } from 'playwright';
import type { ILogger } from '../../../../application/ports/ILogger';
import type { NavigationResult, WaitResult } from '../../../../application/ports/AutomationResults';
import type { LoggerPort } from '../../../../application/ports/LoggerPort';
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
import { IRACING_SELECTORS, IRACING_TIMEOUTS, IRACING_URLS } from './IRacingSelectors';
@@ -23,7 +24,7 @@ export class IRacingDomNavigator {
constructor(
private readonly config: Required<PlaywrightConfig>,
private readonly browserSession: PlaywrightBrowserSession,
private readonly logger?: ILogger,
private readonly logger?: LoggerPort,
private readonly onWizardDismissed?: () => Promise<void>,
) {}
@@ -43,7 +44,7 @@ export class IRacingDomNavigator {
return this.browserSession.getPage();
}
async navigateToPage(url: string): Promise<NavigationResult> {
async navigateToPage(url: string): Promise<NavigationResultDTO> {
const page = this.getPage();
if (!page) {
return { success: false, url, loadTime: 0, error: 'Browser not connected' };
@@ -78,7 +79,7 @@ export class IRacingDomNavigator {
}
}
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResult> {
async waitForElement(target: string, maxWaitMs?: number): Promise<WaitResultDTO> {
const page = this.getPage();
if (!page) {
return { success: false, target, waitedMs: 0, found: false, error: 'Browser not connected' };

View File

@@ -1,5 +1,5 @@
import type { Page } from 'playwright';
import type { ILogger } from '../../../../application/ports/ILogger';
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import { IRACING_SELECTORS, BLOCKED_KEYWORDS } from './IRacingSelectors';
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';
@@ -8,7 +8,7 @@ export class SafeClickService {
constructor(
private readonly config: Required<PlaywrightConfig>,
private readonly browserSession: PlaywrightBrowserSession,
private readonly logger?: ILogger,
private readonly logger?: LoggerPort,
) {}
private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void {

View File

@@ -1,9 +1,14 @@
import { IAutomationEngine, ValidationResult } from '../../../../application/ports/IAutomationEngine';
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
import { ISessionRepository } from '../../../../application/ports/ISessionRepository';
import { StepTransitionValidator } from '@gridpilot/automation/domain/services/StepTransitionValidator';
import type { AutomationEnginePort } from '../../../../application/ports/AutomationEnginePort';
import { HostedSessionConfig } from '../../../../domain/entities/HostedSessionConfig';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
type ValidationResult = {
isValid: boolean;
error?: string;
};
/**
* Real Automation Engine Adapter.
@@ -22,13 +27,13 @@ import { StepTransitionValidator } from '@gridpilot/automation/domain/services/S
* browser automation when available. See docs/ARCHITECTURE.md
* for the updated automation strategy.
*/
export class AutomationEngineAdapter implements IAutomationEngine {
export class AutomationEngineAdapter implements AutomationEnginePort {
private isRunning = false;
private automationPromise: Promise<void> | null = null;
constructor(
private readonly browserAutomation: IBrowserAutomation,
private readonly sessionRepository: ISessionRepository
private readonly sessionRepository: SessionRepositoryPort
) {}
async validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult> {

View File

@@ -1,17 +1,22 @@
import { IAutomationEngine, ValidationResult } from '../../../../application/ports/IAutomationEngine';
import { HostedSessionConfig } from '@gridpilot/automation/domain/entities/HostedSessionConfig';
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
import { ISessionRepository } from '../../../../application/ports/ISessionRepository';
import { StepTransitionValidator } from '@gridpilot/automation/domain/services/StepTransitionValidator';
import type { AutomationEnginePort } from '../../../../application/ports/AutomationEnginePort';
import { HostedSessionConfig } from '../../../../domain/entities/HostedSessionConfig';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { SessionRepositoryPort } from '../../../../application/ports/SessionRepositoryPort';
import { StepTransitionValidator } from '../../../../domain/services/StepTransitionValidator';
export class MockAutomationEngineAdapter implements IAutomationEngine {
type ValidationResult = {
isValid: boolean;
error?: string;
};
export class MockAutomationEngineAdapter implements AutomationEnginePort {
private isRunning = false;
private automationPromise: Promise<void> | null = null;
constructor(
private readonly browserAutomation: IBrowserAutomation,
private readonly sessionRepository: ISessionRepository
private readonly sessionRepository: SessionRepositoryPort
) {}
async validateConfiguration(config: HostedSessionConfig): Promise<ValidationResult> {

View File

@@ -1,13 +1,11 @@
import { StepId } from '@gridpilot/automation/domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/IScreenAutomation';
import {
NavigationResult,
FormFillResult,
ClickResult,
WaitResult,
ModalResult,
AutomationResult,
} from '../../../../application/ports/AutomationResults';
import { StepId } from '../../../../domain/value-objects/StepId';
import type { IBrowserAutomation } from '../../../../application/ports/ScreenAutomationPort';
import type { NavigationResultDTO } from '../../../../application/dto/NavigationResultDTO';
import type { FormFillResultDTO } from '../../../../application/dto/FormFillResultDTO';
import type { ClickResultDTO } from '../../../../application/dto/ClickResultDTO';
import type { WaitResultDTO } from '../../../../application/dto/WaitResultDTO';
import type { ModalResultDTO } from '../../../../application/dto/ModalResultDTO';
import type { AutomationResultDTO } from '../../../../application/dto/AutomationResultDTO';
interface MockConfig {
simulateFailures?: boolean;
@@ -37,7 +35,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async connect(): Promise<AutomationResult> {
async connect(): Promise<AutomationResultDTO> {
this.connected = true;
return { success: true };
}
@@ -50,7 +48,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
return this.connected;
}
async navigateToPage(url: string): Promise<NavigationResult> {
async navigateToPage(url: string): Promise<NavigationResultDTO> {
const delay = this.randomDelay(200, 800);
await this.sleep(delay);
return {
@@ -60,7 +58,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async fillFormField(fieldName: string, value: string): Promise<FormFillResult> {
async fillFormField(fieldName: string, value: string): Promise<FormFillResultDTO> {
const delay = this.randomDelay(100, 500);
await this.sleep(delay);
return {
@@ -70,7 +68,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async clickElement(selector: string): Promise<ClickResult> {
async clickElement(selector: string): Promise<ClickResultDTO> {
const delay = this.randomDelay(50, 300);
await this.sleep(delay);
return {
@@ -79,7 +77,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async waitForElement(selector: string, maxWaitMs: number = 5000): Promise<WaitResult> {
async waitForElement(selector: string, maxWaitMs: number = 5000): Promise<WaitResultDTO> {
const delay = this.randomDelay(100, 1000);
await this.sleep(delay);
@@ -92,7 +90,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async handleModal(stepId: StepId, action: string): Promise<ModalResult> {
async handleModal(stepId: StepId, action: string): Promise<ModalResultDTO> {
if (!stepId.isModalStep()) {
throw new Error(`Step ${stepId.value} is not a modal step`);
}
@@ -106,7 +104,7 @@ export class MockBrowserAutomationAdapter implements IBrowserAutomation {
};
}
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResult> {
async executeStep(stepId: StepId, config: Record<string, unknown>): Promise<AutomationResultDTO> {
if (this.shouldSimulateFailure()) {
throw new Error(`Simulated failure at step ${stepId.value}`);
}

View File

@@ -5,11 +5,12 @@
import type { BrowserWindow } from 'electron';
import { ipcMain } from 'electron';
import { Result } from '../../../shared/result/Result';
import type { ICheckoutConfirmationPort, CheckoutConfirmationRequest } from '../../../application/ports/ICheckoutConfirmationPort';
import { CheckoutConfirmation } from '@gridpilot/automation/domain/value-objects/CheckoutConfirmation';
import { Result } from '../../../../shared/result/Result';
import type { CheckoutConfirmationPort } from '../../../application/ports/CheckoutConfirmationPort';
import type { CheckoutConfirmationRequestDTO } from '../../../application/dto/CheckoutConfirmationRequestDTO';
import { CheckoutConfirmation } from '../../../domain/value-objects/CheckoutConfirmation';
export class ElectronCheckoutConfirmationAdapter implements ICheckoutConfirmationPort {
export class ElectronCheckoutConfirmationAdapter implements CheckoutConfirmationPort {
private mainWindow: BrowserWindow;
private pendingConfirmation: {
resolve: (confirmation: CheckoutConfirmation) => void;
@@ -40,7 +41,7 @@ export class ElectronCheckoutConfirmationAdapter implements ICheckoutConfirmatio
}
async requestCheckoutConfirmation(
request: CheckoutConfirmationRequest
request: CheckoutConfirmationRequestDTO
): Promise<Result<CheckoutConfirmation>> {
try {
// Only allow one pending confirmation at a time

View File

@@ -1,6 +1,7 @@
import type { ILogger, LogContext } from '../../../application/ports/ILogger';
import type { LoggerPort } from '../../../application/ports/LoggerPort';
import type { LogContext } from '../../../application/ports/LoggerContext';
export class NoOpLogAdapter implements ILogger {
export class NoOpLogAdapter implements LoggerPort {
debug(_message: string, _context?: LogContext): void {}
info(_message: string, _context?: LogContext): void {}
@@ -11,7 +12,7 @@ export class NoOpLogAdapter implements ILogger {
fatal(_message: string, _error?: Error, _context?: LogContext): void {}
child(_context: LogContext): ILogger {
child(_context: LogContext): LoggerPort {
return this;
}

View File

@@ -1,4 +1,6 @@
import type { ILogger, LogContext, LogLevel } from '../../../application/ports/ILogger';
import type { LoggerPort } from '@gridpilot/automation/application/ports/LoggerPort';
import type { LogContext } from '@gridpilot/automation/application/ports/LoggerContext';
import type { LogLevel } from '@gridpilot/automation/application/ports/LoggerLogLevel';
import { loadLoggingConfig, type LoggingEnvironmentConfig } from '../../config/LoggingConfig';
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
@@ -18,7 +20,7 @@ const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
*
* This provides structured JSON logging to stdout with the same interface.
*/
export class PinoLogAdapter implements ILogger {
export class PinoLogAdapter implements LoggerPort {
private readonly config: LoggingEnvironmentConfig;
private readonly baseContext: LogContext;
private readonly levelPriority: number;
@@ -106,7 +108,7 @@ export class PinoLogAdapter implements ILogger {
this.log('fatal', message, context, error);
}
child(context: LogContext): ILogger {
child(context: LogContext): LoggerPort {
return new PinoLogAdapter(this.config, { ...this.baseContext, ...context });
}

View File

@@ -1,4 +1,4 @@
import type { LogLevel } from '../../application/ports/ILogger';
import type { LogLevel } from '@gridpilot/automation/application/ports/LoggerLogLevel';
export type LogEnvironment = 'development' | 'production' | 'test';

View File

@@ -1,8 +1,8 @@
import { AutomationSession } from '@gridpilot/automation/domain/entities/AutomationSession';
import { SessionStateValue } from '@gridpilot/automation/domain/value-objects/SessionState';
import { ISessionRepository } from '../../application/ports/ISessionRepository';
import { AutomationSession } from '../../domain/entities/AutomationSession';
import { SessionStateValue } from '../../domain/value-objects/SessionState';
import type { SessionRepositoryPort } from '../../application/ports/SessionRepositoryPort';
export class InMemorySessionRepository implements ISessionRepository {
export class InMemorySessionRepository implements SessionRepositoryPort {
private sessions: Map<string, AutomationSession> = new Map();
async save(session: AutomationSession): Promise<void> {

View File

@@ -1,7 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"rootDir": "..",
"outDir": "dist",
"declaration": true,
"declarationMap": false