inspector/.claude/skills/testing/SKILL.md
cliffhall f95abb2715 chore: tighten the testing skill's description
Measured before and after at the same sample size (n=4), with the six other
model-invoked skills unchanged:

  which command runs the TUI suite      25% → 100%
  asserting on a Mantine transition     50% → 100%
  which tier spawns the built binary    25% → 100%

The lever is the description's SHAPE, not its length: leading with the actions
and then enumerating concrete situations ("Use when … ; when … ; when …") beats
a noun-phrase list of contents ending in a generic "use for any question
about …". The same edit took `pre-push-gate` from 3/5 to 5/5 earlier.

Re-ran the full eval to check the change did not simply move attention off
another skill, which is the failure the previous commit recorded: 40/46, up from
38/46, and the failing set is strictly smaller — `testing` went from three
failures to one, nothing else moved. That remaining case measured 100% in
isolation and 67% in the full run, which is sampling variance rather than a
finding.

AGENTS.md now extends the re-run rule to cover a description edit, not just a
flip, and records the shape that works.

Signed-off-by: cliffhall <cliff@futurescale.com>
2026-08-30 00:53:07 -04:00

8 KiB

name description disable-model-invocation
testing Run, place and fix tests in this repo. Use when choosing which npm command runs a given suite (web unit, web integration, Storybook, cli, tui, launcher, scripts); when deciding where a new test file belongs — beside its source, under src/test/, or in a client's __tests__/; when a per-file coverage check fails or a v8 ignore is in question; when asking which test tier spawns the built binary rather than importing it; or when rendering, mounting or asserting on Mantine components and their transitions in a test. false

Testing

Every change needs tests, and every file must clear ≥90% on lines, statements, functions, and branches. That rule and the React/Mantine conventions live in AGENTS.md; this skill is where a test goes, how to run it, and how to clear the gate.

Where the test file goes

Side-by-side by default; src/test/ only for what can't be co-located; and the Node clients are different.

clients/web — side-by-side

<Name>.test.tsx (or .test.ts for non-React modules) next to the source. Components, hooks, lib/, utils/. This is the overwhelming majority; a web-owned test living under src/test/ instead is a bug.

