- Session loop now handles round_over by clicking #ss-next-btn (the
scoresheet modal button) instead of exiting early. Waits for next
round or game_over before continuing.
- SessionPool detects expired tokens on acquire and re-logins
automatically, writing fresh credentials to .env.stresstest.
- Added 2s post-game delay before goto('/') so the server can process
game completion before WebSocket disconnect.
- Wired dashboard metrics (games_completed, moves_total, errors),
activity log entries, and player tiles for both populate and stress
scenarios.
- Bumped screencast resolution to 960x540 and set headless viewport
to 960x800 for better click-to-watch framing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
/**
|
|
* Stress scenario — rapid short games with chaos injection.
|
|
*
|
|
* Partitions sessions into N rooms (default 4), runs gamesPerRoom
|
|
* short 1-hole games per room in parallel. While each game plays,
|
|
* a background loop injects chaos events (rapid clicks, tab blur,
|
|
* brief offline) with 5% per-turn probability to hunt race
|
|
* conditions and recovery bugs.
|
|
*/
|
|
|
|
import type {
|
|
Scenario,
|
|
ScenarioContext,
|
|
ScenarioResult,
|
|
ScenarioError,
|
|
Session,
|
|
} from '../core/types';
|
|
import { runOneMultiplayerGame } from './shared/multiplayer-game';
|
|
import { maybeInjectChaos } from './shared/chaos';
|
|
|
|
interface StressConfig {
|
|
gamesPerRoom: number;
|
|
holes: number;
|
|
decks: number;
|
|
rooms: number;
|
|
cpusPerRoom: number;
|
|
thinkTimeMs: [number, number];
|
|
interGamePauseMs: number;
|
|
chaosChance: number;
|
|
}
|
|
|
|
function chunk<T>(arr: T[], size: number): T[][] {
|
|
const out: T[][] = [];
|
|
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
return out;
|
|
}
|
|
|
|
async function sleep(ms: number): Promise<void> {
|
|
return new Promise((r) => setTimeout(r, ms));
|
|
}
|
|
|
|
async function runStressRoom(
|
|
ctx: ScenarioContext,
|
|
cfg: StressConfig,
|
|
roomIdx: number,
|
|
sessions: Session[],
|
|
): Promise<{ completed: number; errors: ScenarioError[]; chaosFired: number }> {
|
|
const roomId = `room-${roomIdx}`;
|
|
let completed = 0;
|
|
let chaosFired = 0;
|
|
const errors: ScenarioError[] = [];
|
|
|
|
ctx.dashboard.update(roomId, {
|
|
players: sessions.map((s) => ({ key: s.key, score: null, isActive: false })),
|
|
});
|
|
|
|
for (let gameNum = 0; gameNum < cfg.gamesPerRoom; gameNum++) {
|
|
if (ctx.signal.aborted) break;
|
|
|
|
ctx.dashboard.update(roomId, { game: gameNum + 1, totalGames: cfg.gamesPerRoom });
|
|
ctx.dashboard.log('info', `${roomId}: starting game ${gameNum + 1}/${cfg.gamesPerRoom}`);
|
|
|
|
// Background chaos loop — runs concurrently with the game turn loop.
|
|
// Delay the first tick by 3 seconds so room creation + joiners + game
|
|
// start have time to complete without chaos interference (rapid clicks
|
|
// or brief_offline during lobby setup can prevent #create-room-btn
|
|
// from becoming stable).
|
|
let chaosActive = true;
|
|
const chaosLoop = (async () => {
|
|
await sleep(3000);
|
|
while (chaosActive && !ctx.signal.aborted) {
|
|
await sleep(500);
|
|
for (const session of sessions) {
|
|
const e = await maybeInjectChaos(
|
|
session,
|
|
cfg.chaosChance,
|
|
ctx.logger,
|
|
roomId,
|
|
);
|
|
if (e) chaosFired++;
|
|
}
|
|
}
|
|
})();
|
|
|
|
const result = await runOneMultiplayerGame(ctx, sessions, {
|
|
roomId,
|
|
holes: cfg.holes,
|
|
decks: cfg.decks,
|
|
cpusPerRoom: cfg.cpusPerRoom,
|
|
thinkTimeMs: cfg.thinkTimeMs,
|
|
});
|
|
|
|
chaosActive = false;
|
|
await chaosLoop;
|
|
|
|
if (result.completed) {
|
|
completed++;
|
|
ctx.dashboard.incrementMetric('games_completed');
|
|
ctx.dashboard.incrementMetric('moves_total', result.turns);
|
|
ctx.dashboard.log('info', `${roomId}: game ${gameNum + 1} complete — ${result.turns} turns`);
|
|
ctx.logger.info('game_complete', {
|
|
room: roomId,
|
|
game: gameNum + 1,
|
|
turns: result.turns,
|
|
});
|
|
} else {
|
|
ctx.dashboard.incrementMetric('errors');
|
|
ctx.dashboard.log('error', `${roomId}: game ${gameNum + 1} failed — ${result.error}`);
|
|
errors.push({
|
|
room: roomId,
|
|
reason: 'game_failed',
|
|
detail: result.error,
|
|
timestamp: Date.now(),
|
|
});
|
|
ctx.logger.error('game_failed', { room: roomId, error: result.error });
|
|
}
|
|
|
|
await sleep(cfg.interGamePauseMs);
|
|
}
|
|
|
|
return { completed, errors, chaosFired };
|
|
}
|
|
|
|
const stress: Scenario = {
|
|
name: 'stress',
|
|
description: 'Rapid short games for stability & race condition hunting',
|
|
needs: { accounts: 16, rooms: 4, cpusPerRoom: 2 },
|
|
defaultConfig: {
|
|
gamesPerRoom: 50,
|
|
holes: 1,
|
|
decks: 1,
|
|
rooms: 4,
|
|
cpusPerRoom: 2,
|
|
thinkTimeMs: [50, 150],
|
|
interGamePauseMs: 200,
|
|
chaosChance: 0.05,
|
|
},
|
|
|
|
async run(ctx: ScenarioContext): Promise<ScenarioResult> {
|
|
const start = Date.now();
|
|
const cfg = ctx.config as unknown as StressConfig;
|
|
const perRoom = Math.floor(ctx.sessions.length / cfg.rooms);
|
|
if (perRoom * cfg.rooms !== ctx.sessions.length) {
|
|
throw new Error(
|
|
`stress: ${ctx.sessions.length} sessions does not divide evenly into ${cfg.rooms} rooms`,
|
|
);
|
|
}
|
|
const roomSessions = chunk(ctx.sessions, perRoom);
|
|
|
|
const results = await Promise.allSettled(
|
|
roomSessions.map((s, idx) => runStressRoom(ctx, cfg, idx, s)),
|
|
);
|
|
|
|
let gamesCompleted = 0;
|
|
let chaosFired = 0;
|
|
const errors: ScenarioError[] = [];
|
|
results.forEach((r, idx) => {
|
|
if (r.status === 'fulfilled') {
|
|
gamesCompleted += r.value.completed;
|
|
chaosFired += r.value.chaosFired;
|
|
errors.push(...r.value.errors);
|
|
} else {
|
|
errors.push({
|
|
room: `room-${idx}`,
|
|
reason: 'room_threw',
|
|
detail: r.reason instanceof Error ? r.reason.message : String(r.reason),
|
|
timestamp: Date.now(),
|
|
});
|
|
}
|
|
});
|
|
|
|
return {
|
|
gamesCompleted,
|
|
errors,
|
|
durationMs: Date.now() - start,
|
|
customMetrics: { chaos_fired: chaosFired },
|
|
};
|
|
},
|
|
};
|
|
|
|
export default stress;
|