@webpresso/opencode-plugin 3.1.12 → 3.1.13

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,523 @@
1
+ # Full reference moved from SKILL.md
2
+
3
+ # Testing Philosophy
4
+
5
+ `/test-driven-development` is folded into this skill. For behavior changes,
6
+ write or strengthen the failing proof first, watch it fail for the right reason,
7
+ implement the smallest code that passes, and refactor only while the proof stays
8
+ green.
9
+
10
+ ## Core Philosophy
11
+
12
+ **Integration tests give confidence. Unit tests give speed. Bullshit tests give false confidence.**
13
+
14
+ ### The Iron Laws
15
+
16
+ 1. **NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST** (TDD enforced)
17
+ 2. **E2E tests NEVER call internal APIs** - must go through HTTP/browser
18
+ 3. **85%+ mutation score** for new logic code
19
+ 4. **Test strategy: Integration-first (Trophy), Unit-scaled (Pyramid)**
20
+ - Start with integration tests for confidence
21
+ - Extract unit tests for speed
22
+ - Target: ~70% unit, ~15% integration, ~5% worker, ~10% E2E
23
+
24
+ ## What Makes a Test "Bullshit"
25
+
26
+ | Bullshit Pattern | Why It's Bad | Detection |
27
+ | ------------------------------- | ------------------------------------------ | -------------------------------- |
28
+ | **Mocking the database** | Tests pass even if SQL is completely wrong | `vi.mock('../database')` |
29
+ | **Mocking business logic** | Never tests actual logic, just mocks | `vi.mock('../services')` |
30
+ | **Mocking framework internals** | Real middleware/validation never runs | Mocking Hono context |
31
+ | **Testing implementation** | Breaks on refactoring, tests nothing real | `expect(spy).toHaveBeenCalled()` |
32
+ | **Tautological assertions** | Tests that `true === true` | `expect(result).toBeTruthy()` |
33
+ | **Over-mocking** (>5 mocks) | Integration in disguise, slower | `>5 vi.mock()` calls |
34
+
35
+ **If your tests pass but production fails, you have bullshit tests.**
36
+
37
+ ## Unit Tests vs E2E Tests: What They Actually Test
38
+
39
+ **Critical Distinction**: Unit tests verify **implementation**, E2E tests verify **user experience**.
40
+
41
+ ### Unit Tests Miss UX Issues
42
+
43
+ | What Unit Tests Check | What They Miss | What E2E Tests Catch |
44
+ | --------------------------------------- | ---------------------------------------------------------------- | ----------------------------- |
45
+ | "Does function throw on invalid input?" | Can users still accomplish their task when some data is invalid? | Graceful degradation patterns |
46
+ | "Does function return correct type?" | Is the error message helpful to users? | User-facing error clarity |
47
+ | "Does function handle null?" | Can users recover from errors? | Error recovery workflows |
48
+ | 100% valid or 100% invalid scenarios | Mixed scenarios (50% valid, 50% invalid) | Real-world data mixtures |
49
+
50
+ ### Red Flags: Unit Tests Testing Wrong Thing
51
+
52
+ **Warning signs your unit tests might miss UX issues:**
53
+
54
+ 1. **`expect(...).rejects.toThrow()` without graceful degradation test**
55
+
56
+ ```typescript
57
+ // DANGER: Tests that function throws, not that users can recover
58
+ await expect(service.listItems()).rejects.toThrow("Invalid item");
59
+
60
+ // BETTER: Test graceful degradation
61
+ const items = await service.listItems();
62
+ const validItems = items.filter((i) => !i.malformed);
63
+ expect(validItems.length).toBeGreaterThan(0); // Can still see valid items
64
+ ```
65
+
66
+ 2. **Testing 100% success and 100% failure, but not 50% success**
67
+
68
+ ```typescript
69
+ // INCOMPLETE: Only tests extremes
70
+ it('should list all items when all valid', ...)
71
+ it('should throw when all invalid', ...)
72
+
73
+ // COMPLETE: Tests mixed scenarios
74
+ it('should list valid items even when some are invalid', ...)
75
+ ```
76
+
77
+ 3. **No integration/E2E test for "user wants to browse/list" operations**
78
+
79
+ ```typescript
80
+ // MISSING: No test for user browsing with mixed data
81
+ // Unit tests only: listItems() with all valid, listItems() with all invalid
82
+
83
+ // ADD: E2E test for browsing
84
+ it("should show all valid items in list even if some are corrupted", async () => {
85
+ await createValidItem("item-1");
86
+ await createInvalidItem("corrupted");
87
+ await createValidItem("item-2");
88
+
89
+ await actionFn("list", [], {});
90
+
91
+ expect(output).toContain("item-1"); // User sees valid items
92
+ expect(output).toContain("item-2");
93
+ expect(output).toContain("corrupted"); // Indicated as error
94
+ });
95
+ ```
96
+
97
+ ### Case Study: List Command Bug
98
+
99
+ **Scenario**: `listBlueprints()` threw error on first invalid blueprint, preventing users from seeing ANY blueprints.
100
+
101
+ **Why Unit Tests Didn't Catch It**:
102
+
103
+ ```typescript
104
+ // <package>/src/service/BlueprintService.test.ts
105
+ it("should throw readable error for Zod validation failures", async () => {
106
+ // ...invalid blueprint setup...
107
+ await expect(service.listBlueprints()).rejects.toThrow("Invalid frontmatter");
108
+ // Test PASSES — function throws as expected
109
+ // BUT: Users can't see ANY blueprints if ONE is invalid
110
+ });
111
+ ```
112
+
113
+ **How E2E Test Caught It**:
114
+
115
+ ```typescript
116
+ // <package>/src/commands/blueprint/router.integration.test.ts
117
+ it("should handle listing with mixed valid and invalid blueprints", async () => {
118
+ await createBlueprint("valid-1");
119
+ await createBlueprint("valid-2");
120
+ await createInvalidBlueprint("corrupted");
121
+
122
+ await actionFn("list", [], { tui: false });
123
+
124
+ expect(output).toContain("valid-1"); // FAILS — entire list crashed
125
+ // Discovered: "fail fast" design is poor UX for list operations
126
+ });
127
+ ```
128
+
129
+ **Fix**: Return malformed summary instead of throwing, enabling graceful degradation.
130
+
131
+ **Lesson**: Unit test verified code did what was written (throw on error), but E2E test verified code did what users need (show valid items).
132
+
133
+ ### When to Suspect Unit Tests Miss UX
134
+
135
+ **High-risk scenarios for UX issues:**
136
+
137
+ 1. **List/Browse operations** - Users expect to see valid items even if some are invalid
138
+ 2. **Batch operations** - Users expect partial success, not all-or-nothing
139
+ 3. **Error handling** - Users expect helpful messages and recovery paths
140
+ 4. **Multi-step workflows** - Users expect graceful degradation at each step
141
+ 5. **Validation** - Users expect specific field errors, not "invalid input"
142
+
143
+ **Action**: For these scenarios, ALWAYS write E2E tests that verify user experience, not just unit tests that verify implementation.
144
+
145
+ ## Test File Naming Convention (ENFORCED)
146
+
147
+ **Critical**: Test file names MUST match their test type.
148
+
149
+ | Test Type | File Pattern | When to Use |
150
+ | ----------- | ----------------------- | -------------------------------- |
151
+ | Unit | `*.test.ts` | Pure functions, no dependencies |
152
+ | Integration | `*.integration.test.ts` | Real DB, real services, file I/O |
153
+ | Vitest E2E | `*.e2e.ts` | Worker/API end-to-end coverage |
154
+ | Playwright | `*.spec.ts` | Browser, HTTP, full stack |
155
+ | Worker | `*.workers.test.ts` | Cloudflare Workers runtime |
156
+
157
+ **Enforcement**:
158
+
159
+ - GritQL rule: `enforce-integration-test-naming.grit`
160
+ - Files using `PGlite`, `createIntegrationContext`, `seedTestScenario` MUST be `.integration.test.ts`
161
+ - Files using real services without mocking MUST be `.integration.test.ts`
162
+ - Violation = Lint error
163
+
164
+ ## When to Write Each Test Type
165
+
166
+ ### Unit Tests (70% - Fast, <10ms)
167
+
168
+ **When**: Pure functions, validators, formatters, utilities with NO dependencies
169
+
170
+ ```typescript
171
+ // GOOD - pure function, no deps
172
+ export function generateSlug(name: string): string {
173
+ return name.toLowerCase().trim().replace(/\s+/g, "-");
174
+ }
175
+
176
+ describe("generateSlug", () => {
177
+ it("should convert to lowercase", () => {
178
+ expect(generateSlug("MyProject")).toBe("myproject");
179
+ });
180
+ });
181
+ ```
182
+
183
+ **STOP and use Integration if:**
184
+
185
+ - Function queries database → Use `.integration.test.ts`
186
+ - Function uses external service → Mock only that service
187
+ - Function has complex state → Integration test
188
+
189
+ **Example**: Router tests separated by type
190
+
191
+ - `router.test.ts` - Pure function tests (appendOption, buildFakeArgv)
192
+ - `router.integration.test.ts` - Real command execution, real filesystem
193
+
194
+ ### Integration Tests (15% - Confidence, 10-100ms)
195
+
196
+ **When**: Database queries, handlers, services with real dependencies
197
+
198
+ ```typescript
199
+ // GOOD - real database with PGlite
200
+ import { createIntegrationContext } from "~/test-utils";
201
+
202
+ describe("Project Service", () => {
203
+ let ctx: IntegrationContext;
204
+
205
+ beforeAll(async () => {
206
+ ctx = await createIntegrationContext();
207
+ });
208
+
209
+ it("should create project", async () => {
210
+ const project = await createProject(ctx.db, {
211
+ organizationId: ctx.orgId,
212
+ name: "Test Project",
213
+ });
214
+
215
+ // Verify REAL database state
216
+ const saved = await ctx.db.query.projects.findFirst({
217
+ where: eq(schema.projects.id, project.id),
218
+ });
219
+
220
+ expect(saved.name).toBe("Test Project");
221
+ });
222
+ });
223
+ ```
224
+
225
+ **Key**: Use `createIntegrationContext()` or `seedTestScenario()` - never mock the DB.
226
+
227
+ **E2E Requirements for Commands/Features:**
228
+
229
+ When adding a new command or feature, you MUST have:
230
+
231
+ 1. **Routing test** - Verifies command is recognized (integration)
232
+ 2. **E2E test** - Verifies command actually works end-to-end
233
+ - Creates real files/data
234
+ - Verifies actual output
235
+ - Tests with real options
236
+ - No dry-run mode
237
+ 3. **Error handling test** - Verifies helpful error messages
238
+ 4. **Edge case tests** - Invalid input, missing data, etc.
239
+
240
+ **Example**: Blueprint `new` command
241
+
242
+ ```typescript
243
+ // GOOD - Full E2E coverage
244
+ describe("E2E: blueprint new command", () => {
245
+ it("should create actual blueprint file with correct structure", async () => {
246
+ await actionFn("new", ["test-bp"], {});
247
+
248
+ // Verify REAL file was created
249
+ const content = await readFile(blueprintPath, "utf8");
250
+ expect(content).toContain("type: blueprint");
251
+ });
252
+
253
+ it("should reject invalid complexity with helpful error", async () => {
254
+ await actionFn("new", ["test"], { complexity: "INVALID" });
255
+ expect(errorSpy).toHaveBeenCalledWith(
256
+ expect.stringContaining("Must be one of: XS, S, M, L, XL"),
257
+ );
258
+ });
259
+ });
260
+ ```
261
+
262
+ **WRONG - Routing only (insufficient)**
263
+
264
+ ```typescript
265
+ it("should route to new command", async () => {
266
+ await actionFn("new", ["test"], { "dry-run": true });
267
+ expect(exitSpy).not.toHaveBeenCalled(); // Proves nothing!
268
+ });
269
+ ```
270
+
271
+ ### E2E Tests (10% - Journeys, 1-30s)
272
+
273
+ **When**: Critical user journeys, cross-service workflows
274
+
275
+ ```typescript
276
+ // GOOD - real HTTP through browser
277
+ test("complete signup flow", async ({ page }) => {
278
+ await page.goto("http://localhost:3001/signup");
279
+ await page.fill('[name="email"]', "test@example.com");
280
+ await page.click('button[type="submit"]');
281
+ await expect(page.locator("text=Welcome")).toBeVisible();
282
+ });
283
+ ```
284
+
285
+ **NEVER**: Call internal handlers directly
286
+
287
+ ```typescript
288
+ // WRONG - bypasses HTTP stack
289
+ const response = await auth.handler(request);
290
+ ```
291
+
292
+ ## Anti-Patterns (NEVER DO)
293
+
294
+ ### Anti-Pattern 1: Mocking the Database
295
+
296
+ ```typescript
297
+ // BULLSHIT - tests mock, not real code
298
+ vi.mock("../database", () => ({
299
+ getDb: vi.fn(() => ({
300
+ query: { projects: { findMany: vi.fn().mockResolvedValue([]) } },
301
+ })),
302
+ }));
303
+
304
+ it("should list projects", async () => {
305
+ const result = await listProjects("org-1");
306
+ expect(result).toHaveLength(0); // Always passes, tests nothing
307
+ });
308
+ ```
309
+
310
+ **Fix**: Use PGlite integration tests
311
+
312
+ ### Anti-Pattern 2: Testing Implementation Details
313
+
314
+ ```typescript
315
+ // BULLSHIT - breaks on refactoring
316
+ it("should call validateInput", async () => {
317
+ const spy = vi.spyOn(utils, "validateInput");
318
+ await processRequest(mockInput);
319
+ expect(spy).toHaveBeenCalledWith(mockInput);
320
+ });
321
+ ```
322
+
323
+ **Fix**: Test behavior and outcomes
324
+
325
+ ```typescript
326
+ // GOOD - tests actual behavior
327
+ it("should reject invalid input", async () => {
328
+ const result = await processRequest(ctx.db, { name: "" });
329
+ expect(result.success).toBe(false);
330
+ expect(result.error).toContain("name is required");
331
+ });
332
+ ```
333
+
334
+ ### Anti-Pattern 3: Weak Assertions
335
+
336
+ ```typescript
337
+ // WEAK - allows equivalent mutants
338
+ expect(result).toBeTruthy();
339
+ expect(array).toHaveLength(3);
340
+
341
+ // STRONG - kills mutants
342
+ expect(result.error).toBe("Entity not found");
343
+ expect(users.map((u) => u.email)).toEqual(["a@test.com", "b@test.com"]);
344
+ ```
345
+
346
+ ## Mutation Testing (85% Target)
347
+
348
+ ### Patterns to Kill Mutants
349
+
350
+ #### Pattern 1: Export Private Helpers
351
+
352
+ Export **pure utility functions** for direct testing to kill mutants. This is the primary mutation-killing pattern.
353
+
354
+ **When to export**:
355
+
356
+ - Pure functions with no side effects (formatters, parsers, validators)
357
+ - Functions that transform data deterministically
358
+ - Helper functions that are independently meaningful
359
+
360
+ **When NOT to export** (test through the public API instead):
361
+
362
+ - React hook internals — test via `renderHook` from `@testing-library/react`
363
+ - Functions that only make sense in context of their parent (internal state machines)
364
+ - Trivial one-liners that are better tested through their caller
365
+
366
+ ```typescript
367
+ // GOOD — pure utility, export and test directly
368
+ export function formatProjectName(name: string): string {
369
+ return name.trim().toLowerCase().replace(/\s+/g, '-')
370
+ }
371
+
372
+ describe('formatProjectName', () => {
373
+ it('should trim whitespace', () => {
374
+ expect(formatProjectName(' Project ')).toBe('project')
375
+ })
376
+ it('should replace spaces with hyphens', () => {
377
+ expect(formatProjectName('My Project')).toBe('my-project')
378
+ })
379
+ })
380
+
381
+ // GOOD — pure parsing/error handling, export and test in node env
382
+ // (from use-service-health.ts)
383
+ export function parseHealthResponse(body: ReadinessResponse): HealthCheckResult { ... }
384
+ export function handleFetchError(error: unknown): HealthCheckResult { ... }
385
+
386
+ // WRONG — don't export hook internals, test via renderHook
387
+ // export function usePollLogic() { ... } // internal to useServiceHealth
388
+ // Instead: renderHook(() => useServiceHealth({ services, pollInterval: 5000 }))
389
+ ```
390
+
391
+ **Hybrid approach** (best of both worlds): When a hook contains pure logic + React lifecycle:
392
+
393
+ 1. Extract pure functions → export and test directly (node env, fast)
394
+ 2. Test hook lifecycle (polling, cleanup, state) → `renderHook` (DOM env, slower)
395
+ 3. This gives maximum mutation coverage with minimum test complexity
396
+
397
+ #### Pattern 2: Test Exact Outputs
398
+
399
+ ```typescript
400
+ // Weak - mutant survives
401
+ expect(result).toBeTruthy();
402
+
403
+ // Strong - mutant killed
404
+ expect(result.error).toBe("Entity not found");
405
+ expect(result.statusCode).toBe(404);
406
+ ```
407
+
408
+ #### Pattern 3: Test Boundary Conditions
409
+
410
+ ```typescript
411
+ describe("formatCount", () => {
412
+ it("should use singular for 1", () => {
413
+ expect(formatCount(1, "project")).toBe("1 project");
414
+ });
415
+ it("should use plural for 0", () => {
416
+ expect(formatCount(0, "project")).toBe("0 projects");
417
+ });
418
+ it("should use plural for multiple", () => {
419
+ expect(formatCount(5, "project")).toBe("5 projects");
420
+ });
421
+ });
422
+ ```
423
+
424
+ ### Low-Value Survivors (Ignore These)
425
+
426
+ - Array declaration mutations (`[] → ["Stryker"]`)
427
+ - Logging branches (`if (debug) logger.log(...)`)
428
+ - Optional chaining where null is impossible
429
+
430
+ ## Test Quality Checklist
431
+
432
+ Before claiming a test is "done":
433
+
434
+ - [ ] **Does it use real dependencies?** (PGlite for DB, real services)
435
+ - [ ] **Are assertions specific?** (Not just `toBeTruthy()`)
436
+ - [ ] **Does it test behavior, not implementation?** (No spy assertions)
437
+ - [ ] **Mutation score ≥85%?** (Run `just test --mutation --package <pkg>`)
438
+ - [ ] **Does it fail if the code breaks?** (Temporarily break code, verify test fails)
439
+ - [ ] **Is it in the right file?** (`.test.ts` for unit, `.integration.test.ts` for DB)
440
+
441
+ ## Mock Budget
442
+
443
+ | Test Type | Max `vi.mock()` | What to Mock |
444
+ | --------------- | --------------- | --------------------------------------------------- |
445
+ | **Unit** | 0-2 | Only pure external services (Stripe, Email) |
446
+ | **Integration** | 0-3 | Only infrastructure you can't run (real containers) |
447
+ | **E2E** | 0 | Nothing - use real services |
448
+
449
+ **Warning**: >5 mocks → Convert to integration test
450
+
451
+ ## Quick Commands
452
+
453
+ These assume a `just`-based task runner; substitute your own as needed.
454
+
455
+ ```bash
456
+ # Run tests
457
+ # WARNING: Never run full suites during iteration. Use single-file verification.
458
+ just test # All tests (FINAL VERIFICATION ONLY)
459
+ just test <package> # Specific package (FINAL VERIFICATION ONLY)
460
+ just test path/to/test.ts # Single file (ITERATION SAFE)
461
+
462
+ # Mutation testing
463
+ just test --mutation --package <package> # Full mutation test
464
+ just test --mutation-diff # Changed packages only
465
+
466
+ # Audit quality
467
+ just test --mutation --package <package> # Check mutation score for package
468
+ just audit-ratios # Check test pyramid (70/15/5/10)
469
+ just qa # Full quality check
470
+ ```
471
+
472
+ ## Decision Tree
473
+
474
+ ```
475
+ What are you testing?
476
+
477
+ ├─ Pure function (no deps) ──────→ .test.ts (Unit)
478
+
479
+ ├─ Database queries ─────────────→ .integration.test.ts (PGlite)
480
+
481
+ ├─ Service with deps ────────────→ .integration.test.ts
482
+
483
+ ├─ Workers/DO/Cloudflare APIs ───→ .workers.test.ts
484
+
485
+ └─ Full user journey ────────────→ .spec.ts (Playwright)
486
+ ```
487
+
488
+ ## Red Flags - STOP
489
+
490
+ If you find yourself:
491
+
492
+ - Writing `vi.mock('../database')` → **STOP** → Use PGlite
493
+ - Testing that a spy was called → **STOP** → Test behavior
494
+ - Using `toBeTruthy()`, `toBeDefined()`, or `toBeTypeOf()` → **STOP** → Test exact values (type-only assertions like `toBeTypeOf('string')` are equally weak — a mutant changing `'alice'` to `'bob'` still passes)
495
+ - Using `toHaveProperty('key')` without a value → **STOP** → Use `toHaveProperty('key', expectedValue)` or `toEqual()` — without the value arg it's just `toBeDefined()` for a property
496
+ - Using `.length).toBeGreaterThan(0)` → **STOP** → Assert exact count with `toHaveLength(n)` or assert exact contents with `toEqual()`/`toContain()`. "At least one" lets mutants add/remove items freely
497
+ - Using `toMatch(/partial/)` when the full value is known → **STOP** → Use `toBe(fullValue)`. Partial matches let mutants change the unmatched portion
498
+ - Using `toContain(oneItem)` on arrays when all items are known → **STOP** → Use `toEqual([...allItems])`. Subset assertions let mutants modify unchecked items
499
+ - Writing >5 mocks in one file → **STOP** → Use integration test
500
+ - Tests pass but you're not confident → **STOP** → Add integration tests
501
+
502
+ ## Industry Context
503
+
504
+ These testing standards are intentionally ambitious relative to industry norms:
505
+
506
+ - **Google** kills ~87% of mutants globally, uses diff-based mutation testing (not global threshold)
507
+ - **Meta** uses LLM-guided mutation testing for compliance-critical code
508
+ - **Sentry** targets ~62% mutation score on their JS SDK
509
+ - An **85% target for core logic** is top-tier and intentional — it reflects the high-reuse nature of shared packages
510
+ - **SMURF framework** (Google, Oct 2024): Speed, Maintainability, Utilization, Reliability, Fidelity — validates the Trophy→Pyramid hybrid approach where unit tests excel at SMUR and integration/E2E excel at Fidelity
511
+
512
+ ### TDD + AI Agents
513
+
514
+ TDD is MORE important with AI-assisted development, not less. AI agents can generate code that passes superficial checks but misses edge cases. The failing test acts as a specification that constrains the AI's output.
515
+
516
+ > "TDD is a superpower when working with AI agents."
517
+ > — Kent Beck, Pragmatic Engineer podcast, June 2025
518
+
519
+ ## Related Skills
520
+
521
+ - **test-driven-development** - TDD workflow
522
+ - **systematic-debugging** - When tests pass but prod fails
523
+ - **verify** - Verify before claiming done
@@ -0,0 +1,35 @@
1
+ ---
2
+ type: skill
3
+ slug: tph
4
+ title: TPH (Testing Philosophy Helper)
5
+ status: active
6
+ scope: repo
7
+ applies_to: [agents]
8
+ related: [testing-philosophy]
9
+ created: "2026-06-13"
10
+ last_reviewed: "2026-07-08"
11
+ name: tph
12
+ description: "/tph — testing-philosophy review: integration-first tests, real-boundary mocks, behavior-proving assertions."
13
+ ---
14
+
15
+ # TPH (Testing Philosophy Helper)
16
+
17
+ This is the short-name alias for the canonical `testing-philosophy` skill.
18
+
19
+ Use the full testing-philosophy workflow in `../testing-philosophy/SKILL.md` before any mechanical smell scan. The `tph` name means the review workflow: integration-first tests, real-boundary mocks only, and assertions that prove behavior.
20
+
21
+ After the judgment review is complete, run the repo's static smell audit as the final mechanical backstop when available:
22
+
23
+ ```bash
24
+ wp audit test-smells
25
+ ```
26
+
27
+ That audit is deliberately narrower than the skill. It can catch over-mocking and smell violations, but it does not replace the testing-philosophy review.
28
+
29
+ Focus on:
30
+
31
+ - integration-first tests over brittle implementation mocks;
32
+ - E2E tests that go through real user/browser or HTTP boundaries;
33
+ - weak assertions, tautologies, and over-mocking;
34
+ - correct test naming and suite placement;
35
+ - mutation-test-worthy coverage for new logic.