clients/web/src/test/ is for the three things that cannot be co-located:

  1. Tests of the repo-root core/ package → src/test/core/…, mirroring the core/ folder layout. core/ physically lives outside clients/web/, is consumed via the @inspector/core alias, and has no test harness of its own. This includes core/json/* and core/client/*.
  2. The integration project → src/test/integration/…, mirroring the core/ source layout (mcp/, mcp/node/, mcp/remote/, auth/, auth/node/, storage/). Placement is the manifest — any file under that folder is picked up by the integration project (node env, 30s timeouts) via a folder glob; there is no enumeration to keep in sync.
  3. Shared test infrastructure — renderWithMantine.tsx, setup.ts, fixtures/, scrollAreaStoryAssertions.ts.

clients/cli, clients/tui, clients/launcher — a top-level __tests__/

All their tests, not beside their source. Their tsconfig.json excludes **/*.test.* and their tsconfig.test.json includes __tests__/**/*, so a co-located src/**/*.test.* lands in no tsconfig project and fails npm run verify:typecheck-coverage (#1791).

Root tooling — scripts/*.test.mjs

A new scripts/*.mjs helper with pure logic gets a sibling *.test.mjs, run by npm run test:scripts (node's built-in runner; the root has no vitest harness by design). ⚠️ Keep the filename *.test.mjs — node --test silently skips a file its glob misses and still exits 0.

Running them

Scope From Command
Web unit clients/web npm run test (test:watch while iterating)
Web integration clients/web npm run test:integration
Web Storybook play fns clients/web npm run test:storybook
CLI clients/cli npm run test (pretest builds test-servers + the bin)
TUI clients/tui npm run test
Launcher clients/launcher npm run test
Root tooling repo root npm run test:scripts
Everything, fast repo root npm run validate
The coverage gate repo root npm run coverage

There is no aggregate root test script — each client self-validates.

In unit tests that expect error output, suppress it from the console.

The tiers, shallowest first

unit (test, per client) → web integration (test:integration, real transports/servers) → out-of-process (clients/cli/__tests__/e2e.test.ts, spawns the built binary) → smokes through the built launcher (npm run smoke) → Storybook play functions (test:storybook) → the published-tarball check (npm run pack:verify, local/release only — needs network).

validate runs the per-client test scripts — so web unit plus cli's out-of-process e2e.test.ts, but not web's integration project, which runs inside the coverage gate. CI therefore has no separate test:integration step.

The coverage gate

Per-file ≥90 on all four dimensions, CI-enforced, across web, cli, tui and launcher. New code must clear 90 on every dimension.

Scope notes:

  • The web coverage include (in clients/web/vite.config.ts) also covers the shared core/ runtime the browser consumes — core/mcp, core/react, core/auth, core/storage, core/logging, core/node, core/json, core/client.
  • ⚠️ That include is a whitelist naming components/hooks/theme/ lib/utils/server. A module placed outside those directories falls out of the gate entirely, silently. The documented exceptions are src/App.tsx (a composition root at ~42% branch coverage — gating it is a dedicated decomposition effort) and the src/main.tsx / src/index.ts bootstraps.
  • CLI tests run in-process by importing runCli() (__tests__/helpers/cli-runner.ts) so src is measured; src/index.ts is the only exclusion. commander uses .exitOverride() so a parse error throws instead of tearing down the test worker.
  • TUI covers all of src/**, React surface included. Components mount through ink-testing-library with the passthrough doubles in __tests__/helpers/; keypresses are driven through stdin. The only exclusion is src/tui-servers.ts (a pure re-export, excluded so it doesn't surface as a misleading 0/0 row).

When a v8 ignore is justified

A genuinely-unreachable branch is annotated at the source rather than waved through by lowering the gate. The acceptable reasons are enumerated in AGENTS.md — do not reach for one that is not on that list.

React tests: renderWithMantine

Always render through renderWithMantine (src/test/renderWithMantine.tsx) — it wraps in MantineProvider with the project theme. Do not hand-roll a bare MantineProvider; it skips the project theme and the helper's options and drifts from every other test.

Note the justification has changed and the old one is wrong: it sets env="test", which makes Mantine skip the animated render — but it does not stop the timers. env is read only at Transition.mjs's render branch, while useTransition() runs before that check and still schedules real window.setTimeouts (opening a <Modal> schedules three 200ms timers). What prevents a timer outliving its file — and throwing an uncaught ReferenceError: window is not defined that fails the whole run, attributed to an innocent file (#1760) — is the leaked-timer safety net in src/test/setup.ts, which is global and covers every unit test however it renders. The rule stands on consistency, not on timer safety.

  • Forced color scheme: pass the option — renderWithMantine(ui, { colorScheme: "dark" }) — rather than hand-rolling defaultColorScheme="dark".
  • Mid-flight transition state (e.g. asserting a data-anim="out" cell during an exit crossfade) is the only reason to use renderWithMantineTransitions. Pass settleMs derived from the component's real animation duration (HEADER_ANIM_MS + 200), do not also use vi.useFakeTimers() in that test (the auto-settle no-ops under fake timers), and if the test unmounts the tree itself use the unmount() the helper returns. The mechanism is documented at length on the helper — read there before changing it.

Storybook play functions

Every screen and element component has a *.stories.tsx; play functions double as interaction tests, run headless in CI and in the local gate.

⚠️ expect(...) from storybook/test returns a promise. Storybook instruments it, so every expect in a play function is awaited — as is any shared helper that wraps one.

Test servers, not mocks

Integration and smoke tests drive a real server over a real transport. See /test-servers for picking and building one.