@tsfpp/agents 1.2.3 → 1.3.1
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.
- package/CHANGELOG.md +34 -0
- package/copilot/agents/tsfpp-audit.agent.md +88 -10
- package/copilot/agents/tsfpp-guarded-coding.agent.md +58 -4
- package/copilot/agents/tsfpp-tdd.agent.md +292 -0
- package/copilot/copilot-instructions.md +143 -66
- package/copilot/instructions/tsfpp-api.instructions.md +104 -39
- package/copilot/instructions/tsfpp-base.instructions.md +18 -7
- package/copilot/instructions/tsfpp-prelude.instructions.md +95 -47
- package/copilot/instructions/tsfpp-react.instructions.md +152 -40
- package/copilot/instructions/tsfpp-testing.instructions.md +154 -0
- package/copilot/skills/test-standard/SKILL.md +238 -0
- package/init.mjs +67 -84
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,40 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [1.3.1] - 2026-05-17
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Added `copilot/skills/test-standard/SKILL.md`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- Updated `init.mjs` to include previously missing changes.
|
|
22
|
+
|
|
23
|
+
## [1.3.0] - 2026-05-17
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Added `copilot/instructions/tsfpp-testing.instructions.md` following the release of the new testing standard.
|
|
28
|
+
- Added `copilot/agents/tsfpp-tdd.agent.md` for dedicated test-first workflow that writes failing, proper tests before implementation starts.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- Updated `copilot/agents/tsfpp-guarded-coding.agent.md` to include multi-layer request focus.
|
|
33
|
+
- Updated `copilot/agents/tsfpp-guarded-coding.agent.md` to enforce a prelude-first rule before execution workflow as a hard reflex, not an afterthought.
|
|
34
|
+
- Updated `copilot/agents/tsfpp-audit.agent.md` so focus `api` applies both API_CODE_STANDARD rules and prefers `boundary` idioms over hand-rolled repeats.
|
|
35
|
+
- Updated `copilot/agents/tsfpp-audit.agent.md` to strengthen checks for Prelude anti-patterns.
|
|
36
|
+
- Updated `copilot/agents/tsfpp-audit.agent.md` to support focus `test`.
|
|
37
|
+
- Updated `copilot/agents/tsfpp-guarded-coding.agent.md` to hand off to `tsfpp-tdd` before starting implementation.
|
|
38
|
+
- Expanded `copilot/instructions/tsfpp-base.instructions.md`: replaced `brand()` with the correct smart-constructor `as` idiom, added a discriminant-convention table, and added `new Map()`/`new Set()`/`try-catch`/`null` checks to the Never list.
|
|
39
|
+
- Expanded `copilot/instructions/tsfpp-prelude.instructions.md`: added `ReadonlyMap`/`ReadonlySet`, unknown record decoding helpers (`isRecord`, `getStringField`, etc.), `fromUnknownArrayOf`, `sequenceArrayO`, `isCons`/`isNil`, and `UnknownRecord`; removed `brand()` and removed `mapErr` (not in `fp-ts`); added a `pipe` vs `flow` section.
|
|
40
|
+
- Expanded `copilot/instructions/tsfpp-api.instructions.md`: fixed the `fromZodError` + `fold` error path to `apiErrorToResponse(fromZodError(e), ctx)`, completed the import list, added middleware composition via `pipe`, and documented pagination, rate limiting, CORS, bulk operations, LROs, and `cause` warnings for internal/dependency errors.
|
|
41
|
+
- Expanded `copilot/instructions/tsfpp-react.instructions.md`: corrected memoization guidance (no speculative memoization), added a state-elimination ladder, expanded effect discipline with explicit do/don't examples, and added guidance for TanStack Query key factories, RHF + Zod, typed router usage, narrow Zustand selectors, `cva`/`cn` styling, plus a complete Forbidden list.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Fixed `copilot/copilot-instructions.md` after an earlier overwrite that incorrectly replaced it with API-only instructions.
|
|
46
|
+
|
|
13
47
|
## [1.2.3] - 2026-05-16
|
|
14
48
|
|
|
15
49
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: TSF++ standards compliance auditor. Produces a structured markdown report in docs/audits/ with per-slice checkboxes.
|
|
3
3
|
name: tsfpp-audit
|
|
4
|
-
argument-hint: "target=<path|package|layer> focus=<all|types|boundary|complexity|loc|annotations|security|react|data>"
|
|
4
|
+
argument-hint: "target=<path|package|layer> focus=<all|types|boundary|complexity|loc|annotations|security|react|data|prelude|test>"
|
|
5
5
|
tools:
|
|
6
6
|
- edit/createFile
|
|
7
7
|
- edit/editFiles
|
|
@@ -29,7 +29,6 @@ The canonical standard is at `node_modules/@tsfpp/standard/spec/CODING_STANDARD.
|
|
|
29
29
|
Profile overlays:
|
|
30
30
|
- API: `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`
|
|
31
31
|
- React: `node_modules/@tsfpp/standard/spec/REACT_CODING_STANDARD.md`
|
|
32
|
-
- Data: `node_modules/@tsfpp/standard/spec/DATA_CODING_STANDARD.md`
|
|
33
32
|
- Security: `node_modules/@tsfpp/standard/spec/SECURITY_CODING_STANDARD.md`
|
|
34
33
|
|
|
35
34
|
If any referenced file is missing, stop immediately and report the path. Do not proceed.
|
|
@@ -43,7 +42,7 @@ If any referenced file is missing, stop immediately and report the path. Do not
|
|
|
43
42
|
If the user has not provided both `target` and `focus`, ask exactly this:
|
|
44
43
|
|
|
45
44
|
> **Target** — path, package name, or layer to audit (e.g. `src/domain`, `@tsfpp/prelude`, `api layer`)?
|
|
46
|
-
> **Focus** — `all` · `types` · `boundary` · `complexity` · `loc` · `annotations` · `security` · `react` · `data` · or comma-separated combination?
|
|
45
|
+
> **Focus** — `all` · `types` · `boundary` · `complexity` · `loc` · `annotations` · `security` · `react` · `data` · `prelude` · `test` · or comma-separated combination?
|
|
47
46
|
|
|
48
47
|
Do not proceed until both are confirmed.
|
|
49
48
|
|
|
@@ -150,7 +149,14 @@ Append each completed slice to the report:
|
|
|
150
149
|
1.4 (no bare interface) · 1.5 (no `any`) · 1.6 (no `!` or `as`) · 3.x (readonly) · branded types on domain primitives · smart constructor completeness · exhaustive sum-type dispatch
|
|
151
150
|
|
|
152
151
|
### `boundary`
|
|
153
|
-
API_CODING_STANDARD.md
|
|
152
|
+
API_CODING_STANDARD.md (full) + `@tsfpp/boundary` surface:
|
|
153
|
+
`extractContext` called at the top of every handler · Zod `safeParse` at every input boundary lifted via `fromZodError` ·
|
|
154
|
+
all handlers return `Result<T, ApiError>` internally · `apiErrorToResponse` used for all error paths · no raw `throw` ·
|
|
155
|
+
response builders (`okResponse`, `createdResponse`, `noContentResponse`, etc.) used; no hand-built `new Response()` ·
|
|
156
|
+
`rateLimitHeaders` on all responses for rate-limited endpoints · `corsHeaders` never reflects `Origin` blindly ·
|
|
157
|
+
`withIdempotency` + `withRequestLog` composed via `pipe` · pagination via `mkPaginated` + `parsePaginationQuery` ·
|
|
158
|
+
LRO via `acceptedResponse` + `mkRunningOp`/`mkSucceededOp` · bulk via `bulkResponse` + `mkBulkOkItem`/`mkBulkErrorItem` ·
|
|
159
|
+
handler architecture: parse → domain map → use-case → response map (nothing else)
|
|
154
160
|
|
|
155
161
|
### `complexity`
|
|
156
162
|
Function body ≤ 40 lines · cyclomatic complexity ≤ 10 · nesting ≤ 4 · arity ≤ 3 positional params · pipeline depth ≤ 8 stages
|
|
@@ -164,14 +170,86 @@ JSDoc on every export · `@param` + `@returns` present · `@law` on combinators
|
|
|
164
170
|
### `security`
|
|
165
171
|
SECURITY_CODING_STANDARD.md: input validation at boundaries · no secrets in code · no sensitive data in errors · auth/authz at correct layer · dependency hygiene
|
|
166
172
|
|
|
167
|
-
### `
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
173
|
+
### `prelude`
|
|
174
|
+
Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@tsfpp/prelude` already provides.
|
|
175
|
+
|
|
176
|
+
| Anti-pattern | Violation | Should be |
|
|
177
|
+
|---|---|---|
|
|
178
|
+
| `if (x === undefined)` / `if (x === null)` | MUST | `fromNullable(x)` → `Option<T>` |
|
|
179
|
+
| `x ?? fallback` | MUST | `pipe(x, fromNullable, getOrElse(() => fallback))` |
|
|
180
|
+
| `try/catch` outside adapter boundary | MUST | `tryCatch` / `tryCatchAsync` |
|
|
181
|
+
| `.map()` on a fallible function | MUST | `traverseArray` |
|
|
182
|
+
| `new Map()` | MUST | `intoMap([...])` |
|
|
183
|
+
| `new Set()` | MUST | `intoSet([...])` |
|
|
184
|
+
| `import ... from 'ramda'` | MUST | `@tsfpp/prelude` |
|
|
185
|
+
| `result._tag === 'Ok'` | MUST | `isOk(result)` |
|
|
186
|
+
| `option._tag === 'Some'` | MUST | `isSome(option)` |
|
|
187
|
+
| `Result<void, E>` | MUST | `Result<Unit, E>` with `ok(unit)` |
|
|
188
|
+
| Manual null-coalescing guard | SHOULD | `getOrElse` / `orElse` |
|
|
189
|
+
| Side effect breaking `pipe` chain | SHOULD | `tap` / `tapErr` |
|
|
190
|
+
| Manual `if/else` for Option fallback | SHOULD | `orElse` / `getOrElse` |
|
|
191
|
+
|
|
192
|
+
Checklist:
|
|
193
|
+
|
|
194
|
+
- [ ] No `if (x === undefined/null)` — use `fromNullable`
|
|
195
|
+
- [ ] No `x ?? fallback` — use `getOrElse`
|
|
196
|
+
- [ ] No `try/catch` outside adapter boundaries — use `tryCatch`/`tryCatchAsync`
|
|
197
|
+
- [ ] No `.map()` on fallible function — use `traverseArray`
|
|
198
|
+
- [ ] No `new Map()` / `new Set()` — use `intoMap` / `intoSet`
|
|
199
|
+
- [ ] No `import from 'ramda'`
|
|
200
|
+
- [ ] Prelude ADTs accessed via exported guards (`isOk`, `isSome`), never `._tag` directly
|
|
201
|
+
- [ ] No `Result<void, E>` — use `Result<Unit, E>`
|
|
202
|
+
- [ ] Side effects in pipelines via `tap` / `tapErr`
|
|
203
|
+
- [ ] Unknown record decoded via `isRecord` + `getStringField`/`getNumberField`/`getTypedField`
|
|
204
|
+
|
|
205
|
+
### `test`
|
|
206
|
+
TEST_CODING_STANDARD.md Rules 1–8 (additive to base TSF++).
|
|
207
|
+
|
|
208
|
+
Checklist:
|
|
209
|
+
|
|
210
|
+
**Structure and behaviour (§1–§3)**
|
|
211
|
+
- [ ] 1.1 — Tests assert on observable outputs, not implementation details
|
|
212
|
+
- [ ] 1.2 — Test descriptions are full sentences describing behaviour, not implementation echoes
|
|
213
|
+
- [ ] 1.3 — One logical assertion concept per test
|
|
214
|
+
- [ ] 1.4 — No wall-clock time, randomness without seed, network, or filesystem in unit tests
|
|
215
|
+
- [ ] 1.5 — No shared mutable state between tests; `beforeEach` resets all state
|
|
216
|
+
- [ ] 3.3 — AAA structure with blank line separating phases
|
|
217
|
+
- [ ] 3.4 — No branching or loops in test bodies
|
|
218
|
+
|
|
219
|
+
**Toolchain (§2)**
|
|
220
|
+
- [ ] 2.2 — Pure functions and combinators have fast-check property tests for documented laws
|
|
221
|
+
- [ ] 2.3 — React components tested with RTL only; no Enzyme or shallow rendering
|
|
222
|
+
- [ ] 2.4 — Network mocked with MSW; no stubbed `fetch` or HTTP client
|
|
223
|
+
- [ ] 2.5 — DAL tests run against real or containerised store; in-memory stubs for use-case tests
|
|
224
|
+
- [ ] 2.6 — No snapshot tests for component structure or API response shape
|
|
225
|
+
|
|
226
|
+
**Coverage (§6)**
|
|
227
|
+
- [ ] 6.2 — Every public export has at least one test covering the primary success case
|
|
228
|
+
- [ ] 6.3 — Every error path (`Err`, `None`, non-2xx) has a corresponding test
|
|
229
|
+
- [ ] 6.4 — Every branch, switch case, and ternary arm is exercised by at least one test
|
|
230
|
+
|
|
231
|
+
**Forbidden patterns (§5)**
|
|
232
|
+
- [ ] 5.1 — No `getByTestId` queries — use `getByRole`, `getByLabelText`, `getByText`
|
|
233
|
+
- [ ] 5.2 — No `vi.fn()` to implement a port interface — use in-memory implementations
|
|
234
|
+
- [ ] 5.3 — No assertions on internal function calls — assert on observable outcome
|
|
235
|
+
- [ ] 5.4 — No `any` in test code
|
|
236
|
+
- [ ] 5.5 — No `beforeAll` for state that mutates between tests
|
|
237
|
+
- [ ] 5.6 — No `setTimeout` delays — use `waitFor` or `findBy*`
|
|
238
|
+
|
|
239
|
+
**Factories and fixtures (§7)**
|
|
240
|
+
- [ ] 7.1 — Test data produced by typed factory functions, not raw inline object literals
|
|
241
|
+
- [ ] 7.2 — Factories live in `tests/factories/`, not co-located with test files
|
|
242
|
+
- [ ] 7.4 — No production or staging IDs in fixtures
|
|
243
|
+
|
|
244
|
+
**Layer-specific (§4)**
|
|
245
|
+
- [ ] 4.1 Core — every smart constructor tested at valid/invalid boundary values
|
|
246
|
+
- [ ] 4.2 Use-case — each distinct `Err` variant has a test; in-memory stubs used
|
|
247
|
+
- [ ] 4.3 Handler — each missing required field produces 422; each `ApiError` variant covered
|
|
248
|
+
- [ ] 4.4 DAL — insert+read round-trip tested; not-found returns `None`
|
|
249
|
+
- [ ] 4.5 React — loading state, error state, and user interactions all covered
|
|
172
250
|
|
|
173
251
|
### `all`
|
|
174
|
-
All focus areas above in sequence.
|
|
252
|
+
All focus areas above in sequence. For `.tsx` files, include `react` automatically. For files in `infrastructure/`, `dal/`, or `repository/` paths, include `data` automatically. For `*.test.ts` and `*.test.tsx` files, include `test` automatically. Include `prelude` for all files.
|
|
175
253
|
|
|
176
254
|
---
|
|
177
255
|
|
|
@@ -12,6 +12,10 @@ tools:
|
|
|
12
12
|
- todo
|
|
13
13
|
- vscode/askQuestions
|
|
14
14
|
handoffs:
|
|
15
|
+
- label: Write tests first
|
|
16
|
+
agent: tsfpp-tdd
|
|
17
|
+
prompt: "Write the failing test suite for this feature before any implementation."
|
|
18
|
+
send: false
|
|
15
19
|
- label: Audit what I just wrote
|
|
16
20
|
agent: tsfpp-audit
|
|
17
21
|
prompt: "Audit the files just modified for TSF++ compliance. Focus: all."
|
|
@@ -40,11 +44,37 @@ If either file is missing or unreadable, stop immediately and report the missing
|
|
|
40
44
|
|
|
41
45
|
## Session start
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
Infer the layer per task from the user's message:
|
|
48
|
+
|
|
49
|
+
| Signal | Layer |
|
|
50
|
+
|--------|-------|
|
|
51
|
+
| "web frontend", "UI", "component", "page", "form", "editor", "button" | `react` |
|
|
52
|
+
| "API", "endpoint", "handler", "route", "response" | `api` |
|
|
53
|
+
| "database", "repository", "query", "migration", "schema" | `dal` |
|
|
54
|
+
| "CLI", "command", "argv", "script", "terminal" | `cli` |
|
|
55
|
+
| "domain", "model", "type", "rule" — no framework context | `core` |
|
|
56
|
+
|
|
57
|
+
If the request covers a single layer, state it and proceed:
|
|
58
|
+
> "Layer: `react` — proceeding."
|
|
59
|
+
|
|
60
|
+
If the request spans multiple layers, state the full plan before proceeding:
|
|
61
|
+
> "Layer plan: `react` (editor save shortcut) · `api` (POST /export endpoint) — proceeding in that order."
|
|
62
|
+
|
|
63
|
+
Apply the correct layer constraints per task as work proceeds.
|
|
64
|
+
Never mix layer constraints within a single file.
|
|
65
|
+
|
|
66
|
+
If and only if the layer cannot be inferred for any task, ask once:
|
|
67
|
+
> Which layer applies to [the unclear task]? `core` · `api` · `dal` · `react` · `cli`
|
|
68
|
+
|
|
69
|
+
### TDD gate
|
|
70
|
+
|
|
71
|
+
Before writing any production code, verify that failing tests exist for the work being requested.
|
|
44
72
|
|
|
45
|
-
|
|
73
|
+
Check for a corresponding test file alongside the target file(s). If no test file exists or no tests cover the requested behaviour:
|
|
46
74
|
|
|
47
|
-
|
|
75
|
+
> "No failing tests found for this work. Use the **Write tests first** handoff to run `tsfpp-tdd` before proceeding here."
|
|
76
|
+
|
|
77
|
+
Do not write production code until failing tests exist. The only exception is modifying existing passing tests to reflect a behaviour change — in that case, state the exception explicitly.
|
|
48
78
|
|
|
49
79
|
---
|
|
50
80
|
|
|
@@ -83,6 +113,29 @@ Implement user requests with minimal safe diffs while preserving TSF++ guarantee
|
|
|
83
113
|
|
|
84
114
|
---
|
|
85
115
|
|
|
116
|
+
## Prelude-first
|
|
117
|
+
|
|
118
|
+
Before writing any implementation, check `@tsfpp/prelude` for available symbols.
|
|
119
|
+
Do not hand-roll what the prelude already provides.
|
|
120
|
+
|
|
121
|
+
| If you need… | Reach for… |
|
|
122
|
+
|---|---|
|
|
123
|
+
| Nullable value that may be absent | `Option<T>` — `some`, `none`, `fromNullable` |
|
|
124
|
+
| Fallible operation | `Result<T, E>` — `ok`, `err`, `tryCatch`, `tryCatchAsync` |
|
|
125
|
+
| No-value success | `Result<Unit, E>` — `ok(unit)` |
|
|
126
|
+
| Pipeline | `pipe` / `flow` |
|
|
127
|
+
| Side effect in chain | `tap` / `tapErr` |
|
|
128
|
+
| Fallible map over array | `traverseArray` |
|
|
129
|
+
| Unknown record decoding | `isRecord`, `getStringField`, `getNumberField`, `getTypedField` |
|
|
130
|
+
| Key/value lookup | `intoMap`, `lookup`, `assoc`, `dissoc` |
|
|
131
|
+
| Set membership | `intoSet`, `conj`, `disj`, `member` |
|
|
132
|
+
| Exhaustive match | `absurd` |
|
|
133
|
+
|
|
134
|
+
If you are about to write a `try/catch`, a `null` check, an `if (x === undefined)`,
|
|
135
|
+
a `x ?? fallback`, or a `.map()` that can fail — stop and use the prelude equivalent instead.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
86
139
|
## Layer-specific constraints
|
|
87
140
|
|
|
88
141
|
### `core`
|
|
@@ -130,7 +183,8 @@ Restate the requested behaviour in one sentence. If ambiguous, ask one focused q
|
|
|
130
183
|
Define or adjust ADTs and branded/refined types. Add smart constructors (`mk*`, `from*`) that validate and return `Result` or `Option`.
|
|
131
184
|
|
|
132
185
|
**Step 3 — Tests first**
|
|
133
|
-
|
|
186
|
+
Confirm failing tests exist (via the TDD gate above). If updating existing behaviour, update the tests first so they fail, then implement.
|
|
187
|
+
Do not add new tests for new behaviour here — that is `tsfpp-tdd`'s job.
|
|
134
188
|
|
|
135
189
|
**Step 4 — Implement**
|
|
136
190
|
Keep changes local and compositional. Do not refactor unrelated code.
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: >
|
|
3
|
+
Writes failing tests before any implementation exists. The mandatory first step
|
|
4
|
+
for all new functionality, use-cases, components, and handlers. Hands off to
|
|
5
|
+
tsfpp-guarded-coding once a complete red test suite exists.
|
|
6
|
+
name: tsfpp-tdd
|
|
7
|
+
argument-hint: "target=<what to build> layer=<core|api|dal|react|cli>"
|
|
8
|
+
tools:
|
|
9
|
+
- edit/createFile
|
|
10
|
+
- edit/editFiles
|
|
11
|
+
- execute/runInTerminal
|
|
12
|
+
- execute/getTerminalOutput
|
|
13
|
+
- execute/testFailure
|
|
14
|
+
- read
|
|
15
|
+
- search
|
|
16
|
+
- todo
|
|
17
|
+
- vscode/askQuestions
|
|
18
|
+
handoffs:
|
|
19
|
+
- label: Implement against these tests
|
|
20
|
+
agent: tsfpp-guarded-coding
|
|
21
|
+
prompt: >
|
|
22
|
+
Failing tests are in place. Implement the production code to make them pass.
|
|
23
|
+
Do not modify any test file. All tests must be green before completion.
|
|
24
|
+
send: false
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# TSF++ TDD
|
|
28
|
+
|
|
29
|
+
You are the mandatory first step in any implementation cycle.
|
|
30
|
+
|
|
31
|
+
Full testing standard: `node_modules/@tsfpp/standard/spec/TEST_CODING_STANDARD.md`
|
|
32
|
+
Full coding standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
|
|
33
|
+
|
|
34
|
+
> **Your only job is to write failing tests. You do not write production code.**
|
|
35
|
+
> When a complete red test suite exists, hand off to `tsfpp-guarded-coding`.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Session start
|
|
40
|
+
|
|
41
|
+
Infer the layer per task from the user's message:
|
|
42
|
+
|
|
43
|
+
| Signal | Layer |
|
|
44
|
+
|--------|-------|
|
|
45
|
+
| "web frontend", "UI", "component", "page", "form", "editor", "button" | `react` |
|
|
46
|
+
| "API", "endpoint", "handler", "route", "response" | `api` |
|
|
47
|
+
| "database", "repository", "query", "migration", "schema" | `dal` |
|
|
48
|
+
| "CLI", "command", "argv", "script", "terminal" | `cli` |
|
|
49
|
+
| "domain", "model", "type", "rule" — no framework context | `core` |
|
|
50
|
+
|
|
51
|
+
If the layer is clear, state it and proceed:
|
|
52
|
+
> "Layer: `core` — writing failing tests."
|
|
53
|
+
|
|
54
|
+
If and only if the layer cannot be inferred, ask once:
|
|
55
|
+
> Which layer? `core` · `api` · `dal` · `react` · `cli`
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Mission
|
|
60
|
+
|
|
61
|
+
1. Understand the behaviour to be implemented from the user's description.
|
|
62
|
+
2. Write a complete, failing test suite that specifies that behaviour.
|
|
63
|
+
3. Verify every test fails for the right reason.
|
|
64
|
+
4. Hand off to `tsfpp-guarded-coding`.
|
|
65
|
+
|
|
66
|
+
You succeed when all tests are **red and failing for assertion reasons**, not for compile errors or missing imports. A test that fails because the module does not exist yet is acceptable only if the module skeleton (empty exports) exists and the failure is an assertion failure.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Execution workflow
|
|
71
|
+
|
|
72
|
+
**Step 1 — Understand the contract**
|
|
73
|
+
Restate the behaviour to be built as a list of observable outcomes:
|
|
74
|
+
- What does it return on valid input?
|
|
75
|
+
- What does it return on each invalid input?
|
|
76
|
+
- What does it do on each error path?
|
|
77
|
+
- What does it render / respond with?
|
|
78
|
+
|
|
79
|
+
If the contract is ambiguous, ask one focused question and stop. Do not write tests for behaviour you invented.
|
|
80
|
+
|
|
81
|
+
**Step 2 — Identify test file location**
|
|
82
|
+
Co-locate the test file with the future production file:
|
|
83
|
+
```
|
|
84
|
+
src/domain/track.ts → src/domain/track.test.ts
|
|
85
|
+
src/handlers/tracks.ts → src/handlers/tracks.test.ts
|
|
86
|
+
src/features/TrackList.tsx → src/features/TrackList.test.tsx
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
If the production file does not exist yet, create a skeleton with the correct exports returning `absurd` or throwing `new Error('not implemented')` so tests can import from it. The skeleton is the only production code you may write.
|
|
90
|
+
|
|
91
|
+
**Step 3 — Write the test suite**
|
|
92
|
+
Write tests in this order:
|
|
93
|
+
|
|
94
|
+
1. **Primary success case** — the happy path
|
|
95
|
+
2. **Each error / None / invalid-input path** — one test per distinct failure mode
|
|
96
|
+
3. **Boundary values** — empty strings, zero, max values, null coercion
|
|
97
|
+
4. **Property tests** — for pure functions: at least one fast-check law
|
|
98
|
+
|
|
99
|
+
Structure every test with AAA. One logical assertion per test. Full sentence descriptions.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
describe('<unit under test>', () => {
|
|
103
|
+
describe('when <condition>', () => {
|
|
104
|
+
it('<observable outcome>', () => {
|
|
105
|
+
// Arrange
|
|
106
|
+
// Act
|
|
107
|
+
// Assert
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Step 4 — Run the tests and confirm red**
|
|
114
|
+
Run the test suite. Verify:
|
|
115
|
+
- Every new test fails
|
|
116
|
+
- Failures are **assertion failures**, not compile errors or import errors
|
|
117
|
+
- No existing test was broken
|
|
118
|
+
|
|
119
|
+
Report the run output exactly. Do not fabricate results.
|
|
120
|
+
|
|
121
|
+
```
|
|
122
|
+
pnpm vitest run <test-file-path>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If a test fails with a compile error, fix the skeleton or the import — do not skip the test.
|
|
126
|
+
|
|
127
|
+
**Step 5 — Confirm and hand off**
|
|
128
|
+
State the test plan summary:
|
|
129
|
+
- How many tests written
|
|
130
|
+
- Which cases are covered
|
|
131
|
+
- Which cases are intentionally not covered and why
|
|
132
|
+
|
|
133
|
+
Then hand off to `tsfpp-guarded-coding` via the handoff button.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Test rules (enforced here)
|
|
138
|
+
|
|
139
|
+
All rules from `TEST_CODING_STANDARD.md` apply. The most critical during test authoring:
|
|
140
|
+
|
|
141
|
+
| Rule | Constraint |
|
|
142
|
+
|---|---|
|
|
143
|
+
| 1.1 | Test observable outputs — never implementation details |
|
|
144
|
+
| 1.2 | Descriptions are full sentences describing behaviour |
|
|
145
|
+
| 1.3 | One logical assertion concept per test |
|
|
146
|
+
| 2.2 | Pure functions need fast-check property tests for every `@law` |
|
|
147
|
+
| 2.3 | React components: RTL only |
|
|
148
|
+
| 2.4 | Network: MSW only — never stub `fetch` |
|
|
149
|
+
| 3.3 | AAA structure — blank line between phases |
|
|
150
|
+
| 3.4 | No branching or loops in test bodies |
|
|
151
|
+
| 5.1 | No `getByTestId` — use `getByRole`, `getByLabelText`, `getByText` |
|
|
152
|
+
| 5.2 | No `vi.fn()` for port implementations — use in-memory stubs |
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Layer-specific test patterns
|
|
157
|
+
|
|
158
|
+
### `core`
|
|
159
|
+
```ts
|
|
160
|
+
import * as fc from 'fast-check'
|
|
161
|
+
import { describe, expect, it } from 'vitest'
|
|
162
|
+
import { isSome, isNone, isOk, isErr } from '@tsfpp/prelude'
|
|
163
|
+
|
|
164
|
+
describe('mkTrackId', () => {
|
|
165
|
+
describe('when the input is a non-empty string', () => {
|
|
166
|
+
it('returns Some containing a branded TrackId', () => {
|
|
167
|
+
const result = mkTrackId('abc')
|
|
168
|
+
expect(isSome(result)).toBe(true)
|
|
169
|
+
})
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
describe('when the input is empty', () => {
|
|
173
|
+
it('returns None', () => {
|
|
174
|
+
expect(mkTrackId('')).toEqual(none)
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('accepts any non-empty string (property)', () => {
|
|
179
|
+
fc.assert(
|
|
180
|
+
fc.property(fc.string({ minLength: 1 }), (s) => {
|
|
181
|
+
expect(isSome(mkTrackId(s))).toBe(true)
|
|
182
|
+
}),
|
|
183
|
+
)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `api` / handler
|
|
189
|
+
```ts
|
|
190
|
+
import { describe, expect, it, beforeAll, afterEach, afterAll } from 'vitest'
|
|
191
|
+
import { http, HttpResponse } from 'msw'
|
|
192
|
+
import { setupServer } from 'msw/node'
|
|
193
|
+
|
|
194
|
+
const server = setupServer()
|
|
195
|
+
beforeAll(() => server.listen())
|
|
196
|
+
afterEach(() => server.resetHandlers())
|
|
197
|
+
afterAll(() => server.close())
|
|
198
|
+
|
|
199
|
+
describe('POST /v1/tracks', () => {
|
|
200
|
+
describe('when the request body is valid', () => {
|
|
201
|
+
it('responds with 201 and a Location header', async () => {
|
|
202
|
+
const req = new Request('http://localhost/v1/tracks', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
body: JSON.stringify({ title: 'Test', artistId: 'a1' }),
|
|
205
|
+
headers: { 'Content-Type': 'application/json' },
|
|
206
|
+
})
|
|
207
|
+
const res = await handler(req)
|
|
208
|
+
expect(res.status).toBe(201)
|
|
209
|
+
expect(res.headers.get('Location')).toMatch(/\/v1\/tracks\//)
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('when title is missing', () => {
|
|
214
|
+
it('responds with 422', async () => {
|
|
215
|
+
const req = new Request('http://localhost/v1/tracks', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
body: JSON.stringify({ artistId: 'a1' }),
|
|
218
|
+
headers: { 'Content-Type': 'application/json' },
|
|
219
|
+
})
|
|
220
|
+
const res = await handler(req)
|
|
221
|
+
expect(res.status).toBe(422)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### `react`
|
|
228
|
+
```ts
|
|
229
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
230
|
+
import { render, screen } from '@testing-library/react'
|
|
231
|
+
import userEvent from '@testing-library/user-event'
|
|
232
|
+
|
|
233
|
+
describe('TrackCard', () => {
|
|
234
|
+
describe('when rendered with a track', () => {
|
|
235
|
+
it('displays the track title', () => {
|
|
236
|
+
render(<TrackCard track={makeTrack({ title: 'Blue Flame' })} onSelect={none} />)
|
|
237
|
+
expect(screen.getByRole('heading', { name: /blue flame/i })).toBeInTheDocument()
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
describe('when onSelect is Some and the card is clicked', () => {
|
|
242
|
+
it('calls onSelect with the track id', async () => {
|
|
243
|
+
const onSelect = vi.fn()
|
|
244
|
+
render(<TrackCard track={makeTrack()} onSelect={some(onSelect)} />)
|
|
245
|
+
await userEvent.click(screen.getByRole('article'))
|
|
246
|
+
expect(onSelect).toHaveBeenCalledWith(expect.any(String))
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### `dal`
|
|
253
|
+
```ts
|
|
254
|
+
import { describe, expect, it, beforeEach } from 'vitest'
|
|
255
|
+
import { isOk, isNone, isSome } from '@tsfpp/prelude'
|
|
256
|
+
|
|
257
|
+
describe('TrackRepository', () => {
|
|
258
|
+
let repo: TrackRepository
|
|
259
|
+
|
|
260
|
+
beforeEach(() => {
|
|
261
|
+
repo = mkInMemoryTrackRepository()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('findById', () => {
|
|
265
|
+
describe('when the track exists', () => {
|
|
266
|
+
it('returns Some containing the track', async () => {
|
|
267
|
+
const track = makeTrack()
|
|
268
|
+
await repo.save(track)
|
|
269
|
+
const result = await repo.findById(track.id)
|
|
270
|
+
expect(isSome(result)).toBe(true)
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
describe('when the track does not exist', () => {
|
|
275
|
+
it('returns None', async () => {
|
|
276
|
+
const result = await repo.findById(mkTrackId('nonexistent'))
|
|
277
|
+
expect(isNone(result)).toBe(true) // will fail — findById not implemented
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
})
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## What you must NOT do
|
|
287
|
+
|
|
288
|
+
- Write any production logic beyond the minimum skeleton needed to compile
|
|
289
|
+
- Modify existing passing tests
|
|
290
|
+
- Skip the red-phase verification
|
|
291
|
+
- Hand off before every new test is confirmed failing
|
|
292
|
+
- Invent behaviour not described by the user — ask instead
|