mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-02 17:40:02 +02:00
* feat: add surface-aware exploratory QA and ship documentation gates * test: preserve delegated QA setup authority after main integration * fix(qa): clarify exploration order and preserve report artifacts * test(qa): follow the shared setup reference directly * refactor(ship): make verification and recovery routes explicit * test(ship): align evidence and review guards with explicit routes * fix(workflows): clarify ship recovery and functional QA evidence * fix(workflows): clarify approval recovery and full QA coverage * refactor(workflows): order review transactions and clarify ship state * fix(ship): clarify final verification and fail closed at publication * fix(evals): attribute native atomic documentation writes * fix(ship): clarify recovery and documentation lifecycle guidance * fix(test): preserve observed native placeholder styling in CI * fix(codex): report watchdog timeouts without a process-exit race * Checkpoint functional QA implementation and workflow validation repairs * Fix documentation and shared-review fixture contracts * docs: clarify judge reuse and evaluation supervision * test: align review evidence and selected case contracts * test: verify append-only documentation checkpoints and recovery * fix: qualify QA workflows and CI validation repairs * fix: launch shared-libs fixture scripts on Windows * fix: qualify QA deadlines, fixture isolation, and shard cleanup * fix: preserve qualified QA and cancellation repairs * fix: enforce functional fixture authority and share strict event decoding * fix: retain free-test evidence and explain recovery * fix: reject malformed native evidence after decoder consolidation * test: use reliable capture for telemetry privacy filters * test: refresh measured quick coverage and document validation costs * Fix native fixture receipts and preserve VM validation evidence * Align negative judge controls with upstream clarity policy * Fix report-only QA preparation and public evidence handling * Clarify QA-only preparation and current-report preservation * Stream Ship quality judgments with an explicit 64k response contract * Validate compact judge reasoning locally with supported wire schema * Align functional QA fixture instructions with evidence acceptance * Bind native browser diagnostics to execution evidence and align review verdicts * Preserve native diagnostic line boundaries * Serialize functional QA evidence from native captures * Keep large QA evidence fixture payload out of Windows argv
301 lines
20 KiB
TypeScript
301 lines
20 KiB
TypeScript
import { expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { E2E_TOUCHFILES, selectTests } from './helpers/touchfiles';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
|
|
test('QA-only fixture regressions and owned sections select the no-fix consumer', () => {
|
|
expect(selectTests(['test/qa-only-fixture.test.ts'], E2E_TOUCHFILES).selected).toEqual(['qa-only-no-fix']);
|
|
for (const file of ['test/helpers/qa-browser-deadline-evidence.ts', 'test/qa-browser-deadline-evidence.test.ts', 'test/fixtures/qa-only-observation-public.json',
|
|
'test/fixtures/qa-only-browser-probe.ts', 'test/qa-only-browser-probe.test.ts',
|
|
'test/helpers/qa-only-cleanup.ts', 'test/qa-only-cleanup.test.ts']) {
|
|
expect(selectTests([file], E2E_TOUCHFILES).selected).toEqual(['qa-only-no-fix']);
|
|
}
|
|
expect(selectTests(['browse/test/fixtures/qa-only.html'], E2E_TOUCHFILES).selected).toEqual([
|
|
'aside-browse-basic', 'aside-browse-flow', 'qa-only-no-fix', 'carve-section-loading',
|
|
]);
|
|
expect(selectTests(['qa/sections/scope.md'], E2E_TOUCHFILES).selected).toContain('qa-only-no-fix');
|
|
});
|
|
|
|
test.each(['success', 'max-turns', 'dependencies', 'metadata', 'edit', 'source-write', 'source-delete', 'source-add', 'setup', 'runner', 'recording', 'runner-recording', 'artifact-error',
|
|
'process-error', 'slow-setup', 'exhausted-setup', 'late-timeout', 'late-retry', 'late-failure-retry', 'unknown-timeout',
|
|
'cleanup', 'cleanup-retry', 'runner-cleanup', 'cleanup-recording', 'remove-error', 'server-stop',
|
|
'missing-deadline', 'unguarded-probe', 'forged-receipt', 'deadline-write', 'reset-deadline',
|
|
'preparation-missing', 'preparation-failed', 'preparation-unacknowledged', 'preparation-wrong-path', 'preparation-late',
|
|
'preparation-subagent', 'preparation-crossed-parent', 'preparation-empty', 'preparation-delayed-ack'])('the registered QA-only attempt owns setup, recording and cleanup: %s', scenario => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'qa-only-body-'));
|
|
const script = path.join(dir, 'body.fixture.test.ts');
|
|
const factsPath = path.join(dir, 'facts.json');
|
|
fs.writeFileSync(script, `
|
|
import { afterAll, describe, expect, mock, spyOn, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
const root = ${JSON.stringify(ROOT)}, scenario = ${JSON.stringify(scenario)};
|
|
const directories = [], attempts = [], records = [], servers = [], cleanups = [];
|
|
const { stopQaOnlyBrowser } = await import(path.join(root, 'test/helpers/qa-only-cleanup.ts'));
|
|
mock.module(path.join(root, 'test/helpers/qa-only-cleanup.ts'), () => ({ stopQaOnlyBrowser: async (cwd, budget) => {
|
|
const artifact = attempts.at(-1) && path.join(${JSON.stringify(dir)}, 'e2e-runs', attempts.at(-1).runId, 'qa-reports/qa-only-report.md');
|
|
const fact = { cwd, budget, done: false, artifactExists: !!artifact && fs.existsSync(artifact) };
|
|
cleanups.push(fact);
|
|
try { await stopQaOnlyBrowser(cwd, budget); } finally { fact.done = true; }
|
|
} }));
|
|
const originalRm = fs.rmSync;
|
|
if (scenario === 'remove-error') spyOn(fs, 'rmSync').mockImplementation((target, options) => {
|
|
if (directories.includes(target)) throw new Error('QA-only removal failed');
|
|
return originalRm(target, options);
|
|
});
|
|
mock.module(path.join(root, 'test/helpers/eval-store.ts'), () => ({ getProjectEvalDir: () => ${JSON.stringify(path.join(dir, 'evals'))} }));
|
|
const late = scenario.startsWith('late-'), workMs = 1000, drainMs = 80;
|
|
const realNow = Date.now, controlledClock = ['slow-setup', 'exhausted-setup'].includes(scenario);
|
|
const clockStart = realNow();
|
|
let clock = clockStart;
|
|
if (controlledClock) Date.now = () => clock;
|
|
let finalized = false, outerMs;
|
|
const collector = { addTest: row => {
|
|
const cwd = directories.at(-1);
|
|
records.push({ ...row, elapsedMs: Date.now() - clockStart, afterFinalization: finalized, fixtureExists: fs.existsSync(cwd), serverStopped: servers.at(-1).stopped, cleanupDone: cleanups.at(-1)?.done });
|
|
if (['recording', 'runner-recording', 'cleanup-recording'].includes(scenario)) throw new Error('QA-only recorder failed');
|
|
} };
|
|
mock.module(path.join(root, 'test/helpers/aside-available.ts'), () => ({ asideAvailable: () => false }));
|
|
mock.module(path.join(root, 'browse/test/test-server.ts'), () => ({ startTestServer: () => {
|
|
const server = Bun.serve({ port: 0, fetch: () => new Response('owned test server') });
|
|
const fact = { stopped: false, url: 'http://127.0.0.1:' + server.port };
|
|
const stop = server.stop.bind(server);
|
|
server.stop = () => { fact.stopped = true; stop(true); if (scenario === 'server-stop') throw new Error('QA-only server stop failed'); };
|
|
servers.push(fact);
|
|
fact.cleanup = () => stop(true);
|
|
return { server, url: fact.url };
|
|
} }));
|
|
if (late) mock.module(path.join(root, 'test/helpers/eval-budgets.ts'), () => ({
|
|
JUDGE_MS: 120000, CAPTURE_MS: workMs, CAPTURE_LONG_MS: 600000,
|
|
}));
|
|
mock.module(path.join(root, 'test/helpers/e2e-helpers.ts'), () => ({
|
|
ROOT: root, browseBin: process.execPath, runId: 'qa-only-free', evalsEnabled: true, selectedTests: ['qa-only-no-fix'],
|
|
describeIfSelected: (name, ids, body) => { if (ids.includes('qa-only-no-fix')) describe(name, body); },
|
|
testConcurrentIfSelected: (id, body, budget) => {
|
|
if (id !== 'qa-only-no-fix') return;
|
|
outerMs = budget;
|
|
const deadline = late ? (budget >= workMs + 5000 ? budget - 5000 + 500 : budget) : 5000;
|
|
test(id, body, deadline);
|
|
},
|
|
copyDirSync: (source, target) => {
|
|
if (scenario === 'setup') throw new Error('QA-only setup failed');
|
|
fs.cpSync(source, target, { recursive: true });
|
|
},
|
|
setupBrowseShims: cwd => {
|
|
directories.push(cwd); fs.mkdirSync(path.join(cwd, 'browse/bin'), { recursive: true });
|
|
if (controlledClock) clock += scenario === 'slow-setup' ? 295000 : 300000;
|
|
},
|
|
logCost() {}, createEvalCollector: () => collector, finalizeEvalCollector: async () => { finalized = true; },
|
|
recordE2E: (_collector, name, suite, result, extra) => collector.addTest({ name, suite,
|
|
exit_reason: result.exitReason, model: result.model, cost_usd: result.costEstimate.estimatedCost,
|
|
transcript: result.transcript, ...extra }),
|
|
}));
|
|
mock.module(path.join(root, 'test/helpers/session-runner.ts'), () => ({
|
|
SESSION_DRAIN_GRACE_MS: late ? drainMs : 5000,
|
|
runSkillTest: async options => {
|
|
const cwd = options.workingDirectory;
|
|
const fact = { cwd, exists: fs.existsSync(cwd), recordedBeforeCapture: records.length,
|
|
reportBefore: fs.existsSync(path.join(cwd, 'qa-reports/qa-only-report.md')), captureMs: options.timeout,
|
|
runId: options.runId, publicStreamDiagnostics: options.publicStreamDiagnostics };
|
|
attempts.push(fact);
|
|
expect(options.maxTurns).toBe(40);
|
|
expect(options.timeout).toBeGreaterThan(0);
|
|
expect(options.timeout).toBeLessThanOrEqual(late ? workMs : 300000);
|
|
expect(options.tools).toEqual(['Bash', 'Read', 'Write', 'Glob']);
|
|
expect(options.allowedTools).toEqual(options.tools);
|
|
const result = { exitReason: 'success', model: 'fixture-model', toolCalls: [],
|
|
costEstimate: { estimatedCost: 0.25 }, transcript: [{ type: 'result', total_cost_usd: 0.25 }] };
|
|
if (!fact.exists) return { ...result, exitReason: 'exit_code_1' };
|
|
fact.initial = fs.readFileSync(path.join(cwd, 'index.html'), 'utf8');
|
|
if (['cleanup', 'runner-cleanup', 'cleanup-recording'].includes(scenario) || scenario === 'cleanup-retry' && attempts.length === 1) {
|
|
fs.mkdirSync(path.join(cwd, '.gstack'), { recursive: true });
|
|
fs.writeFileSync(path.join(cwd, '.gstack/browse.json'), '{}');
|
|
}
|
|
if (['runner', 'runner-recording', 'runner-cleanup'].includes(scenario)) throw new Error('QA-only runner failed');
|
|
if (scenario === 'dependencies') {
|
|
for (const relative of ['qa/sections/scope.md', 'qa/sections/browser-setup.md', 'qa/sections/qa-patterns.md',
|
|
'qa/sections/system-functional.md', 'qa/sections/exploratory.md', 'qa-only/sections/exploratory.md',
|
|
'qa/templates/qa-report-template.md', 'qa/templates/functional-report-template.md']) {
|
|
expect(fs.readFileSync(path.join(cwd, relative), 'utf8')).toBe(fs.readFileSync(path.join(root, relative), 'utf8'));
|
|
}
|
|
expect(options.prompt).toContain('owned installed skill assets');
|
|
for (const directory of ['qa/sections', 'qa-only/sections', 'qa/templates']) expect(options.prompt).toContain(path.join(cwd, directory));
|
|
expect(options.prompt).toContain('Do not discover or read an ambient skill installation');
|
|
expect(options.prompt).toContain(path.join(root, 'bin', 'gstack-qa-deadline'));
|
|
expect(options.prompt).toContain('never replace or edit it directly');
|
|
expect(options.prompt).toContain('Scope this run to homepage load and console health');
|
|
expect(options.prompt).toContain('Preserve that decoded object in checkpoints');
|
|
expect(options.prompt).toContain(path.join(cwd, 'fixture-browser-probe.ts'));
|
|
expect(fs.readFileSync(path.join(cwd, 'fixture-browser-probe.ts'), 'utf8')).toBe(fs.readFileSync(path.join(root, 'test/fixtures/qa-only-browser-probe.ts'), 'utf8'));
|
|
}
|
|
fs.mkdirSync(path.join(cwd, 'qa-reports'), { recursive: true });
|
|
fs.writeFileSync(path.join(cwd, 'qa-reports/qa-only-report.md'), 'owned report');
|
|
const reportCall = { tool: 'Write', input: {
|
|
file_path: path.join(cwd, 'qa-reports', scenario === 'preparation-wrong-path' ? 'other.md' : 'qa-only-report.md'),
|
|
content: scenario === 'preparation-empty' ? ' ' : 'owned report',
|
|
}, output: 'Write completed' };
|
|
if (scenario !== 'preparation-missing') result.toolCalls.push(reportCall);
|
|
fs.writeFileSync(path.join(cwd, 'qa-reports/checkpoint.json'), JSON.stringify({ attempt: attempts.length }));
|
|
if (scenario === 'artifact-error') fs.writeFileSync(${JSON.stringify(path.join(dir, 'e2e-runs'))}, 'blocked artifact directory');
|
|
if (scenario === 'slow-setup') {
|
|
clock += options.timeout + 5000;
|
|
fact.existsAfterDrain = fs.existsSync(cwd);
|
|
return { ...result, exitReason: 'timeout' };
|
|
}
|
|
if (late && (scenario !== 'late-retry' || attempts.length === 1)) {
|
|
await new Promise(resolve => setTimeout(resolve, options.timeout + drainMs));
|
|
fact.existsAfterDrain = fs.existsSync(cwd);
|
|
return { ...result, exitReason: 'timeout' };
|
|
}
|
|
if (scenario === 'unknown-timeout') return { ...result, exitReason: 'timeout', costEstimate: { estimatedCost: 0 }, transcript: [] };
|
|
if (scenario === 'edit') result.toolCalls.push({ tool: 'Edit', input: { file_path: path.join(cwd, 'index.html') } });
|
|
if (scenario === 'source-write') {
|
|
fs.writeFileSync(path.join(cwd, 'index.html'), 'modified source');
|
|
result.toolCalls.push({ tool: 'Write', input: { file_path: path.join(cwd, 'index.html') } });
|
|
}
|
|
if (scenario === 'source-delete') fs.unlinkSync(path.join(cwd, 'index.html'));
|
|
if (scenario === 'source-add') fs.writeFileSync(path.join(cwd, 'new-product-file.html'), 'unexpected product file');
|
|
if (scenario === 'max-turns') result.exitReason = 'error_max_turns';
|
|
if (scenario === 'process-error') result.exitReason = 'exit_code_1';
|
|
if (scenario !== 'missing-deadline') {
|
|
const guard = path.join(root, 'bin/gstack-qa-deadline'), deadline = path.join(cwd, 'qa-reports/deadline.json');
|
|
if (scenario === 'metadata') {
|
|
for (const args of [['rev-parse', 'HEAD'], ['rev-parse', '--short', 'HEAD'], ['log', '-1', '--format=%cI'], ['branch', '--show-current']]) {
|
|
expect(fs.existsSync(deadline)).toBe(false);
|
|
const child = spawnSync('git', ['-C', cwd, ...args], { encoding: 'utf8', timeout: 5000 });
|
|
expect(child.error).toBeUndefined();
|
|
expect(child.status, child.stderr).toBe(0);
|
|
expect(child.stdout.trim().length).toBeGreaterThan(0);
|
|
result.toolCalls.push({ tool: 'Bash', input: { command: "git -C '" + cwd + "' " + args.join(' ') }, output: child.stdout + child.stderr });
|
|
}
|
|
}
|
|
const invoke = args => {
|
|
const child = spawnSync(process.execPath, [guard, ...args], { cwd, encoding: 'utf8', timeout: 5000 });
|
|
if (child.error) throw child.error;
|
|
expect(child.status, child.stderr).toBe(0);
|
|
result.toolCalls.push({ tool: 'Bash', input: { command: [process.execPath, guard, ...args].map(arg => JSON.stringify(arg)).join(' ') }, output: child.stdout + child.stderr });
|
|
};
|
|
invoke(['start', deadline, '30']);
|
|
invoke(['run', deadline, '--', process.execPath, '--version']);
|
|
if (scenario === 'unguarded-probe') result.toolCalls.push({ tool: 'Bash', input: { command: process.execPath + ' --version' }, output: 'bare probe' });
|
|
if (scenario === 'forged-receipt') result.toolCalls.at(-1).output = result.toolCalls.at(-1).output.replace('"budgetMs":30000', '"budgetMs":90000');
|
|
if (scenario === 'deadline-write') result.toolCalls.push({ tool: 'Write', input: { file_path: deadline }, output: '' });
|
|
if (scenario === 'reset-deadline') { fs.unlinkSync(deadline); invoke(['start', deadline, '30']); }
|
|
}
|
|
if (scenario === 'preparation-late') { result.toolCalls.splice(result.toolCalls.indexOf(reportCall), 1); result.toolCalls.push(reportCall); }
|
|
const publicEvents = result.toolCalls.flatMap((call, index) => {
|
|
const id = 'native-' + index, isReport = call === reportCall;
|
|
const parent = isReport && scenario === 'preparation-subagent' ? 'child' : null;
|
|
return [{ type: 'assistant', parent_tool_use_id: parent, message: { content: [{ type: 'tool_use', id, name: call.tool, input: call.input }] } },
|
|
...(isReport && scenario === 'preparation-unacknowledged' ? [] : [{ type: 'user',
|
|
parent_tool_use_id: isReport && scenario === 'preparation-crossed-parent' ? 'child' : parent,
|
|
message: { content: [{ type: 'tool_result', tool_use_id: id, content: call.output ?? '',
|
|
is_error: isReport && scenario === 'preparation-failed' }] } }])];
|
|
});
|
|
if (scenario === 'preparation-delayed-ack') { const ack = publicEvents.splice(1, 1)[0]; publicEvents.splice(3, 0, ack); }
|
|
result.transcript.unshift(...publicEvents);
|
|
return result;
|
|
},
|
|
}));
|
|
afterAll(async () => {
|
|
finalized = true;
|
|
if (late) await new Promise(resolve => setTimeout(resolve, 300));
|
|
fs.writeFileSync(${JSON.stringify(factsPath)}, JSON.stringify({ directories, attempts, records, servers, cleanups, outerMs }));
|
|
for (const server of servers) server.cleanup();
|
|
Date.now = realNow;
|
|
});
|
|
await import(path.join(root, 'test/skill-e2e-qa-workflow.test.ts'));
|
|
`);
|
|
try {
|
|
const child = spawnSync(process.execPath, ['test', ...(scenario.endsWith('-retry') ? ['--retry', '1'] : []), script], {
|
|
cwd: ROOT, encoding: 'utf8', timeout: 15_000,
|
|
env: { ...process.env, EVALS: '', EVALS_ALL: '', EVALS_RUN_ID: 'qa-only-parent-run', TMPDIR: dir, TMP: dir, TEMP: dir },
|
|
});
|
|
expect(child.error).toBeUndefined();
|
|
const shouldFail = scenario.startsWith('preparation-') || ['edit', 'source-write', 'source-delete', 'source-add', 'setup', 'runner', 'recording', 'runner-recording', 'artifact-error', 'process-error', 'slow-setup', 'exhausted-setup', 'late-timeout', 'late-failure-retry', 'unknown-timeout', 'missing-deadline', 'unguarded-probe', 'forged-receipt', 'deadline-write', 'reset-deadline', 'cleanup', 'runner-cleanup', 'cleanup-recording', 'remove-error', 'server-stop'].includes(scenario);
|
|
expect(child.status, child.stdout + child.stderr).toBe(shouldFail ? 1 : 0);
|
|
expect(child.stderr).not.toContain('Unhandled error between tests');
|
|
const { directories, attempts, records, servers, cleanups, outerMs } = JSON.parse(fs.readFileSync(factsPath, 'utf8'));
|
|
const count = scenario.endsWith('-retry') ? 2 : 1;
|
|
expect(directories).toHaveLength(count);
|
|
expect(servers).toHaveLength(count);
|
|
expect(records).toHaveLength(count);
|
|
expect(cleanups).toHaveLength(count);
|
|
if (scenario.startsWith('preparation-')) expect(records[0].error).toContain('QA preparation:');
|
|
if (['missing-deadline', 'unguarded-probe', 'forged-receipt', 'deadline-write', 'reset-deadline'].includes(scenario)) expect(records[0].passed).toBe(false);
|
|
expect(attempts).toHaveLength(['setup', 'exhausted-setup'].includes(scenario) ? 0 : count);
|
|
for (const [index, cwd] of directories.entries()) {
|
|
const retained = ['artifact-error', 'cleanup', 'runner-cleanup', 'cleanup-recording', 'remove-error', 'server-stop'].includes(scenario) || scenario === 'cleanup-retry' && index === 0;
|
|
expect(fs.existsSync(cwd)).toBe(retained);
|
|
expect(servers[index].stopped).toBe(true);
|
|
expect(records[index].afterFinalization).toBe(false);
|
|
expect(records[index].fixtureExists).toBe(retained);
|
|
expect(records[index].serverStopped).toBe(true);
|
|
expect(records[index].cleanupDone).toBe(true);
|
|
expect(cleanups[index].budget).toBeGreaterThan(0);
|
|
expect(cleanups[index].budget).toBeLessThanOrEqual(4000);
|
|
expect(records[index].passed).toBe(scenario === 'recording' || !shouldFail && !(['late-retry', 'cleanup-retry'].includes(scenario) && index === 0));
|
|
if (scenario.includes('cleanup') && !(scenario === 'cleanup-retry' && index === 1)) expect(records[index].error).toContain('QA-only browser cleanup: invalid daemon identity');
|
|
}
|
|
for (const [index, attempt] of attempts.entries()) {
|
|
expect(attempt.exists).toBe(true);
|
|
expect(attempt.initial).toBe(fs.readFileSync(path.join(ROOT, 'browse/test/fixtures/qa-only.html'), 'utf8'));
|
|
expect(attempt.reportBefore).toBe(false);
|
|
expect(attempt.recordedBeforeCapture).toBe(index);
|
|
expect(attempt.publicStreamDiagnostics).toBe(true);
|
|
expect(attempt.runId).toMatch(new RegExp('^qa-only-parent-run-qa-only-\\d+-' + (index + 1) + '$'));
|
|
if (!scenario.startsWith('runner') && scenario !== 'artifact-error') {
|
|
expect(cleanups[index].artifactExists).toBe(true);
|
|
const preserved = path.join(dir, 'e2e-runs', attempt.runId, 'qa-reports');
|
|
expect(fs.readFileSync(path.join(preserved, 'qa-only-report.md'), 'utf8')).toBe('owned report');
|
|
expect(JSON.parse(fs.readFileSync(path.join(preserved, 'checkpoint.json'), 'utf8'))).toEqual({ attempt: index + 1 });
|
|
}
|
|
if (attempt.existsAfterDrain !== undefined) expect(attempt.existsAfterDrain).toBe(true);
|
|
}
|
|
if (scenario.startsWith('late-')) {
|
|
expect(outerMs).toBe(1000 + 80 + 5000);
|
|
expect(records[0]).toMatchObject({ passed: false, exit_reason: 'timeout', cost_usd: 0.25 });
|
|
expect(records[0].error).toContain('timeout');
|
|
if (count === 2) expect(directories[0]).not.toBe(directories[1]);
|
|
if (scenario === 'late-failure-retry') expect(records[1]).toMatchObject({ passed: false, exit_reason: 'timeout', cost_usd: 0.25 });
|
|
} else expect(outerMs).toBe(300000 + 5000 + 5000);
|
|
if (scenario === 'slow-setup') {
|
|
expect(attempts[0].captureMs).toBe(5000);
|
|
expect(records[0]).toMatchObject({ passed: false, exit_reason: 'timeout', elapsedMs: 305000 });
|
|
expect(outerMs - records[0].elapsedMs).toBe(5000);
|
|
}
|
|
if (scenario === 'exhausted-setup') {
|
|
expect(records[0].error).toContain('QA-only work budget exhausted');
|
|
expect(records[0].elapsedMs).toBe(300000);
|
|
expect(outerMs - records[0].elapsedMs).toBe(10000);
|
|
}
|
|
if (scenario === 'setup' || scenario === 'exhausted-setup' || scenario === 'runner' || scenario === 'runner-recording' || scenario === 'runner-cleanup') {
|
|
expect(records[0]).toMatchObject({ passed: false, exit_reason: 'harness_error', cost_usd: 0 });
|
|
expect(records[0].error).toContain('cost and usage unavailable');
|
|
if (scenario === 'runner-recording') {
|
|
expect(child.stderr).toContain('QA-only runner failed');
|
|
expect(child.stderr).toContain('QA-only recorder failed');
|
|
}
|
|
if (scenario === 'runner-cleanup') {
|
|
expect(records[0].error).toContain('QA-only runner failed');
|
|
expect(child.stderr).toContain('QA-only runner failed');
|
|
expect(child.stderr).toContain('QA-only browser cleanup: invalid daemon identity');
|
|
}
|
|
} else if (scenario === 'unknown-timeout') {
|
|
expect(records[0]).toMatchObject({ passed: false, exit_reason: 'timeout', cost_usd: 0 });
|
|
expect(records[0].error).toContain('actual cost is unknown');
|
|
} else expect(records[0].cost_usd).toBe(0.25);
|
|
if (scenario === 'cleanup-recording') {
|
|
expect(child.stderr).toContain('QA-only recorder failed');
|
|
expect(child.stderr).toContain('QA-only browser cleanup: invalid daemon identity');
|
|
}
|
|
} finally {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|