mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-11 21:49:00 +02:00
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
This commit is contained in:
1 parent
65bfb0ce49
commit
dcaea52800
333 files changed
+41755
-7357
No files matched your search
@@ -0,0 +1,140 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { isProcessAlive } from '../../browse/src/error-handling';
|
||||
import { readPidCmdline, readPidStartTime } from '../../browse/src/xvfb';
|
||||
import { isAgentRecordGone, isOurAgent, readAgentRecord } from '../../browse/src/terminal-agent-control';
|
||||
|
||||
export async function stopQaOnlyBrowser(directory: string, timeoutMs: number): Promise<void> {
|
||||
if (!Number.isFinite(timeoutMs) || timeoutMs <= 100) throw new Error('QA-only browser cleanup: no settlement budget remains; retaining fixture');
|
||||
const worker = Bun.spawn([process.execPath, import.meta.path, directory, String(timeoutMs - 100)], {
|
||||
stdout: 'ignore', stderr: 'pipe',
|
||||
});
|
||||
let timedOut = false;
|
||||
const timer = setTimeout(() => { timedOut = true; worker.kill('SIGKILL'); }, timeoutMs);
|
||||
const stderr = new Response(worker.stderr).text();
|
||||
try {
|
||||
const code = await worker.exited;
|
||||
const error = await stderr;
|
||||
if (timedOut) throw new Error('QA-only browser cleanup: owned worker settlement deadline exceeded; retaining fixture');
|
||||
if (code !== 0) throw new Error(error.trim() || `QA-only browser cleanup: worker exited ${code}; retaining fixture`);
|
||||
} finally { clearTimeout(timer); }
|
||||
}
|
||||
|
||||
async function settleOwnedBrowser(directory: string, timeoutMs: number): Promise<void> {
|
||||
const started = performance.now();
|
||||
const deadline = started + timeoutMs;
|
||||
let pending = ['identity verification'];
|
||||
const fail = (message: string): never => { throw new Error(`QA-only browser cleanup: ${message}`); };
|
||||
const remaining = () => {
|
||||
const ms = Math.floor(deadline - performance.now());
|
||||
if (ms <= 0) fail(`owned process settlement deadline exceeded (${pending.join(', ')}); retaining fixture`);
|
||||
return ms;
|
||||
};
|
||||
if (fs.lstatSync(directory).isSymbolicLink()) fail('fixture directory is a link');
|
||||
const root = fs.realpathSync(directory);
|
||||
const stateDir = path.join(root, '.gstack');
|
||||
if (!fs.existsSync(stateDir)) return;
|
||||
if (fs.lstatSync(stateDir).isSymbolicLink()) fail('state directory is a link');
|
||||
const stateFile = path.join(stateDir, 'browse.json');
|
||||
const agentFile = path.join(stateDir, 'terminal-agent-pid');
|
||||
for (const file of [stateFile, agentFile]) {
|
||||
if (fs.existsSync(file) && !fs.lstatSync(file).isFile()) fail('state record is not a regular file');
|
||||
}
|
||||
if (!fs.existsSync(stateFile)) {
|
||||
if (fs.existsSync(agentFile)) fail('terminal record has no daemon state');
|
||||
return;
|
||||
}
|
||||
const raw = fs.readFileSync(stateFile, 'utf8');
|
||||
const state = JSON.parse(raw);
|
||||
if (!Number.isSafeInteger(state.pid) || state.pid <= 1
|
||||
|| typeof state.instanceId !== 'string' || !state.instanceId) fail('invalid daemon identity');
|
||||
const agentRaw = fs.existsSync(agentFile) ? fs.readFileSync(agentFile, 'utf8') : undefined;
|
||||
const agent = readAgentRecord(stateDir);
|
||||
if (!agentRaw || !agent) fail('terminal identity is unavailable');
|
||||
if (!isProcessAlive(state.pid)) fail('daemon exited before its owned processes could be identified');
|
||||
const daemonStart = readPidStartTime(state.pid);
|
||||
const command = readPidCmdline(state.pid);
|
||||
if (!daemonStart || !(typeof state.serverPath === 'string' && command.includes(state.serverPath)
|
||||
|| command.includes('--server') && /browse/.test(command))) fail('daemon identity is unavailable');
|
||||
let cwd: string;
|
||||
if (process.platform === 'linux') cwd = fs.realpathSync(`/proc/${state.pid}/cwd`);
|
||||
else {
|
||||
const result = spawnSync('lsof', ['-a', '-p', String(state.pid), '-d', 'cwd', '-Fn'], {
|
||||
encoding: 'utf8', timeout: Math.min(1000, remaining()),
|
||||
});
|
||||
if (result.error || result.status !== 0) fail('daemon working directory is unavailable');
|
||||
cwd = result.stdout.split('\n').find(line => line.startsWith('n'))?.slice(1) ?? '';
|
||||
}
|
||||
if (cwd !== root) fail('daemon belongs to another fixture');
|
||||
if (agent && (agent.ownerPid !== state.pid || agent.ownerStartTime !== daemonStart
|
||||
|| !isAgentRecordGone(agent) && !isOurAgent(agent, state.pid))) fail('terminal ownership is unconfirmed');
|
||||
const processes = spawnSync('ps', ['-eo', 'pid=,ppid='], {
|
||||
encoding: 'utf8', timeout: Math.min(1000, remaining()),
|
||||
});
|
||||
if (processes.error || processes.status !== 0) fail('owned child identities are unavailable');
|
||||
const rows = processes.stdout.trim().split('\n').map(line => line.trim().split(/\s+/).map(Number));
|
||||
const descendants = new Set<number>([state.pid]);
|
||||
for (let previous = 0; previous !== descendants.size;) {
|
||||
previous = descendants.size;
|
||||
for (const [pid, parent] of rows) if (descendants.has(parent)) descendants.add(pid);
|
||||
}
|
||||
const chromium = [...descendants].filter(pid => pid !== state.pid && /chrom|headless_shell/i.test(readPidCmdline(pid)))
|
||||
.map(pid => ({ pid, start: readPidStartTime(pid) }));
|
||||
if (!chromium.length || chromium.some(child => !child.start)) fail('Chromium identity is unavailable');
|
||||
if (state.chromiumPid !== undefined && !chromium.some(child => child.pid === state.chromiumPid
|
||||
&& child.start === state.chromiumStartTime)) fail('Chromium ownership is unconfirmed');
|
||||
const unchanged = () => {
|
||||
if (fs.existsSync(stateFile) && fs.readFileSync(stateFile, 'utf8') !== raw) fail('daemon state was replaced');
|
||||
if (fs.existsSync(agentFile) && fs.readFileSync(agentFile, 'utf8') !== agentRaw) fail('terminal state was replaced');
|
||||
};
|
||||
unchanged();
|
||||
if (readPidStartTime(state.pid) !== daemonStart || readPidCmdline(state.pid) !== command) fail('daemon identity changed before stop');
|
||||
unchanged();
|
||||
remaining();
|
||||
try { process.kill(state.pid, 'SIGINT'); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error; }
|
||||
const settled = (pid: number, start: string) => {
|
||||
if (!isProcessAlive(pid)) return true;
|
||||
const actual = readPidStartTime(pid);
|
||||
if (actual && actual !== start) return true;
|
||||
if (process.platform === 'linux') {
|
||||
try { return fs.readFileSync(`/proc/${pid}/stat`, 'utf8').match(/^\d+ \(.*\) ([A-Z])/u)?.[1] === 'Z'; }
|
||||
catch { return !isProcessAlive(pid); }
|
||||
}
|
||||
const result = spawnSync('ps', ['-p', String(pid), '-o', 'stat='], {
|
||||
encoding: 'utf8', timeout: Math.min(1000, remaining()),
|
||||
});
|
||||
return result.status === 0 && result.stdout.trim().startsWith('Z');
|
||||
};
|
||||
for (;;) {
|
||||
unchanged();
|
||||
remaining();
|
||||
pending = [
|
||||
...(!settled(state.pid, daemonStart) ? [`daemon ${state.pid}`] : []),
|
||||
...chromium.filter(child => !settled(child.pid, child.start)).map(child => `Chromium ${child.pid}`),
|
||||
...(agent && !isAgentRecordGone(agent) ? [`terminal ${agent.pid}`] : []),
|
||||
];
|
||||
if (!pending.length) {
|
||||
unchanged();
|
||||
remaining();
|
||||
return;
|
||||
}
|
||||
if (pending.length === 1 && pending[0] === `daemon ${state.pid}`
|
||||
&& performance.now() - started >= Math.min(1000, timeoutMs / 2)) {
|
||||
if (readPidStartTime(state.pid) !== daemonStart || readPidCmdline(state.pid) !== command) fail('daemon identity changed before final termination');
|
||||
unchanged();
|
||||
try { process.kill(state.pid, 'SIGKILL'); }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error; }
|
||||
}
|
||||
await Bun.sleep(Math.min(25, remaining()));
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
try { await settleOwnedBrowser(process.argv[2], Number(process.argv[3])); }
|
||||
catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user