feat(soak): core types + Deferred primitive
Establishes the Scenario/Session/Logger/DashboardReporter contracts the rest of the harness builds on. Deferred is the building block for RoomCoordinator's host→joiners handoff. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
20
tests/soak/core/deferred.ts
Normal file
20
tests/soak/core/deferred.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Promise deferred primitive — lets external code resolve or reject
|
||||
* a promise. Used by RoomCoordinator for host→joiners handoff.
|
||||
*/
|
||||
|
||||
export interface Deferred<T> {
|
||||
promise: Promise<T>;
|
||||
resolve(value: T): void;
|
||||
reject(error: unknown): void;
|
||||
}
|
||||
|
||||
export function deferred<T>(): Deferred<T> {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (error: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
185
tests/soak/core/types.ts
Normal file
185
tests/soak/core/types.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Core type definitions for the soak harness.
|
||||
*
|
||||
* Contracts here are consumed by runner.ts, SessionPool, scenarios,
|
||||
* and the dashboard. Keep this file small and stable.
|
||||
*/
|
||||
|
||||
import type { BrowserContext, Page } from 'playwright-core';
|
||||
|
||||
// =============================================================================
|
||||
// GolfBot structural interface
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Structural interface for the real `GolfBot` class from
|
||||
* `tests/e2e/bot/golf-bot.ts`. We can't type-import the real class
|
||||
* because (a) it lives outside this package's `rootDir`, and (b) it
|
||||
* imports `@playwright/test` which isn't in this package's deps.
|
||||
*
|
||||
* Instead we declare the narrow public contract the soak harness
|
||||
* actually calls. `SessionPool` constructs the real class at runtime
|
||||
* via a dynamic require and casts it to this interface. When golf-bot
|
||||
* gains new methods the harness wants, add them here — TypeScript will
|
||||
* flag drift at the first call site.
|
||||
*/
|
||||
|
||||
export type GamePhase =
|
||||
| 'lobby'
|
||||
| 'waiting_for_flip'
|
||||
| 'playing'
|
||||
| 'round_over'
|
||||
| 'game_over'
|
||||
| 'unknown';
|
||||
|
||||
export interface StartGameOptions {
|
||||
holes?: number;
|
||||
decks?: number;
|
||||
initialFlips?: number;
|
||||
flipMode?: 'never' | 'always' | 'endgame';
|
||||
knockPenalty?: boolean;
|
||||
jokerMode?: 'none' | 'standard' | 'lucky-swing' | 'eagle-eye';
|
||||
}
|
||||
|
||||
export interface TurnResult {
|
||||
success: boolean;
|
||||
action: string;
|
||||
details?: Record<string, unknown>;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface GolfBot {
|
||||
readonly page: Page;
|
||||
goto(url?: string): Promise<void>;
|
||||
createGame(playerName: string): Promise<string>;
|
||||
joinGame(roomCode: string, playerName: string): Promise<void>;
|
||||
addCPU(profileName?: string): Promise<void>;
|
||||
startGame(options?: StartGameOptions): Promise<void>;
|
||||
isMyTurn(): Promise<boolean>;
|
||||
waitForMyTurn(timeout?: number): Promise<boolean>;
|
||||
getGamePhase(): Promise<GamePhase>;
|
||||
getGameState(): Promise<Record<string, unknown>>;
|
||||
playTurn(): Promise<TurnResult>;
|
||||
completeInitialFlips(): Promise<void>;
|
||||
isFrozen(timeout?: number): Promise<boolean>;
|
||||
takeScreenshot(label: string): Promise<Buffer>;
|
||||
getConsoleErrors?(): string[];
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Accounts & sessions
|
||||
// =============================================================================
|
||||
|
||||
export interface Account {
|
||||
/** Stable key used in logs, e.g. "soak_00". */
|
||||
key: string;
|
||||
username: string;
|
||||
password: string;
|
||||
/** JWT returned from /api/auth/login, may be refreshed by SessionPool. */
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
account: Account;
|
||||
context: BrowserContext;
|
||||
page: Page;
|
||||
bot: GolfBot;
|
||||
/** Convenience mirror of account.key. */
|
||||
key: string;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Scenarios
|
||||
// =============================================================================
|
||||
|
||||
export interface ScenarioNeeds {
|
||||
/** Total number of authenticated sessions the scenario requires. */
|
||||
accounts: number;
|
||||
/** How many rooms to partition sessions into (default: 1). */
|
||||
rooms?: number;
|
||||
/** CPUs to add per room (default: 0). */
|
||||
cpusPerRoom?: number;
|
||||
}
|
||||
|
||||
/** Free-form per-scenario config merged with CLI flags. */
|
||||
export type ScenarioConfig = Record<string, unknown>;
|
||||
|
||||
export interface ScenarioError {
|
||||
room: string;
|
||||
reason: string;
|
||||
detail?: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface ScenarioResult {
|
||||
gamesCompleted: number;
|
||||
errors: ScenarioError[];
|
||||
durationMs: number;
|
||||
customMetrics?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface ScenarioContext {
|
||||
/** Merged config: CLI flags → env → scenario defaults → runner defaults. */
|
||||
config: ScenarioConfig;
|
||||
/** Pre-authenticated sessions; ordered. */
|
||||
sessions: Session[];
|
||||
coordinator: RoomCoordinatorApi;
|
||||
dashboard: DashboardReporter;
|
||||
logger: Logger;
|
||||
signal: AbortSignal;
|
||||
/** Reset the per-room watchdog. Call at each progress point. */
|
||||
heartbeat(roomId: string): void;
|
||||
}
|
||||
|
||||
export interface Scenario {
|
||||
name: string;
|
||||
description: string;
|
||||
defaultConfig: ScenarioConfig;
|
||||
needs: ScenarioNeeds;
|
||||
run(ctx: ScenarioContext): Promise<ScenarioResult>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Room coordination
|
||||
// =============================================================================
|
||||
|
||||
export interface RoomCoordinatorApi {
|
||||
announce(roomId: string, code: string): void;
|
||||
await(roomId: string, timeoutMs?: number): Promise<string>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Dashboard reporter
|
||||
// =============================================================================
|
||||
|
||||
export interface RoomState {
|
||||
phase?: string;
|
||||
currentPlayer?: string;
|
||||
hole?: number;
|
||||
totalHoles?: number;
|
||||
game?: number;
|
||||
totalGames?: number;
|
||||
moves?: number;
|
||||
players?: Array<{ key: string; score: number | null; isActive: boolean }>;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface DashboardReporter {
|
||||
update(roomId: string, state: Partial<RoomState>): void;
|
||||
log(level: 'info' | 'warn' | 'error', msg: string, meta?: object): void;
|
||||
incrementMetric(name: string, by?: number): void;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Logger
|
||||
// =============================================================================
|
||||
|
||||
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
export interface Logger {
|
||||
debug(msg: string, meta?: object): void;
|
||||
info(msg: string, meta?: object): void;
|
||||
warn(msg: string, meta?: object): void;
|
||||
error(msg: string, meta?: object): void;
|
||||
child(meta: object): Logger;
|
||||
}
|
||||
Reference in New Issue
Block a user