Files
gstack/test/qa-only-cleanup.test.ts
T
Garry Tan dcaea52800 v1.91.7.0 feat: add functional QA and pre-publication docs checks (#2983)
* 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
2026-09-29 06:07:35 -07:00

219 lines
12 KiB
TypeScript

import { expect, spyOn, test } from 'bun:test';
import * as fs from 'node:fs';
import { spawnSync } from 'node:child_process';
import * as os from 'node:os';
import * as path from 'node:path';
import { stopQaOnlyBrowser } from './helpers/qa-only-cleanup';
import { isAgentRecordGone, isOurAgent, readAgentRecord, spawnTerminalAgent, stopAgentByRecord } from '../browse/src/terminal-agent-control';
import { readPidStartTime } from '../browse/src/xvfb';
const ROOT = path.resolve(import.meta.dir, '..');
const waitFor = async (check: () => boolean, timeout = 10_000) => {
const deadline = performance.now() + timeout;
while (!check()) {
if (performance.now() >= deadline) throw new Error('QA cleanup fixture did not become ready');
await Bun.sleep(25);
}
};
test('missing owned state never consults ambient state or bootstraps a daemon', async () => {
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'qa-clean-'));
const prior = process.env.BROWSE_STATE_FILE;
const ambient = path.join(dir, 'ambient.json');
fs.writeFileSync(ambient, 'must not read or mutate');
process.env.BROWSE_STATE_FILE = ambient;
try {
await stopQaOnlyBrowser(dir, 200);
expect(fs.readdirSync(dir)).toEqual(['ambient.json']);
expect(fs.readFileSync(ambient, 'utf8')).toBe('must not read or mutate');
} finally {
if (prior === undefined) delete process.env.BROWSE_STATE_FILE;
else process.env.BROWSE_STATE_FILE = prior;
fs.rmSync(dir, { recursive: true, force: true });
}
});
test('linked owned state cannot redirect cleanup to a sibling', async () => {
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'qa-clean-'));
const sibling = path.join(dir, 'sibling');
fs.mkdirSync(sibling);
fs.writeFileSync(path.join(sibling, 'browse.json'), 'untouched');
fs.symlinkSync(sibling, path.join(dir, '.gstack'));
try {
await expect(stopQaOnlyBrowser(dir, 200)).rejects.toThrow('state directory is a link');
expect(fs.readFileSync(path.join(sibling, 'browse.json'), 'utf8')).toBe('untouched');
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
});
test.each(['ack-only', 'replaced-state', 'terminal-mismatch', 'chromium-mismatch', 'missing-terminal', 'blocked-identity'])('cleanup refuses unconfirmed settlement: %s', async scenario => {
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'qa-clean-'));
const stateDir = path.join(dir, '.gstack');
fs.mkdirSync(stateDir);
const script = path.join(dir, 'server.ts');
const stateFile = path.join(stateDir, 'browse.json');
const requestFile = path.join(dir, 'request.json');
const signalFile = path.join(dir, 'signal.json');
fs.writeFileSync(script, `
import * as fs from 'node:fs';
const chromium = Bun.spawn([process.execPath, '-e', 'setInterval(() => {}, 1000)', 'chromium']);
process.on('SIGTERM', async () => { chromium.kill(); await chromium.exited; process.exit(0); });
fs.writeFileSync(${JSON.stringify(path.join(dir, 'chromium-pid'))}, String(chromium.pid));
process.on('SIGINT', () => {
fs.writeFileSync(${JSON.stringify(signalFile)}, JSON.stringify({ signal: 'SIGINT' }));
if (${JSON.stringify(scenario)} === 'replaced-state') {
const file = ${JSON.stringify(stateFile)};
fs.writeFileSync(file, JSON.stringify({ ...JSON.parse(fs.readFileSync(file, 'utf8')), instanceId: 'successor' }));
}
});
const server = Bun.serve({ port: 0, async fetch(request) {
fs.writeFileSync(${JSON.stringify(requestFile)}, JSON.stringify(await request.json()));
return new Response('Unexpected HTTP request');
} });
fs.writeFileSync(${JSON.stringify(path.join(dir, 'port'))}, String(server.port));
`);
const daemon = Bun.spawn([process.execPath, script], { cwd: dir, stdout: 'ignore', stderr: 'inherit' });
let agent: ReturnType<typeof readAgentRecord>;
let restore: (() => void) | undefined;
let worker: ReturnType<typeof Bun.spawn> | undefined;
try {
await waitFor(() => fs.existsSync(path.join(dir, 'port')));
const port = Number(fs.readFileSync(path.join(dir, 'port'), 'utf8'));
const chromiumPid = Number(fs.readFileSync(path.join(dir, 'chromium-pid'), 'utf8'));
expect(spawnTerminalAgent({ stateFile, serverPort: port, ownerPid: daemon.pid, cwd: dir,
extraEnv: { GSTACK_TERMINAL_OWNER_WATCHDOG_MS: '25' } })).toBeGreaterThan(0);
agent = readAgentRecord(stateDir);
expect(agent).not.toBeNull();
fs.writeFileSync(stateFile, JSON.stringify({ pid: daemon.pid, port, token: 'fixture-local-token', instanceId: 'owned', serverPath: script,
chromiumPid, chromiumStartTime: scenario === 'chromium-mismatch' ? 'stale start' : readPidStartTime(chromiumPid) }));
const agentFile = path.join(stateDir, 'terminal-agent-pid');
if (scenario === 'terminal-mismatch') fs.writeFileSync(agentFile, JSON.stringify({ ...agent, ownerPid: process.pid }));
if (scenario === 'missing-terminal') fs.unlinkSync(agentFile);
if (scenario === 'blocked-identity') {
const preload = path.join(dir, 'block-identity.ts');
fs.writeFileSync(preload, `
import * as fs from 'node:fs';
const original = Bun.spawnSync;
Bun.spawnSync = (...args) => {
if (args[0][0] === 'ps') {
fs.writeFileSync(${JSON.stringify(path.join(dir, 'identity-blocked'))}, String(process.pid));
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
fs.writeFileSync(${JSON.stringify(path.join(dir, 'late-identity'))}, 'unexpected late work');
}
return original(...args);
};
`);
const original = Bun.spawn;
const spy = spyOn(Bun, 'spawn').mockImplementation(((args: string[], options: any) => {
expect(args[1]).toBe(path.join(ROOT, 'test/helpers/qa-only-cleanup.ts'));
worker = original([args[0], '--preload', preload, ...args.slice(1)], options);
return worker;
}) as typeof Bun.spawn);
restore = () => spy.mockRestore();
}
const started = performance.now();
const errors = { 'ack-only': 'settlement deadline exceeded', 'replaced-state': 'daemon state was replaced',
'terminal-mismatch': 'terminal ownership is unconfirmed', 'chromium-mismatch': 'Chromium ownership is unconfirmed',
'missing-terminal': 'terminal identity is unavailable', 'blocked-identity': 'owned worker settlement deadline exceeded' };
await expect(stopQaOnlyBrowser(dir, 300)).rejects.toThrow(errors[scenario]);
expect(performance.now() - started).toBeLessThan(1000);
expect(daemon.exitCode).toBeNull();
expect(readPidStartTime(chromiumPid)).not.toBe('');
expect(fs.existsSync(stateFile)).toBe(true);
if (scenario === 'blocked-identity') {
expect(fs.readFileSync(path.join(dir, 'identity-blocked'), 'utf8')).toBe(String(worker!.pid));
expect(await worker!.exited).toBe(137);
expect(fs.existsSync(path.join(dir, 'late-identity'))).toBe(false);
}
if (['ack-only', 'replaced-state'].includes(scenario)) expect(JSON.parse(fs.readFileSync(signalFile, 'utf8'))).toEqual({ signal: 'SIGINT' });
else expect(fs.existsSync(signalFile)).toBe(false);
expect(fs.existsSync(requestFile)).toBe(false);
} finally {
restore?.();
if (agent) expect(stopAgentByRecord(agent, 300)).toBe(true);
daemon.kill();
await daemon.exited;
fs.rmSync(dir, { recursive: true, force: true });
}
}, 15_000);
test.each(['owned-endpoint', 'sibling-endpoint'])('real Chromium and terminal settle before removal without harming a live sibling or recreating state: %s', async scenario => {
const page = Bun.serve({ port: 0, fetch: () => new Response('<h1>Owned QA fixture</h1>', { headers: { 'Content-Type': 'text/html' } }) });
const fixtures: Array<{ dir: string; child: ReturnType<typeof Bun.spawn>; state?: any; agent?: ReturnType<typeof readAgentRecord> }> = [];
const launch = async () => {
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'qa-live-'));
const stateFile = path.join(dir, '.gstack/browse.json');
const child = Bun.spawn([process.execPath, path.join(ROOT, 'browse/src/server.ts')], {
cwd: dir, stdout: Bun.file(path.join(dir, 'server.log')), stderr: Bun.file(path.join(dir, 'server-error.log')),
env: { ...process.env, PATH: path.dirname(process.execPath) + path.delimiter + process.env.PATH,
BROWSE_STATE_FILE: stateFile, BROWSE_PORT: '0', BROWSE_PARENT_PID: '0', BROWSE_HEADED: '0', BROWSE_HEADLESS_SKIP: '0',
GSTACK_TERMINAL_OWNER_WATCHDOG_MS: '25', GSTACK_AGENT_WATCHDOG_TICK_MS: '100',
TMPDIR: dir, TMP: dir, TEMP: dir, CHROMIUM_PROFILE: path.join(dir, 'profile') },
});
const fixture = { dir, child, state: undefined as any, agent: undefined as ReturnType<typeof readAgentRecord> };
fixtures.push(fixture);
await waitFor(() => fs.existsSync(stateFile) && fs.existsSync(path.join(dir, '.gstack/terminal-port')));
fixture.state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
fixture.agent = readAgentRecord(path.dirname(stateFile));
expect(fixture.state.pid).toBe(child.pid);
const children = spawnSync('ps', ['-eo', 'pid=,ppid=,args='], { encoding: 'utf8', timeout: 1000 });
expect(children.status).toBe(0);
expect(children.stdout.split('\n').some(line => {
const row = line.trim().split(/\s+/);
return Number(row[1]) === child.pid && /chrom|headless_shell/.test(row.slice(2).join(' '));
})).toBe(true);
expect(isOurAgent(fixture.agent!, child.pid)).toBe(true);
const response = await fetch(`http://127.0.0.1:${fixture.state.port}/command`, { method: 'POST',
headers: { Authorization: `Bearer ${fixture.state.token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ command: 'goto', args: [`http://127.0.0.1:${page.port}/`] }), signal: AbortSignal.timeout(3000) });
expect(response.ok, await response.text()).toBe(true);
return fixture;
};
try {
const owned = await launch(), sibling = await launch();
const stateFile = path.join(owned.dir, '.gstack/browse.json');
const original = fs.readFileSync(stateFile, 'utf8');
fs.writeFileSync(stateFile, JSON.stringify(sibling.state));
await expect(stopQaOnlyBrowser(owned.dir, 1000)).rejects.toThrow('daemon belongs to another fixture');
fs.writeFileSync(stateFile, original);
if (scenario === 'sibling-endpoint') fs.writeFileSync(stateFile, JSON.stringify({
...owned.state, port: sibling.state.port, token: sibling.state.token,
}));
const started = performance.now();
let cleanupError: unknown;
try { await stopQaOnlyBrowser(owned.dir, 4000); }
catch (error) { cleanupError = error; }
expect(isOurAgent(sibling.agent!, sibling.child.pid), 'Sibling terminal must survive cleanup even when only its endpoint was copied').toBe(true);
if (cleanupError) throw cleanupError;
expect(performance.now() - started).toBeLessThan(4000);
expect([0, 137]).toContain(await owned.child.exited);
expect(isAgentRecordGone(owned.agent!)).toBe(true);
fs.rmSync(owned.dir, { recursive: true, force: true });
const until = performance.now() + 250;
while (performance.now() < until) {
expect(fs.existsSync(owned.dir)).toBe(false);
expect(isOurAgent(sibling.agent!, sibling.child.pid)).toBe(true);
await Bun.sleep(25);
}
expect((await fetch(`http://127.0.0.1:${sibling.state.port}/health`, { signal: AbortSignal.timeout(1000) })).ok).toBe(true);
await stopQaOnlyBrowser(sibling.dir, 4000);
expect([0, 137]).toContain(await sibling.child.exited);
expect(isAgentRecordGone(sibling.agent!)).toBe(true);
fs.rmSync(sibling.dir, { recursive: true, force: true });
await Bun.sleep(100);
expect(fixtures.every(fixture => !fs.existsSync(fixture.dir))).toBe(true);
} finally {
page.stop(true);
for (const fixture of fixtures) {
if (!fs.existsSync(fixture.dir)) continue;
if (fixture.state) fs.writeFileSync(path.join(fixture.dir, '.gstack/browse.json'), JSON.stringify(fixture.state));
try { await stopQaOnlyBrowser(fixture.dir, 4000); }
catch { fixture.child.kill('SIGINT'); }
await Promise.race([fixture.child.exited, Bun.sleep(3000)]);
if (fixture.child.exitCode === null) fixture.child.kill('SIGKILL');
await fixture.child.exited;
if (fixture.agent) expect(stopAgentByRecord(fixture.agent, 300)).toBe(true);
fs.rmSync(fixture.dir, { recursive: true, force: true });
}
}
}, 30_000);