@jaybeeuu/agent-cortex 1.41.0 → 1.42.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.
Files changed (71) hide show
  1. package/.github/workflows/ci.yml +5 -5
  2. package/CHANGELOG.md +60 -0
  3. package/agents/ralph-plan/agent.md +7 -3
  4. package/agents/ralph-plan.agent.md +7 -3
  5. package/claude-extras/scripts/statusline-command.sh +0 -0
  6. package/docs/ideas/ban-content-assertion-tests.md +3 -1
  7. package/docs/ideas/dedicated-review-tests-skill.md +71 -0
  8. package/docs/ideas/unify-the-ticket-and-task-decomposition-workflows.md +71 -0
  9. package/package.json +5 -2
  10. package/plugin.json +1 -1
  11. package/scripts/install-instructions.sh +0 -0
  12. package/scripts/sync-plugin-version.sh +0 -0
  13. package/skills/engineering/grill-with-docs/SKILL.md +3 -2
  14. package/skills/engineering/improve-codebase-architecture/REFERENCE.md +5 -0
  15. package/skills/engineering/improve-codebase-architecture/SKILL.md +2 -1
  16. package/skills/planning/create-task/REFERENCE.md +1 -1
  17. package/skills/planning/create-task/SKILL.md +9 -4
  18. package/skills/planning/create-task/scripts/create-chores.ts +8 -1
  19. package/skills/planning/prd-to-tasks/REFERENCE.md +2 -2
  20. package/skills/planning/prd-to-tasks/SKILL.md +9 -4
  21. package/skills/planning/request-refactor-plan/FORMAT.md +5 -0
  22. package/skills/planning/request-refactor-plan/SKILL.md +3 -2
  23. package/skills/planning/spec-to-tickets/REFERENCE.md +82 -0
  24. package/skills/planning/spec-to-tickets/SKILL.md +143 -0
  25. package/skills/planning/write-a-prd/FORMAT.md +6 -0
  26. package/skills/planning/write-a-prd/SKILL.md +9 -6
  27. package/skills/planning/write-a-ticket/EXAMPLES.md +133 -0
  28. package/skills/planning/write-a-ticket/SKILL.md +77 -59
  29. package/skills/productivity/bd-tool/SKILL.md +27 -2
  30. package/skills/productivity/grill-me/SKILL.md +11 -0
  31. package/skills/review/review-security/scripts/install-gitleaks.sh +0 -0
  32. package/skills/style/style-code/SKILL.md +4 -0
  33. package/skills/style/style-tests/EXAMPLES.md +548 -0
  34. package/skills/style/style-tests/SKILL.md +34 -35
  35. package/skills/workflow/plan/SKILL.md +20 -8
  36. package/skills/workflow/ralph/REFERENCE.md +3 -1
  37. package/skills/workflow/run-pipeline-stage/SKILL.md +6 -1
  38. package/skills/workflow/using-agent-skills/SKILL.md +1 -0
  39. package/test/create-chores.test.mjs +85 -0
  40. package/test/fake-bd.test.mjs +72 -0
  41. package/test/helpers/fake-bd.mjs +87 -0
  42. package/extensions/agent-guardrails/node_modules/.bin/pi +0 -21
  43. package/extensions/agent-guardrails/node_modules/.bin/tsc +0 -21
  44. package/extensions/agent-guardrails/node_modules/.bin/tsserver +0 -21
  45. package/extensions/agent-guardrails/node_modules/.bin/tsx +0 -21
  46. package/extensions/agent-modes/node_modules/.bin/pi +0 -21
  47. package/extensions/agent-modes/node_modules/.bin/tsc +0 -21
  48. package/extensions/agent-modes/node_modules/.bin/tsserver +0 -21
  49. package/extensions/agent-modes/node_modules/.bin/tsx +0 -21
  50. package/extensions/skill-stats/node_modules/.bin/pi +0 -21
  51. package/extensions/skill-stats/node_modules/.bin/tsc +0 -21
  52. package/extensions/skill-stats/node_modules/.bin/tsserver +0 -21
  53. package/extensions/skill-stats/node_modules/.bin/tsx +0 -21
  54. package/extensions/skill-stats/pnpm-lock.yaml +0 -329
  55. package/extensions/subagent/node_modules/.bin/pi +0 -21
  56. package/extensions/subagent/node_modules/.bin/pi-ai +0 -21
  57. package/extensions/subagent/node_modules/.bin/tsc +0 -21
  58. package/extensions/subagent/node_modules/.bin/tsserver +0 -21
  59. package/extensions/subagent/node_modules/.bin/tsx +0 -21
  60. package/skills/planning/create-task/scripts/node_modules/.bin/tsc +0 -21
  61. package/skills/planning/create-task/scripts/node_modules/.bin/tsserver +0 -21
  62. package/skills/planning/create-task/scripts/node_modules/.bin/tsx +0 -21
  63. package/skills/planning/create-task/scripts/pnpm-lock.yaml +0 -342
  64. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsc +0 -21
  65. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsserver +0 -21
  66. package/skills/productivity/validate-agent-dir/scripts/node_modules/.bin/tsx +0 -21
  67. package/skills/productivity/validate-agent-dir/scripts/pnpm-lock.yaml +0 -329
  68. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsc +0 -21
  69. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsserver +0 -21
  70. package/skills/workflow/run-pipeline-stage/scripts/node_modules/.bin/tsx +0 -21
  71. package/skills/workflow/run-pipeline-stage/scripts/pnpm-lock.yaml +0 -342
