mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
df0a0f7fd2
* perf(package): strip comments from the Apple runner source the npm package ships The packager copies apple/runner/** into dist/ as Swift source, removing only its AGENT_DEVICE_RUNNER_UNIT_TESTS blocks, so doc comments and design notes were downloaded on every install: 71.9 kB of 441.2 kB of packaged runner Swift. Add a lexical scanner for the removal. A regex cannot do this: `//` and `/*` open a comment only in code position, raw literals move their own delimiter and escape with the `#` count, interpolation segments hold code and further literals, and Swift block comments nest. A construct the scanner cannot account for throws at packaging time instead of shipping Swift that does not compile. * fix(package): keep Swift regex literals out of the comment scanner `#/foo//bar/#` is a valid extended regex literal with no comment in it, but the scanner only knew the `#"` raw-string family, so it read the literal's `//` as a line comment and shipped `let pattern = #/foo` — Swift that does not compile. Add `#/…/#` and `##/…/##` as a literal context: matching `#` counts, the single- and multi-line forms, Swift's own-line rule for a multi-line closing delimiter, and the `\/` escape that keeps one from closing early. Bare `/…/` literals stay unresolvable, because the same `/` opens a comment, divides, and starts a regex literal, and only the parse separates them. Where one could begin — an expression position whose `/` is not followed by a space, a tab or `)` — packaging throws by file and line instead of rewriting bytes it cannot prove are code. Divisions (`width/2`, `Double(3)/Double(4)`), the recording scripts' shebang and `(/)` keep flowing through. * fix(package): keep the packaged runner source on the checkout's line numbers `dist/apple/runner/**` is the Swift a user's `xcodebuild` and the runner name a file and line in (it lands in runner.log), so those numbers are only worth reading if they point at the same line of `apple/runner/**`. Both rewriting passes now empty the lines they remove instead of deleting them: comment removal (889 lines, 889 B) and the pre-existing unit-test `#if` block strip, which was moving everything below a block by up to 883 lines (3,737 lines, 3,737 B). `dist/apple/runner/` 555,907 B -> 488,635 B (-67,272 B, -12.1%); its Swift alone 441,196 B -> 373,924 B (-15.2%). Parity costs 4,626 B of the 71,898 B the previous head saved. Nothing in the repo compiles the packaged source, so a mis-lex that failed to throw would ship Swift that does not build and no gate would see it. Add `pnpm check:packaged-runner-swift`: it packages into a throwaway root and asserts line-count parity plus the line of every declaration each packaged file still carries, then runs `swiftc -parse` over all 44 files. The parse half reports itself skipped where no Swift toolchain exists, so the gate is declared on the macOS lane, where both halves run.
394 lines
15 KiB
TypeScript
394 lines
15 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { assertCatalogComplete, CHECK_CATALOG, resolveCommand } from './checks.ts';
|
|
import { ALL_CHECKS, selectChecks, type CheckId, type SelectInput } from './model.ts';
|
|
|
|
function plan(changedFiles: string[], extra: Partial<SelectInput> = {}) {
|
|
return selectChecks({
|
|
changedFiles,
|
|
packageEntryFiles: ['src/index.ts', 'packages/selectors/src/index.ts'],
|
|
...extra,
|
|
});
|
|
}
|
|
|
|
function ids(changedFiles: string[]): CheckId[] {
|
|
return plan(changedFiles).checks;
|
|
}
|
|
|
|
test('production source selects static/build gates and delegates tests to Vitest', () => {
|
|
const result = plan(['packages/selectors/src/index.ts']);
|
|
assert.equal(result.failOpen, false);
|
|
for (const id of [
|
|
'format',
|
|
'lint',
|
|
'typecheck',
|
|
'layering',
|
|
'build',
|
|
'vitest-related',
|
|
] as const) {
|
|
assert.ok(result.checks.includes(id), `expected ${id}`);
|
|
}
|
|
assert.ok(!result.checks.includes('provider-integration'));
|
|
// Every selected check documents why it was chosen.
|
|
for (const id of result.checks) {
|
|
assert.ok(result.reasons.some((reason) => reason.check === id));
|
|
}
|
|
});
|
|
|
|
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'));
|
|
assert.ok(result.includes('coverage'));
|
|
assert.ok(result.includes('vitest-related'));
|
|
});
|
|
|
|
test('unit test files delegate affected-test discovery to Vitest', () => {
|
|
const result = ids(['src/daemon/selectors.test.ts']);
|
|
assert.ok(result.includes('vitest-related'));
|
|
assert.ok(!result.includes('unit'));
|
|
assert.ok(!result.includes('provider-integration'));
|
|
});
|
|
|
|
test('Vitest owns project and support-module relationships through one check', () => {
|
|
for (const file of [
|
|
'test/integration/provider-scenarios/foo.test.ts',
|
|
'test/integration/provider-scenarios/fixtures.ts',
|
|
'test/integration/interaction-contract/fixtures.ts',
|
|
'test/output-economy/fixtures.ts',
|
|
'src/__tests__/test-utils/session.ts',
|
|
]) {
|
|
assert.ok(ids([file]).includes('vitest-related'), `expected Vitest ownership for ${file}`);
|
|
}
|
|
});
|
|
|
|
test('root node-integration support modules select the node integration suite', () => {
|
|
assert.ok(ids(['test/integration/test-helpers.ts']).includes('integration-node'));
|
|
});
|
|
|
|
test('the shared coverage declaration table selects the node integration suite and the macOS lane', () => {
|
|
const result = ids(['test/integration/command-coverage/declarations.ts']);
|
|
assert.ok(result.includes('integration-node'), 'expected integration-node ownership');
|
|
assert.ok(result.includes('macos-coverage'), 'expected macos-coverage ownership');
|
|
assert.ok(
|
|
!result.includes('vitest-related'),
|
|
'declarations.ts is resolved by node --test, not by Vitest',
|
|
);
|
|
});
|
|
|
|
test('android-adb stub test delegates project ownership to Vitest', () => {
|
|
const result = ids(['packages/platform-android/src/__tests__/notifications.test.ts']);
|
|
assert.ok(result.includes('vitest-related'));
|
|
});
|
|
|
|
test('Swift runner change selects both XCUITest platform builds', () => {
|
|
// Each platform build is its own gate in its own lane, so a Swift change owns both.
|
|
// (The Apple device lanes ride along: the runner is what those lanes boot — device-lanes.ts.)
|
|
assert.deepEqual(ids(['apple/runner/Sources/Runner/Main.swift']), [
|
|
'swift-runner-ios',
|
|
'swift-runner-macos',
|
|
'packaged-runner-swift',
|
|
'replay-ios',
|
|
'replay-ios-device',
|
|
'replay-macos',
|
|
]);
|
|
assert.ok(ids(['packages/platform-apple/src/core/Support.swift']).includes('swift-runner-ios'));
|
|
});
|
|
|
|
test('a runner XCTest source also selects the test-list and package-source check', () => {
|
|
// Distinct from the rule above, which owns Swift *anywhere*: renaming a method under
|
|
// AgentDeviceRunnerUITests/ silently shrinks ios.yml's hand-written `-only-testing:` list
|
|
// (#1781 A7), and the platform builds cannot see that — they compile fine either way.
|
|
assert.deepEqual(
|
|
ids(['apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift']),
|
|
[
|
|
'swift-runner-ios',
|
|
'swift-runner-macos',
|
|
'xctest-selection',
|
|
'packaged-runner-swift',
|
|
'replay-ios',
|
|
'replay-ios-device',
|
|
'replay-macos',
|
|
],
|
|
);
|
|
// The bug the file filter used to have: membership is the directory, not the name.
|
|
assert.ok(
|
|
ids([
|
|
'apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift',
|
|
]).includes('xctest-selection'),
|
|
);
|
|
// Swift elsewhere in the runner still selects only the builds.
|
|
assert.ok(!ids(['apple/runner/Sources/Runner/Main.swift']).includes('xctest-selection'));
|
|
});
|
|
|
|
test('the scripts that rewrite runner Swift select the packaged-source check', () => {
|
|
// Every packaged byte comes out of these two, and the rewrite they perform is invisible to
|
|
// every other gate: no repo target compiles dist/apple/runner/**.
|
|
for (const file of [
|
|
'scripts/package-apple-runner-source.mjs',
|
|
'scripts/strip-swift-comments.mjs',
|
|
]) {
|
|
assert.ok(ids([file]).includes('packaged-runner-swift'), file);
|
|
}
|
|
// Runner Swift outside the XCTest directory owns it too — the packager rewrites all of it.
|
|
assert.ok(ids(['apple/runner/Sources/Runner/Main.swift']).includes('packaged-runner-swift'));
|
|
});
|
|
|
|
test('Android helper change selects the android-helpers build', () => {
|
|
assert.deepEqual(ids(['android/snapshot-helper/src/Main.kt']), [
|
|
'android-helpers',
|
|
'replay-android',
|
|
]);
|
|
assert.deepEqual(ids(['android/ime-helper/AndroidManifest.xml']), [
|
|
'android-helpers',
|
|
'replay-android',
|
|
]);
|
|
});
|
|
|
|
test('Android package test fixture selects the unit suite instead of failing open', () => {
|
|
const fixture =
|
|
'packages/platform-android/src/__tests__/test-utils/fixtures/android-helper-apk.fixture';
|
|
const result = plan([fixture]);
|
|
assert.equal(result.failOpen, false);
|
|
assert.deepEqual(result.checks, ['unit']);
|
|
assert.deepEqual(
|
|
result.reasons.filter((reason) => reason.rule === 'own:android-package-test-fixture'),
|
|
[
|
|
{
|
|
check: 'unit',
|
|
path: fixture,
|
|
rule: 'own:android-package-test-fixture',
|
|
detail: 'the Android package test fixture is consumed by the unit suite',
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
test('MCP metadata change selects the mcp-metadata check', () => {
|
|
assert.deepEqual(ids(['server.json']), ['mcp-metadata']);
|
|
});
|
|
|
|
test('public package surface change selects the build and the published-package gate via exports', () => {
|
|
const result = ids(['src/index.ts']);
|
|
assert.ok(result.includes('build'));
|
|
// A public entry is the one surface a consumer resolves by name, so building it is not enough:
|
|
// check:package proves it still imports from an install with no workspace links.
|
|
assert.ok(result.includes('package'));
|
|
assert.ok(ids(['packages/selectors/src/index.ts']).includes('package'));
|
|
});
|
|
|
|
test('docs-only change selects no checks and records the docs paths', () => {
|
|
const result = plan(['docs/adr/0011.md', 'README.md', 'website/page.mdx.md']);
|
|
assert.equal(result.failOpen, false);
|
|
assert.deepEqual(result.checks, []);
|
|
assert.equal(result.docsOnlyPaths.length, 3);
|
|
});
|
|
|
|
test('agent guidance owns its focused contract instead of disappearing as docs-only', () => {
|
|
for (const file of ['AGENTS.md', 'CONTEXT.md', 'docs/agents/testing.md']) {
|
|
const result = plan([file]);
|
|
assert.deepEqual(result.checks, ['agent-guidance']);
|
|
assert.deepEqual(result.docsOnlyPaths, []);
|
|
}
|
|
});
|
|
|
|
test('test app source selects root lint and format plus its isolated typecheck', () => {
|
|
const result = plan(['examples/test-app/app/index.tsx']);
|
|
assert.equal(result.failOpen, false);
|
|
// Plus the mobile lanes that install the fixture app it builds (device-lanes.ts).
|
|
assert.deepEqual(result.checks, [
|
|
'format',
|
|
'lint',
|
|
'test-app-typecheck',
|
|
'replay-ios',
|
|
'replay-ios-device',
|
|
'replay-android',
|
|
]);
|
|
});
|
|
|
|
test('the image-size parser mitigation is selected when its defining files change', () => {
|
|
for (const file of [
|
|
'examples/test-app/patches/image-size@1.2.1.patch',
|
|
'examples/test-app/security/image-size-security.test.mjs',
|
|
'examples/test-app/pnpm-workspace.yaml',
|
|
]) {
|
|
const result = plan([file]);
|
|
assert.equal(result.failOpen, false, `${file} must not fail open`);
|
|
assert.ok(
|
|
result.checks.includes('test-app-security'),
|
|
`expected test-app-security for ${file}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('unknown path fails open to the full check set', () => {
|
|
const result = plan(['fixtures/unknown.data']);
|
|
assert.equal(result.failOpen, true);
|
|
assert.deepEqual(result.checks, [...ALL_CHECKS]);
|
|
assert.equal(result.failOpenReasons[0]?.rule, 'unknown-path');
|
|
});
|
|
|
|
test('a non-.ts fixture under an owned root fails open (format alone is not ownership)', () => {
|
|
const result = plan(['test/integration/provider-scenarios/fixtures/device.json']);
|
|
assert.equal(result.failOpen, true);
|
|
assert.deepEqual(result.checks, [...ALL_CHECKS]);
|
|
assert.equal(result.failOpenReasons[0]?.rule, 'ambiguous-path');
|
|
});
|
|
|
|
test('a frozen replay-compat corpus script selects the unit lane and the provenance verifier', () => {
|
|
const result = plan(['test/replay-compat/scripts/examples/gesture-lab.v0.16.8.ad']);
|
|
assert.equal(result.failOpen, false);
|
|
assert.ok(result.checks.includes('unit'));
|
|
assert.ok(result.checks.includes('replay-compat'));
|
|
});
|
|
|
|
test('a replay-compat manifest edit selects the provenance verifier', () => {
|
|
const result = plan(['test/replay-compat/manifest.ts']);
|
|
assert.equal(result.failOpen, false);
|
|
assert.ok(result.checks.includes('replay-compat'));
|
|
});
|
|
|
|
test('skills guidance change is docs-only', () => {
|
|
const result = plan(['skills/agent-device/SKILL.md']);
|
|
assert.equal(result.failOpen, false);
|
|
assert.deepEqual(result.docsOnlyPaths, ['skills/agent-device/SKILL.md']);
|
|
assert.deepEqual(result.checks, []);
|
|
});
|
|
|
|
test('workspace package source selects static gates, fallow, layering, and the build', () => {
|
|
for (const file of [
|
|
'packages/kernel/src/errors.ts',
|
|
'packages/contracts/src/facades/device.ts',
|
|
'packages/capture-kit/src/app-log-live-handle.ts',
|
|
]) {
|
|
const result = plan([file]);
|
|
assert.equal(result.failOpen, false, file);
|
|
for (const id of [
|
|
'format',
|
|
'lint',
|
|
'typecheck',
|
|
// Package source is inside fallow's scope; an extraction into packages/
|
|
// must not take a symbol's dead-code coverage with it.
|
|
'fallow',
|
|
'layering',
|
|
'build',
|
|
'vitest-related',
|
|
] as const) {
|
|
assert.ok(result.checks.includes(id), `expected ${id} for ${file}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('a workspace package manifest fails open — it rewires resolution globally', () => {
|
|
const result = plan(['packages/kernel/package.json']);
|
|
assert.equal(result.failOpen, true);
|
|
assert.equal(result.failOpenReasons[0]?.rule, 'workflow-tooling');
|
|
});
|
|
|
|
test('workflow/tooling and selector implementation changes fail open', () => {
|
|
assert.equal(plan(['.github/workflows/ci.yml']).failOpenReasons[0]?.rule, 'workflow-tooling');
|
|
assert.equal(plan(['package.json']).failOpenReasons[0]?.rule, 'workflow-tooling');
|
|
assert.equal(plan(['vitest.config.ts']).failOpenReasons[0]?.rule, 'workflow-tooling');
|
|
assert.equal(
|
|
plan(['scripts/check-affected/model.ts']).failOpenReasons[0]?.rule,
|
|
'selector-owning',
|
|
);
|
|
assert.deepEqual(plan(['docs/agents/testing.md']).checks, ['agent-guidance']);
|
|
});
|
|
|
|
test('a fail-open path in a mixed changeset forces the full set', () => {
|
|
const result = plan(['packages/selectors/src/index.ts', 'bin/agent-device.mjs']);
|
|
assert.equal(result.failOpen, true);
|
|
assert.deepEqual(result.checks, [...ALL_CHECKS]);
|
|
});
|
|
|
|
test('empty changeset selects nothing', () => {
|
|
const result = plan([]);
|
|
assert.equal(result.failOpen, false);
|
|
assert.deepEqual(result.checks, []);
|
|
});
|
|
|
|
test('catalog covers exactly the CheckId universe', () => {
|
|
assert.doesNotThrow(assertCatalogComplete);
|
|
});
|
|
|
|
test('every catalog command resolves against the real package scripts', () => {
|
|
// Against package.json rather than a fixture map: a fixture has to be updated by
|
|
// hand for every new gate, which is exactly the drift the registry exists to stop.
|
|
const scripts = (
|
|
JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as {
|
|
scripts: Record<string, string>;
|
|
}
|
|
).scripts;
|
|
for (const spec of CHECK_CATALOG) {
|
|
assert.ok(resolveCommand(spec, scripts, 'origin/main').length >= 2, `${spec.id} must resolve`);
|
|
}
|
|
const fallow = CHECK_CATALOG.find((spec) => spec.id === 'fallow')!;
|
|
assert.deepEqual(resolveCommand(fallow, scripts, 'origin/dev'), [
|
|
'pnpm',
|
|
'run',
|
|
'check:fallow',
|
|
'--base',
|
|
'origin/dev',
|
|
]);
|
|
});
|
|
|
|
test('a missing package script makes command resolution throw', () => {
|
|
const spec = CHECK_CATALOG.find((entry) => entry.id === 'lint')!;
|
|
assert.throws(() => resolveCommand(spec, {}, 'origin/main'), /does not exist/);
|
|
});
|
|
|
|
test('unit and coverage checks preserve their package-script owners', () => {
|
|
const scripts = { 'check:unit': 'x', 'check:coverage-changed': 'x' };
|
|
const unit = CHECK_CATALOG.find((entry) => entry.id === 'unit')!;
|
|
const coverage = CHECK_CATALOG.find((entry) => entry.id === 'coverage')!;
|
|
assert.deepEqual(resolveCommand(unit, scripts, 'origin/main'), ['pnpm', 'run', 'check:unit']);
|
|
assert.deepEqual(resolveCommand(coverage, scripts, 'origin/main'), [
|
|
'pnpm',
|
|
'run',
|
|
'check:coverage-changed',
|
|
]);
|
|
});
|
|
|
|
test('vitest-related delegates changed paths to Vitest instead of modeling projects', () => {
|
|
const related = CHECK_CATALOG.find((entry) => entry.id === 'vitest-related')!;
|
|
assert.deepEqual(resolveCommand(related, {}, 'origin/main', ['src/a.ts', 'test/fixture.ts']), [
|
|
'pnpm',
|
|
'exec',
|
|
'vitest',
|
|
'related',
|
|
'--run',
|
|
'--passWithNoTests',
|
|
'src/a.ts',
|
|
'test/fixture.ts',
|
|
]);
|
|
assert.equal(
|
|
resolveCommand(related, {}, 'origin/main', ['src/a.ts']).some((arg) =>
|
|
arg.startsWith('--maxWorkers='),
|
|
),
|
|
false,
|
|
'worker sizing belongs to vitest.config.ts',
|
|
);
|
|
});
|
|
|
|
// Guards the catalog against reality, not fixtures: the self-test above uses a
|
|
// hand-built scripts map, so this resolves every catalog entry against the real
|
|
// package.json. A renamed/removed script fails here instead of
|
|
// leaving `pnpm check:affected` broken on the exact command the docs advertise.
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
|
|
test('catalog resolves against the real package.json', () => {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as {
|
|
scripts?: Record<string, string>;
|
|
};
|
|
const scripts = pkg.scripts ?? {};
|
|
for (const spec of CHECK_CATALOG) {
|
|
assert.doesNotThrow(
|
|
() => resolveCommand(spec, scripts, 'origin/main'),
|
|
`catalog entry "${spec.id}" must resolve against the real package.json`,
|
|
);
|
|
}
|
|
});
|