A playbook that stops an AI from writing happy-path-only tests for your Node/TypeScript backend.
Most "AI, write my tests" runs stop at a few green checks and a coverage number that looks fine on paper — with permissions, edge cases, and refused-access paths left untested. test-casebook-back-js forces an agent to plan every case from the source before writing anything, weight the persona matrix on what gets refused (not just what's allowed), gate every block behind an independent review, and hold a real coverage floor.
It's not theory — it's been run for real, end to end, on eight Node frameworks:
| Framework | Tests | Type/lint | Coverage | One real thing it found |
|---|---|---|---|---|
| NestJS | 40/40 | tsc/ESLint clean |
98.84% | fresh Nest scaffold ships strict: false by default |
| AdonisJS 6 | 39/39 | tsc/ESLint clean |
100%* | real runner is Japa, not Jest/Vitest |
| Express | 47/47 | tsc/ESLint clean |
97.5% | jsonwebtoken's sub claim types as string, not number |
| tRPC | 48/48 | tsc/ESLint clean |
100%* | blanket jest.useFakeTimers() hangs real in-process HTTP |
| GraphQL/Apollo | 47/47 | tsc/ESLint clean |
98.47% | Apollo always returns HTTP 200 — assert on errors[].extensions.code |
| Fastify | 47/47 | tsc/ESLint clean |
98.29% | even app.inject() hangs under blanket fake timers |
| Hapi | 47/47 | tsc/ESLint clean |
100%* | @hapi/hapi ships its own types now — no @types/hapi__hapi |
| Koa | 47/47 | tsc/ESLint clean |
100%* | ctx.throw()'s never type only narrows if ctx is explicitly typed |
*on the code the scenario touches.
Full receipts, including every bug and fix, in docs/testing-guide/.
What a run actually looks like (NestJS worked example)
$ npm test
...
Test Suites: 9 passed, 9 total
Tests: 40 passed, 40 total
$ npx tsc --noEmit && npx eslint .
(no output — clean)
$ npm run test:cov
All files | 98.84 | ...
Full walkthrough, including the "fresh Nest scaffold ships strict: false" finding this run surfaced, in docs/testing-guide/nestjs.md.
# from your project's root
npx test-casebook-back-js init --force [--coverage=90]
# or from a checkout of this repo, targeting your project as cwd
node bin/casebook-back-js-init.mjs init --force [--coverage=90]
# then, in Claude Code, opened on your project:
# invoke the `test-casebook-back-js` skillThat scaffolds AGENTS.md, docs/, and .claude/ into your project. From there an agent detects your stack (NestJS? Fastify? plain Express?) and follows the playbook — see the detection table below for what changes per framework.
This doctrine is about to be run for real against actual open-source Node/TS projects, not synthetic demos, with results (and hopefully PRs back to maintainers) published as they land.
- Suggest a project — open an issue: a link and why it's a good candidate.
- Run it yourself — scaffold it into your own project and report back what it found.
- Volunteer your project — if you maintain a Node backend and don't mind an experimental test suite showing up as a PR, say so.
The core (AGENTS.md Steps 1–6) applies to any Node/TS backend — plain Express, no framework assumptions. A few things are detected and applied only if present in package.json:
Detected via package.json |
If present | If absent |
|---|---|---|
@nestjs/core |
Use Nest Guards/decorators/DI conventions | Adapt to the project's own framework (Adonis Bouncer, bespoke Express middleware) |
@adonisjs/core |
Use Adonis Bouncer policies | N/A |
vitest |
Use Vitest | Default: Jest |
@japa/runner |
Use Japa — AdonisJS's real test runner, not Jest/Vitest | N/A |
@trpc/server |
Drive gated procedures through a real HTTP round-trip (@trpc/client), not createCaller alone |
N/A |
@apollo/server/graphql |
Two validation layers (schema + business-rule); assert on errors[].extensions.code, not HTTP status |
N/A |
fastify |
Default error handler reads a thrown Error's .statusCode; app.inject() isn't immune to the fake-timers hazard |
N/A |
@hapi/hapi |
Auth is a first-class scheme/strategy; validate via Joi as a route option; skip @types/hapi__hapi, it ships its own types |
N/A |
koa |
No built-in router/body-parser/auth; test via supertest against app.callback() |
N/A |
strict in tsconfig.json |
Already the definition-of-done bar | Turn it on — part of Step 3, not optional |
An ORM (typeorm, @prisma/client, drizzle-orm) |
Seed personas through its repository API | Adapt to whatever's actually used — in-memory store is a legitimate fallback, not a downgrade |
Not every Node backend runs NestJS or an ORM. Those are real and valuable, but a plain Express + Jest project gets the full core method without being handed instructions for packages it doesn't have.
AGENTS.md— the playbook itself..claude/skills/test-casebook-back-js/+.claude/agents/{test-writer-back-js,test-reviewer-back-js}— orchestrates plan → write → review → commit..claude/hooks/test-casebook-back-js-gate.mjs— a Claude CodePreToolUsehook that blocks writing to a*.spec.ts/*.e2e-spec.tsfile until atask-test.mdplan exists above it.docs/strategy.md— why this doctrine, and why it's a third sibling repo rather than folding into either of the other two.docs/conventions.md— test naming, thetask-test.mdshape, persona naming.docs/testing-guide/— one file per worked example (nestjs.md,adonisjs.md,express.md,trpc.md,graphql.md,fastify.md,hapi.md,koa.md), each with the full run: commands, numbers, and the real bugs found.bin/casebook-back-js-init.mjs— the scaffolder used above, with a--coverage=<1-100>flag.
- npm package —
npx test-casebook-back-js init, ornpm i -D test-casebook-back-jsto pin a version. The package shipsAGENTS.md,docs/,bin/and the.claude/skill and sub-agents, so an update propagates by bumping the dependency.initalso checks the npm registry for a newer published version and prints an update hint if one exists (silent, non-blocking, if offline). - Claude Code skill + sub-agents — the primary path, shown in Quickstart.
- Scaffolder alone —
node bin/casebook-back-js-init.mjs init [--force] [--coverage=<n>]from a checkout. - Docs directly — hand
AGENTS.md(and the relevantdocs/testing-guide/*.md) to any agent.
Releases are automated: merging a PR to main that changes package.json's version triggers CI to publish that version to npm (see CONTRIBUTING.md).
Contributions are welcome, no permission needed — see CONTRIBUTING.md.
Same method — plan first, exhaustive not happy-path, persona matrix dense on refused cases, independent review gate, enforced coverage floor — ported to each ecosystem's own tooling:
test-casebook— frontend/DOM (Nuxt, React, Vue, Svelte, Astro, Laravel/Livewire)test-casebook-back-php— PHP backends
MIT