test(flaky): qa-fix-loop late-timeout freezes its clock through setup and registers the outer budget (GSTA-139)

The #3413 qa-only template: the late scenario's real setup ran inside its
1000 ms mocked work budget and a 1160 ms native deadline, so under load the
attempt never started (0/10 under 24 busy loops, attempts length 0). The clock
now stands still from the body's start until the capture begins, the capture
gets exactly 1000 ms, and the native deadline is the registered 6080 ms outer
budget. Gate edit: captureBudgetMs is asserted equal to 1000.

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-28 13:48:43 +00:00
1 parent acbbd9f7f9
commit 545c110833
1 file changed
+10 -7
+10 -7
View File
@@ -21,7 +21,9 @@ const scenario = ${JSON.stringify(scenario)};
const attempts = [], records = [], servers = [];
const late = scenario === 'late-timeout';
const workMs = 1000, drainMs = 80;
let finalized = false, outerMs;
const realNow = Date.now;
let finalized = false, outerMs, frozenAt;
if (late) Date.now = () => frozenAt ?? realNow();
const collector = { addTest: entry => {
records.push({ ...entry, afterFinalization: finalized });
if (scenario === 'recording') throw new Error('QA recorder failure');
@@ -46,10 +48,10 @@ mock.module(path.join(root, 'test/helpers/e2e-helpers.ts'), () => ({
testConcurrentIfSelected: (id, body, budget) => {
if (id !== 'qa-fix-loop') return;
outerMs = budget;
// Keep the actual native outer deadline, scaling only its 5s recording
// margin to80ms. The pre-fix equal work/outer deadline stays equal.
const nativeBudget = late ? (budget >= workMs + 5000 ? budget - 5000 + 80 : budget) : 5000;
test(id, body, nativeBudget);
// The late attempt's clock stands still through its real setup, so the
// mocked work budget reaches the capture whole; the deadline is the
// registered outer budget.
test(id, late ? () => { frozenAt = realNow(); return body(); } : body, late ? budget : 5000);
},
copyDirSync: (source, target) => fs.cpSync(source, target, { recursive: true }),
setupBrowseShims: cwd => fs.mkdirSync(path.join(cwd, 'browse/bin'), { recursive: true }),
@@ -62,6 +64,7 @@ mock.module(path.join(root, 'test/helpers/e2e-helpers.ts'), () => ({
mock.module(path.join(root, 'test/helpers/session-runner.ts'), () => ({
SESSION_DRAIN_GRACE_MS: late ? drainMs : 5000,
runSkillTest: async options => {
frozenAt = undefined;
const cwd = options.workingDirectory;
const git = (...args) => execFileSync('git', args, { cwd, encoding: 'utf8', timeout: 5000 });
const initial = fs.readFileSync(path.join(cwd, 'index.html'), 'utf8');
@@ -102,6 +105,7 @@ afterAll(async () => {
// behind child exit; this reproduces the observed late recording race.
if (late) await new Promise(resolve => setTimeout(resolve, 200));
fs.writeFileSync(${JSON.stringify(factsPath)}, JSON.stringify({ attempts, records, servers, outerMs }));
Date.now = realNow;
});
await import(path.join(root, 'test/skill-e2e-qa-workflow.test.ts'));
`);
@@ -152,8 +156,7 @@ await import(path.join(root, 'test/skill-e2e-qa-workflow.test.ts'));
}
if (scenario === 'late-timeout') {
expect(outerMs).toBe(1000 + 80 + 5000);
expect(attempts[0].captureBudgetMs).toBeGreaterThan(0);
expect(attempts[0].captureBudgetMs).toBeLessThan(1000);
expect(attempts[0].captureBudgetMs).toBe(1000);
expect(records[0].exit_reason).toBe('timeout');
expect(child.stderr).not.toContain('Unhandled error between tests');
}