mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
e4c3b420a4
* ci: w3-1824 experiment — trace fork signals and plant pid sentinels in the Coverage job
Temporary instrumentation for #1824. Every vitest fork logs each real
process.kill it sends to a foreign pid (and every kill/pkill it spawns);
the Coverage job parks sentinel processes on the pids the Apple runner
tests fabricate (4141/4242/4343/4444) and reports which of them survive
the run. Reverted before this PR leaves draft.
* test: refuse foreign-pid signals from unit-test workers
A vitest worker may signal only itself and the processes it spawned.
src/__tests__/hermetic-signal-setup.ts records any other process.kill,
answers it with ESRCH (so best-effort kill paths proceed as if the pid
were dead), and fails the sending test by name in afterEach.
The senders this catches today are the Apple runner tests, which
fabricate runner child pids (4242, 4141, 4343, 4444) and mocked the
liveness reads in host-process.ts but not the signal writes:
killRunnerProcessTree delivered real SIGINT/SIGTERM/SIGKILL to those
pids and their process groups — 146 signals per run of
runner-session.test.ts. On the CI runner the sibling vitest forks live
in that pid band, so the Coverage job periodically lost one fork
mid-file with no test attributed (issue #1824, 6 of the last 40 red CI
runs).
The group-signal write moves behind signalProcessGroupBestEffort in
host-process.ts, next to signalPidsBestEffort, so the runner tests mock
the signal seam in the same place they already mock the liveness reads.
Refs #1824
* Revert "ci: w3-1824 experiment — trace fork signals and plant pid sentinels in the Coverage job"
This reverts commit a8a020099c.
* test: refuse spawned kill/pkill writes too, and share the guard with the mutation lane
Review of #1854 found three gaps in the first pass:
- The guard intercepted process.kill only, so the spawned half of the same
function family was unguarded: runner-disposal spawns `pkill -P <pid>` and
`pkill -f 'xcodebuild.*AgentDeviceRunner.env.session-...'`, and
request-router-open.test.ts fired that pattern kill twice per suite run. On a
developer machine with a live Apple runner, `pnpm test` could reach it. The
setup file now refuses kill/pkill/killall spawns with ENOENT — which the
best-effort callers already tolerate — and records them the same way; that
test stubs the Apple tool seam.
- vitest.mutation.config.ts hard-coded its own setupFiles list, so the Stryker
lane ran without the guard. SETUP_FILES is now exported from vitest.config.ts
and imported there, next to the SUBPROCESS_STUB_TESTS import that already
crossed the same boundary.
- 'processes it spawned' meant direct children only; a grandchild started
through a shell wrapper was refused with advice that did not fit. The docs and
the failure message now say direct children and name the remedy.
Synchronous spawns are no longer remembered as own pids: spawnSync and
execFileSync have already exited when they return, so keeping their pids would
license a signal to whatever inherits them next.
Refs #1824
* test: end signal authority at child exit, and guard the promisified execFile path
Re-review of #1854 found two holes in the guard itself, both the class it
exists to close:
- An async child's pid stayed authorized for the worker's lifetime after it
was reaped. A pid is a claim on a process-table slot, and the kernel reissues
that slot once it is free, so 'I spawned this pid once' licensed a signal to
whatever holds it now. Authority now ends on exit/close. Cleanup that signals
a child must gate on isProcessAlive, which is what the existing cleanup paths
already do.
- wrapSpawner copied execFile's original util.promisify.custom onto the
wrapper, and promisify() resolves through that symbol instead of calling the
function — so every promisified caller got an unguarded execFile, bypassing
both the kill-binary refusal and child tracking. That path is now wrapped too.
hermetic-signal-setup.test.ts covers both, plus the allowed cases they could
regress into: a reaped child's pid is refused, a promisified execFile cannot
smuggle a pkill, a promisified child is still tracked, a live foreign pid is
refused, and signal 0 stays free. Reverting either fix reds three of them.
Refs #1824
162 lines
7.6 KiB
TypeScript
162 lines
7.6 KiB
TypeScript
import { defineConfig } from 'vitest/config';
|
|
import { resolveVitestMaxWorkers } from './scripts/lib/vitest-concurrency.ts';
|
|
import slowTestGateReporter from './scripts/vitest-slow-test-reporter.ts';
|
|
|
|
// Files that spawn a real subprocess per case, so under broad file parallelism the
|
|
// spawns get starved past an internal budget and production returns a generic
|
|
// timeout instead of the asserted error. The subprocess-stub project below runs
|
|
// them one at a time to bound that contention; per-file `process.env` isolation is
|
|
// already delivered by `pool: forks` + `isolate: true` on every project.
|
|
// Membership and the project's deletion test live in issue #1823.
|
|
export const SUBPROCESS_STUB_TESTS: readonly string[] = [
|
|
// Stubs npx plus the package managers and spawns a real Metro dev server per case.
|
|
'src/__tests__/client-metro.test.ts',
|
|
// The SUT is the subprocess watchdog: a node subprocess per case, one hangs on purpose (#1414).
|
|
'scripts/fuzz/harness.test.ts',
|
|
// Replays the fuzz corpus through that same worker watchdog, waiting its per-case budget.
|
|
'scripts/fuzz/corpus-replay.test.ts',
|
|
];
|
|
|
|
// Imported by vitest.mutation.config.ts so the two lanes cannot drift: a guard
|
|
// added here must reach the Stryker sandbox too.
|
|
export const SETUP_FILES = [
|
|
'src/__tests__/hermetic-env-setup.ts',
|
|
'src/__tests__/hermetic-signal-setup.ts',
|
|
'src/__tests__/process-memo-setup.ts',
|
|
];
|
|
|
|
export default defineConfig({
|
|
test: {
|
|
// Redirects TMPDIR to one per-run directory for the whole invocation (all
|
|
// projects, every worker) and removes it once at the end — see the file
|
|
// for why a single global hook beats per-file cleanup here.
|
|
globalSetup: ['scripts/vitest-tmpdir-global-setup.ts'],
|
|
// Wall-clock discipline: unit tests must not wait real time. Measured
|
|
// 2026-07-04: the suite's duration was bounded by files sleeping through
|
|
// production timeout budgets. slowTestThreshold surfaces creep in local
|
|
// output; the slow-test reporter enforces the ratchet (pinned offenders
|
|
// only shrink). Isolation stays ON and pool stays forks: measured
|
|
// --no-isolate = 205s wall vs 48s (module state thrashes across files),
|
|
// threads = no change.
|
|
slowTestThreshold: 500,
|
|
// Vitest otherwise derives 11 workers from this 12-core host. Three
|
|
// concurrent Codex worktrees can then request 33 workers and starve the
|
|
// subprocess/test-server paths behind exact timeout budgets. Two workers
|
|
// per local invocation preserves useful parallelism while leaving host
|
|
// headroom. CI stays uncapped so Vitest derives the runner-appropriate
|
|
// worker count from the isolated machine's available CPU pool.
|
|
maxWorkers: resolveVitestMaxWorkers(),
|
|
// hermetic-env-setup clears worker-scoped device claims after every case.
|
|
// Capping explicit `test.concurrent` work at one enforces that teardown
|
|
// assumption without reducing ordinary file-level parallelism.
|
|
maxConcurrency: 1,
|
|
// Gate reporters for every lane; a `--reporter` flag would replace them, so no lane passes one.
|
|
reporters: ['default', slowTestGateReporter()],
|
|
projects: [
|
|
{
|
|
test: {
|
|
name: 'unit-core',
|
|
// Explicit script entries keep maintained conformance guards in the
|
|
// unit suite without waking every ad-hoc *.test.ts under scripts/.
|
|
include: [
|
|
'src/**/*.test.ts',
|
|
'packages/*/src/**/*.test.ts',
|
|
'scripts/__tests__/help-conformance-bench.test.ts',
|
|
'scripts/__tests__/help-conformance-error-recovery-coverage.test.ts',
|
|
'scripts/__tests__/help-conformance-sample-outputs.test.ts',
|
|
'scripts/__tests__/help-conformance-topic-coverage.test.ts',
|
|
'scripts/__tests__/agent-setup-startup-contract.test.ts',
|
|
'scripts/__tests__/npm-skills-exclusion.test.ts',
|
|
'scripts/__tests__/simulator-skills-contract.test.ts',
|
|
// Parses ios.yml and the runner's Swift sources: no Xcode, no simulator, and
|
|
// the check it guards is what keeps the PR lane's `-only-testing:` list honest.
|
|
'scripts/__tests__/xctest-selection.test.ts',
|
|
// The nightly XCTest lane's reporter/liveness check, which otherwise only ever
|
|
// executes on a macOS runner at 04:30.
|
|
'scripts/__tests__/xctest-run-summary.test.ts',
|
|
// The Fallow fixture policy is executable configuration: unused exports are exempt,
|
|
// but fixture modules remain visible to the other analysis families.
|
|
'scripts/__tests__/fallow-fixture-policy.test.ts',
|
|
// The publishing gate's closure audit against fixture packages: parse-only, and the
|
|
// only place the gate's failure direction is exercised at all (the gate itself needs a
|
|
// real `npm pack`, so CI can only watch a healthy package pass).
|
|
'scripts/__tests__/package-closure-audit.test.ts',
|
|
// The Bundle Size lane's PR-comment path: spawns the real script against a
|
|
// stubbed fetch, so it needs no network; pins retry/reconcile/fatal outcomes.
|
|
'scripts/__tests__/size-report-post-comment.test.ts',
|
|
// Parses CI configuration only, so this action guard needs no device or subprocess lane.
|
|
'test/ci/upload-agent-device-artifacts.test.ts',
|
|
// #1781 A9: pins the root-doc paths-ignore entries directly against the
|
|
// real workflow YAML, parse-only like its sibling above.
|
|
'test/ci/root-docs-paths-ignore.test.ts',
|
|
// The frozen replay-compat corpus (#1417): parse-only, no device or
|
|
// subprocess work, so it belongs in the fast lane next to the
|
|
// grammar it guards.
|
|
'test/replay-compat/corpus.test.ts',
|
|
// The daemon RPC wire ledger (#1432): parses source and hashes
|
|
// declarations, so it needs no history, network, or device — the
|
|
// released-tag half runs in its own full-history job.
|
|
'test/wire-compat/wire-compat.test.ts',
|
|
'test/wire-compat/wire-mutations.test.ts',
|
|
// The Maestro conformance oracle runs via `node --test` in its own CI
|
|
// job (scripts/maestro-conformance), like the layering guard.
|
|
],
|
|
exclude: [...SUBPROCESS_STUB_TESTS],
|
|
setupFiles: SETUP_FILES,
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
name: 'subprocess-stub',
|
|
include: [...SUBPROCESS_STUB_TESTS],
|
|
setupFiles: SETUP_FILES,
|
|
fileParallelism: false,
|
|
isolate: true,
|
|
maxWorkers: 1,
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
name: 'provider-integration',
|
|
include: ['test/integration/provider-scenarios/**/*.test.ts'],
|
|
setupFiles: SETUP_FILES,
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
name: 'interaction-contract',
|
|
include: ['test/integration/interaction-contract/**/*.test.ts'],
|
|
setupFiles: SETUP_FILES,
|
|
},
|
|
},
|
|
{
|
|
test: {
|
|
name: 'output-economy',
|
|
include: ['test/output-economy/**/*.test.ts'],
|
|
setupFiles: SETUP_FILES,
|
|
},
|
|
},
|
|
],
|
|
coverage: {
|
|
provider: 'v8',
|
|
reporter: ['text', 'html', 'lcov', 'json-summary'],
|
|
thresholds: {
|
|
statements: 78,
|
|
lines: 80,
|
|
},
|
|
include: ['src/**/*.ts', 'packages/*/src/**/*.ts'],
|
|
exclude: [
|
|
'src/**/*.test.ts',
|
|
'src/**/__tests__/**',
|
|
'src/**/*-types.ts',
|
|
'src/**/types.ts',
|
|
'src/sdk/**',
|
|
'src/bin.ts',
|
|
'src/client/client-types.ts',
|
|
'src/core/interactor-types.ts',
|
|
'src/remote/remote-config.ts',
|
|
],
|
|
},
|
|
},
|
|
});
|