@@ -43,13 +43,13 @@ For new tasks with pipeline expansion:
43
43
 
44
44
  ```bash
45
45
  # use the create-task skill, or manually:
46
- bd create "<title>" -d "<description>" -p <0-3>
46
+ bd create "<title>" -d "<description>" -p <0-3> --validate
47
47
  ```
48
48
 
49
49
  For parent-child relationships (create with `--parent`):
50
50
 
51
51
  ```bash
52
- bd create "<title>" --parent <parent-id>
52
+ bd create "<title>" --parent <parent-id> --validate
53
53
  ```
54
54
 
55
55
  For blocking dependencies between beads:
@@ -58,6 +58,31 @@ For blocking dependencies between beads:
58
58
  bd dep add <id> <blocked-by-id> --type blocks
59
59
  ```
60
60
 
61
+ ### Required sections
62
+
63
+ Create substantive beads with `--validate` so a section-less bead is rejected before it is
64
+ recorded. `bd create --validate` requires these sections per type (matching is case-insensitive);
65
+ `bd lint` checks the same contract on existing beads and exits non-zero when any are missing:
66
+
67
+ | Type | Required sections |
68
+ |---|---|
69
+ | `task`, `feature` | `## Acceptance Criteria` |
70
+ | `epic` | `## Success Criteria` |
71
+ | `bug` | `## Steps to Reproduce`, `## Acceptance Criteria` |
72
+ | `chore` | none |
73
+
74
+ Plan what you write: the type's required sections are the target of the planning interview
75
+ (`write-a-prd`, `request-refactor-plan`, `plan`, `grill-me`, …). Do not invent section content —
76
+ surface an assumption and confirm it with the human. Chore beads carry no required sections; their
77
+ description should reference the parent/requirements bead rather than restating criteria.
78
+
79
+ Review after creating — lint the beads you just made and fix any flagged ones:
80
+
81
+ ```bash
82
+ bd lint <new-id>... # scoped to your beads; exits non-zero on missing sections
83
+ bd lint # the whole open backlog (may flag pre-existing beads)
84
+ ```
85
+
61
86
  ## Completing work
62
87
 
63
88
  ```bash
@@ -8,3 +8,14 @@ Interview me relentlessly about every aspect of this plan until we reach a share
8
8
  Ask the questions one at a time.
9
9
 
10
10
  If a question can be answered by exploring the codebase, explore the codebase instead.
11
+
12
+ ## Completion: the bead's required sections
13
+
14
+ When this interview feeds a bead, the bead type's required sections are the target — their
15
+ contract is in the `bd-tool` skill's "Required sections" table (task/feature → `## Acceptance
16
+ Criteria`; epic → `## Success Criteria`; bug → Steps to Reproduce + Acceptance Criteria). The
17
+ interview is complete only when there is enough agreed detail to fill every required section.
18
+
19
+ Never invent section content. If you must infer an answer, state it as an assumption and get the
20
+ human to confirm it before it becomes bead content. Leave unresolved branches open — an open
21
+ question is better than a fabricated answer.
@@ -23,6 +23,7 @@ description: Defines this project's coding style, conventions, and architectural
23
23
  - **Clarity over cleverness.** Code is read many more times than it is written. Clever code optimises for the writing moment; clear code optimises for every subsequent reading.
24
24
  - **Validate at system edges, trust the type system within.** Inbound data from API ingress, deserialisation, or inter-service communication is untrusted until validated. Within application boundaries, trust the type system to enforce correct calls — don't re-validate at every internal module boundary.
