Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 8 additions & 26 deletions docs/agents/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,14 @@
Serialized validation requires macOS or Linux.
On Windows, use plan mode or focused checks.
Both broad commands and heavy `pnpm gate` ids acquire the same host lock.
Wait for the owner to finish, or cancel your waiter with Ctrl-C.
Wait for the owner to finish.
Do not add lock wrappers. Do not delete lock files.
A wait ends after two hours. Nested validation fails immediately.
A wait ends after two hours.
Focused tests do not lock. Avoid subprocess-heavy tests beside a broad gate.

The runner prints progress and a receipt path. The receipt contains:
The runner prints progress and a receipt path. The receipt contains refs, checkout state, command logs, exits, skips, and Vitest results.

- Refs.
- Checkout state.
- Command logs.
- Exits.
- Skips.
- Vitest results.

Execution stops at the first failure. Unexecuted checks remain `not-run`.
Execution stops at the first failure.

- `pnpm check:affected`: compact plan.
- `pnpm check:affected --verbose`: plan with selection reasons.
Expand All @@ -47,15 +40,9 @@ Do not reduce selection to get a pass.

Read required-check status on the PR.
The receipt lists skipped checks and owners in `results[].reason`.
GitHub owns:

- Full coverage.
- Native builds.
- Device lanes.
- Provider integration.
- Compatibility that uses history.

Related tests do not replace those obligations.
GitHub owns full coverage, native builds, device lanes, provider integration, and compatibility that uses history.
On a narrowed plan, related tests replace only provider integration.
A production source change also runs the unit suite (about 3 minutes), because some unit gates read sources as text.
Check the Integration Tests and Coverage jobs at the actual PR head.
For docs-only changes, run the selected documentation checks.
Do not add tests that assert prose.
Expand All @@ -73,12 +60,7 @@ Never call a failing gate passed.
| Base comparison unavailable or inconclusive | Record the missing tool/device/evidence and next action. | Blocked, cause unproven. An authorized draft can record this. |
| Failure source fixed, environment repaired, or new upstream commit contains the fix | Run the failing check. Then run the affected gate on the current head. | Replace blocked status only with new evidence. |

Match failures by:

- Check.
- Location.
- Diagnostic code.
- Assertion.
Match failures by check, location, diagnostic code, and assertion.

Until a named owner or issue accepts the blocker, the implementing agent owns diagnosis.
Do not loop on an unchanged failure.
Expand Down
19 changes: 18 additions & 1 deletion scripts/check-affected/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function ids(changedFiles: string[]): CheckId[] {
return plan(changedFiles).checks;
}

