mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
885c1486bb
* test(ci): single-retry policy for enumerated contention-flaky files * fix: satisfy fallow * test(ci): read failures through a lane reporter so timeouts stay distinguishable * test(ci): cover the lane reporter and drop its duplicated boilerplate * chore(fallow): own the retry lane's tool-loaded export seams * test(ci): block retries on non-test failures and classify timeouts structurally * test(ci): decide retry eligibility from runner metadata and route gate verdicts through blockers * refactor(ci): name the retry policy's rules in code instead of comments * test(ci): mark runner-aborted timeouts inside the runner instead of inferring them * test(ci): make timeout provenance a per-run secret, not a writable flag Cover direct task.meta mutation in the real child-Vitest fixture gate. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(ci): retry the failed files in the first run's project and coverage modes Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: drop deleted repo-health file from the retry list after #1480 Rebase onto main post-#1480: the SkillGym/repo-health descope deleted scripts/repo-health/run.test.ts, whose CONTENTION_RETRY_FILES entry would now fail this PR's own missing-file check, and inlined the slow-test budgets into the reporter, resolving the budgets-module import. Envelope comments now point at scripts/lib/lane-envelope.ts instead of the closed #1430. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FUv7bvbWNryuXgSBuqTtep --------- Co-authored-by: Michał Pierzchała <thymikee@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
111 lines
4.2 KiB
TypeScript
111 lines
4.2 KiB
TypeScript
import type { Reporter, TestCase, TestModule } from 'vitest/node';
|
|
import { recordRunBlocker } from './lib/run-blocker-bus.ts';
|
|
|
|
/**
|
|
* Slow-test ratchet (see docs/agents/testing.md "Speed rules").
|
|
*
|
|
* Unit tests must not wait real time: measured 2026-07-04, the unit suite's
|
|
* wall clock was bounded by files whose tests slept through production
|
|
* timeouts (a 10.8s test proving "times out" by waiting the full 10s budget).
|
|
* This reporter fails the run when a unit test exceeds the enforced budget.
|
|
*/
|
|
|
|
// Unit tests must not wait real time; integration scenarios drive a real
|
|
// daemon request path and get more room.
|
|
const UNIT_BUDGET_MS = 2_500;
|
|
const INTEGRATION_BUDGET_MS = 15_000;
|
|
|
|
// Enforcement fires at 2x budget: host load legitimately stretches a
|
|
// borderline test by tens of percent, and a wall-clock gate that flakes under
|
|
// contention trains people to ignore it. Between budget and 2x budget the
|
|
// gate reports without failing.
|
|
const ENFORCE_FACTOR = 2;
|
|
type Offender = { key: string; durationMs: number; budgetMs: number; enforce: boolean };
|
|
|
|
function budgetForPath(relativePath: string): number {
|
|
return relativePath.startsWith('src/') ? UNIT_BUDGET_MS : INTEGRATION_BUDGET_MS;
|
|
}
|
|
|
|
/**
|
|
* Classify one finished test against its budget. Exported for the unit test —
|
|
* the reporter shell below is a thin vitest-callback adapter around this.
|
|
*/
|
|
export function classifySlowTest(params: {
|
|
root: string;
|
|
moduleId: string;
|
|
name: string;
|
|
fullName: string;
|
|
durationMs: number;
|
|
}): Offender | null {
|
|
const relative =
|
|
params.root && params.moduleId.startsWith(params.root)
|
|
? params.moduleId.slice(params.root.length + 1)
|
|
: params.moduleId;
|
|
const budgetMs = budgetForPath(relative);
|
|
if (params.durationMs <= budgetMs) return null;
|
|
const fullKey = `${relative} :: ${params.fullName.split(' > ').join(' ')}`;
|
|
return {
|
|
key: fullKey,
|
|
durationMs: params.durationMs,
|
|
budgetMs,
|
|
enforce: params.durationMs > budgetMs * ENFORCE_FACTOR,
|
|
};
|
|
}
|
|
|
|
/** Render the gate outcome; returns true when the run must fail. */
|
|
export function reportSlowTests(offenders: Offender[], write: (message: string) => void): boolean {
|
|
if (offenders.length === 0) return false;
|
|
const sorted = [...offenders].sort((a, b) => b.durationMs - a.durationMs);
|
|
const line = (o: Offender): string =>
|
|
` ${(o.durationMs / 1000).toFixed(2)}s (budget ${o.budgetMs / 1000}s) ${o.key}`;
|
|
const failing = sorted.filter((o) => o.enforce);
|
|
const warning = sorted.filter((o) => !o.enforce);
|
|
if (warning.length > 0) {
|
|
write(
|
|
`\nSlow-test gate: ${warning.length} test(s) over budget (within the ${ENFORCE_FACTOR}x load-variance band, not failing):\n` +
|
|
warning.map(line).join('\n'),
|
|
);
|
|
}
|
|
if (failing.length === 0) return false;
|
|
write(
|
|
`\nSlow-test gate: ${failing.length} test(s) exceeded ${ENFORCE_FACTOR}x the wall-clock budget.\n` +
|
|
`Tests must not wait real time — inject the timeout/poll budget or assert the budget is\n` +
|
|
`wired instead of waiting it out (docs/agents/testing.md). If the runtime cost is genuinely\n` +
|
|
`irreducible, document the reason in the owning test or move it out of the unit lane.\n` +
|
|
failing.map(line).join('\n'),
|
|
);
|
|
return true;
|
|
}
|
|
|
|
export default function slowTestGateReporter(): Reporter {
|
|
const offenders: Offender[] = [];
|
|
let root = '';
|
|
return {
|
|
onInit(ctx: { config: { root: string } }): void {
|
|
root = ctx.config.root;
|
|
},
|
|
onTestCaseResult(testCase: TestCase): void {
|
|
const result = testCase.result();
|
|
if (result.state !== 'passed' && result.state !== 'failed') return;
|
|
const offender = classifySlowTest({
|
|
root,
|
|
moduleId: (testCase.module as TestModule).moduleId,
|
|
name: testCase.name,
|
|
fullName: testCase.fullName,
|
|
durationMs: testCase.diagnostic()?.duration ?? 0,
|
|
});
|
|
if (offender) offenders.push(offender);
|
|
},
|
|
onTestRunEnd(): void {
|
|
// eslint-disable-next-line no-console
|
|
if (reportSlowTests(offenders, (message) => console.error(message))) {
|
|
process.exitCode = 1;
|
|
recordRunBlocker({
|
|
kind: 'slow-test gate',
|
|
detail: `${offenders.filter((offender) => offender.enforce).length} test(s) exceeded the wall-clock budget`,
|
|
});
|
|
}
|
|
},
|
|
};
|
|
}
|