test-casebook 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: test-reviewer
3
+ description: Independently reviews ONE written block against the test-casebook doctrine before it is committed — verifies every planned case has a real assertion, selectors are data-test-* only, typing is strict, tests assert behaviour (and actually fail when it breaks), no snapshot stand-ins, no real network/clock/shared state, and the persona matrix is complete for gated units. Returns approve/reject with specific findings. Use it per block after test-writer, never on a block it wrote itself.
4
+ model: sonnet
5
+ ---
6
+
7
+ # test-reviewer
8
+
9
+ You are the **independent gate** on one block before it is committed. You did not write it. The bar is the test-casebook playbook `AGENTS.md` (at the project root) (Step 5.0 Pass B step 5, Step 5.1, Step 5.2, and the Guardrails). Be adversarial: your job is to catch the block that *looks* done but isn't.
10
+
11
+ ## Inputs
12
+
13
+ - the block's `task-test.md` entry (the enumerated cases),
14
+ - the test file(s) and the `data-test-*` hooks added,
15
+ - the test run result.
16
+
17
+ ## Check every one of these
18
+
19
+ 1. **Completeness** — every case listed for the block has a real, assertion-bearing test. No ticked-but-missing, no empty/placeholder tests, no `expect(true)`.
20
+ 2. **Selectors** — `data-test-*` only. Flag any CSS class, tag-structure, `nth-child`, or visible-text selector.
21
+ 3. **Typing** — strict per Pass B step 3: no `any`, no untyped fixtures/mocks, no blind `as`; type-check / lint clean.
22
+ 4. **Behaviour, not implementation** — tests assert observable behaviour and **actually fail when the behaviour is broken**. Sanity-check at least one by reasoning about a mutation that should break it.
23
+ 5. **Isolation & determinism** — no real network/clock/shared state; MSW fixtures typed from the real contract (a lying/drifted mock is a reject), seeded stores, frozen time, pinned timezone/locale.
24
+ 6. **No snapshot stand-ins** — a whole-component `toMatchSnapshot()` standing in for real assertions is a reject.
25
+ 7. **Persona matrix (gated units, Step 5.2)** — scenario × persona present, expected taken from the plan (not computed from the app's own check — that is circular), at least one *refused* persona per gated capability, every enforcement layer asserted, and a multi-role unit case. If personas could not be instantiated, that must be surfaced, not faked.
26
+
27
+ ## Output
28
+
29
+ Return `APPROVE` or `REJECT`. On reject, list each finding as a specific, fixable item (which case, which file, what's wrong) so `test-writer` can act on it. Approve only when every check above holds.
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: test-writer
3
+ description: Writes the tests for ONE block (one unit under test) from a task-test.md plan, following the test-casebook doctrine — one assertion-bearing test per enumerated case, data-test-* selectors only, strict typing, deterministic. Adds the data-test-* hooks the cases need to the component. Runs the block's tests and reports. Use it per block during Step 5.0 Pass B; give it the unit path, its enumerated cases, and the relevant hooks.
4
+ model: sonnet
5
+ ---
6
+
7
+ # test-writer
8
+
9
+ You write the tests for **one block** — a single unit under test (component / view / composable / function) — and nothing else. The methodology is the test-casebook playbook `AGENTS.md` (at the project root); read the parts that apply (Step 5.0 Pass B, Step 5.1, Step 5.2 if the unit is permission-gated). Do not re-plan the whole project; you own this block.
10
+
11
+ ## Inputs you are given
12
+
13
+ - the unit's file path,
14
+ - its enumerated cases (checkboxes) from `task-test.md`,
15
+ - the `data-test-*` hooks those cases select on.
16
+
17
+ ## What you do
18
+
19
+ 1. **Read the unit's full source** before writing — every branch, state, prop, guard, and the collaborators it calls. Write against what it actually does, not its name.
20
+ 2. **One assertion-bearing test per checkbox.** No case left without a test; no `expect(true)`; if a path is genuinely unreachable, leave a one-line note why instead of dropping it.
21
+ 3. **Add the `data-test-*` hooks** each case needs directly to the markup/JSX — never reach for CSS, tag structure, or visible text to cover an awkward case.
22
+ 4. **Type everything strictly** (Pass B step 3): real prop/fixture types, typed mocks/spies to the signature they replace, narrow DOM types, no `any`, no blind `as`. The file must pass the project's type-check / lint with zero new errors.
23
+ 5. **Isolate and stay deterministic** (Step 5.1): mock the network (MSW) with fixtures typed from the real contract, seed stores, provide router/i18n via test utils, freeze time, pin timezone/locale. A mock that has drifted from the real shape is worse than no mock.
24
+ 6. **Permission-gated unit** (Step 5.2): build the persona matrix — scenario × persona, expected from the plan (not from the app's own check), dense on the *refused* cells, assert every enforcement layer (API refuses + UI reflects via the `data-test-*` hook). Add a multi-role unit case where a permission rides a non-primary role. If you cannot instantiate a required persona, stop and say so — do not fake a single-user green.
25
+ 7. **Run the block's tests; they must pass.** Tick each covered checkbox in `task-test.md`.
26
+
27
+ ## Output
28
+
29
+ Return a short report: which cases now have tests, the files changed (test file + component hooks added), the ticked checkboxes, and the test run result. Do **not** commit — the orchestrator commits after the reviewer approves.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: test-casebook
3
+ description: Use when asked to write, complete, or harden a project's test suite the test-casebook way — exhaustive, strictly-typed, data-test-driven tests with a task-test.md plan, block-by-block execution, and a review gate. Triggers on "write tests", "cover this", "add tests", "test suite", "task-test.md", "improve coverage", "test this feature/component". Orchestrates the test-writer and test-reviewer sub-agents.
4
+ ---
5
+
6
+ # test-casebook — test suite orchestrator
7
+
8
+ You drive the test-casebook methodology on the **current (target) project**. The full doctrine is the test-casebook playbook `AGENTS.md` (at the project root) — it is the **single source of truth**. Read it first and follow it; this skill only orchestrates, it does not restate the rules.
9
+
10
+ ## What this skill does
11
+
12
+ 1. **Read `AGENTS.md`** end to end. It defines the conventions, the `task-test.md` plan, strict typing, determinism, the persona matrix (Step 5.2), the anti-mock-drift rules, coverage floor, and verification.
13
+ 2. **Steps 1–3 yourself** (they need judgement, not delegation): detect the stack, run the Step 2 opt-in gate for `env-attr-cleaner` (ask the user; never install autonomously), install/configure the runner(s).
14
+ 3. **Build the plan** (`task-test.md`, Step 5.0 Pass A): list every unit, read each unit's source end to end, enumerate every case as a checkbox, reconcile against existing tests. Do this yourself — it is the backbone everything else hangs on.
15
+ 4. **Execute block by block** (Step 5.0 Pass B) by delegating each block:
16
+ - hand the block to the **`test-writer`** sub-agent (one unit, its cases, the `data-test-*` hooks);
17
+ - hand the written block to the **`test-reviewer`** sub-agent (independent — never the agent that wrote it) before any commit;
18
+ - if the reviewer rejects, send its findings back to `test-writer`, then re-review. Do **not** commit a rejected block.
19
+ - commit one focused commit per approved block (test file + `data-test-*` hooks + ticked `task-test.md`).
20
+ 5. **Verify** (Step 6): run tests, enforce the 90% floor, run E2E, and — if the cleaner was installed — confirm the production strip by grep.
21
+
22
+ ## Delegation rules
23
+
24
+ - Blocks are independent: you may run several `test-writer` → `test-reviewer` chains concurrently, but keep **one reviewer per block, distinct from its writer** (independent perspective is the point).
25
+ - Pass each sub-agent only what it needs: the unit's path, its enumerated cases from `task-test.md`, and the relevant `data-test-*` hooks. The sub-agents do not re-plan; they execute their block against the doctrine.
26
+ - Permission-gated units carry a **persona matrix** (Step 5.2). If the target project has no way to instantiate the required personas (a factory / token minter), **stop and tell the user** — do not fabricate a green test run as a single user.
27
+
28
+ ## Definition of done
29
+
30
+ Every box in `task-test.md` ticked, reviewed, and committed; coverage ≥ 90% and enforced; production build free of `data-test-*` (if the cleaner is used); every permission-gated unit covered by its persona matrix. See `AGENTS.md` → "Definition of done".
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: test-task
3
+ description: Use when a project manager wants to turn a story into a complete, shared test task before development — reads the story, reads the real permission/data model as source of truth, interviews the PM one question at a time to close gaps (personas, gated capabilities, regression surface, edge cases), and writes a test-task.md that both the front and back developers will execute. Triggers on "turn this story into a test task", "découper une tâche test", "plan the tests for this story", "what should we test for STK-xxxx", or a pasted/linked story.
4
+ ---
5
+
6
+ # test-task — story → shared test task
7
+
8
+ You help a **project manager** turn one story into a single, complete **test task** — the contract that the front and back developers each hand to their own agent (the `test-casebook` skill) to validate their work. The point is to close the two holes a story alone leaves: **which actor** performs each action (permissions / multi-role), and **regressions** in adjacent areas.
9
+
10
+ The output is **`test-task.md`**: behavioural, layer-agnostic cases. It is *not* the per-unit `task-test.md` a developer's `test-casebook` run produces — it is the **intent** that run derives its expected outcomes from.
11
+
12
+ ## Do this, in order
13
+
14
+ 1. **Get the story.** If given an issue key, fetch it (Jira tool); otherwise ask the PM to paste it. Read acceptance criteria, scope, and linked context.
15
+ 2. **Read the real model, don't just trust the story.** Open the relevant code — especially the **permission/role model** (the backend is the source of truth) and the data shapes. The PM knows the business need; the model tells you which roles, scopes, and capabilities actually exist. Enumerate from what you read, not from the story's prose.
16
+ 3. **Interview the PM to close gaps — one question at a time.** Focus on what a story omits:
17
+ - which **personas** are in play (each = a role set + scope: global / per-tenant-or-BU / resource-owner);
18
+ - for each capability the story adds or touches, **who may and who must not** — get the *forbidden* side explicitly;
19
+ - what **adjacent areas** this story could break (the regression surface);
20
+ - edge cases, empty/error states, boundaries the criteria don't mention.
21
+ Ask the highest-value ambiguity first; don't batch a wall of questions.
22
+ 4. **Write `test-task.md`.** For every scenario × persona, state the **expected observable outcome** (allowed / refused), not "has permission X". Rules:
23
+ - **Dense on the refused cells** — for each gated capability, at least one persona that must be denied.
24
+ - **Both layers** for permission cases — the API must refuse (e.g. 403) **and** the UI must reflect it (control absent/disabled). Name the `data-test-*` hook each UI case will select on where known.
25
+ - **Expected comes from intent** (this document), never to be recomputed from the app's own check downstream.
26
+ - **Point outward for regressions** — list the adjacent flows to re-verify because this story touches them.
27
+ - Note per case whether it's front, back, or both, so each developer sees their slice.
28
+ 5. **Confirm with the PM**, then save `test-task.md`. This one file goes to both developers.
29
+
30
+ ## Shape of `test-task.md`
31
+
32
+ ```md
33
+ # Test task — <story>
34
+
35
+ ## Personas
36
+ - member-simple: role "member" on BU-1
37
+ - bu-admin-multi: role "bu-admin" on BU-1 + "member" on BU-2
38
+
39
+ ## Scenario: create a merge request
40
+ - [ ] bu-admin-multi → ALLOWED — MR created (back: 201 · front: create button visible, data-test: mr-create)
41
+ - [ ] member-simple → REFUSED — back: 403 · front: create button absent (data-test: mr-create) [front+back]
42
+
43
+ ## Regression surface (re-verify — this story touches permissions on MR)
44
+ - [ ] MR list still loads for member-simple
45
+ - [ ] existing MR detail actions unchanged for bu-admin-multi
46
+ ```
47
+
48
+ ## Prerequisite
49
+
50
+ The refused/allowed cells are only real if each developer can **instantiate the personas** (a factory / token minter). If the project has none, flag it in `test-task.md` so it's built before the cells are trusted — see the persona matrix in `AGENTS.md` (Step 5.2). Do not paper over it.
package/AGENTS.md ADDED
@@ -0,0 +1,392 @@
1
+ # AGENTS.md — test-casebook testing playbook
2
+
3
+ > **For an AI coding agent (Claude Code, etc.).** You have been handed this file (or the test-casebook repository) and asked to set up the **test-casebook test-attribute methodology** and write the test suite for a project. This file lives in the test-casebook repo; **apply its steps to the project you are currently working in** (the "target project"), not to test-casebook itself.
4
+
5
+ `env-attr-cleaner` is a build-time **cleaner**: you write `data-test-*` attributes for testing, they stay in `development`/`test` builds, and they are stripped from `staging`/`production`. The goal of this playbook is: the **same selectors** drive your unit, integration and E2E tests, and never reach users.
6
+
7
+ ## Two parts — cleaner (optional) and testing methodology
8
+
9
+ This playbook has two separable halves, and they live in **separate repos**:
10
+
11
+ - **Part A — the cleaner (optional, opt-in).** Steps 1–2 plus the production strip-check in Step 6. Published to npm as **`env-attr-cleaner`** (Bun runtime: **`env-attr-cleaner-bun`**); its per-framework wiring docs live in that repo. **Never install it on your own — ask the user first** (see Step 2). Reference it only by its **published npm package name**, never by a repo path.
12
+ - **Part B — the testing methodology.** Steps 3–6 — runners, attributes, the `task-test.md` plan, coverage, verification. Lives here (`test-casebook`) and stands on its own: it does **not** require the cleaner.
13
+
14
+ Prose docs are split by concern: per-framework **wiring** docs ship with the `env-attr-cleaner` repo; the **testing guide** and conventions ship here (`docs/`). Below they are referenced as **"the docs"** — substitute the real repo / site URLs as they go live. Keep the dependency **one-way**: the methodology knows about the cleaner, never the reverse.
15
+
16
+ ## Definition of done
17
+
18
+ When you finish, all of these must be true:
19
+
20
+ 1. **If the user opted into the cleaner** (Step 2), it is installed and wired into the target project's build. If they declined, this item and item 6 are N/A.
21
+ 2. The test runner(s) for the chosen layers are installed and configured.
22
+ 3. Components expose `data-test-id` / `data-test-class` per the conventions below.
23
+ 4. A `task-test.md` plan exists, lists every unit and its enumerated cases (see Step 5.0), and **every box in it is ticked, reviewed, and committed** — i.e. tests exist for every layer the project needs (unit, integration, E2E), **cover every branch and state of each unit under test**, are **strictly typed**, **pass**, and each block was validated by a review agent before its commit.
24
+ 5. Test coverage is **at least 90%** (lines and branches) and the runner is configured to **fail below that threshold** (see Step 6).
25
+ 6. **If the cleaner was installed,** a production build contains **no** `data-test-*` attributes (verified by grep).
26
+ 7. Every **permission-gated** unit is covered by a **persona matrix** (Step 5.2) — scenario × persona, expected from the plan, at least one *refused* persona per gated capability, and every enforcement layer asserted — not a single happy-path user.
27
+
28
+ Work through the steps in order. Do not skip verification.
29
+
30
+ ---
31
+
32
+ ## Step 1 — Detect the stack
33
+
34
+ Read the target project's `package.json` dependencies and config files, then pick the row:
35
+
36
+ | If you see… | Framework | Bundler adapter |
37
+ |---|---|---|
38
+ | `nuxt` | Nuxt | `vite` (inside `nuxt.config` → `vite.plugins`) |
39
+ | `vue` + `vite` (no `nuxt`) | Vue | `vite` |
40
+ | `react` + `vite` (no `next`) | React | `vite` |
41
+ | `next` | Next.js | `webpack` (inside `next.config` → `webpack()`) |
42
+ | `svelte` / `@sveltejs/kit` | Svelte / SvelteKit | `vite` |
43
+ | `astro` | Astro | `vite` (inside `astro.config` → `vite.plugins`) |
44
+ | `bun` runtime build only | Bun | use the **`env-attr-cleaner-bun`** package instead |
45
+ | `@angular/core` | Angular | **Not supported — stop.** See the Angular page in the `env-attr-cleaner` repo docs; do not try to wire env-attr-cleaner into an Angular build. |
46
+
47
+ Match the target project's **package manager** (pnpm / npm / yarn / bun) for every install command below — detect it from the lockfile.
48
+
49
+ ---
50
+
51
+ ## Step 2 — Install and wire the cleaner (ask first)
52
+
53
+ **Confirmation gate — do not skip.** The cleaner is optional. Before you install or change anything:
54
+
55
+ 1. **Ask the user whether they want to use `env-attr-cleaner` at all.** The testing methodology (Steps 3–6) works without it; the only thing lost without it is the automatic strip of `data-test-*` from production builds (Step 6.4). If they decline, skip the rest of Step 2 and the Step 6 strip-check, and continue with Part B.
56
+ 2. **Do not run any install command autonomously.** Only install once the user has explicitly confirmed. Never add the dependency, edit the build config, or run `pnpm add` before that confirmation.
57
+
58
+ Once the user has confirmed, install:
59
+
60
+ ```bash
61
+ pnpm add -D env-attr-cleaner # or npm i -D / yarn add -D
62
+ ```
63
+
64
+ Wire it with the adapter from Step 1. The config object is identical everywhere — only the import and the place it goes change:
65
+
66
+ ```ts
67
+ envAttrCleaner({
68
+ environments: {
69
+ development: [],
70
+ test: [],
71
+ staging: ['data-test-*', 'data-debug-*'],
72
+ production: ['data-test-*', 'data-debug-*'],
73
+ },
74
+ })
75
+ ```
76
+
77
+ - **Vite (Vue / React / Svelte):** `import { vite as envAttrCleaner } from 'env-attr-cleaner'` → add `envAttrCleaner({...})` to `plugins` in `vite.config.ts`.
78
+ - **Nuxt:** add it to `vite.plugins` in `nuxt.config.ts`.
79
+ - **Astro:** add it to `vite.plugins` in `astro.config.ts`.
80
+ - **Next.js:** `import { webpack as envAttrCleaner } from 'env-attr-cleaner'` → `webpack(config){ config.plugins.push(envAttrCleaner({...})); return config }` in `next.config.ts`.
81
+
82
+ The keys map to `NODE_ENV`. The arrays list the patterns to **strip**; every other `data-*` (HTMX, Alpine, ARIA helpers) is preserved. For exact per-framework snippets, read the matching framework page in the `env-attr-cleaner` repo (`docs/frameworks/`).
83
+
84
+ ---
85
+
86
+ ## Step 3 — Install the test runner(s)
87
+
88
+ Install only the layers the project needs. Use the stack for the detected framework:
89
+
90
+ | Framework | Unit / Integration | E2E |
91
+ |---|---|---|
92
+ | React / Next.js | `vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdom @vitejs/plugin-react` | `@playwright/test` and/or `cypress` |
93
+ | Vue | `vitest @vue/test-utils @vitejs/plugin-vue jsdom` | `@playwright/test` and/or `cypress` |
94
+ | Nuxt | `vitest @nuxt/test-utils @vue/test-utils happy-dom` | `@playwright/test` and/or `cypress` |
95
+ | Svelte | `vitest @testing-library/svelte @testing-library/user-event @testing-library/jest-dom jsdom` | `@playwright/test` and/or `cypress` |
96
+ | Astro | `vitest` (uses Astro's built-in **Container API**, no extra DOM lib) | `@playwright/test` and/or `cypress` |
97
+
98
+ Then configure the runner:
99
+
100
+ - **Vitest:** create `vitest.config.ts` with the framework plugin and `test.environment` (`jsdom`/`happy-dom`; for Nuxt use `environment: 'nuxt'` via `defineVitestConfig` from `@nuxt/test-utils/config`; for Astro use `getViteConfig` from `astro/config`).
101
+ - **Coverage (every framework — React, Vue, Nuxt, Svelte, Astro):** all of these run on Vitest, so the coverage setup is the same everywhere. Enable it and set a hard **90%** floor so the suite fails below it — install `@vitest/coverage-v8` and add this `test.coverage` block to whichever Vitest config the framework uses above:
102
+ ```ts
103
+ test: {
104
+ coverage: {
105
+ provider: 'v8',
106
+ reporter: ['text', 'html'],
107
+ thresholds: { lines: 90, branches: 90, functions: 90, statements: 90 },
108
+ },
109
+ }
110
+ ```
111
+ Scope coverage to the source you own (`coverage.include`) and exclude framework / vendor / generated / config files (`coverage.exclude`) so the 90% reflects only the code you wrote — not the framework's own (see Step 5.1).
112
+ - **Testing Library** keys `getByTestId` to a single attribute. Set it to `data-test-id` once in a test setup file so the unit/integration snippets below work:
113
+ ```ts
114
+ import { configure } from '@testing-library/dom'
115
+ configure({ testIdAttribute: 'data-test-id' })
116
+ ```
117
+ - **Playwright:** `npx playwright init`. **Cypress:** add `cypress.config.ts`. Point both at a `development`/`test` build URL.
118
+
119
+ ---
120
+
121
+ ## Step 4 — Attribute conventions
122
+
123
+ Add hooks to components as you write tests. Two attributes, two jobs:
124
+
125
+ - **`data-test-id`** → one **unique** element. Selected with `getByTestId` (or `[data-test-id="..."]`).
126
+ - **`data-test-class`** → a **group** of elements of the same role. Selected with a list query / count (there is **no** `getByTestClass`).
127
+
128
+ Rules:
129
+
130
+ - Name by **role**, never by style, color, or DOM position: `login-submit`, `product-card` — not `blue-btn`, `first-div`.
131
+ - Put the attribute directly in the markup/JSX. env-attr-cleaner strips it in `staging`/`production`, so it costs nothing in shipped output. **Static, dynamic (`={...}`), bound (`:data-test-id`), unquoted and value-less forms are all stripped** — use whichever the component needs.
132
+ - **Never** branch runtime logic on these attributes (absent in production). They are test/debug metadata only.
133
+ - **Never** select tests on CSS classes, tag structure, or visible text. Select on `data-test-*`.
134
+
135
+ ---
136
+
137
+ ## Step 5 — Write the tests
138
+
139
+ Cover three layers. **Unit** = one component in isolation. **Integration** = several components / a full view together. **E2E** = the real app in a browser. Always cover **both** selection styles — `data-test-id` (single) and `data-test-class` (group).
140
+
141
+ ### Step 5.0 — Plan in `task-test.md` first, then execute block by block (mandatory)
142
+
143
+ The snippets below are **happy-path examples, not a target**. A test file that only re-implements them is a failed task. Do **not** start writing tests by hand from a guess. Work in two passes — **plan, then execute** — so coverage is explicit and checkable instead of held in your head.
144
+
145
+ #### Pass A — Build the plan (`task-test.md`)
146
+
147
+ Create a `task-test.md` at the target project root. It is the single source of truth for what must be tested. Fill it like this:
148
+
149
+ 1. **List every unit under test** — each component / view / composable / function that needs a test, grouped into one **block per unit**. **Include units that already have tests** — existing tests are a starting point, not a reason to skip the unit (see "Existing tests" below).
150
+ 2. For each block, **read that unit's full source end to end** — every line of the function/SFC, plus the hooks, composables, helpers, and child components it calls. Do not plan from the name, the props list, or a guess; open the file and follow each path.
151
+ 3. From what you actually read, **enumerate every case as a checkbox** under that block:
152
+ - **every prop** — required vs optional, each meaningful value, defaults, and the `undefined`/`null` case;
153
+ - **every conditional render** — each branch of every `if` / ternary / `&&` / `v-if` / `switch` / early return, including the "renders nothing" branch;
154
+ - **every state** — loading, empty, populated, success, **error**, and disabled/readonly;
155
+ - **every user interaction and event** — click, type, submit, blur, keyboard, plus the handler's success **and** failure outcomes;
156
+ - **every boundary and edge case** — empty list, single item, many items, max/min length, zero, negative, very long text, special characters, async race / double-submit;
157
+ - **every guard the code already contains** — if the source checks for something (a null, a length, a permission), list both sides of that check.
158
+ - **every authorization gate** — if the unit reads a role, permission, or ownership / scope check (renders, enables, or hides on it, or calls a gated action), one user is not enough: it needs a **persona matrix** (Step 5.2), with at least one persona that is *refused*.
159
+
160
+ Enumerate **only your unit's own logic** — do not list cases that merely re-test framework or third-party behaviour (see Step 5.1).
161
+ 4. Note, per block, the **layer** (unit / integration / E2E) and the **`data-test-*` hooks** each case will select on.
162
+
163
+ Example shape:
164
+
165
+ ```md
166
+ ## src/components/LoginForm.vue — unit + integration
167
+ - [ ] renders the unique hooks (data-test-id: login-email, login-submit)
168
+ - [ ] groups inputs by role (data-test-class: form-input ×2)
169
+ - [ ] submit with valid input → emits success / navigates to dashboard
170
+ - [ ] submit with invalid email → shows error state, no navigation
171
+ - [ ] submit while pending → button disabled, no double-submit
172
+ - [ ] server error → error message rendered, form stays editable
173
+ - [ ] optional `prefill` prop undefined → fields empty
174
+ ```
175
+
176
+ **Existing tests — audit, don't trust.** A unit that already has a test file is **not** done. For each such unit, after reading its source and enumerating the cases above, **read its existing test file too** and reconcile it against your case list:
177
+ - a case the existing test already covers **well** (real assertion, `data-test-*` selector, strict typing) → tick it as already-covered and note the file;
178
+ - a case that is **missing** → leave it unchecked, to be written in Pass B;
179
+ - an existing test that is **weak** (only happy path, asserts on text/CSS/structure instead of `data-test-*`, uses `any`/untyped fixtures, or doesn't really fail when behaviour breaks) → add a checkbox to **fix/retype/rewrite** it, not just to leave it.
180
+
181
+ So the plan covers both green-field units and the backfill/repair of already-written tests. Old tests get migrated to `data-test-*` selectors and strict typing as part of this — they are held to the exact same bar as new ones.
182
+
183
+ #### Pass B — Execute each block
184
+
185
+ Work through `task-test.md` **one block at a time**. For each block (ideally hand the block off to a dedicated sub-agent so it stays focused on that one unit):
186
+
187
+ 1. **Write one assertion-bearing test per checkbox.** Every case in the block must map to at least one test; a checkbox with no test is an incomplete task, not a stylistic choice. If a path is genuinely unreachable, leave a one-line comment saying why instead of silently dropping it.
188
+ 2. **Add the `data-test-*` hooks** each case needs to the component as you go, rather than reaching for text or CSS to cover an awkward case.
189
+ 3. **Type the test fully — no `any`.** In a TypeScript project the test file and everything in it must be typed as strictly as the production code:
190
+ - import and use the component's **real prop / fixture types** (`ComponentProps<typeof X>`, the exported interfaces) — never retype props inline or with `any`;
191
+ - type every **fixture, factory, and sample object** to its domain type so a shape change breaks the test at compile time;
192
+ - type **mocks and spies** to the signature they replace (`vi.fn<Args, Return>()` / `MockedFunction<typeof fn>`, `vi.mocked(...)`), and type mocked module returns to the real return type;
193
+ - keep query results at their **narrow DOM type** (`HTMLInputElement` etc.) instead of widening, so `.value`/attributes are checked;
194
+ - if a type is genuinely unknowable, use `unknown` + a narrowing assertion, **never `any`** or a blind `as`.
195
+ The test file must pass the project's type-check / lint with zero new errors or suppressions.
196
+ 4. **Run the block's tests; they must pass.** Then **tick the checkbox** in `task-test.md`.
197
+ 5. **Review before committing — hand the block to a dedicated review agent.** Once the block's boxes are ticked and its tests are green, a separate review agent (not the one that wrote the tests) validates the block against this playbook before anything is committed. It checks that:
198
+ - every case listed for the block in `task-test.md` has a real, assertion-bearing test (no ticked-but-missing, no empty/placeholder tests, no `expect(true)`);
199
+ - selectors are `data-test-*` only — no class/text/structure selectors slipped in;
200
+ - typing is strict per Pass B step 3 — no `any`, no untyped fixtures/mocks, no blind `as`, and the type-check / lint is clean;
201
+ - the tests assert behaviour, not implementation details, and actually fail when the behaviour is broken (sanity-check at least one);
202
+ - no full-DOM snapshot is standing in for real assertions, and tests don't touch the real network/clock/shared state (MSW, seeded stores, fake timers — see Step 5.1).
203
+
204
+ If the reviewer flags anything, fix it and re-review — **do not commit a block the reviewer rejected.**
205
+ 6. **Commit the block only when the reviewer approves and the tests are green.** One focused commit per block (the new/changed test file, the `data-test-*` hooks added to the component, and the ticked `task-test.md`). Then move to the next block.
206
+
207
+ The task is done only when every box in `task-test.md` is ticked, reviewed, and committed. Use the same discipline for plain functions/composables: read the whole function, list every input partition and every return/throw path, give each a checkbox, and cover each.
208
+
209
+ ### Step 5.1 — Isolate the unit, stay deterministic, don't test the framework
210
+
211
+ These three rules apply to every test you write or repair.
212
+
213
+ **Isolate the unit from its collaborators.** A unit test must fail only when the unit itself breaks — never because of the network, a store, the router, or a heavy child. Replace those at the boundary:
214
+ - **Network / HTTP** — never hit a real endpoint. Intercept with **MSW** (`msw`) and return typed fixtures; cover the success, empty, and error-status branches by swapping handlers per test. **Type those fixtures from the real API contract** (the backend's response schema / resource definition), never from a hand-written guess — derive the fixture type from the **same types the production code consumes**, so a mock of an endpoint or method that no longer exists, or a response shape that has drifted, fails at type-check. A lying mock is worse than no mock: it greens the test while production breaks.
215
+ - **Stores (Pinia / Zustand / Redux)** — mount with a fresh, seeded test store (`createTestingPinia()`, a wrapped provider) so each test starts from a known state; never share state across tests.
216
+ - **Router / i18n / theme** — provide them through the framework's test utilities (Vue `global.plugins`, a React provider wrapper, `@nuxt/test-utils`) with a minimal config, not the real app bootstrap.
217
+ - **Heavy or out-of-scope children** — stub them to a placeholder that still carries the `data-test-*` hook you assert on, so the test stays about this unit. At **integration** level, render the real children instead.
218
+
219
+ Type every stub / handler to the real signature (Pass B step 3) — a stub that has drifted from the real shape is worse than no stub. For **critical flows** (auth, permission gates, data mutations), don't rely on mocked unit tests alone — exercise at least one real path end-to-end in E2E against a running stack, so a lying mock can never hide the whole flow.
220
+
221
+ **Keep every test deterministic.** Same result on every machine, every run:
222
+ - freeze time — `vi.useFakeTimers()` with a fixed date; advance with `vi.advanceTimersByTime` / `await vi.runAllTimersAsync()` instead of real waits;
223
+ - pin **timezone and locale** (e.g. `TZ=UTC`, a fixed `Intl` locale) so date / number / format assertions don't shift between machines;
224
+ - never rely on unseeded `Math.random`, real `Date.now()`, real timers, network latency, or test execution order;
225
+ - await the DOM — `findBy*` / `waitFor` / `await flushPromises()` before asserting on async updates; never assert on a render that hasn't settled.
226
+
227
+ **Don't test the framework — test your code.** Coverage and cases are about *your* logic, not the library's:
228
+ - do **not** write cases that assert a framework / library feature works (that `v-if` toggles, that the router navigates, that a third-party UI component renders its own markup) — that is the framework's own test suite's job;
229
+ - treat third-party components and the framework runtime as trusted boundaries — assert on **your** inputs to them and **your** handling of their outputs, not their internals;
230
+ - exclude framework / vendor / generated code from coverage (`coverage.exclude`) so the 90% measures only the code you wrote.
231
+
232
+ ### Step 5.2 — Permission-gated units: the persona matrix
233
+
234
+ When a unit's behaviour depends on **who** the current user is — a role, a permission, an ownership / tenant / scope check — one happy-path user is not a test. This is the single biggest source of escaped bugs, because a harness that always authenticates as the same user (or only ever mints single-role users) never exercises the other actors. Apply this whenever Step 5.0 flagged an authorization gate.
235
+
236
+ **Personas live in one shared catalogue.** Declare them once, declaratively — each persona is a **named actor** with its role / permission assignments, scoped as the target project's model requires (global, per-tenant / organisation, or per-resource-owner). Both the unit / integration tests and the E2E suite read personas from this **same** catalogue, so front and back assert the *same* actors. When both sides drive the same persona through the same scenario and disagree, the divergence is the bug.
237
+
238
+ **Mint each persona fresh; never mutate one user on the fly.** Seed / create the persona and authenticate as it (a fresh token) per test. Mutating a single user's rights mid-run tests a state the app never really is in, leaks state across tests, and — if rights are baked into the token at login — silently keeps the old permissions, greening a broken case. Fresh persona = the real "logs in with role set X" path, and deterministic.
239
+
240
+ **Build a matrix, not a list.** For each **scenario** the unit exposes × each **persona**, state the expected outcome. The permission is the *input* that sets the expected result; you assert the **observable outcome** — element present / absent, action succeeds / is refused — never "does this user hold permission Y" (that just re-tests the framework's gate).
241
+
242
+ - **The expected outcome comes from the plan / human intent, not from the app's own check.** Computing "expected = what `can()` returns" and then asserting against `can()` is circular: if the gate is wrong, the expectation is wrong too, and the test greens over the bug. The `task-test.md` case says what *should* happen; the test verifies the system against that.
243
+ - **Weight the matrix on the refused cells.** "This persona can" is the easy half; the bugs live in "this persona must **not**, and is actually prevented." For every gated capability, test at least one persona that is denied.
244
+ - **Assert every layer that enforces.** If a server *and* a client both guard the same capability, assert both: the API refuses (e.g. 403) **and** the UI reflects it (the gated element is absent / disabled — assert its absence via the `data-test-*` hook). A UI that hides a button while the API still allows the call is a security bug this matrix exists to catch — and so is the reverse.
245
+ - **Aggregation across several roles needs unit coverage.** An E2E harness that only mints single-role users will never surface a bug in how rights *combine* across roles. Add a unit test built on an actor carrying the permission on a **non-primary** role, so an aggregation regression fails at the unit level.
246
+
247
+ **Prerequisite — you must be able to *become* each persona.** This chapter needs the persona catalogue **and** a way to instantiate each one (a factory / token minter). If that capability does not exist in the target project, **say so and stop** — do not write a green test you cannot actually drive as the intended persona, and do not silently fall back to the default user. A matrix run as one user is the very hole this step closes.
248
+
249
+ ### Unit + integration (React / Next.js shown; adapt the render API per framework)
250
+
251
+ ```tsx
252
+ import { render, screen } from '@testing-library/react'
253
+ import userEvent from '@testing-library/user-event'
254
+ import { describe, it, expect } from 'vitest'
255
+ import { LoginForm } from './LoginForm'
256
+
257
+ describe('LoginForm', () => {
258
+ it('exposes the unique hooks (data-test-id)', () => {
259
+ render(<LoginForm />)
260
+ expect(screen.getByTestId('login-email')).toBeInTheDocument()
261
+ expect(screen.getByTestId('login-submit')).toBeInTheDocument()
262
+ })
263
+
264
+ it('groups inputs by role (data-test-class)', () => {
265
+ const { container } = render(<LoginForm />)
266
+ expect(container.querySelectorAll('[data-test-class="form-input"]')).toHaveLength(2)
267
+ })
268
+
269
+ it('runs the flow (integration)', async () => {
270
+ const user = userEvent.setup()
271
+ render(<LoginPage />)
272
+ await user.type(screen.getByTestId('login-email'), 'user@example.com')
273
+ await user.click(screen.getByTestId('login-submit'))
274
+ expect(await screen.findByTestId('dashboard')).toBeInTheDocument()
275
+ })
276
+ })
277
+ ```
278
+
279
+ The block above is the *shape*; the example below is the **bar** — one unit, every state covered, network mocked, fixtures typed, DOM awaited. A real `task-test.md` block looks like this, not like the three-test snippet above:
280
+
281
+ ```tsx
282
+ import { render, screen } from '@testing-library/react'
283
+ import { http, HttpResponse } from 'msw'
284
+ import { setupServer } from 'msw/node'
285
+ import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'
286
+ import { ProductList } from './ProductList'
287
+ import type { Product } from '@/types/product'
288
+
289
+ const sample: Product[] = [
290
+ { id: '1', name: 'Keyboard', price: 49 },
291
+ { id: '2', name: 'Mouse', price: 25 },
292
+ ]
293
+
294
+ const server = setupServer()
295
+ beforeAll(() => server.listen())
296
+ afterEach(() => server.resetHandlers())
297
+ afterAll(() => server.close())
298
+
299
+ describe('ProductList', () => {
300
+ it('shows the loading hook before the request resolves', () => {
301
+ server.use(http.get('/api/products', () => HttpResponse.json(sample)))
302
+ render(<ProductList />)
303
+ expect(screen.getByTestId('products-loading')).toBeInTheDocument()
304
+ })
305
+
306
+ it('renders one card per product (populated, data-test-class)', async () => {
307
+ server.use(http.get('/api/products', () => HttpResponse.json(sample)))
308
+ const { container } = render(<ProductList />)
309
+ await screen.findByTestId('products-ready')
310
+ expect(container.querySelectorAll('[data-test-class="product-card"]')).toHaveLength(2)
311
+ })
312
+
313
+ it('shows the empty state when the list is empty', async () => {
314
+ server.use(http.get('/api/products', () => HttpResponse.json([])))
315
+ render(<ProductList />)
316
+ expect(await screen.findByTestId('products-empty')).toBeInTheDocument()
317
+ })
318
+
319
+ it('shows the error state when the request fails', async () => {
320
+ server.use(http.get('/api/products', () => new HttpResponse(null, { status: 500 })))
321
+ render(<ProductList />)
322
+ expect(await screen.findByTestId('products-error')).toBeInTheDocument()
323
+ })
324
+ })
325
+ ```
326
+
327
+ Note the discipline: typed fixture (`Product[]`), MSW swapped per test to drive each branch, `await findBy*` before asserting, both selector styles, and **every state** (loading / populated / empty / error) — not just the happy path. For a debounced/timed unit, pair this with `vi.useFakeTimers()` and a fixed date (Step 5.1).
328
+
329
+ Per-framework equivalents for the render/query API:
330
+ - **Vue / Nuxt:** `mount(Component)` / `mountSuspended(Component)`; `wrapper.find('[data-test-id="..."]')`, `wrapper.findAll('[data-test-class="..."]')`, `.setValue()`, `.trigger('submit')`.
331
+ - **Svelte:** `render(Component)` from `@testing-library/svelte`; same `screen.getByTestId` + `container.querySelectorAll('[data-test-class="..."]')`.
332
+ - **Astro:** `await AstroContainer.create()` then `renderToString(Component)`; assert with `html.includes('data-test-id="..."')` and count `data-test-class` occurrences. (Interactive flows live in client islands → cover them in E2E.)
333
+
334
+ ### E2E (Playwright shown; Cypress is the same selectors)
335
+
336
+ ```ts
337
+ import { test, expect } from '@playwright/test'
338
+
339
+ test('user logs in', async ({ page }) => {
340
+ await page.goto('/login')
341
+ await page.locator('[data-test-id="login-email"]').fill('user@example.com')
342
+ await page.locator('[data-test-id="login-submit"]').click()
343
+ await expect(page).toHaveURL('/dashboard')
344
+ await expect(page.locator('[data-test-class="form-input"]')).toHaveCount(2)
345
+ })
346
+ ```
347
+
348
+ For deeper scenario snippets (forms, tables, modals, auth, i18n, state…), read the testing guide in `docs/testing-guide/`.
349
+
350
+ ---
351
+
352
+ ## Step 6 — Verify (do not skip)
353
+
354
+ 1. **Run the tests** with the project's commands; they must pass. Unit/integration run under `NODE_ENV=test`, where attributes are present.
355
+ 2. **Run coverage and enforce the 90% floor:**
356
+ ```bash
357
+ pnpm vitest run --coverage # or the project's coverage script
358
+ # must report ≥ 90% lines & branches and exit 0 (thresholds fail the run otherwise)
359
+ ```
360
+ If coverage is below 90%, read the coverage report, find the uncovered lines/branches, and **go back to Step 5.0** — they map to cases missing from `task-test.md`. Add those cases and their tests; do not lower the threshold to pass.
361
+ 3. **Run E2E** against a `development`/`test` build (attributes present).
362
+ 4. **Build for production and confirm the strip:**
363
+ ```bash
364
+ pnpm build
365
+ grep -r "data-test" <output-dir> # dist/ · .next/static · .output/public
366
+ # must return nothing
367
+ ```
368
+
369
+ If the grep finds anything, the cleaner is not wired correctly — recheck Step 2 (right adapter, `enforce: 'pre'`, correct `NODE_ENV`).
370
+
371
+ ---
372
+
373
+ ## Guardrails
374
+
375
+ - Do **not** install or wire the cleaner without the user's explicit confirmation, and warn them its package / URL is moving to a new repo (see Step 2). The methodology (Part B) runs without it.
376
+ - Do **not** invent a new attribute scheme; use `data-test-id` / `data-test-class`.
377
+ - Do **not** weaken tests to pass (no class/text/structure selectors).
378
+ - Do **not** stop at the happy path. The Step 5 examples are a starting point; ship tests only after the unit's source has been read and every box in the `task-test.md` plan (Step 5.0) has a passing test.
379
+ - Do **not** skip the `task-test.md` plan and write tests straight from a guess — the plan, built from reading the source, is what makes coverage exhaustive instead of basic.
380
+ - Do **not** ship loosely-typed tests. No `any`, no untyped fixtures/mocks, no blind `as`; reuse the production types and let the test file pass the project's type-check (see Step 5.0 Pass B).
381
+ - Do **not** leave pre-existing tests untouched. A unit that already had tests is held to the same bar — audit each one against the case list, migrate it to `data-test-*` selectors and strict typing, and backfill every missing case (see Step 5.0 "Existing tests").
382
+ - Do **not** lower or disable the 90% coverage threshold to make the suite pass. Below 90% means cases are missing — add the tests, don't move the bar.
383
+ - Do **not** test the framework or third-party libraries — they test themselves. Cover your own logic only, treat libraries as trusted boundaries, and exclude vendor/framework code from coverage (see Step 5.1).
384
+ - Do **not** let tests touch the real network, clock, or shared state. Mock the network (MSW), seed stores, freeze time, and pin timezone/locale so tests are isolated and deterministic (see Step 5.1).
385
+ - Do **not** trust a mock that has drifted from reality. Type fixtures from the real API contract so a mock of a non-existent method / endpoint, or a wrong response shape, fails at type-check — a lying mock greens the test while production breaks (see Step 5.1).
386
+ - Do **not** test a permission-gated unit with a single user. Build the persona matrix (Step 5.2): scenario × persona, dense on the *refused* cells, asserting every enforcement layer. A single happy-path user is the biggest source of escaped permission bugs.
387
+ - Do **not** compute a case's expected result from the app's own permission check and assert against it — that is circular and greens over a broken gate. The expected outcome comes from the plan / human intent.
388
+ - Do **not** simulate personas by mutating one user's rights on the fly, and do **not** fall back to the default user when a persona can't be minted — mint each persona fresh and authenticate as it, or stop and flag the missing capability (see Step 5.2).
389
+ - Do **not** use full-DOM snapshot tests as a substitute for assertions. A `toMatchSnapshot()` over rendered markup asserts nothing meaningful, inflates coverage, and gets regenerated blindly with `-u` on every change. Assert on explicit `data-test-*` hooks and behaviour instead. (A tiny, intentional inline snapshot of one serialized value is fine; a whole-component snapshot standing in for real cases is not.)
390
+ - Do **not** remove or rename other `data-*` attributes.
391
+ - Do **not** attempt Angular — env-attr-cleaner cannot strip its templates (see the Angular page in the `env-attr-cleaner` repo docs).
392
+ - Keep the cleaner config's `staging`/`production` arrays in sync with any extra patterns you introduce (e.g. `data-debug-*`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tcharlyto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # test-casebook
2
+
3
+ > A testing methodology and AI-agent playbook for exhaustive, strictly-typed, `data-test`-driven test suites.
4
+
5
+ `test-casebook` is the **doctrine** half of the testing workflow: the conventions, strategy, scenario guide, and the agent playbook (`AGENTS.md`) that an AI coding agent applies to a target project to produce complete, reliable tests.
6
+
7
+ It pairs with [`env-attr-cleaner`](https://github.com/techmefr/env-attr-cleaner) — the build-time tool that strips `data-test-*` attributes from production. The dependency is **one-way**: the methodology knows about the cleaner, the cleaner knows nothing about the methodology.
8
+
9
+ ## What's inside
10
+
11
+ - **`AGENTS.md`** — the playbook. Handed to an AI coding agent, it drives: detect the stack, (optionally) wire the cleaner, plan every case in `task-test.md`, execute block by block with a reviewer, enforce strict typing and coverage, and verify. Includes the persona matrix for permission-gated units and the anti-mock-drift rules.
12
+ - **`.claude/skills/test-task/`** — *(project manager, upstream)* turns a story into a shared **`test-task.md`**: interviews to close gaps (personas, gated capabilities, regression surface) and reads the real permission model, so the contract is complete before dev.
13
+ - **`.claude/skills/test-casebook/`** + **`.claude/agents/{test-writer,test-reviewer}`** — *(developer, downstream)* executes that contract on the code — plan per unit, write, review, verify.
14
+ - **`docs/strategy.md`** — why `data-test-*` over CSS / structural / text selectors.
15
+ - **`docs/conventions.md`** — naming rules for `data-test-id` / `data-test-class`.
16
+ - **`docs/testing-guide/`** — ready-to-use scenario snippets.
17
+
18
+ ## The flow
19
+
20
+ Story → **`test-task` skill** (PM) writes one `test-task.md` → given to the front **and** back developer → each runs the **`test-casebook` skill** on their side, deriving expected outcomes from that same `test-task.md`. One contract, two implementations; permission cases assert both layers, so a front/back divergence surfaces immediately.
21
+
22
+ ## Core idea
23
+
24
+ Write dedicated `data-test-*` attributes for testing. The **same selectors** drive unit, integration and E2E tests, never reach production (stripped by `env-attr-cleaner`), and stay stable across refactors because they're decoupled from style, structure and text.
25
+
26
+ The playbook's job is to make coverage **exhaustive instead of happy-path**: plan from reading the source, enumerate every case (props, branches, states, interactions, guards, permissions), and verify each one — not re-implement a few examples.
27
+
28
+ ## How it's consumed
29
+
30
+ Three channels, **one source of truth** (this repo's `AGENTS.md` + docs), never re-authored per channel:
31
+
32
+ - **Claude Code skill + sub-agents** — `.claude/skills/test-casebook/` orchestrates the run; `.claude/agents/test-writer` and `.claude/agents/test-reviewer` execute and gate each block. Open the repo (or a project it's been scaffolded into) in Claude Code and invoke the `test-casebook` skill.
33
+ - **npx scaffolder** — `npx test-casebook init` drops `AGENTS.md`, `docs/` and `.claude/` (skill + agents) into any project (agent-agnostic). Use `--force` to overwrite existing files.
34
+ - **Docs** — the guides in `docs/`.
35
+
36
+ ## Status
37
+
38
+ Pre-release (not yet published to npm). Usable today via the Claude Code skill, the scaffolder (`node bin/test-casebook.mjs init`), or by handing `AGENTS.md` to any agent; marketplace packaging is in progress.
39
+
40
+ ## License
41
+
42
+ MIT