inspector/.claude/skills/testing/SKILL.md
cliffhall a344225e23 docs: accept several skill names in skills:eval and re-aim overlapping cases
Adds a multi-name filter to `npm run skills:eval` so a set of related
skills can be iterated together, and makes an unknown name a hard error
rather than an empty run — a typo would otherwise report a green 0/0
that reads exactly like a clean pass of the skill that was meant.

Re-aims two `local-dev` cases and one `test-servers` case that partially
overlapped `AGENTS.md`. Partial overlap oscillates across runs rather
than sitting at zero, so it reads as eval noise instead of a defect in
the case; `docs/skill-authoring.md` now records that failure mode and
the observed scores.

Widens the `test-servers` description to the "Use when …" shape and
points the `testing` skill at it, since anything needing a real server
to run against is a procedure `testing` does not carry. That growth
moves the skill listing from 2,921 to 3,144 of 4,000 characters, so the
figure recorded in AGENTS.md is updated to match.

Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-01 00:18:43 -04:00

8.3 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.

⚠️ Anything that needs a real server to run against — an integration test, a smoke, reproducing a bug by hand — is /test-servers, and you have to load it. Integration and smoke tests here drive a real server over a real transport rather than a mock, so picking, building and connecting to a fixture is a procedure of its own that this skill does not carry.

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.