refactor(test-kit): one named-export entry, and pin its shape in R83

Merged
TH
5h ago
+1119-715

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 Durationimport share
before (subpaths, head of #3659)155.95 s23 %
after (single entry)154.46 s28 %

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:

  • exports is exactly .;
  • no dependencies/devDependencies/peerDependencies entry on platform-*, provider-*, @agent-device/daemon or agent-device (root), and no import of them or relative reach out of packages/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