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
42 lines
2.0 KiB
TypeScript
42 lines
2.0 KiB
TypeScript
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { defineConfig } from 'vitest/config';
|
|
import { readTestScope, threadHostileTestFiles } from './scripts/mutation/test-scope.ts';
|
|
import { workspaceSourceAliases } from './scripts/mutation/workspace-aliases.ts';
|
|
import { SETUP_FILES, SUBPROCESS_STUB_TESTS } from './vitest.config.ts';
|
|
|
|
const repoRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Workspace-package aliases for the Stryker sandbox (#1490 W0). Stryker copies
|
|
// the tree into .stryker-tmp and symlinks node_modules back to the real repo,
|
|
// so a `@agent-device/*` specifier resolved through pnpm's link escapes the
|
|
// sandbox: mutants written into the sandbox copy never load, and
|
|
// `vitest related` finds no tests for a mutated package file. Aliasing each
|
|
// EXPORTED specifier to its source file — resolved relative to this config,
|
|
// which Stryker copies into the sandbox — keeps resolution inside the mutated
|
|
// tree. Entries come from the shared exports-map reader, never a wildcard, so
|
|
// this cannot resolve package internals the boundary forbids (R11).
|
|
const workspaceAliases = workspaceSourceAliases(repoRoot);
|
|
|
|
// Test scope for the decision-kernel mutation lane (issue #1415).
|
|
//
|
|
// `scripts/mutation/run.ts` derives the per-run scope from Vitest's module graph
|
|
// (`vitest related` over the mutated files) and hands it over through
|
|
// AGENT_DEVICE_MUTATION_TEST_FILES; the fallback is the deterministic unit suite,
|
|
// which keeps `pnpm exec stryker run` usable by hand. Excluded either way: the
|
|
// subprocess-stub group and the CLI-capture tests — see
|
|
// scripts/mutation/test-scope.ts for why, and why excluding them cannot hide a
|
|
// surviving mutant.
|
|
const scope = readTestScope();
|
|
|
|
export default defineConfig({
|
|
resolve: {
|
|
alias: workspaceAliases,
|
|
},
|
|
test: {
|
|
include: scope ?? ['src/**/*.test.ts', 'packages/*/src/**/*.test.ts'],
|
|
exclude: [...SUBPROCESS_STUB_TESTS, ...threadHostileTestFiles(repoRoot), '**/node_modules/**'],
|
|
setupFiles: [...SETUP_FILES],
|
|
},
|
|
});
|