25
25
  - **Prefer result objects for domain errors; reserve exceptions for emergencies.** Expected failures — validation errors, not-found, conflict — belong in a typed result the caller can handle. Thrown `Error` should signal something genuinely broken: a programming mistake, an invariant violation, or unrecoverable state.
26
+ - **A function either returns a value or changes state — never both.** This is command/query separation at function scale: a command mutates (writes, I/O, logging) and returns nothing — `void` or `Promise<void>`; a query is pure and returns a value. Returning data *about* a mutation — the saved row, a count, the previous value, a success boolean — makes one function two operations wearing one name. Two carve-outs: a command that can fail may return a typed result (`Result<T, E>`), which reports an outcome rather than data about the world and so composes with the result-object guidance above; and a fluent builder whose whole purpose is construction may return `this` to chain. Neither licenses a passthrough return — `arr.push()`-style returns stay banned.
26
27
  - **Deep modules beat shallow ones.** A tight public API with complexity hidden behind it is easier to use, test, and change than a flat sprawl of interdependent helpers.
27
28
  - **Be a class when you are stateful.** When a module must hold mutable state and expose methods that transform or manage it, model it as a class with public methods and real private fields (`#name`) — not a factory function returning a closure of methods. Native `#private` is enforced at runtime; TypeScript's `private` modifier is compile-time only and erases away. This is orthogonal to the functional guidance elsewhere: small components, explicit props, and hooks still govern UI and pure transforms, but a stateful object is a class, and pretending otherwise is a functional idiom wrapped around an object.
28
29
  - **Abstraction must earn its keep, but local helpers are free.** An abstraction extracted into a shared module before its shape is proven nearly always guesses wrong. But extracting a local helper to clarify domain logic is always worthwhile — even at first use. The goal is readable code, not zero duplication.
@@ -49,6 +50,7 @@ description: Defines this project's coding style, conventions, and architectural
49
50
  - **Broadening scope.** A change that fixes a bug and also renames three modules and extracts an abstraction is three changes. Split it.
50
51
  - **Skipping edge validation.** Inbound data at API ingress, deserialisation, or inter-service boundaries is untrusted until validated. Within the application, trust the type system. A missed edge validation silently poisons downstream callers.
51
52
  - **Unstructured errors.** A thrown string or `{ message }` object leaves the caller guessing what failed and why. Prefer typed result objects for expected domain failures — use thrown `Error` only when something has genuinely gone wrong (programming error, invariant violation, unrecoverable state).
53
+ - **A mutator that returns.** A function that changes state *and* returns something about that change — a count, the mutated object, the previous value — is two operations wearing one name. Split it into a command that mutates and a query that reads.
52
54
  - **Large modules with mixed concerns.** A file doing data loading, state orchestration, and rendering is three modules waiting to be extracted.
53
55
  - **Premature cross-module abstraction.** Extracting a shared utility into a common module before the pattern has proven itself across multiple callers nearly always guesses the wrong shape. Local helpers that clarify intent are fine at first use — keep them within the module boundary until the pattern repeats.
54
56
 
@@ -64,6 +66,7 @@ description: Defines this project's coding style, conventions, and architectural
64
66
  | "I'll throw an Error for this validation failure" | Expected domain failures should be typed results the caller can handle. Reserve thrown `Error` for things that are truly broken. |
65
67
  | "I'll extract this into a shared util — it might be useful later" | Extract locally first if it clarifies the code. Promote to shared only when at least a third real caller emerges. Guessing the shape before then wastes time and creates coupling. |
66
68
  | "A factory closure is more functional/pure" | State is state — a class names it. A closure over mutable state is an object with extra steps: the state is still there, the API is less discoverable, and nothing is gained. Use `#private` fields and public methods. |
69
+ | "It's convenient to return the saved row / new count from the write" | That return turns a command into a query mid-flight: the caller now depends on data the mutation happens to produce. Return a typed result for the outcome, and let a separate query read the state if the caller needs it. |
67
70
 
68
71
  ## Cross-skill references
69
72
 
@@ -93,5 +96,6 @@ scoping) and the stateful-object-vs-factory pair.
93
96
  - [ ] Modules are split by ownership, not just for reshuffling code
94
97
  - [ ] State transitions are explicit and encapsulated behind a clear API
95
98
  - [ ] Stateful modules with methods are classes using `#private` fields, not factory closures
99
+ - [ ] Functions are commands (mutate, return nothing) or queries (pure, return a value) — never both
96
100
  - [ ] Tests and docs updated where behaviour changed
97
101
  - [ ] Staged changes scanned with `review-security` before committing
