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:
adlee-was-taken
2026-04-11 17:25:04 -04:00
parent 2c20b6c7b5
commit 6df81e6f8d
2 changed files with 227 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
import { describe, it, expect } from 'vitest';
import { parseArgs, mergeConfig } from '../config';
describe('parseArgs', () => {
it('parses --scenario and numeric flags', () => {
const r = parseArgs(['--scenario=populate', '--rooms=4', '--games-per-room=10']);
expect(r.scenario).toBe('populate');
expect(r.rooms).toBe(4);
expect(r.gamesPerRoom).toBe(10);
});
it('parses watch mode', () => {
const r = parseArgs(['--scenario=populate', '--watch=none']);
expect(r.watch).toBe('none');
});
it('rejects unknown watch mode', () => {
expect(() => parseArgs(['--scenario=populate', '--watch=bogus'])).toThrow();
});
it('--list sets listOnly', () => {
const r = parseArgs(['--list']);
expect(r.listOnly).toBe(true);
});
it('--dry-run sets dryRun', () => {
const r = parseArgs(['--scenario=populate', '--dry-run']);
expect(r.dryRun).toBe(true);
});
it('parses --accounts, --cpus-per-room, --dashboard-port, --target, --run-id, --holes', () => {
const r = parseArgs([
'--scenario=stress',
'--accounts=8',
'--cpus-per-room=2',
'--dashboard-port=7777',
'--target=http://localhost:8000',
'--run-id=test-1',
'--holes=1',
]);
expect(r.accounts).toBe(8);
expect(r.cpusPerRoom).toBe(2);
expect(r.dashboardPort).toBe(7777);
expect(r.target).toBe('http://localhost:8000');
expect(r.runId).toBe('test-1');
expect(r.holes).toBe(1);
});
it('throws on non-numeric integer flag', () => {
expect(() => parseArgs(['--rooms=four'])).toThrow(/Invalid integer/);
});
});
describe('mergeConfig', () => {
it('CLI flags override scenario defaults', () => {
const cfg = mergeConfig(
{ gamesPerRoom: 20 },
{},
{ gamesPerRoom: 5, holes: 9 },
);
expect(cfg.gamesPerRoom).toBe(20);
expect(cfg.holes).toBe(9);
});
it('env overrides scenario defaults but CLI overrides env', () => {
const cfg = mergeConfig(
{ holes: 5 }, // CLI
{ SOAK_HOLES: '3' }, // env
{ holes: 9 }, // defaults
);
expect(cfg.holes).toBe(5); // CLI wins
});
it('env overrides scenario defaults when CLI is absent', () => {
const cfg = mergeConfig(
{}, // no CLI
{ SOAK_HOLES: '3' }, // env
{ holes: 9 }, // defaults
);
expect(cfg.holes).toBe(3); // env wins over defaults
});
it('scenario defaults fill in unset values', () => {
const cfg = mergeConfig(
{},
{},
{ gamesPerRoom: 3, holes: 9 },
);
expect(cfg.gamesPerRoom).toBe(3);
expect(cfg.holes).toBe(9);
});
it('env numeric keys are parsed to integers', () => {
const cfg = mergeConfig(
{},
{ SOAK_ROOMS: '4', SOAK_ACCOUNTS: '16' },
{},
);
expect(cfg.rooms).toBe(4);
expect(cfg.accounts).toBe(16);
});
});