mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-03 01:46:55 +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
479 lines
22 KiB
TypeScript
479 lines
22 KiB
TypeScript
import { afterAll, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
|
|
const CLI = path.resolve(import.meta.dir, '../bin/gstack-qa-deadline');
|
|
const ROOT = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'qa-deadline-'));
|
|
const LEAF = path.join(ROOT, 'leaf.ts');
|
|
const DRIVER = path.join(ROOT, 'driver.ts');
|
|
fs.writeFileSync(LEAF, `
|
|
import { writeFileSync } from 'node:fs';
|
|
setInterval(() => {}, 1000);
|
|
await new Promise(resolve => process.stdout.write('leaf ready\\n', resolve));
|
|
writeFileSync(process.argv[2], String(process.pid));
|
|
`);
|
|
fs.writeFileSync(DRIVER, `
|
|
import { spawn } from 'node:child_process';
|
|
import { existsSync, writeFileSync } from 'node:fs';
|
|
writeFileSync(process.argv[5], String(process.pid));
|
|
const leaf = spawn(process.execPath, [process.argv[3], process.argv[4]], {
|
|
stdio: 'inherit', detached: process.platform === 'win32',
|
|
});
|
|
leaf.unref();
|
|
const readyBy = Date.now() + 3000;
|
|
while (!existsSync(process.argv[4])) {
|
|
if (Date.now() > readyBy) process.exit(11);
|
|
await Bun.sleep(10);
|
|
}
|
|
if (process.argv[2] === 'early') process.exit(7);
|
|
setInterval(() => {}, 1000);
|
|
`);
|
|
afterAll(() => fs.rmSync(ROOT, { recursive: true, force: true }));
|
|
|
|
function fixture() {
|
|
const dir = fs.mkdtempSync(path.join(ROOT, 'case-'));
|
|
return { dir, receipt: path.join(dir, 'deadline.json'), marker: path.join(dir, 'effect'),
|
|
leaf: path.join(dir, 'leaf.pid'), direct: path.join(dir, 'direct.pid') };
|
|
}
|
|
|
|
function cli(args: string[], preload?: string) {
|
|
return spawnSync(process.execPath, [...(preload ? ['--preload', preload] : []), CLI, ...args], {
|
|
encoding: 'utf8', timeout: 10_000,
|
|
});
|
|
}
|
|
|
|
function receipt(output: string) {
|
|
const line = output.trim();
|
|
expect(line.startsWith('QA_DEADLINE ')).toBe(true);
|
|
const value = JSON.parse(line.slice('QA_DEADLINE '.length));
|
|
expect(value.guard).toBe('qa-deadline');
|
|
return value;
|
|
}
|
|
|
|
function background(args: string[], preload?: string) {
|
|
const child = spawn(process.execPath, [...(preload ? ['--preload', preload] : []), CLI, ...args], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
let stdout = '', stderr = '';
|
|
child.stdout!.on('data', chunk => { stdout += chunk; });
|
|
child.stderr!.on('data', chunk => { stderr += chunk; });
|
|
const result = new Promise<{ code: number | null; signal: NodeJS.Signals | null; stdout: string; stderr: string }>((resolve, reject) => {
|
|
child.once('error', reject);
|
|
child.once('close', (code, signal) => resolve({ code, signal, stdout, stderr }));
|
|
});
|
|
return { child, result };
|
|
}
|
|
|
|
function alive(pid: number) {
|
|
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
}
|
|
|
|
async function ready(file: string) {
|
|
for (let i = 0; i < 300 && !fs.existsSync(file); i++) await Bun.sleep(10);
|
|
expect(fs.existsSync(file)).toBe(true);
|
|
const pid = Number(fs.readFileSync(file, 'utf8'));
|
|
expect(pid).toBeGreaterThan(0);
|
|
return pid;
|
|
}
|
|
|
|
async function dead(pid: number) {
|
|
for (let i = 0; i < 100 && alive(pid); i++) await Bun.sleep(20);
|
|
expect(alive(pid)).toBe(false);
|
|
}
|
|
|
|
function cleanup(files: string[]) {
|
|
for (const file of files) {
|
|
try {
|
|
const pid = Number(fs.readFileSync(file, 'utf8'));
|
|
if (Number.isSafeInteger(pid) && pid > 0 && alive(pid)) process.kill(pid, 'SIGKILL');
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
test('start publishes a validated immutable receipt and status reports real remaining time', () => {
|
|
const f = fixture();
|
|
const start = cli(['start', f.receipt, '30']);
|
|
expect(start.status, start.stderr).toBe(0);
|
|
expect(start.stdout.startsWith('\nQA_DEADLINE ')).toBe(true);
|
|
expect(start.stdout.endsWith('\n')).toBe(true);
|
|
const state = JSON.parse(fs.readFileSync(f.receipt, 'utf8'));
|
|
expect(state.version).toBe(1);
|
|
expect(Date.parse(state.deadlineAt) - Date.parse(state.startedAt)).toBe(30_000);
|
|
expect(state.budgetMs).toBe(30_000);
|
|
const status = cli(['status', f.receipt]);
|
|
expect(status.status).toBe(0);
|
|
const observed = receipt(status.stdout);
|
|
expect(observed.remainingMs).toBeGreaterThan(0);
|
|
expect(observed.remainingMs).toBeLessThanOrEqual(30_000);
|
|
expect(observed.expired).toBe(false);
|
|
expect(observed.remainingMs).toBe(Date.parse(observed.deadlineAt) - Date.parse(observed.observedAt));
|
|
const before = fs.readFileSync(f.receipt, 'utf8');
|
|
expect(cli(['start', f.receipt, '300']).status).toBe(2);
|
|
expect(fs.readFileSync(f.receipt, 'utf8')).toBe(before);
|
|
if (process.platform !== 'win32') expect(fs.statSync(f.receipt).mode & 0o777).toBe(0o400);
|
|
});
|
|
|
|
test('concurrent start cannot reset or publish partial state', async () => {
|
|
const f = fixture();
|
|
const results = await Promise.all([background(['start', f.receipt, '30']).result, background(['start', f.receipt, '60']).result]);
|
|
expect(results.map(result => result.code).sort()).toEqual([0, 2]);
|
|
expect(cli(['status', f.receipt]).status).toBe(0);
|
|
expect(fs.readdirSync(f.dir)).toEqual(['deadline.json']);
|
|
});
|
|
|
|
test('caller deadline clamps the receipt and a later caller deadline cannot extend it', () => {
|
|
const f = fixture();
|
|
const earlier = new Date(Date.now() + 10_000).toISOString();
|
|
const first = cli(['start', f.receipt, '30', earlier]);
|
|
expect(first.status, first.stderr).toBe(0);
|
|
expect(receipt(first.stdout).deadlineAt).toBe(earlier);
|
|
const second = cli(['start', path.join(f.dir, 'later.json'), '1', new Date(Date.now() + 60_000).toISOString()]);
|
|
const state = receipt(second.stdout);
|
|
expect(Date.parse(state.deadlineAt) - Date.parse(state.startedAt)).toBe(1000);
|
|
const fractional = cli(['start', path.join(f.dir, 'fractional.json'), '1.001']);
|
|
expect(receipt(fractional.stdout).budgetMs).toBe(1001);
|
|
});
|
|
|
|
test('R66 expired-before-probe refuses dispatch without any side effect', () => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '30', '2026-09-27T14:33:01Z']).status).toBe(124);
|
|
const result = cli(['run', f.receipt, '--', process.execPath, '-e', 'require("fs").writeFileSync(process.argv[1], "probed")', f.marker]);
|
|
expect(result.status).toBe(124);
|
|
expect(result.stdout).toBe('');
|
|
expect(receipt(result.stderr).event).toBe('expired');
|
|
expect(fs.existsSync(f.marker)).toBe(false);
|
|
const status = cli(['status', f.receipt]);
|
|
expect(status.status).toBe(124);
|
|
expect(receipt(status.stdout)).toMatchObject({ expired: true, remainingMs: 0 });
|
|
});
|
|
|
|
test('expiry during dispatch preparation is rechecked before creating a child', () => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '30']).status).toBe(0);
|
|
const preload = path.join(f.dir, 'clock-jump.ts');
|
|
fs.writeFileSync(preload, `const now = Date.now(); let reads = 0; Date.now = () => now + (reads++ ? 60000 : 0);`);
|
|
const result = cli(['run', f.receipt, '--', process.execPath, '-e', 'require("fs").writeFileSync(process.argv[1], "probed")', f.marker], preload);
|
|
expect(result.status).toBe(124);
|
|
expect(receipt(result.stderr).event).toBe('expired');
|
|
expect(fs.existsSync(f.marker)).toBe(false);
|
|
});
|
|
|
|
test.each(['0', '-1', 'NaN', 'Infinity', '2147484', '1e3', '0.0001', ''])('invalid duration fails closed: %s', seconds => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, seconds]).status).toBe(2);
|
|
expect(fs.existsSync(f.receipt)).toBe(false);
|
|
});
|
|
|
|
test.each(['not a date', '2026-02-30T00:00:00Z', '2026-09-27T14:33:01+00:00'])('invalid caller UTC fails closed: %s', earlier => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '30', earlier]).status).toBe(2);
|
|
expect(fs.existsSync(f.receipt)).toBe(false);
|
|
});
|
|
|
|
test.each(['missing', 'json', 'version', 'extended', 'future-start', 'directory'])('invalid guard state cannot dispatch: %s', mode => {
|
|
const f = fixture();
|
|
if (mode === 'directory') fs.mkdirSync(f.receipt);
|
|
else if (mode !== 'missing') {
|
|
const state = { version: mode === 'version' ? 2 : 1, startedAt: new Date(Date.now() + (mode === 'future-start' ? 60_000 : 0)).toISOString(),
|
|
deadlineAt: new Date(Date.now() + 120_000).toISOString(), budgetMs: mode === 'extended' ? 1000 : 120_000 };
|
|
fs.writeFileSync(f.receipt, mode === 'json' ? '{' : JSON.stringify(state));
|
|
}
|
|
const result = cli(['run', f.receipt, '--', process.execPath, '-e', 'require("fs").writeFileSync(process.argv[1], "probed")', f.marker]);
|
|
expect(result.status).toBe(2);
|
|
expect(fs.existsSync(f.marker)).toBe(false);
|
|
expect(cli(['status', f.receipt]).status).toBe(2);
|
|
if (!['missing', 'directory'].includes(mode)) {
|
|
const before = fs.readFileSync(f.receipt, 'utf8');
|
|
expect(cli(['start', f.receipt, '30']).status).toBe(2);
|
|
expect(fs.readFileSync(f.receipt, 'utf8')).toBe(before);
|
|
}
|
|
});
|
|
|
|
test('symlinked receipt or parent and malformed paths are refused', () => {
|
|
const f = fixture();
|
|
const parent = path.join(f.dir, 'linked-parent');
|
|
fs.symlinkSync(f.dir, parent, process.platform === 'win32' ? 'junction' : 'dir');
|
|
expect(cli(['start', path.join(parent, 'guard.json'), '30']).status).toBe(2);
|
|
expect(fs.existsSync(path.join(f.dir, 'guard.json'))).toBe(false);
|
|
if (process.platform !== 'win32') {
|
|
fs.symlinkSync(f.marker, f.receipt);
|
|
expect(cli(['start', f.receipt, '30']).status).toBe(2);
|
|
expect(cli(['run', f.receipt, '--', process.execPath, '-e', 'process.exit(0)']).status).toBe(2);
|
|
expect(fs.existsSync(f.marker)).toBe(false);
|
|
}
|
|
expect(cli(['start', f.dir + path.sep + '..' + path.sep + 'escaped.json', '30']).status).toBe(2);
|
|
expect(cli(['start', path.join(f.dir, 'missing', 'guard.json'), '30']).status).toBe(2);
|
|
});
|
|
|
|
test('argv with spaces and shell metacharacters is literal; stdout and nonzero child status are preserved', () => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '30']).status).toBe(0);
|
|
const argument = `spaces ; $(echo not-evaluated) & ${f.marker}`;
|
|
const result = cli(['run', f.receipt, '--', process.execPath, '-e', 'process.stdout.write(process.argv[1]); process.exit(17)', argument]);
|
|
expect(result.status).toBe(17);
|
|
expect(result.stdout).toBe(argument);
|
|
expect(result.stderr).not.toContain(argument);
|
|
expect(fs.existsSync(f.marker)).toBe(false);
|
|
expect(cli(['run', f.receipt, '--', path.join(f.dir, 'missing-command')]).status).toBe(127);
|
|
expect(cli(['run', f.receipt, process.execPath]).status).toBe(2);
|
|
});
|
|
|
|
test('Windows containment initialization failure refuses dispatch', () => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '30']).status).toBe(0);
|
|
const preload = path.join(f.dir, 'failed-job.ts');
|
|
fs.writeFileSync(preload, `Object.defineProperty(process, 'platform', { value: 'win32' }); Object.defineProperty(process, 'pid', { value: 0 });`);
|
|
const result = cli(['run', f.receipt, '--', process.execPath, '-e', 'require("fs").writeFileSync(process.argv[1], "probed")', f.marker], preload);
|
|
expect(result.status).toBe(2);
|
|
expect(result.stderr).toContain('Windows process containment is unavailable');
|
|
expect(fs.existsSync(f.marker)).toBe(false);
|
|
});
|
|
|
|
test('guard receipts cannot masquerade as extra native JSON in merged Bash output', () => {
|
|
const f = fixture();
|
|
expect(receipt(cli(['start', f.receipt, '30']).stdout).event).toBe('start');
|
|
const result = cli(['run', f.receipt, '--', process.execPath, '-e', 'console.log(JSON.stringify({native:true}))']);
|
|
expect(result.status).toBe(0);
|
|
expect(result.stdout).toBe('{"native":true}\n');
|
|
const events = result.stderr.split('\n').filter(line => line.startsWith('QA_DEADLINE ')).map(receipt);
|
|
expect(events.map(event => event.event)).toEqual(['started', 'finished']);
|
|
for (const event of events) {
|
|
expect(Number.isFinite(Date.parse(event.observedAt))).toBe(true);
|
|
expect(Number.isFinite(Date.parse(event.deadlineAt))).toBe(true);
|
|
}
|
|
expect((result.stdout + result.stderr).trim().split('\n').filter(line => line.startsWith('{'))).toHaveLength(1);
|
|
expect(receipt(cli(['run', f.receipt]).stderr).event).toBe('error');
|
|
});
|
|
|
|
test.each(['stdout', 'stderr', 'both'])('receipt framing survives newline-free child %s on one shared capture descriptor', stream => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '30']).status).toBe(0);
|
|
const capture = path.join(f.dir, 'merged-output');
|
|
const fd = fs.openSync(capture, 'w');
|
|
const payload = stream === 'both' ? 'stdout textstderr text' : '{"native":true}';
|
|
const command = stream === 'both'
|
|
? 'require("fs").writeSync(1, "stdout text"); require("fs").writeSync(2, "stderr text")'
|
|
: `require("fs").writeSync(${stream === 'stdout' ? 1 : 2}, ${JSON.stringify(payload)})`;
|
|
try {
|
|
const result = spawnSync(process.execPath, [CLI, 'run', f.receipt, '--', process.execPath, '-e', command], {
|
|
stdio: ['ignore', fd, fd], timeout: 10_000,
|
|
});
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status).toBe(0);
|
|
} finally {
|
|
fs.closeSync(fd);
|
|
}
|
|
const merged = fs.readFileSync(capture, 'utf8');
|
|
const lines = merged.split('\n');
|
|
const events = lines.filter(line => line.startsWith('QA_DEADLINE ')).map(receipt);
|
|
expect(events.map(event => event.event)).toEqual(['started', 'finished']);
|
|
expect(merged.replace(/\nQA_DEADLINE [^\n]*\n/g, '')).toBe(payload);
|
|
expect(merged.endsWith('\n')).toBe(true);
|
|
if (stream !== 'both') expect(JSON.parse(lines.find(line => line === payload)!)).toEqual({ native: true });
|
|
});
|
|
|
|
test.each(['start', 'expired', 'finished', 'timeout', 'blocked-forever'])('guard %s receipts survive a blocked output pipe without extending command execution', async mode => {
|
|
const f = fixture();
|
|
const timedOut = mode === 'timeout' || mode === 'blocked-forever';
|
|
if (mode !== 'start') expect(cli(['start', f.receipt, timedOut ? '2' : '30',
|
|
...(mode === 'expired' ? ['2000-01-01T00:00:00Z'] : [])]).status).toBe(mode === 'expired' ? 124 : 0);
|
|
const preload = path.join(f.dir, 'blocked-output.ts');
|
|
const queued = path.join(f.dir, 'queued');
|
|
fs.writeFileSync(preload, `
|
|
import * as fs from 'node:fs';
|
|
import { spyOn } from 'bun:test';
|
|
const createWriteStream = fs.createWriteStream;
|
|
spyOn(fs, 'createWriteStream').mockImplementation((...args) => {
|
|
const stream = createWriteStream(...args);
|
|
if (args[1]?.fd !== ${mode === 'start' ? 1 : 2}) return stream;
|
|
const write = stream.write.bind(stream);
|
|
let filled = false;
|
|
stream.write = (chunk, ...rest) => {
|
|
if (typeof chunk === 'string' && chunk.startsWith('\\nQA_DEADLINE ')) {
|
|
if (!filled) { filled = true; write(Buffer.alloc(2 * 1024 * 1024, 32)); write('\\n'); }
|
|
const result = write(chunk, ...rest);
|
|
fs.writeFileSync(${JSON.stringify(queued)}, 'queued');
|
|
return result;
|
|
}
|
|
return write(chunk, ...rest);
|
|
};
|
|
return stream;
|
|
});
|
|
`);
|
|
const args = mode === 'start' ? ['start', f.receipt, '30'] : mode === 'expired'
|
|
? ['run', f.receipt, '--', process.execPath, '-e', 'require("fs").writeFileSync(process.argv[1], "probed")', f.marker]
|
|
: ['run', f.receipt, '--', process.execPath, DRIVER, timedOut ? 'timeout' : 'early', LEAF, f.leaf, f.direct];
|
|
const runner = background(args, preload);
|
|
const blocked = mode === 'start' ? runner.child.stdout! : runner.child.stderr!;
|
|
blocked.pause();
|
|
try {
|
|
for (let i = 0; i < 300 && !fs.existsSync(queued); i++) await Bun.sleep(10);
|
|
expect(fs.existsSync(queued)).toBe(true);
|
|
if (mode === 'finished' || timedOut) {
|
|
await dead(await ready(f.leaf));
|
|
await dead(await ready(f.direct));
|
|
}
|
|
expect(runner.child.exitCode).toBeNull();
|
|
if (mode === 'blocked-forever') {
|
|
const code = await new Promise<number | null>(resolve => runner.child.once('exit', resolve));
|
|
expect(code).toBe(2);
|
|
blocked.resume();
|
|
await runner.result;
|
|
return;
|
|
}
|
|
blocked.resume();
|
|
const result = await runner.result;
|
|
expect(result.code).toBe(mode === 'start' ? 0 : mode === 'finished' ? 7 : 124);
|
|
const output = mode === 'start' ? result.stdout : result.stderr;
|
|
const events = output.trim().split('\n').filter(line => line.startsWith('QA_DEADLINE ')).map(receipt);
|
|
expect(events.map(event => event.event)).toEqual(mode === 'start' ? ['start'] : mode === 'expired' ? ['expired'] : ['started', 'finished']);
|
|
if (mode === 'expired') expect(fs.existsSync(f.marker)).toBe(false);
|
|
if (mode === 'timeout') expect(events.at(-1).timedOut).toBe(true);
|
|
} finally {
|
|
blocked.resume();
|
|
runner.child.kill('SIGKILL');
|
|
cleanup([f.leaf, f.direct]);
|
|
await runner.result;
|
|
}
|
|
}, 15_000);
|
|
|
|
test('native receipt writes cannot block the output-settlement deadline on a full pipe', async () => {
|
|
const f = fixture();
|
|
const preload = path.join(f.dir, 'full-pipe.ts');
|
|
fs.writeFileSync(preload, `
|
|
import { write } from 'node:fs';
|
|
write(1, Buffer.alloc(2 * 1024 * 1024, 32), () => {});
|
|
await Bun.sleep(100);
|
|
`);
|
|
const runner = background(['start', f.receipt, '30'], preload);
|
|
runner.child.stdout!.pause();
|
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
try {
|
|
const code = await Promise.race([
|
|
new Promise<number | null>(resolve => runner.child.once('exit', resolve)),
|
|
new Promise<null>(resolve => { timer = setTimeout(() => resolve(null), 7000); }),
|
|
]);
|
|
expect(fs.existsSync(f.receipt)).toBe(true);
|
|
expect(code).toBe(2);
|
|
} finally {
|
|
clearTimeout(timer);
|
|
runner.child.stdout!.resume();
|
|
runner.child.kill('SIGKILL');
|
|
await runner.result;
|
|
}
|
|
}, 15_000);
|
|
|
|
test('internal worker relays every guard receipt before its containment process exits', async () => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '30']).status).toBe(0);
|
|
const worker = spawn(process.execPath, [CLI, '--receipt-worker', 'run', f.receipt, '--', process.execPath, '-e', 'process.exit(17)'], {
|
|
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
|
|
});
|
|
const messages: any[] = [];
|
|
worker.on('message', message => messages.push(message));
|
|
const code = await new Promise<number | null>(resolve => worker.once('close', resolve));
|
|
expect(code).toBe(17);
|
|
expect(messages.map(message => message.type)).toEqual(['qa-deadline-receipt', 'qa-deadline-receipt']);
|
|
expect(messages.map(message => message.receipt.event)).toEqual(['started', 'finished']);
|
|
expect(messages.at(-1).receipt).toMatchObject({ timedOut: false, exitCode: 17 });
|
|
expect(cli(['--receipt-worker', 'status', f.receipt]).status).toBe(2);
|
|
});
|
|
|
|
test('completion classification and observedAt use the same pre-cleanup instant', () => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '30']).status).toBe(0);
|
|
const preload = path.join(f.dir, 'cleanup-clock.ts');
|
|
fs.writeFileSync(preload, `
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { ChildProcess } from 'node:child_process';
|
|
const NativeDate = Date;
|
|
const deadline = NativeDate.parse(JSON.parse(readFileSync(${JSON.stringify(f.receipt)}, 'utf8')).deadlineAt);
|
|
let now = NativeDate.now();
|
|
globalThis.Date = class extends NativeDate {
|
|
constructor(...args) { super(...(args.length ? args : [now])); }
|
|
static now() { return now; }
|
|
};
|
|
const advance = () => { now = deadline + 1000; writeFileSync(${JSON.stringify(f.marker)}, String(now)); };
|
|
const kill = process.kill.bind(process);
|
|
process.kill = (...args) => { advance(); return kill(...args); };
|
|
const childKill = ChildProcess.prototype.kill;
|
|
ChildProcess.prototype.kill = function(...args) { advance(); return childKill.apply(this, args); };
|
|
`);
|
|
const result = cli(['run', f.receipt, '--', process.execPath, '-e', 'process.exit(0)'], preload);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const finished = receipt(result.stderr.trim().split('\n').at(-1)!);
|
|
expect(finished.timedOut).toBe(false);
|
|
expect(Number(fs.readFileSync(f.marker, 'utf8'))).toBeGreaterThan(Date.parse(finished.deadlineAt));
|
|
expect(Date.parse(finished.observedAt)).toBeLessThan(Date.parse(finished.deadlineAt));
|
|
});
|
|
|
|
test('unsupported platform refuses dispatch rather than weakening containment', () => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '30']).status).toBe(0);
|
|
const preload = path.join(f.dir, 'unsupported.ts');
|
|
fs.writeFileSync(preload, `Object.defineProperty(process, 'platform', { value: 'freebsd' });`);
|
|
const result = cli(['run', f.receipt, '--', process.execPath, '-e', 'require("fs").writeFileSync(process.argv[1], "probed")', f.marker], preload);
|
|
expect(result.status).toBe(2);
|
|
expect(result.stderr).toContain('Process containment is unavailable');
|
|
expect(fs.existsSync(f.marker)).toBe(false);
|
|
});
|
|
|
|
test.each(['timeout', 'early'])('owned process tree closes without pipe hangs after %s', async mode => {
|
|
const f = fixture();
|
|
const sibling = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore' });
|
|
const siblingClosed = new Promise<void>(resolve => sibling.once('close', () => resolve()));
|
|
try {
|
|
expect(cli(['start', f.receipt, mode === 'timeout' ? '3' : '10']).status).toBe(0);
|
|
const result = cli(['run', f.receipt, '--', process.execPath, DRIVER, mode, LEAF, f.leaf, f.direct]);
|
|
expect(result.error).toBeUndefined();
|
|
expect(result.status, result.stderr).toBe(mode === 'timeout' ? 124 : 7);
|
|
expect(result.stdout).toContain('leaf ready');
|
|
const finished = receipt(result.stderr.trim().split('\n').at(-1)!);
|
|
expect(finished.timedOut).toBe(mode === 'timeout');
|
|
expect(Number.isFinite(Date.parse(finished.observedAt))).toBe(true);
|
|
await dead(await ready(f.leaf));
|
|
await dead(await ready(f.direct));
|
|
expect(alive(sibling.pid!)).toBe(true);
|
|
} finally {
|
|
cleanup([f.leaf, f.direct]);
|
|
sibling.kill('SIGKILL');
|
|
await siblingClosed;
|
|
}
|
|
}, 15_000);
|
|
|
|
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
|
|
test.skipIf(process.platform === 'win32')(`interruption ${signal} closes the owned process tree`, async () => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '10']).status).toBe(0);
|
|
const runner = background(['run', f.receipt, '--', process.execPath, DRIVER, 'timeout', LEAF, f.leaf, f.direct]);
|
|
try {
|
|
const leaf = await ready(f.leaf);
|
|
runner.child.kill(signal);
|
|
expect((await runner.result).code).toBe(signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 129);
|
|
await dead(leaf);
|
|
await dead(await ready(f.direct));
|
|
} finally {
|
|
runner.child.kill('SIGKILL');
|
|
cleanup([f.leaf, f.direct]);
|
|
await runner.result;
|
|
}
|
|
}, 15_000);
|
|
}
|
|
|
|
test.skipIf(process.platform !== 'win32')('abrupt Windows wrapper exit closes the job', async () => {
|
|
const f = fixture();
|
|
expect(cli(['start', f.receipt, '10']).status).toBe(0);
|
|
const runner = background(['run', f.receipt, '--', process.execPath, DRIVER, 'timeout', LEAF, f.leaf, f.direct]);
|
|
try {
|
|
const leaf = await ready(f.leaf);
|
|
runner.child.kill('SIGKILL');
|
|
await runner.result;
|
|
await dead(leaf);
|
|
await dead(await ready(f.direct));
|
|
} finally {
|
|
runner.child.kill('SIGKILL');
|
|
cleanup([f.leaf, f.direct]);
|
|
await runner.result;
|
|
}
|
|
}, 15_000);
|