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>
532 lines
21 KiB
TypeScript
532 lines
21 KiB
TypeScript
// Gate for the contention single-retry policy (#1419). A retry that can hide an
|
|
// assertion failure is worse than no retry at all, so both halves are asserted
|
|
// here: the shape of the enumerated list (owned waivers, real files, no globs)
|
|
// and the decisions the lane makes from a failed run.
|
|
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
import { runCmdSync } from '../../src/utils/exec.ts';
|
|
import { runWithContentionRetry, type TestRun } from './contention-retry-lane.ts';
|
|
import { processBlockers } from './contention-retry-blockers.ts';
|
|
import contentionRetryReporter, {
|
|
failedTestCase,
|
|
runBlockers,
|
|
writeFailureReport,
|
|
} from './contention-retry-reporter.ts';
|
|
import { RUNNER_TIMEOUT_META, RUNNER_TIMEOUT_TOKEN_ENV } from './runner-timeout-meta.ts';
|
|
import { firstRunArgs, RETRY_COVERAGE_DIR, retryRunArgs } from './contention-retry-args.ts';
|
|
import {
|
|
CONTENTION_RETRY_FILES,
|
|
expiredRetryEntries,
|
|
formatRetrySummary,
|
|
isRunnerTimeout,
|
|
parseFailureReport,
|
|
planContentionRetry,
|
|
SUBPROCESS_STUB_TESTS,
|
|
type TestFailure,
|
|
} from './contention-retry.ts';
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, '../..');
|
|
const FIXTURE_CONFIG = 'test/contention-retry-fixtures/vitest.fixture.config.ts';
|
|
|
|
const LISTED = 'src/daemon/__tests__/request-router-open.test.ts';
|
|
// Vitest's own timeout error, verbatim (@vitest/runner `makeTimeoutError`).
|
|
const VITEST_TIMEOUT = {
|
|
name: 'Error',
|
|
message:
|
|
'Test timed out in 5000ms.\nIf this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".',
|
|
};
|
|
const TIMEOUT_MESSAGE = `Error: ${VITEST_TIMEOUT.message}`;
|
|
const ASSERTION_MESSAGE = 'AssertionError: expected "DEVICE_IN_USE" to be "OK"';
|
|
|
|
function failure(overrides: Partial<TestFailure> = {}): TestFailure {
|
|
return {
|
|
file: LISTED,
|
|
testName: 'opens a session',
|
|
message: TIMEOUT_MESSAGE,
|
|
timeout: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function assertionFailure(overrides: Partial<TestFailure> = {}): TestFailure {
|
|
return failure({ message: ASSERTION_MESSAGE, timeout: false, ...overrides });
|
|
}
|
|
|
|
/** The run's secret, minted by the lane and never visible to a test module. */
|
|
const TOKEN = 'a4ec0a4a-0f77-4f2e-9d4a-6d7f1d3f0d21';
|
|
|
|
/** What the runner-timeout setup file writes on a test the runner aborted. */
|
|
const RUNNER_ABORTED = { [RUNNER_TIMEOUT_META]: TOKEN };
|
|
|
|
function testCaseStub(
|
|
errors: ReadonlyArray<Record<string, unknown>>,
|
|
overrides: { state?: string; meta?: Record<string, unknown> } = {},
|
|
): unknown {
|
|
return {
|
|
fullName: 'opens a session',
|
|
module: { moduleId: `${repoRoot}/${LISTED}` },
|
|
meta: () => overrides.meta ?? RUNNER_ABORTED,
|
|
result: () => ({ state: overrides.state ?? 'failed', errors }),
|
|
};
|
|
}
|
|
|
|
function lane(runs: { first: TestRun; retry?: TestRun; today?: Date }): {
|
|
result: ReturnType<typeof runWithContentionRetry>;
|
|
rerun: string[][];
|
|
} {
|
|
const rerun: string[][] = [];
|
|
const result = runWithContentionRetry({
|
|
runAll: () => Promise.resolve(runs.first),
|
|
runFiles: (files) => {
|
|
rerun.push([...files]);
|
|
return Promise.resolve(runs.retry ?? { ok: true, failures: [] });
|
|
},
|
|
commit: 'c'.repeat(40),
|
|
configHash: 'sha256:deadbeef',
|
|
vitestVersion: '4.1.8',
|
|
startedAtMs: 0,
|
|
now: () => 1_000,
|
|
today: runs.today,
|
|
});
|
|
return { result, rerun };
|
|
}
|
|
|
|
test('every retry-list entry is an owned waiver naming why the file spawns or waits', () => {
|
|
for (const entry of CONTENTION_RETRY_FILES) {
|
|
assert.ok(
|
|
fs.existsSync(path.join(repoRoot, entry.file)),
|
|
`${entry.file} does not exist — remove the retry entry with the test`,
|
|
);
|
|
assert.ok(
|
|
!/[*?{}[\]]/.test(entry.file),
|
|
`${entry.file} looks like a glob; the retry set is enumerated`,
|
|
);
|
|
assert.match(
|
|
entry.reason,
|
|
/spawn|wait|poll|socket/i,
|
|
`${entry.file} must name the spawn/wait that makes it contention-flaky`,
|
|
);
|
|
assert.match(
|
|
entry.trackingIssue,
|
|
/^https:\/\/github\.com\/callstack\/agent-device\/issues\/\d+$/,
|
|
);
|
|
assert.match(entry.reviewBy, /^\d{4}-\d{2}-\d{2}$/);
|
|
}
|
|
const files = CONTENTION_RETRY_FILES.map((entry) => entry.file);
|
|
assert.equal(new Set(files).size, files.length, 'duplicate retry entries');
|
|
});
|
|
|
|
test('the repeat offenders and every subprocess-stub file are retry-eligible', () => {
|
|
const files = new Set(CONTENTION_RETRY_FILES.map((entry) => entry.file));
|
|
for (const offender of [
|
|
'src/daemon/__tests__/request-router-open.test.ts',
|
|
'src/platforms/apple/core/__tests__/runner-client.test.ts',
|
|
'src/platforms/apple/core/__tests__/runner-xctestrun.test.ts',
|
|
'scripts/__tests__/help-conformance-bench.test.ts',
|
|
]) {
|
|
assert.ok(files.has(offender), `${offender} must stay retry-eligible`);
|
|
}
|
|
for (const stub of SUBPROCESS_STUB_TESTS) assert.ok(files.has(stub));
|
|
assert.ok(SUBPROCESS_STUB_TESTS.length < CONTENTION_RETRY_FILES.length);
|
|
});
|
|
|
|
test('vitest projects read the shared constant instead of re-listing globs', () => {
|
|
const config = fs.readFileSync(path.join(repoRoot, 'vitest.config.ts'), 'utf8');
|
|
assert.match(config, /from '\.\/scripts\/lib\/contention-retry\.ts'/);
|
|
});
|
|
|
|
test('an expired waiver fails the gate; the committed list is not expired', () => {
|
|
assert.deepEqual(expiredRetryEntries(new Date()), []);
|
|
const expired = expiredRetryEntries(new Date('2099-01-01T00:00:00Z'));
|
|
assert.equal(expired.length, CONTENTION_RETRY_FILES.length);
|
|
});
|
|
|
|
test('the expiry gate fails the run before any test executes', async () => {
|
|
const { result, rerun } = lane({
|
|
first: { ok: false, failures: [failure()] },
|
|
today: new Date('2099-01-01T00:00:00Z'),
|
|
});
|
|
const resolved = await result;
|
|
assert.equal(resolved.ok, false);
|
|
assert.match(resolved.summary, /Retry list expired/);
|
|
assert.deepEqual(rerun, []);
|
|
});
|
|
|
|
test('only the runner-owned abort mark classifies a failure as a timeout', () => {
|
|
assert.ok(isRunnerTimeout([VITEST_TIMEOUT], RUNNER_ABORTED, TOKEN));
|
|
// `task.meta` is test-writable, so a mark that is not the run's secret is worthless.
|
|
for (const forged of [true, 'true', 'agentDeviceRunnerTimeout', '', undefined]) {
|
|
assert.ok(!isRunnerTimeout([VITEST_TIMEOUT], { [RUNNER_TIMEOUT_META]: forged }, TOKEN));
|
|
}
|
|
// No secret configured means nothing is eligible.
|
|
assert.ok(!isRunnerTimeout([VITEST_TIMEOUT], RUNNER_ABORTED, undefined));
|
|
assert.ok(!isRunnerTimeout([VITEST_TIMEOUT], RUNNER_ABORTED, ''));
|
|
// Without the runner's mark, no message and no error shape can earn a retry.
|
|
for (const impostor of [
|
|
{ name: 'Error', message: VITEST_TIMEOUT.message },
|
|
{ name: 'AssertionError', message: VITEST_TIMEOUT.message },
|
|
{ name: 'Error', message: 'connect ETIMEDOUT 127.0.0.1:8080' },
|
|
{ name: 'Error', message: 'Closing timeout while tearing down the daemon' },
|
|
]) {
|
|
assert.ok(!isRunnerTimeout([impostor], {}, TOKEN), `${impostor.name}: ${impostor.message}`);
|
|
assert.ok(!isRunnerTimeout([impostor], undefined, TOKEN));
|
|
assert.ok(!isRunnerTimeout([impostor], { [RUNNER_TIMEOUT_META]: 'yes' }, TOKEN));
|
|
}
|
|
// An aborted test that also produced an assertion diff is a regression.
|
|
assert.ok(
|
|
!isRunnerTimeout(
|
|
[VITEST_TIMEOUT, { name: 'AssertionError', message: 'x', expected: 1, actual: 2 }],
|
|
RUNNER_ABORTED,
|
|
TOKEN,
|
|
),
|
|
);
|
|
assert.ok(!isRunnerTimeout([], RUNNER_ABORTED, TOKEN));
|
|
});
|
|
|
|
test('a real Vitest run marks only runner-aborted tests as retry-eligible', () => {
|
|
// The classifier's inputs come from the runner, so this case drives a real
|
|
// child Vitest run over test/contention-retry-fixtures/ rather than stubs.
|
|
const report = path.join(repoRoot, '.tmp/contention-retry/fixture-gate.json');
|
|
fs.rmSync(report, { force: true });
|
|
runCmdSync('pnpm', ['exec', 'vitest', 'run', '--config', FIXTURE_CONFIG], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
CONTENTION_RETRY_FAILURES: report,
|
|
[RUNNER_TIMEOUT_TOKEN_ENV]: TOKEN,
|
|
},
|
|
allowFailure: true,
|
|
});
|
|
const { failures } = parseFailureReport(JSON.parse(fs.readFileSync(report, 'utf8')), repoRoot);
|
|
assert.deepEqual(Object.fromEntries(failures.map((entry) => [entry.testName, entry.timeout])), {
|
|
'genuine runner timeout': true,
|
|
'timeout message thrown immediately': false,
|
|
// The forgery this gate exists for: blocking the event loop past the
|
|
// budget does not abort the signal, so the throw stays a plain failure.
|
|
'timeout message thrown after blocking the event loop past the budget': false,
|
|
'assertion failure after blocking the event loop past the budget': false,
|
|
'plain assertion failure': false,
|
|
// `task.meta` is test-writable, but the mark is the run's secret and the
|
|
// setup file takes it out of the environment before any test module loads.
|
|
'test writes the provenance marker itself': false,
|
|
'test writes the marker after blocking the event loop past the budget': false,
|
|
'test writes the marker from the environment it can still read': false,
|
|
});
|
|
// Only the genuine timeout may reach a rerun.
|
|
const listed = failures.map((entry) => ({ ...entry, file: LISTED }));
|
|
const plan = planContentionRetry(listed);
|
|
assert.equal(plan.retry, false);
|
|
assert.equal(planContentionRetry(listed.filter((entry) => entry.timeout)).retry, true);
|
|
});
|
|
|
|
test('an assertion message quoting a timeout still fails on the first run', async () => {
|
|
const impostor = failedTestCase(
|
|
testCaseStub([{ name: 'AssertionError', message: VITEST_TIMEOUT.message }], {
|
|
meta: {},
|
|
}) as never,
|
|
TOKEN,
|
|
);
|
|
assert.ok(impostor);
|
|
assert.equal(impostor.timeout, false);
|
|
const { result, rerun } = lane({
|
|
first: { ok: false, failures: [{ ...impostor, file: LISTED }] },
|
|
});
|
|
assert.equal((await result).ok, false);
|
|
assert.deepEqual(rerun, []);
|
|
});
|
|
|
|
test('a test that timed out AND failed an assertion is not retry-eligible', () => {
|
|
const mixed = failedTestCase(
|
|
testCaseStub([
|
|
VITEST_TIMEOUT,
|
|
{ name: 'AssertionError', message: 'expected 1 to be 2', expected: 2, actual: 1 },
|
|
]) as never,
|
|
TOKEN,
|
|
);
|
|
assert.equal(mixed?.timeout, false);
|
|
});
|
|
|
|
test('the lane reporter keeps the real error message a timeout is classified by', () => {
|
|
const failed = failedTestCase(testCaseStub([VITEST_TIMEOUT]) as never, TOKEN);
|
|
assert.deepEqual(failed, {
|
|
file: `${repoRoot}/${LISTED}`,
|
|
testName: 'opens a session',
|
|
message: TIMEOUT_MESSAGE,
|
|
timeout: true,
|
|
});
|
|
assert.equal(failedTestCase(testCaseStub([], { state: 'passed' }) as never, TOKEN), null);
|
|
|
|
const target = path.join(repoRoot, '.tmp/contention-retry/reporter-gate.json');
|
|
writeFailureReport({ failures: [failure()], blockers: [] }, target);
|
|
assert.deepEqual(parseFailureReport(JSON.parse(fs.readFileSync(target, 'utf8')), repoRoot), {
|
|
failures: [failure()],
|
|
blockers: [],
|
|
});
|
|
assert.ok(contentionRetryReporter().onTestCaseResult);
|
|
});
|
|
|
|
test('the Coverage lane keeps the configured reporters, failure sink included', async () => {
|
|
const { reporters } = await import('../../vitest.config.ts');
|
|
const plain = reporters({});
|
|
const laneReporters = reporters({ CONTENTION_RETRY_FAILURES: '/tmp/failures.json' });
|
|
assert.equal(plain.length, 2, 'default + slow-test gate');
|
|
assert.equal(
|
|
laneReporters.length,
|
|
plain.length + 1,
|
|
'the failure sink is added, never substituted',
|
|
);
|
|
// The slow-test gate must survive into the retry lane.
|
|
assert.ok(laneReporters.every((reporter) => Boolean(reporter)));
|
|
assert.ok(typeof laneReporters.at(-1) === 'object');
|
|
const runner = fs.readFileSync(
|
|
path.join(repoRoot, 'scripts/lib/contention-retry-run.ts'),
|
|
'utf8',
|
|
);
|
|
assert.ok(
|
|
!/['"]--reporter/.test(runner),
|
|
'a --reporter flag replaces the configured reporters and would drop the slow-test gate',
|
|
);
|
|
});
|
|
|
|
test('the retry reruns the failed files in the first run modes', () => {
|
|
const modes = { projects: ['unit-core', 'subprocess-stub'], coverage: true };
|
|
assert.deepEqual(firstRunArgs(modes), [
|
|
'--project',
|
|
'unit-core',
|
|
'--project',
|
|
'subprocess-stub',
|
|
'--coverage',
|
|
]);
|
|
const retry = retryRunArgs(modes, [LISTED]);
|
|
assert.deepEqual(retry, [
|
|
'--project',
|
|
'unit-core',
|
|
'--project',
|
|
'subprocess-stub',
|
|
'--coverage',
|
|
`--coverage.reportsDirectory=${RETRY_COVERAGE_DIR}`,
|
|
// A subset of files can never meet whole-suite thresholds, and a first-run
|
|
// threshold failure is a blocker, so the retry never runs after one.
|
|
'--coverage.thresholds.statements=0',
|
|
'--coverage.thresholds.lines=0',
|
|
LISTED,
|
|
]);
|
|
// The gate reads the first run's report; the retry must not overwrite it.
|
|
assert.notEqual(RETRY_COVERAGE_DIR, 'coverage');
|
|
assert.deepEqual(retryRunArgs({ projects: [], coverage: false }, [LISTED]), [LISTED]);
|
|
|
|
const runner = fs.readFileSync(
|
|
path.join(repoRoot, 'scripts/lib/contention-retry-run.ts'),
|
|
'utf8',
|
|
);
|
|
assert.match(runner, /runAll: \(\) => runVitest\(firstRunArgs\(modes\)/);
|
|
assert.match(runner, /runFiles: \(files\) =>\s*runVitest\(retryRunArgs\(modes, files\)/);
|
|
});
|
|
|
|
test('non-test failures block the retry instead of being rerun away', async () => {
|
|
const covered = processBlockers({
|
|
ok: false,
|
|
failureCount: 1,
|
|
output: [
|
|
' Test Files 1 failed (11)',
|
|
'ERROR: Coverage for lines (79.5%) does not meet global threshold (80%)',
|
|
].join('\n'),
|
|
});
|
|
assert.deepEqual(
|
|
covered.map((blocker) => blocker.kind),
|
|
['coverage threshold'],
|
|
);
|
|
assert.deepEqual(
|
|
processBlockers({ ok: false, failureCount: 0, output: 'Error: worker exited' }).map(
|
|
(blocker) => blocker.kind,
|
|
),
|
|
['unexplained failure'],
|
|
);
|
|
assert.deepEqual(processBlockers({ ok: true, failureCount: 0, output: '' }), []);
|
|
|
|
const moduleErrors = runBlockers(
|
|
[
|
|
{ moduleId: `${repoRoot}/${LISTED}`, errors: () => [{ message: 'Cannot find module x' }] },
|
|
] as never,
|
|
[{ name: 'Error', message: 'Unhandled rejection\n at foo' }],
|
|
);
|
|
assert.deepEqual(
|
|
moduleErrors.map((blocker) => blocker.kind),
|
|
['unhandled error', 'module error'],
|
|
);
|
|
|
|
// A retry-eligible timeout alongside any of them still fails the job.
|
|
for (const blockers of [covered, moduleErrors]) {
|
|
const { result, rerun } = lane({ first: { ok: false, failures: [failure()], blockers } });
|
|
const resolved = await result;
|
|
assert.equal(resolved.ok, false);
|
|
assert.deepEqual(rerun, [], 'a blocked run must never be rerun');
|
|
assert.equal(resolved.envelope.data.retryCount, 0);
|
|
assert.match(resolved.summary, /No retry: the run failed for a reason a rerun cannot re-check/);
|
|
}
|
|
});
|
|
|
|
test('a gate that fails the run without failing a test blocks the retry', async () => {
|
|
const { default: slowTestGateReporter } = await import('../vitest-slow-test-reporter.ts');
|
|
const gate = slowTestGateReporter();
|
|
const exitCode = process.exitCode;
|
|
const stderr = console.error;
|
|
console.error = () => {};
|
|
try {
|
|
gate.onInit?.({ config: { root: repoRoot } } as never);
|
|
// Passing, but far past the unit budget: no failed test, run must still fail.
|
|
gate.onTestCaseResult?.({
|
|
name: 'INJECTED slow test',
|
|
fullName: 'INJECTED slow test',
|
|
module: { moduleId: `${repoRoot}/${LISTED}` },
|
|
diagnostic: () => ({ duration: 30_000 }),
|
|
result: () => ({ state: 'passed', errors: [] }),
|
|
} as never);
|
|
gate.onTestRunEnd?.([] as never, [] as never, 'failed' as never);
|
|
} finally {
|
|
console.error = stderr;
|
|
process.exitCode = exitCode;
|
|
}
|
|
// The gate published its verdict on the shared channel; the sink drains it.
|
|
const blockers = runBlockers([], []);
|
|
assert.deepEqual(
|
|
blockers.map((blocker) => blocker.kind),
|
|
['slow-test gate'],
|
|
);
|
|
assert.deepEqual(runBlockers([], []), [], 'draining is one-shot');
|
|
|
|
// A retry-eligible timeout in the same run must not rerun the gate away.
|
|
const { result, rerun } = lane({ first: { ok: false, failures: [failure()], blockers } });
|
|
const resolved = await result;
|
|
assert.equal(resolved.ok, false);
|
|
assert.deepEqual(rerun, []);
|
|
assert.match(resolved.summary, /slow-test gate/);
|
|
});
|
|
|
|
test('two timed-out tests in one file are one retry, counted once', async () => {
|
|
const failures = [failure(), failure({ testName: 'closes a session' })];
|
|
const plan = planContentionRetry(failures);
|
|
assert.deepEqual(plan.retry && plan.files, [LISTED]);
|
|
const { result, rerun } = lane({ first: { ok: false, failures } });
|
|
const resolved = await result;
|
|
assert.deepEqual(rerun, [[LISTED]]);
|
|
assert.equal(resolved.envelope.data.retryCount, 1);
|
|
assert.deepEqual(resolved.envelope.data.retried, [
|
|
{
|
|
file: LISTED,
|
|
testNames: ['opens a session', 'closes a session'],
|
|
trackingIssue: CONTENTION_RETRY_FILES.find((entry) => entry.file === LISTED)?.trackingIssue,
|
|
},
|
|
]);
|
|
assert.match(resolved.summary, /Retried 1 timeout-shaped file\(s\)/);
|
|
assert.equal(resolved.summary.split('\n').filter((line) => line.includes(LISTED)).length, 1);
|
|
});
|
|
|
|
test('reporter failures are read as repo-relative paths, names, and messages', () => {
|
|
const failures = parseFailureReport(
|
|
{
|
|
failures: [
|
|
{
|
|
file: `${repoRoot}/${LISTED}`,
|
|
testName: 'opens a session',
|
|
message: TIMEOUT_MESSAGE,
|
|
timeout: true,
|
|
},
|
|
{ testName: 'no file' },
|
|
],
|
|
},
|
|
repoRoot,
|
|
);
|
|
assert.deepEqual(failures.failures, [failure()]);
|
|
});
|
|
|
|
test('an unreadable report yields no retry-eligible failures', () => {
|
|
assert.deepEqual(parseFailureReport({}, repoRoot), { failures: [], blockers: [] });
|
|
assert.deepEqual(parseFailureReport({ failures: 'nope' }, repoRoot), {
|
|
failures: [],
|
|
blockers: [],
|
|
});
|
|
assert.equal(planContentionRetry([]).retry, false);
|
|
});
|
|
|
|
test('a timeout in a listed file retries exactly that file, once', () => {
|
|
const plan = planContentionRetry([failure()]);
|
|
assert.deepEqual(plan, { retry: true, files: [LISTED], failures: [failure()] });
|
|
});
|
|
|
|
test('an assertion failure in a listed file never retries', () => {
|
|
const plan = planContentionRetry([assertionFailure()]);
|
|
assert.equal(plan.retry, false);
|
|
assert.match(plan.reason, /assertion failures never retry/);
|
|
});
|
|
|
|
test('a timeout outside the list never retries, even alongside eligible ones', () => {
|
|
const plan = planContentionRetry([
|
|
failure(),
|
|
failure({ file: 'src/daemon/__tests__/session-store.test.ts' }),
|
|
]);
|
|
assert.equal(plan.retry, false);
|
|
assert.match(plan.reason, /outside the enumerated retry list/);
|
|
});
|
|
|
|
test('an injected assertion failure in a listed file fails the job on the first run', async () => {
|
|
const { result, rerun } = lane({
|
|
first: { ok: false, failures: [assertionFailure()] },
|
|
});
|
|
const resolved = await result;
|
|
assert.equal(resolved.ok, false);
|
|
assert.deepEqual(rerun, [], 'an assertion failure must never be rerun');
|
|
assert.equal(resolved.envelope.data.retryCount, 0);
|
|
assert.equal(resolved.envelope.result, 'fail');
|
|
});
|
|
|
|
test('an injected timeout in a listed file passes on retry with a visible summary line', async () => {
|
|
const { result, rerun } = lane({ first: { ok: false, failures: [failure()] } });
|
|
const resolved = await result;
|
|
assert.equal(resolved.ok, true);
|
|
assert.deepEqual(rerun, [[LISTED]]);
|
|
assert.match(
|
|
resolved.summary,
|
|
/Retried 1 timeout-shaped file\(s\) once — outcome: \*\*passed\*\*/,
|
|
);
|
|
assert.match(resolved.summary, new RegExp(LISTED.replaceAll('.', '\\.')));
|
|
assert.equal(resolved.envelope.data.retryCount, 1);
|
|
assert.equal(resolved.envelope.data.retryOutcome, 'passed');
|
|
assert.equal(resolved.envelope.result, 'pass');
|
|
});
|
|
|
|
test('a file that fails again after its one retry fails the job', async () => {
|
|
const { result } = lane({
|
|
first: { ok: false, failures: [failure()] },
|
|
retry: { ok: false, failures: [failure()] },
|
|
});
|
|
const resolved = await result;
|
|
assert.equal(resolved.ok, false);
|
|
assert.match(resolved.summary, /outcome: \*\*failed\*\*/);
|
|
assert.equal(resolved.envelope.data.retryOutcome, 'failed');
|
|
});
|
|
|
|
test('a green run reports no retry and still emits lane telemetry', async () => {
|
|
const { result } = lane({ first: { ok: true, failures: [] } });
|
|
const resolved = await result;
|
|
assert.equal(resolved.ok, true);
|
|
assert.equal(resolved.summary, '');
|
|
assert.equal(resolved.envelope.lane, 'unit-contention-retry');
|
|
assert.equal(resolved.envelope.data.retryCount, 0);
|
|
assert.equal(resolved.envelope.data.listSize, CONTENTION_RETRY_FILES.length);
|
|
});
|
|
|
|
test('the summary names the tracking issue and review date of every retried file', () => {
|
|
const summary = formatRetrySummary({
|
|
plan: planContentionRetry([failure()]),
|
|
outcome: 'passed',
|
|
});
|
|
const entry = CONTENTION_RETRY_FILES.find((candidate) => candidate.file === LISTED);
|
|
assert.ok(entry);
|
|
assert.match(summary, new RegExp(entry.trackingIssue.replaceAll('/', '\\/')));
|
|
assert.match(summary, new RegExp(entry.reviewBy));
|
|
});
|