fix(qa-evidence): count required post-repair probes only after the fixture-declared final source edit (W3-6/W3-14 confirm round)

GSTACK_QA_REQUIRED_PROBES reminders counted any complete capture, so a
pre-repair duplicate or partial capture looked like post-repair coverage. The
caller may now declare GSTACK_QA_REQUIRED_AFTER_EDIT with absolute paths and a
clean-fixture time: only captures started after the last edit under those
paths count, none count before the first edit, and a malformed declaration
counts none. Without the declaration every capture counts as before. The
webhook fix fixture declares its src/ directory. A free test covers
pre-repair-only coverage (nudges), complete post-repair coverage (no nudge)
and a second edit (nudges again).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-17 20:37:26 +00:00
1 parent 4617b4ce98
commit c27c2e8412
3 files changed
+59 -3

No files matched your search

+27 -2
View File
@@ -138,12 +138,37 @@ function completeReceipts(root: string): Record<string, any>[] {
const completeCaptures = (root: string): string[] => completeReceipts(root).map(receipt => receipt.id);
const latestCompleteCapture = (root: string): string | undefined => completeCaptures(root).at(-1);
/** Required native probes the caller declared (GSTACK_QA_REQUIRED_PROBES, a JSON array of child commands) that no complete capture has run yet. Informational only. */
function latestMtimeMs(target: string): number {
let stat: fs.Stats;
try { stat = fs.lstatSync(target); } catch { return 0; }
if (!stat.isDirectory()) return stat.mtimeMs;
return Math.max(stat.mtimeMs, ...fs.readdirSync(target).map(name => latestMtimeMs(path.join(target, name))));
}
/**
* Earliest start time a capture needs to count toward required probes. The caller
* may declare GSTACK_QA_REQUIRED_AFTER_EDIT={"paths":[absolute paths],"since":ISO}:
* only captures started after the last edit under those paths count, and none
* count before the first edit after since. Without it every capture counts.
*/
function requiredBoundaryMs(): number {
let declared: unknown;
try { declared = JSON.parse(process.env.GSTACK_QA_REQUIRED_AFTER_EDIT ?? 'null'); } catch { return Infinity; }
if (declared === null) return -Infinity;
if (!object(declared) || !Array.isArray(declared.paths) || !declared.paths.every(item => typeof item === 'string' && path.isAbsolute(item))
|| !Number.isFinite(Date.parse(declared.since))) return Infinity;
const edited = Math.max(0, ...declared.paths.map(latestMtimeMs));
return edited > Date.parse(declared.since) ? Math.floor(edited) : Infinity;
}
/** Required native probes the caller declared (GSTACK_QA_REQUIRED_PROBES, a JSON array of child commands) that no counted complete capture has run yet. Informational only. */
function requiredRemaining(root: string): { requiredRemaining?: string[] } {
let required: unknown;
try { required = JSON.parse(process.env.GSTACK_QA_REQUIRED_PROBES ?? 'null'); } catch { return {}; }
if (!Array.isArray(required) || !required.every(item => typeof item === 'string')) return {};
const run = new Set(completeReceipts(root).map(receipt => Array.isArray(receipt.argv) ? receipt.argv.join(' ') : ''));
const boundary = requiredBoundaryMs();
const run = new Set(completeReceipts(root).filter(receipt => Date.parse(receipt.startedAt) >= boundary)
.map(receipt => Array.isArray(receipt.argv) ? receipt.argv.join(' ') : ''));
return { requiredRemaining: required.filter(command => !run.has(command)) };
}
+3 -1
View File
@@ -104,6 +104,7 @@ export async function runQAFunctionalCase(entry: { id: string; family: QAFamily;
fixtureGit(fixture.root, ['commit', '-m', 'Bind current QA instructions to fixture']);
fixture.revision = fixtureGit(fixture.root, ['rev-parse', 'HEAD']);
if (fixtureGit(fixture.root, ['status', '--porcelain'])) throw new Error('QA fixture must start clean');
const fixtureCleanAt = new Date().toISOString();
observer = await observeQAWrites(fixture.root, { evidenceProducer: true, atomicWriteMode: entry.mode });
await runRecordedOfficeHoursAttempt({
collector, name: entry.id, suite: 'Functional QA native E2E',
@@ -117,7 +118,8 @@ export async function runQAFunctionalCase(entry: { id: string; family: QAFamily;
timeout, completionReserveMs: timeout / 4,
testName: entry.id, runId, signal, env: { CLAUDE_CONFIG_DIR: fixture.config,
GIT_OPTIONAL_LOCKS: '0', QA_STATE_ROOT: path.join(fixture.root, '.qa-state'),
...(qaFunctionalRequiredProbes(entry) ? { GSTACK_QA_REQUIRED_PROBES: JSON.stringify(qaFunctionalRequiredProbes(entry)) } : {}) },
...(qaFunctionalRequiredProbes(entry) ? { GSTACK_QA_REQUIRED_PROBES: JSON.stringify(qaFunctionalRequiredProbes(entry)) } : {}),
...(entry.mode === 'qa' && qaFunctionalRequiredProbes(entry) ? { GSTACK_QA_REQUIRED_AFTER_EDIT: JSON.stringify({ paths: [path.join(fixture.root, 'src')], since: fixtureCleanAt }) } : {}) },
});
return result;
},
+29
View File
@@ -523,6 +523,35 @@ test('captures list declared-but-unrun required probes without judging them', ()
expect(receipt(result.stdout).requiredRemaining).toEqual(['bun run probe -- reject']);
});
test('post-repair required probes count only captures started after the last fixture-declared source edit', () => {
const f = fixture();
const src = path.join(f.root, 'src');
fs.mkdirSync(src);
fs.writeFileSync(path.join(src, 'worker.ts'), 'export const effect = 1;\n');
const old = new Date(Date.now() - 60_000);
fs.utimesSync(path.join(src, 'worker.ts'), old, old);
fs.utimesSync(src, old, old);
const probe = (program: string) => [process.execPath, '-e', program];
const [duplicate, partial] = [probe('console.log(JSON.stringify({scenario:"duplicate"}))'), probe('console.log(JSON.stringify({scenario:"partial"}))')];
const env = { ...process.env, GSTACK_QA_REQUIRED_PROBES: JSON.stringify([duplicate.join(' '), partial.join(' ')]),
GSTACK_QA_REQUIRED_AFTER_EDIT: JSON.stringify({ paths: [src], since: new Date(Date.now() - 30_000).toISOString() }) };
const capture = (captureId: string, argv: string[]) => {
const after = captureId === '001' ? [] : ['--after', String(Number(captureId) - 1).padStart(3, '0'), '--hypothesis', 'The previous scenario result shows which invariant the next probe must check.'];
const result = spawnSync(process.execPath, [CLI, 'capture', f.root, captureId, '--timeout-ms', '4000', ...after, '--', ...argv], { cwd: f.root, encoding: 'utf8', timeout: 10_000, env });
expect(result.status, result.stderr).toBe(0);
return receipt(result.stdout).requiredRemaining as string[];
};
expect(capture('001', duplicate)).toEqual([duplicate.join(' '), partial.join(' ')]);
expect(capture('002', partial)).toEqual([duplicate.join(' '), partial.join(' ')]);
fs.writeFileSync(path.join(src, 'worker.ts'), 'export const effect = 2;\n');
expect(capture('003', duplicate)).toEqual([partial.join(' ')]);
expect(capture('004', partial)).toEqual([]);
const secondEdit = new Date(Date.now() + 60_000);
fs.writeFileSync(path.join(src, 'worker.ts'), 'export const effect = 3;\n');
fs.utimesSync(path.join(src, 'worker.ts'), secondEdit, secondEdit);
expect(capture('005', probe('console.log(JSON.stringify({scenario:"happy"}))'))).toEqual([duplicate.join(' '), partial.join(' ')]);
});
test('materialize accepts a single limits string as a one-item list', () => {
const f = fixture();
expect(f.capture('001', 'console.log(JSON.stringify({ step: 1 }))').status).toBe(0);