feat(soak): CLI parsing + config precedence
parseArgs pulls --scenario/--rooms/--watch/etc from argv, mergeConfig layers scenarioDefaults → env → CLI so CLI flags always win. 12 Vitest unit tests cover both parse happy/edge paths and the 4-way merge precedence matrix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
125
tests/soak/config.ts
Normal file
125
tests/soak/config.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* CLI flag parsing and config precedence for the soak runner.
|
||||
*
|
||||
* Precedence (later wins):
|
||||
* runner defaults → scenario.defaultConfig → env vars → CLI flags
|
||||
*/
|
||||
|
||||
export type WatchMode = 'none' | 'dashboard' | 'tiled';
|
||||
|
||||
export interface CliArgs {
|
||||
scenario?: string;
|
||||
accounts?: number;
|
||||
rooms?: number;
|
||||
cpusPerRoom?: number;
|
||||
gamesPerRoom?: number;
|
||||
holes?: number;
|
||||
watch?: WatchMode;
|
||||
dashboardPort?: number;
|
||||
target?: string;
|
||||
runId?: string;
|
||||
dryRun?: boolean;
|
||||
listOnly?: boolean;
|
||||
}
|
||||
|
||||
const VALID_WATCH: WatchMode[] = ['none', 'dashboard', 'tiled'];
|
||||
|
||||
function parseInt10(s: string, name: string): number {
|
||||
const n = parseInt(s, 10);
|
||||
if (Number.isNaN(n)) throw new Error(`Invalid integer for ${name}: ${s}`);
|
||||
return n;
|
||||
}
|
||||
|
||||
export function parseArgs(argv: string[]): CliArgs {
|
||||
const out: CliArgs = {};
|
||||
for (const arg of argv) {
|
||||
if (arg === '--list') {
|
||||
out.listOnly = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === '--dry-run') {
|
||||
out.dryRun = true;
|
||||
continue;
|
||||
}
|
||||
const m = arg.match(/^--([a-z][a-z0-9-]*)=(.*)$/);
|
||||
if (!m) continue;
|
||||
const [, key, value] = m;
|
||||
switch (key) {
|
||||
case 'scenario':
|
||||
out.scenario = value;
|
||||
break;
|
||||
case 'accounts':
|
||||
out.accounts = parseInt10(value, '--accounts');
|
||||
break;
|
||||
case 'rooms':
|
||||
out.rooms = parseInt10(value, '--rooms');
|
||||
break;
|
||||
case 'cpus-per-room':
|
||||
out.cpusPerRoom = parseInt10(value, '--cpus-per-room');
|
||||
break;
|
||||
case 'games-per-room':
|
||||
out.gamesPerRoom = parseInt10(value, '--games-per-room');
|
||||
break;
|
||||
case 'holes':
|
||||
out.holes = parseInt10(value, '--holes');
|
||||
break;
|
||||
case 'watch':
|
||||
if (!VALID_WATCH.includes(value as WatchMode)) {
|
||||
throw new Error(
|
||||
`Invalid --watch value: ${value} (expected ${VALID_WATCH.join('|')})`,
|
||||
);
|
||||
}
|
||||
out.watch = value as WatchMode;
|
||||
break;
|
||||
case 'dashboard-port':
|
||||
out.dashboardPort = parseInt10(value, '--dashboard-port');
|
||||
break;
|
||||
case 'target':
|
||||
out.target = value;
|
||||
break;
|
||||
case 'run-id':
|
||||
out.runId = value;
|
||||
break;
|
||||
default:
|
||||
// Unknown flag — ignore so scenario-specific flags can slot in later
|
||||
break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge layers in precedence order: defaults → env → cli (later wins).
|
||||
*/
|
||||
export function mergeConfig(
|
||||
cli: Record<string, unknown>,
|
||||
env: Record<string, string | undefined>,
|
||||
defaults: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const merged: Record<string, unknown> = { ...defaults };
|
||||
|
||||
// Env overlay — SOAK_UPPER_SNAKE → lowerCamel in cli space.
|
||||
const envMap: Record<string, string> = {
|
||||
SOAK_HOLES: 'holes',
|
||||
SOAK_ROOMS: 'rooms',
|
||||
SOAK_ACCOUNTS: 'accounts',
|
||||
SOAK_CPUS_PER_ROOM: 'cpusPerRoom',
|
||||
SOAK_GAMES_PER_ROOM: 'gamesPerRoom',
|
||||
SOAK_WATCH: 'watch',
|
||||
SOAK_DASHBOARD_PORT: 'dashboardPort',
|
||||
};
|
||||
const numericKeys = /^(holes|rooms|accounts|cpusPerRoom|gamesPerRoom|dashboardPort)$/;
|
||||
for (const [envKey, cfgKey] of Object.entries(envMap)) {
|
||||
const v = env[envKey];
|
||||
if (v !== undefined) {
|
||||
merged[cfgKey] = numericKeys.test(cfgKey) ? parseInt(v, 10) : v;
|
||||
}
|
||||
}
|
||||
|
||||
// CLI overlay — wins over env and defaults.
|
||||
for (const [k, v] of Object.entries(cli)) {
|
||||
if (v !== undefined) merged[k] = v;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
Reference in New Issue
Block a user