test('production source selects static/build gates and delegates tests to Vitest', () => {
test('production source selects static/build gates, related tests, and the full unit suite', () => {
const result = plan(['packages/selectors/src/index.ts']);
assert.equal(result.failOpen, false);
for (const id of [
Expand All @@ -28,6 +28,7 @@ test('production source selects static/build gates and delegates tests to Vitest
'layering',
'build',
'vitest-related',
'unit',
] as const) {
assert.ok(result.checks.includes(id), `expected ${id}`);
}
Expand All @@ -38,6 +39,22 @@ test('production source selects static/build gates and delegates tests to Vitest
}
});

test('any production source selects the unit suite whose gates read sources as text', () => {
for (const file of [
'src/cli.ts',
'packages/daemon/src/index.ts',
'packages/kernel/src/errors.ts',
]) {
const result = plan([file]);
assert.ok(
result.reasons.some(
(reason) => reason.check === 'unit' && reason.rule === 'own:unit-text-gates',
),
file,
);
}
});

test('platform package source additionally selects provider-integration', () => {
const result = ids(['packages/platform-apple/src/core/app-resolution.ts']);
assert.ok(result.includes('provider-integration'));
Expand Down
15 changes: 15 additions & 0 deletions scripts/check-affected/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,25 @@ const staticTsGates: OwnershipRule = ({ file, isTs, underSrc, underTest }) =>
]
: [];

// Unit-suite gates such as eager-closure-budgets read production sources as text (git trees,
// parsed module graphs) instead of importing them, so `vitest related` cannot select them. A
// production source change therefore runs the whole unit suite, beside the related run that
// covers the other Vitest projects.
function textReadingUnitGates(file: string): SelectionReason {
return reason(
'unit',
file,
'own:unit-text-gates',
'unit-suite gates read production sources as text, which the Vitest module graph cannot trace',
);
}

const srcProdGate: OwnershipRule = ({ file, isSrcProd }) => {
if (!isSrcProd) return [];
return [
reason('layering', file, 'gate:layering', 'layering guard reads production src/ modules'),
reason('build', file, 'src-prod', 'production source is compiled by the build'),
textReadingUnitGates(file),
];
};

Expand Down Expand Up @@ -265,6 +279,7 @@ const workspacePackageOwnership: OwnershipRule = ({ file, isTs }) => {
if (!isTestPath(file)) {
selections.push(
reason('build', file, 'package-src', 'package source is bundled into the published artifact'),
textReadingUnitGates(file),
);
}
return selections;
Expand Down
65 changes: 54 additions & 11 deletions scripts/check-affected/run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ test('runChecks passes the selector change set to Vitest related', async () => {
);
});

test('runChecks skips CI checks, deduplicates related tests, and preserves local ordering', async () => {
test('runChecks skips CI checks, runs every local check of a fail-open plan, and preserves local ordering', async () => {
const executed: string[][] = [];
const execute: CommandExecutor = async (command) => {
executed.push(command);
Expand Down Expand Up @@ -214,23 +214,66 @@ test('runChecks skips CI checks, deduplicates related tests, and preserves local
executed.some((command) => command.includes('test:coverage')),
false,
);
assert.equal(
executed.some((command) => command.includes('check:unit')),
false,
'related tests cover the selected unit graph without repeating the full suite',
);
assert.equal(
executed.some((command) => command.includes('test:integration:provider')),
false,
'related tests cover the selected provider graph without repeating the full suite',
);
// A fail-open plan could not narrow the change, so the related run stands in for nothing.
for (const script of ['check:unit', 'test:integration:provider']) {
assert.ok(
executed.some((command) => command.includes(script)),
`${script} must run when the plan fails open`,
);
}
assert.equal(
executed.some((command) => command.includes('check:coverage-changed')),
false,
'the coverage gate is GitHub-authoritative and must not run locally',
);
});

async function runFailingOn(script: string, changedFiles: string[]): Promise<number> {
const plan = selectChecks({ changedFiles, packageEntryFiles: [] });
assert.equal(plan.failOpen, false, changedFiles.join(', '));
return await runChecks(plan, { scripts: ALL_SCRIPTS }, ARGS, {
execute: async (command) => (command.includes(script) ? 23 : 0),
cwd: '.',
changedFiles,
});
}

// The eager-closure budget and similar unit gates read sources as text, so no related test reaches
// them; a production change that only they reject must still fail the local run (#3995).
test('a production source change fails on a unit-suite failure the related run cannot see', async () => {
for (const changedFiles of [
['src/cli.ts'],
['packages/selectors/src/index.ts'],
['packages/platform-apple/src/runner/runner-cache-metadata.ts'],
['packages/selectors/src/index.ts', 'contracts/fixtures/tap-point-policy.json'],
]) {
assert.equal(await runFailingOn('check:unit', changedFiles), 23, changedFiles.join(', '));
}
});

test('a test-only change leaves the full unit suite to CI', async () => {
assert.equal(await runFailingOn('check:unit', ['packages/daemon/src/selectors.test.ts']), 0);
});

test('the related run still stands in for provider-integration on a narrowed plan', async () => {
const changedFiles = ['packages/platform-apple/src/runner/runner-cache-metadata.ts'];
const plan = selectChecks({ changedFiles, packageEntryFiles: [] });
assert.equal(plan.failOpen, false);
assert.ok(plan.checks.includes('provider-integration'));
const executed: string[][] = [];
const code = await runChecks(plan, { scripts: ALL_SCRIPTS }, ARGS, {
execute: async (command) => {
executed.push(command);
return 0;
},
cwd: '.',
changedFiles,
});
assert.equal(code, 0);
assert.ok(executed.some((command) => command.includes('related')));
assert.ok(!executed.some((command) => command.includes('test:integration:provider')));
});

test('runChecks fails fast on a stale install before running format or any other check', async () => {
const executed: string[][] = [];
const execute: CommandExecutor = async (command) => {
Expand Down
7 changes: 5 additions & 2 deletions scripts/check-affected/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ export async function runChecks(
const execute = options.execute;
const runnable = plan.checks.map(getCheckSpec).filter((spec: CheckSpec) => spec.localRunnable);
const skipped = plan.checks.map(getCheckSpec).filter((spec: CheckSpec) => !spec.localRunnable);
const relatedSelected = plan.checks.includes('vitest-related');
// A fail-open plan could not narrow the change, so no check may stand in for another.
const relatedSelected = !plan.failOpen && plan.checks.includes('vitest-related');
const ciJobs = skipped.length > 0 ? ciJobsByCheck() : new Map<CheckId, string[]>();
for (const spec of skipped) {
const reason = describeOwner(spec.id, ciJobs);
Expand Down Expand Up @@ -253,8 +254,10 @@ function describeOwner(id: CheckId, ciJobs: ReadonlyMap<CheckId, string[]>): str
return `GitHub-authoritative (jobs: ${(ciJobs.get(id) ?? []).join(', ')})`;
}

// Only provider-integration defers to the related run. `unit` never does: its suite holds gates that
// read sources as text, which the related module graph cannot select.
function isCoveredByRelatedTests(spec: CheckSpec, relatedSelected: boolean): boolean {
return relatedSelected && (spec.id === 'unit' || spec.id === 'provider-integration');
return relatedSelected && spec.id === 'provider-integration';
}

async function main(argv = process.argv.slice(2)): Promise<number> {
Expand Down
Loading