refactor(test-kit): one named-export entry, and pin its shape in R83
Stacked on #3659 (R11 devDependencies) on #3645 (R83).
What
@agent-device/test-kit exports exactly "." โ src/index.ts, with every helper a named export (54 names, no export *). The 18 subpaths are gone; every importer (about 470 files across the daemon, root src/, test/ and scripts/; re-run over the daemon tests main gained since) now import @agent-device/test-kit. The rewrite is a mechanical codemod (merged same-file imports, import type kept); the hand-written parts are index.ts, host-process-mock.ts, eight test edits and the R83 additions below.
Why the eight pinOwnProcessStartTime tests changed
They mocked @agent-device/host-kit/process with a factory that did (await import('@agent-device/test-kit')).pinOwnProcessStartTime(importOriginal). With one eager entry that import deadlocks: the entry loads host-kit/command, host-kit/file and capture-kit, which load host-kit/process, which is still being mocked. The helper now takes the module namespace and spies it in place (pinOwnProcessStartTime(hostProcess), registered with beforeEach so vi.restoreAllMocks cannot unpin it); no module is mocked, so no test-kit import happens during a mock factory.
Proof the pin still takes effect: with readCurrentOwnerIdentity made to return a different start time, device-claims.test.ts fails 6 of 28 tests; restored, all 28 pass. The eight tests pass together (103 tests). Rule for future tests: never import test-kit inside a vi.mock factory.
Import-time side effects
None. Each of the 18 modules imports cleanly in plain Node with no top-level effects (the fs.writeFileSync/setInterval lines in legacy-daemon-fixture.ts are inside a template string that becomes a child script). The entry loads in plain Node in ~160 ms (54 exports). The six node:test scripts that import test-kit still pass; the entry pulls vitest into them, which loads harmlessly outside a runner.
Wall time, vitest run --project unit-core (1647 files, 14,222 tests, same on both sides)
| vitest Duration | import share | |
|---|---|---|
| before (subpaths, head of #3659) | 155.95 s | 23 % |
| after (single entry) | 154.46 s | 28 % |
Each test file now imports all of test-kit instead of one module, which shows as the larger import share; the total is unchanged within noise (single run each, shared host).
R83 additions: the shape of test-kit
checkTestKitIsolation (inside R83) pins:
exportsis exactly.;- no
dependencies/devDependencies/peerDependenciesentry onplatform-*,provider-*,@agent-device/daemonoragent-device(root), and no import of them or relative reach out ofpackages/test-kit/. A fixture that needs those packages' code is a test of that package and belongs in it; - no package test-kit depends on may declare test-kit. TypeScript project references are acyclic and test-kit references capture-kit, command-registry, contracts, daemon-contracts, host-kit and kernel, so those six packages' tests keep their helpers local.
Planted in tests: an extra export, each forbidden dependency in all three fields, eight forbidden or escaping imports, and a devDependencies/dependencies cycle through kernel. Only the daemon and the root consume test-kit today; the daemon already declares it in devDependencies (#3659). Every other package's worker adds it to devDependencies and references when its tests start importing it.
Gates
pnpm check:quick, check:layering (373 node tests), check:fallow, check:gate-manifest, check:affected --run, vitest --project unit-core in full, repo-wide pnpm format.
๐ค Generated with Claude Code