@@ -0,0 +1,548 @@
1
+ # Examples
2
+
3
+ `❌ Instead` / `✅ Write` pairs for the rules in `SKILL.md`. Each pair is a pattern seen in
4
+ real test suites; the left column is what to replace, the right is what to replace it with.
5
+
6
+ ## Behaviour over implementation
7
+
8
+ Assert the observable outcome. If a refactor that preserves behaviour breaks the test, the test
9
+ was wrong.
10
+
11
+ ```ts
12
+ // ❌ Instead — spies on an internal collaborator; proves only that it was wired up
13
+ const spy = vi.spyOn(taxCalculator, "compute");
14
+ checkout(cart);
15
+ expect(spy).toHaveBeenCalledWith(cart);
16
+
17
+ // ✅ Write — asserts what the caller observes
18
+ expect(checkout(cart).total).toBe(90);
19
+ ```
20
+
21
+ ```ts
22
+ // ❌ Instead — recomputes the expected value with the implementation's own logic,
23
+ // so a bug in that logic passes the test
24
+ const expected = subtotal * (1 - discountRate);
25
+ expect(computeTotal(cart)).toBe(expected);
26
+
27
+ // ✅ Write — a known-good case with a hard-coded result
28
+ expect(computeTotal(cartOf100With10PercentDiscount)).toBe(90);
29
+ ```
30
+
31
+ ```ts
32
+ // ❌ Instead — exports an internal helper so the test can reach it
33
+ export const __testOnlyNormalise = (s: string) => s.trim().toLowerCase();
34
+
35
+ // ✅ Write — drive the behaviour through the public entry point that uses it
36
+ expect(parseEmail(" Ada@Example.COM ")).toStrictEqual({ address: "ada@example.com" });
37
+ ```
38
+
39
+ ## Test data: vital visible, incidental hidden
40
+
41
+ The values that drive the case belong in the test body. Everything else takes an obvious
42
+ default in a factory.
43
+
44
+ ```ts
45
+ // ❌ Instead — the value under test is hidden in a top-of-file constant.
46
+ // A reader cannot see which field makes this case "admin".
47
+ const ADMIN_USER: User = { id: "u1", name: "Ada", isAdmin: true, locale: "en-GB" };
48
+
49
+ it("grants admin access", () => {
50
+ expect(canEdit(ADMIN_USER)).toBe(true);
51
+ });
52
+
53
+ // ✅ Write — the driving value is on the assertion's line
54
+ it("grants admin access", () => {
55
+ expect(canEdit(createUser({ isAdmin: true }))).toBe(true);
56
+ });
57
+ ```
58
+
59
+ ```ts
60
+ // ❌ Instead — one module-scope object, mutated and reused across tests.
61
+ // These tests now depend on each other and on their order.
62
+ const user: User = { id: "u1", name: "Ada", isAdmin: false };
63
+
64
+ it("denies guest access", () => {
65
+ expect(canEdit(user)).toBe(false);
66
+ });
67
+
68
+ it("grants admin access", () => {
69
+ user.isAdmin = true;
70
+ expect(canEdit(user)).toBe(true);
71
+ }); // order-dependent: the first test fails if this one runs first
72
+
73
+ // ✅ Write — fresh data per test, produced by a factory
74
+ const createUser = (overrides: Partial<User> = {}): User => ({
75
+ id: "u1",
76
+ name: "Ada",
77
+ isAdmin: false,
78
+ ...overrides,
79
+ });
80
+
81
+ it("grants admin access", () => {
82
+ expect(canEdit(createUser({ isAdmin: true }))).toBe(true);
83
+ });
84
+
85
+ it("denies guest access", () => {
86
+ expect(canEdit(createUser())).toBe(false);
87
+ });
88
+ ```
89
+
90
+ ```ts
91
+ // ❌ Instead — the full literal is re-spelled in every test, obscuring the one field that changes
92
+ it("grants admin access", () => {
93
+ expect(canEdit({ id: "u1", name: "Ada", isAdmin: true, locale: "en-GB" })).toBe(true);
94
+ });
95
+
96
+ // ✅ Write — a factory defaults the incidental data, overrides carry the case
97
+ interface SomeThing {
98
+ prop: number;
99
+ otherProp: string;
100
+ }
101
+
102
+ // Colocated with the tests, named for what it builds. Partial is enough for a flat
103
+ // type; reach for a deep-partial only when the type nests.
104
+ const createSomething = (overrides: Partial<SomeThing> = {}): SomeThing => ({
105
+ prop: 0,
106
+ otherProp: "{other-prop}",
107
+ ...overrides,
108
+ });
109
+
110
+ it("overrides only the value under test", () => {
111
+ const fixture = createSomething({ prop: 1 });
112
+
113
+ expect(fixture).toStrictEqual({ prop: 1, otherProp: "{other-prop}" });
114
+ });
115
+ ```
116
+
117
+ ## Mocking: boundaries, not internals
118
+
119
+ Mock the genuine external edge — network, clock, third-party SDK. Do not mock a collaborator
120
+ inside the application; a real or in-memory one is usually cheaper and never lies.
121
+
122
+ ```ts
123
+ // ❌ Instead — the mock replaces an internal collaborator, so the test
124
+ // proves nothing about the real interaction
125
+ vi.mock("../repository/userRepository");
126
+ await saveUser(user);
127
+ expect(userRepository.save).toHaveBeenCalledWith(user);
128
+
129
+ // ✅ Write — a real in-memory implementation at the genuine boundary
130
+ const repository = new InMemoryUserRepository();
131
+ const service = new UserService(repository);
132
+
133
+ await service.save(user);
134
+
135
+ expect(await repository.get(user.id)).toStrictEqual(user);
136
+ ```
137
+
138
+ ```ts
139
+ // ❌ Instead — the test asserts the stub it just configured, not the mapping
140
+ vi.spyOn(http, "get").mockResolvedValue({ status: 200, body: apiUserResponse });
141
+ expect(await fetchUser("u1")).toStrictEqual(apiUserResponse);
142
+
143
+ // ✅ Write — stub the edge, assert the domain mapping the client performs
144
+ vi.spyOn(http, "get").mockResolvedValue({ status: 200, body: apiUserResponse });
145
+ expect(await fetchUser("u1")).toStrictEqual({ id: "u1", displayName: "Ada" });
146
+ ```
147
+
148
+ Mock as little as possible: if a real collaborator is fast, deterministic, and side-effect free,
149
+ use it.
150
+
151
+ ## Integration and e2e
152
+
153
+ These layers prove what units cannot: that your code speaks correctly to a real collaborator,
154
+ and that the assembled system works. They are the layers agents skip, and where flakiness is
155
+ born. See `SKILL.md` for the principles that govern them.
156
+
157
+ ### What each layer proves
158
+
159
+ A unit proves logic in isolation. An integration test proves your code against a real
160
+ collaborator across **one seam**. An e2e test proves the assembled system on a user-visible
161
+ journey. Pick the lowest layer that can prove the behaviour.
162
+
163
+ ```ts
164
+ // ❌ Instead — an "integration" test that mocks the collaborator it exists to integrate with
165
+ vi.mock("../db/pool");
166
+ await saveOrder(order);
167
+ expect(pool.query).toHaveBeenCalledWith(expect.stringContaining("INSERT INTO orders"));
168
+
169
+ // ✅ Write — exercises the real collaborator across the seam
170
+ await withPostgres(async (db) => {
171
+ const repository = new OrderRepository(db);
172
+ await repository.save(order);
173
+
174
+ expect(await repository.get(order.id)).toMatchObject({ id: order.id });
175
+ });
176
+ ```
177
+
178
+ ```ts
179
+ // ❌ Instead — a unit-tested rule re-proved at e2e: slow, and it proves nothing new
180
+ it("applies a 10% discount to a £100 order", async () => {
181
+ await addToCart(page, productPricedAt(100));
182
+ await openCart(page);
183
+
184
+ expect(await page.textContent(".total")).toBe("£90.00");
185
+ });
186
+
187
+ // ✅ Write — e2e proves the journey and the wiring; the rule stays a unit test
188
+ it("lets a signed-in customer complete checkout", async () => {
189
+ await signIn(page, customer);
190
+ await addToCart(page, product);
191
+ await checkout(page);
192
+
193
+ expect(await page.textContent(".confirmation")).toContain("Thank you");
194
+ });
195
+ ```
196
+
197
+ ### Real collaborators, not fakes
198
+
199
+ Run the genuine engine — Postgres, Redis, a queue, object storage — through TestContainers or
200
+ LocalStack. A fake drifts from the real contract and reintroduces exactly the bug the
201
+ integration test was written to catch. Fake only what you genuinely cannot run locally, and
202
+ use a contract double when you do.
203
+
204
+ ```ts
205
+ // ❌ Instead — a hand-rolled fake that does not share the real broker's contract
206
+ const fakeQueue = { send: vi.fn() };
207
+ await publishOrder(queueAdapter(fakeQueue));
208
+ expect(fakeQueue.send).toHaveBeenCalled();
209
+
210
+ // ✅ Write — the real broker in a container
211
+ const queue = await startQueueContainer();
212
+ await publishOrder(queueAdapter(queue));
213
+
214
+ const [received] = await consumeOne(queue, ORDER_TOPIC);
215
+ expect(received).toMatchObject({ id: order.id });
216
+ ```
217
+
218
+ ```ts
219
+ // ❌ Instead — an ad-hoc stub invented per test; nothing pins it to the provider's API
220
+ vi.spyOn(stripe, "charge").mockResolvedValue({ id: "ch_1" });
221
+
222
+ // ✅ Write — a contract double generated from the provider's schema
223
+ server.use(http.post("/v1/charges", () => HttpResponse.json(stripeChargeFixture)));
224
+
225
+ expect(await client.charge(customer, 100)).toMatchObject({ id: "ch_1" });
226
+ ```
227
+
228
+ ### Isolation at the integration layer
229
+
230
+ State is the enemy here. Give every test its own data and its own schema, namespace, or
231
+ transaction; never share a seeded row or a long-lived database between tests or parallel
232
+ workers. Starting a container is expensive and legitimately belongs in `beforeAll` — data
233
+ does not.
234
+
235
+ ```ts
236
+ // ❌ Instead — a shared seeded row mutated across tests; passes alone, fails in a parallel run
237
+ beforeAll(async () => {
238
+ db = await startPostgresContainer();
239
+ await db.exec(seedSql);
240
+ });
241
+
242
+ it("updates the account", async () => {
243
+ await db.update("accounts", { id: "acc-1" }, { balance: 50 });
244
+ });
245
+
246
+ it("reads the seeded balance", async () => {
247
+ expect(await db.get("accounts", "acc-1")).toMatchObject({ balance: 100 });
248
+ });
249
+
250
+ // ✅ Write — container per suite, data per test
251
+ beforeAll(async () => {
252
+ db = await startPostgresContainer();
253
+ });
254
+
255
+ it("updates the account", async () => {
256
+ const account = await createAccount(db, { balance: 100 });
257
+ await updateBalance(db, account.id, 50);
258
+
259
+ expect(await getAccount(db, account.id)).toMatchObject({ balance: 50 });
260
+ });
261
+
262
+ it("reads the inserted balance", async () => {
263
+ const account = await createAccount(db, { balance: 100 });
264
+
265
+ expect(await getAccount(db, account.id)).toMatchObject({ balance: 100 });
266
+ });
267
+ ```
268
+
269
+ ### Async and eventual consistency
270
+
271
+ Integration surfaces are asynchronous by nature. Await the observable signal with a bounded
272
+ deadline; a fixed sleep is both slow and a race it may still lose.
273
+
274
+ ```ts
275
+ // ❌ Instead — sleeps past the queue, then races it anyway
276
+ await publishOrder(queue, order);
277
+ await new Promise((resolve) => setTimeout(resolve, 500));
278
+
279
+ expect(await findProjection(db, order.id)).toBeDefined();
280
+
281
+ // ✅ Write — poll the observable signal with a bounded deadline
282
+ await publishOrder(queue, order);
283
+ await waitFor(async () => (await findProjection(db, order.id)) !== null, {
284
+ timeout: 5_000,
285
+ description: "order projection",
286
+ });
287
+ ```
288
+
289
+ ```ts
290
+ // ❌ Instead — asserts a step that has not run yet
291
+ it("emits an audit event on save", async () => {
292
+ await saveUser(user);
293
+
294
+ expect(await auditLog.events()).toHaveLength(1);
295
+ });
296
+
297
+ // ✅ Write — await the event, then assert it
298
+ it("emits an audit event on save", async () => {
299
+ await saveUser(user);
300
+ const event = await waitForEvent(auditLog, { type: "user.saved" });
301
+
302
+ expect(event).toMatchObject({ userId: user.id });
303
+ });
304
+ ```
305
+
306
+ ### Determinism at the edges
307
+
308
+ Pin the images, freeze the clock, and seed randomness. A test that pulls `latest` or reads the
309
+ wall clock fails eventually for reasons that have nothing to do with the code.
310
+
311
+ ```ts
312
+ // ❌ Instead — floating image tag and the real clock
313
+ const container = await new GenericContainer("postgres:latest").start();
314
+ const invoice = createInvoice({ issuedAt: new Date() });
315
+
316
+ // ✅ Write — pinned image and an injected clock
317
+ const container = await new GenericContainer("postgres:16.4-alpine").start();
318
+ const clock = new FixedClock("2024-01-01T00:00:00Z");
319
+ const invoice = createInvoice({ issuedAt: clock.now() });
320
+ ```
321
+
322
+ ### What e2e should prove
323
+
324
+ E2E is the most expensive layer, so spend it only on what nothing else can prove: that the
325
+ parts are wired together, migrations ran, configuration loaded, and a user can complete the
326
+ journey. Keep the count small.
327
+
328
+ ```ts
329
+ // ❌ Instead — reaches inside the system to assert call order
330
+ it("checks out", async () => {
331
+ await checkout(page);
332
+
333
+ expect(spyOn(inventoryService, "reserve")).toHaveBeenCalledBefore(
334
+ spyOn(paymentService, "charge"),
335
+ );
336
+ });
337
+
338
+ // ✅ Write — asserts what the journey produced
339
+ it("checks out", async () => {
340
+ await checkout(page);
341
+
342
+ expect(await page.textContent(".confirmation")).toContain("Thank you");
343
+ expect(await api.getOrder(orderRef)).toMatchObject({ state: "confirmed" });
344
+ });
345
+ ```
346
+
347
+ ## Assertions: targeted, never tautological
348
+
349
+ ```ts
350
+ // ❌ Instead — every field asserted; any unrelated field change breaks the test
351
+ expect(created).toStrictEqual({
352
+ id: "u1",
353
+ status: "active",
354
+ createdAt: "2024-01-01T00:00:00.000Z",
355
+ updatedAt: "2024-01-01T00:00:00.000Z",
356
+ version: 3,
357
+ });
358
+
359
+ // ✅ Write — assert the fields that carry the behaviour; use `expect.any` for the rest
360
+ expect(created).toMatchObject({ status: "active", id: expect.any(String) });
361
+ ```
362
+
363
+ ```ts
364
+ // ❌ Instead — the count is implied by the argument assertion
365
+ expect(mockSave).toHaveBeenCalledTimes(1);
366
+ expect(mockSave).toHaveBeenCalledWith(user);
367
+
368
+ // ✅ Write — the argument assertion already proves a single, correct call
369
+ expect(mockSave).toHaveBeenCalledWith(user);
370
+ ```
371
+
372
+ ```ts
373
+ // ❌ Instead — cannot fail; delete the implementation and this stays green
374
+ it("handles the request", async () => {
375
+ const result = await handler(request);
376
+ expect(result).toBeDefined();
377
+ });
378
+
379
+ // ✅ Write — names the observable contract
380
+ it("returns 200 for a valid request", async () => {
381
+ const response = await handler(request);
382
+
383
+ expect(response.status).toBe(200);
384
+ });
385
+ ```
386
+
387
+ ```ts
388
+ // ❌ Instead — no assertion at all; a smoke test that proves only "it did not throw"
389
+ it("processes an order", async () => {
390
+ await processOrder(order);
391
+ });
392
+
393
+ // ✅ Write — assert the effect the call was supposed to have
394
+ it("marks a processed order as fulfilled", async () => {
395
+ await processOrder(order);
396
+
397
+ expect(await orders.get(order.id)).toMatchObject({ state: "fulfilled" });
398
+ });
399
+ ```
400
+
401
+ ## Structure and naming
402
+
403
+ ```ts
404
+ // ❌ Instead — the name describes the implementation, and "works" says nothing
405
+ it("calls send once", ...);
406
+ it("works", ...);
407
+
408
+ // ✅ Write — the name states the behaviour the test proves
409
+ it("sends a confirmation email after a successful checkout", ...);
410
+ it("rejects a checkout with an expired card", ...);
411
+ ```
412
+
413
+ ```ts
414
+ // ❌ Instead — unrelated behaviours in one test: a failure does not localise
415
+ it("handles users", async () => {
416
+ expect(await createUser(valid)).toHaveProperty("id");
417
+ await expect(createUser(invalid)).rejects.toThrow();
418
+ expect(await listUsers()).toHaveLength(1);
419
+ });
420
+
421
+ // ✅ Write — one behaviour per test, so the failure name tells you what broke
422
+ it("assigns an id when creating a user", ...);
423
+ it("rejects a user with no email", ...);
424
+ it("lists every created user", ...);
425
+ ```
426
+
427
+ ```ts
428
+ // ❌ Instead — loops zero times and passes when there is nothing to check
429
+ for (const item of cart.items) {
430
+ expect(item.price).toBeGreaterThan(0);
431
+ }
432
+
433
+ // ✅ Write — assert the collection, so an empty cart fails
434
+ expect(cart.items.map((item) => item.price)).toStrictEqual([10, 20, 30]);
435
+ ```
436
+
437
+ ## Determinism and async
438
+
439
+ ```ts
440
+ // ❌ Instead — a sleep races the work and slows the suite
441
+ await new Promise((resolve) => setTimeout(resolve, 100));
442
+ expect(job.state).toBe("done");
443
+
444
+ // ✅ Write — await the observable completion signal
445
+ await job.completion;
446
+ expect(job.state).toBe("done");
447
+ ```
448
+
449
+ ```ts
450
+ // ❌ Instead — real wall clock and random ids make the expected value unknowable
451
+ const invoice = { id: crypto.randomUUID(), issuedAt: new Date() };
452
+
453
+ // ✅ Write — fixed values in the factory; override only what the case needs
454
+ const createInvoice = (overrides: Partial<Invoice> = {}): Invoice => ({
455
+ id: "invoice-1",
456
+ issuedAt: new Date("2024-01-01T00:00:00Z"),
457
+ ...overrides,
458
+ });
459
+ ```
460
+
461
+ ```ts
462
+ // ❌ Instead — the promise is never returned or awaited, so a rejection
463
+ // becomes an unhandled rejection and the test stays green
464
+ it("rejects an invalid payload", () => {
465
+ expect(handler(bad)).rejects.toThrow();
466
+ });
467
+
468
+ // ✅ Write — await the assertion
469
+ it("rejects an invalid payload", async () => {
470
+ await expect(handler(bad)).rejects.toThrow();
471
+ });
472
+ ```
473
+
474
+ ## Isolation
475
+
476
+ ```ts
477
+ // ❌ Instead — focus and skip markers committed; the suite silently runs less
478
+ it.only("saves the user", ...);
479
+ it.skip("handles a timeout", ...);
480
+
481
+ // ✅ Write — remove the marker; fix the failing test or delete it
482
+ it("saves the user", ...);
483
+ it("handles a timeout", ...);
484
+ ```
485
+
486
+ ```ts
487
+ // ❌ Instead — a shared row mutated by both tests; order now matters
488
+ let row: Row;
489
+
490
+ beforeEach(async () => {
491
+ row = await db.insert(makeRow());
492
+ });
493
+
494
+ it("updates the row", async () => {
495
+ await db.update(row.id, { name: "Ada" });
496
+ });
497
+
498
+ it("reads the row", async () => {
499
+ expect(await db.get(row.id)).toMatchObject({ name: "Original" });
500
+ });
501
+
502
+ // ✅ Write — each test owns its data; no cross-test coupling
503
+ it("updates the row", async () => {
504
+ const row = await db.insert(makeRow());
505
+ await db.update(row.id, { name: "Ada" });
506
+
507
+ expect(await db.get(row.id)).toMatchObject({ name: "Ada" });
508
+ });
509
+
510
+ it("reads the row as inserted", async () => {
511
+ const row = await db.insert(makeRow());
512
+
513
+ expect(await db.get(row.id)).toMatchObject({ name: "Original" });
514
+ });
515
+ ```
516
+
517
+ `before`/`after` hooks are for expensive, non-observable infrastructure — starting a server,
518
+ opening a connection — never for data a test asserts on.
519
+
520
+ ## Content assertions
521
+
522
+ Assert the behaviour the file participates in. A comparison against an expected file is a
523
+ content inventory; it breaks on a comment and proves nothing about the code.
524
+
525
+ ```ts
526
+ // ❌ Instead — catalogues the generated file
527
+ const config = await readFile("generated/nginx.conf", "utf8");
528
+ expect(config).toBe("server { listen 80; }");
529
+
530
+ // ✅ Write — asserts what the generator did with the input
531
+ const config = await generateNginxConfig({ port: 8080 });
532
+
533
+ expect(config).toContain("listen 8080;");
534
+ ```
535
+
536
+ ```ts
537
+ // ❌ Instead — snapshots static markdown and calls it a test
538
+ expect(await renderReadme(project)).toMatchSnapshot();
539
+
540
+ // ✅ Write — asserts the renderer produced the section it was asked for
541
+ const readme = await renderReadme({ ...project, title: "Agent Cortex" });
542
+
543
+ expect(readme).toContain("# Agent Cortex");
544
+ ```
545
+
546
+ A golden-output test is legitimate when the expected output is a **product of the behaviour
547
+ under test** — a parser's AST, a compiler's emitted artifact. It is illegitimate when the file
548
+ is static input the test simply re-reads.