@ketecode/loom 0.1.0 → 0.2.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.
- package/README.md +111 -11
- package/agents/code-reviewer/evals/behavior.json +60 -0
- package/agents/inbox-triager/AGENT.md +98 -0
- package/agents/inbox-triager/evals/triggers.json +16 -0
- package/agents/spec-reviewer/AGENT.md +95 -0
- package/agents/spec-reviewer/evals/behavior.json +57 -0
- package/agents/spec-reviewer/evals/triggers.json +16 -0
- package/agents/task-implementer/AGENT.md +89 -0
- package/agents/task-implementer/evals/behavior.json +47 -0
- package/agents/task-implementer/evals/triggers.json +16 -0
- package/bin/kete-loom.js +46 -6
- package/commands/brainstorm.md +25 -0
- package/commands/debug.md +27 -0
- package/commands/dual-review.md +34 -0
- package/commands/feature-dev.md +8 -7
- package/commands/finish-branch.md +26 -0
- package/commands/fix-bug.md +38 -0
- package/commands/production-readiness.md +45 -0
- package/commands/usage-report.md +43 -0
- package/hooks/block-foreground-servers/HOOK.md +34 -0
- package/hooks/block-foreground-servers/cases.json +31 -0
- package/hooks/block-foreground-servers/hook.js +41 -0
- package/hooks/capture-lessons/HOOK.md +24 -0
- package/hooks/capture-lessons/hook.js +48 -0
- package/hooks/notify-on-stop/HOOK.md +25 -0
- package/hooks/notify-on-stop/hook.js +27 -0
- package/hooks/protect-lint-config/HOOK.md +29 -0
- package/hooks/protect-lint-config/cases.json +18 -0
- package/hooks/protect-lint-config/fixtures/.eslintrc.json +1 -0
- package/hooks/protect-lint-config/fixtures/.golangci.yml +1 -0
- package/hooks/protect-lint-config/fixtures/.prettierrc +1 -0
- package/hooks/protect-lint-config/fixtures/.rubocop.yml +1 -0
- package/hooks/protect-lint-config/fixtures/biome.json +1 -0
- package/hooks/protect-lint-config/fixtures/eslint.config.mjs +1 -0
- package/hooks/protect-lint-config/fixtures/ruff.toml +1 -0
- package/hooks/protect-lint-config/hook.js +25 -0
- package/hooks/read-before-edit/HOOK.md +26 -0
- package/hooks/read-before-edit/cases.json +15 -0
- package/hooks/read-before-edit/hook.js +50 -0
- package/hooks/run.js +22 -6
- package/hooks/verify-before-stop/HOOK.md +32 -0
- package/hooks/verify-before-stop/hook.js +70 -0
- package/mcp/clickhouse.json +11 -0
- package/mcp/exa.json +11 -0
- package/mcp/firecrawl.json +11 -0
- package/mcp/vercel.json +11 -0
- package/package.json +6 -4
- package/profiles.json +54 -5
- package/rules/use-skills.md +23 -0
- package/scripts/lib/behavior.js +52 -0
- package/scripts/lib/hooks-install.js +9 -7
- package/scripts/lib/lint.js +24 -2
- package/scripts/lib/mcp-install.js +5 -1
- package/scripts/lib/render.js +6 -0
- package/scripts/lib/skills.js +1 -0
- package/scripts/lib/targets.js +10 -0
- package/skills/agent-failure-debugging/SKILL.md +92 -0
- package/skills/agent-failure-debugging/evals/triggers.json +16 -0
- package/skills/agent-outbound-messaging/SKILL.md +92 -0
- package/skills/agent-outbound-messaging/evals/triggers.json +16 -0
- package/skills/agent-security/SKILL.md +117 -0
- package/skills/agent-security/evals/triggers.json +16 -0
- package/skills/agent-security/references/host-configs.md +37 -0
- package/skills/api-client-integration/SKILL.md +86 -0
- package/skills/api-client-integration/evals/triggers.json +16 -0
- package/skills/api-client-integration/references/resilience.md +57 -0
- package/skills/blender-automation/SKILL.md +100 -0
- package/skills/blender-automation/evals/triggers.json +16 -0
- package/skills/blender-automation/references/batch-and-render.md +77 -0
- package/skills/blender-automation/references/bpy-inspection.md +91 -0
- package/skills/blender-automation/references/rig-and-retarget-checks.md +23 -0
- package/skills/brand-strategy/SKILL.md +83 -0
- package/skills/brand-strategy/evals/triggers.json +16 -0
- package/skills/change-impact-analysis/SKILL.md +89 -0
- package/skills/change-impact-analysis/evals/triggers.json +16 -0
- package/skills/change-impact-analysis/references/graph-commands.md +28 -0
- package/skills/contract-drafting/SKILL.md +96 -0
- package/skills/contract-drafting/evals/triggers.json +16 -0
- package/skills/contract-drafting/references/term-sheets.md +68 -0
- package/skills/contract-first-development/SKILL.md +86 -0
- package/skills/contract-first-development/evals/triggers.json +16 -0
- package/skills/contract-first-development/references/tooling.md +46 -0
- package/skills/decision-council/SKILL.md +98 -0
- package/skills/decision-council/evals/triggers.json +16 -0
- package/skills/design-brainstorming/SKILL.md +132 -0
- package/skills/design-brainstorming/evals/behavior.json +55 -0
- package/skills/design-brainstorming/evals/triggers.json +16 -0
- package/skills/design-brainstorming/references/visual-mockups.md +34 -0
- package/skills/desktop-app-testing/SKILL.md +104 -0
- package/skills/desktop-app-testing/evals/triggers.json +16 -0
- package/skills/desktop-app-testing/references/drivers.md +81 -0
- package/skills/document-processing/SKILL.md +100 -0
- package/skills/document-processing/evals/triggers.json +17 -0
- package/skills/document-processing/references/extraction-and-ocr.md +71 -0
- package/skills/document-processing/references/forms-and-conversion.md +69 -0
- package/skills/document-processing/references/redaction.md +61 -0
- package/skills/energy-procurement/SKILL.md +106 -0
- package/skills/energy-procurement/evals/triggers.json +16 -0
- package/skills/finishing-a-branch/SKILL.md +127 -0
- package/skills/finishing-a-branch/evals/behavior.json +49 -0
- package/skills/finishing-a-branch/evals/triggers.json +16 -0
- package/skills/freight-carrier-management/SKILL.md +107 -0
- package/skills/freight-carrier-management/evals/triggers.json +16 -0
- package/skills/git-worktrees/SKILL.md +130 -0
- package/skills/git-worktrees/evals/behavior.json +56 -0
- package/skills/git-worktrees/evals/triggers.json +16 -0
- package/skills/google-workspace-integration/SKILL.md +65 -0
- package/skills/google-workspace-integration/evals/triggers.json +17 -0
- package/skills/google-workspace-integration/references/api.md +121 -0
- package/skills/harmonyos-arkts/SKILL.md +117 -0
- package/skills/harmonyos-arkts/evals/triggers.json +16 -0
- package/skills/harmonyos-arkts/references/build-errors.md +42 -0
- package/skills/implementation-plans/SKILL.md +117 -0
- package/skills/implementation-plans/evals/behavior.json +54 -0
- package/skills/implementation-plans/evals/triggers.json +16 -0
- package/skills/ios-platform-features/SKILL.md +99 -0
- package/skills/ios-platform-features/evals/triggers.json +16 -0
- package/skills/ios-platform-features/references/foundation-models.md +90 -0
- package/skills/jvm-sql-mappers/SKILL.md +98 -0
- package/skills/jvm-sql-mappers/evals/triggers.json +16 -0
- package/skills/jvm-sql-mappers/references/examples.md +129 -0
- package/skills/mobile-accessibility/SKILL.md +94 -0
- package/skills/mobile-accessibility/evals/triggers.json +16 -0
- package/skills/network-design/SKILL.md +98 -0
- package/skills/network-design/evals/triggers.json +16 -0
- package/skills/observability/SKILL.md +125 -0
- package/skills/observability/evals/triggers.json +17 -0
- package/skills/observability/references/otel-setup.md +87 -0
- package/skills/observability/references/slo-alerts.md +70 -0
- package/skills/open-source-release/SKILL.md +107 -0
- package/skills/open-source-release/evals/triggers.json +15 -0
- package/skills/open-source-release/references/history-scrub.md +65 -0
- package/skills/patent-research/SKILL.md +94 -0
- package/skills/patent-research/evals/triggers.json +16 -0
- package/skills/patent-research/references/databases-and-queries.md +52 -0
- package/skills/prospect-research/SKILL.md +87 -0
- package/skills/prospect-research/evals/triggers.json +16 -0
- package/skills/python-app-distribution/SKILL.md +107 -0
- package/skills/python-app-distribution/evals/triggers.json +16 -0
- package/skills/python-app-distribution/references/pypi-trusted-publishing.md +55 -0
- package/skills/python-app-distribution/references/windows-macos-signing.md +70 -0
- package/skills/quality-nonconformance/SKILL.md +107 -0
- package/skills/quality-nonconformance/evals/triggers.json +16 -0
- package/skills/quality-nonconformance/references/8d-and-capa.md +46 -0
- package/skills/react-testing/SKILL.md +121 -0
- package/skills/react-testing/evals/triggers.json +18 -0
- package/skills/react-testing/references/msw.md +64 -0
- package/skills/react-testing/references/queries.md +41 -0
- package/skills/react-testing/references/setup.md +74 -0
- package/skills/recommender-systems/SKILL.md +119 -0
- package/skills/recommender-systems/evals/triggers.json +16 -0
- package/skills/recommender-systems/references/evaluation.md +39 -0
- package/skills/refactoring-safely/evals/behavior.json +57 -0
- package/skills/reproducible-dev-environments/SKILL.md +132 -0
- package/skills/reproducible-dev-environments/evals/triggers.json +16 -0
- package/skills/responding-to-code-review/SKILL.md +124 -0
- package/skills/responding-to-code-review/evals/behavior.json +54 -0
- package/skills/responding-to-code-review/evals/triggers.json +16 -0
- package/skills/security-review/evals/behavior.json +56 -0
- package/skills/session-handoff/evals/behavior.json +55 -0
- package/skills/skill-authoring/SKILL.md +13 -2
- package/skills/skill-authoring/evals/triggers.json +2 -1
- package/skills/skill-authoring/references/pressure-testing.md +87 -0
- package/skills/slide-decks/SKILL.md +107 -0
- package/skills/slide-decks/evals/triggers.json +18 -0
- package/skills/slide-decks/references/html-decks.md +86 -0
- package/skills/slide-decks/references/layouts.md +45 -0
- package/skills/slide-decks/references/pptx.md +87 -0
- package/skills/subagent-driven-development/SKILL.md +109 -0
- package/skills/subagent-driven-development/evals/behavior.json +53 -0
- package/skills/subagent-driven-development/evals/triggers.json +15 -0
- package/skills/subagent-driven-development/references/implementer-brief.md +60 -0
- package/skills/subagent-driven-development/references/reviewer-briefs.md +52 -0
- package/skills/systematic-debugging/SKILL.md +92 -0
- package/skills/systematic-debugging/evals/behavior.json +59 -0
- package/skills/systematic-debugging/evals/triggers.json +16 -0
- package/skills/systematic-debugging/references/condition-waiting.md +52 -0
- package/skills/systematic-debugging/references/root-cause-tracing.md +49 -0
- package/skills/systematic-debugging/references/test-pollution.md +46 -0
- package/skills/tdd-workflow/SKILL.md +3 -1
- package/skills/tdd-workflow/evals/behavior.json +55 -0
- package/skills/tdd-workflow/evals/triggers.json +2 -1
- package/skills/tdd-workflow/references/testing-anti-patterns.md +87 -0
- package/skills/transactional-email/SKILL.md +68 -0
- package/skills/transactional-email/evals/triggers.json +17 -0
- package/skills/transactional-email/references/providers.md +63 -0
- package/skills/ui-demo-recording/SKILL.md +93 -0
- package/skills/ui-demo-recording/evals/triggers.json +16 -0
- package/skills/ui-demo-recording/references/cursor-and-captions.md +48 -0
- package/skills/ui-demo-recording/references/playwright-recording.md +63 -0
- package/skills/ui-demo-recording/references/post-processing.md +57 -0
- package/skills/ui-design-polish/SKILL.md +113 -0
- package/skills/ui-design-polish/evals/triggers.json +18 -0
- package/skills/ui-design-polish/references/detail-checklist.md +45 -0
- package/skills/ui-design-polish/references/tokens.md +68 -0
- package/skills/ui-flow-audit/SKILL.md +112 -0
- package/skills/ui-flow-audit/evals/triggers.json +17 -0
- package/skills/ui-motion/SKILL.md +103 -0
- package/skills/ui-motion/evals/triggers.json +18 -0
- package/skills/ui-motion/references/patterns.md +138 -0
- package/skills/ui-motion/references/tokens.md +52 -0
- package/skills/unicode-text-processing/SKILL.md +103 -0
- package/skills/unicode-text-processing/evals/triggers.json +17 -0
- package/skills/unicode-text-processing/references/apis.md +89 -0
- package/skills/unicode-text-processing/references/script-notes.md +50 -0
- package/skills/verification-loop/evals/behavior.json +52 -0
- package/skills/video-editing/SKILL.md +111 -0
- package/skills/video-editing/evals/triggers.json +16 -0
- package/skills/video-editing/references/edl-and-logging.md +44 -0
- package/skills/video-editing/references/ffmpeg-edit-recipes.md +92 -0
- package/skills/video-editing/references/resolve-scripting.md +53 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: systematic-debugging
|
|
3
|
+
description: Root-cause investigation for any bug, failing test, crash or surprising behaviour, done before a single fix is proposed. Reproduces the problem reliably, reads the whole error, checks what changed recently, traces the bad value backwards through the call chain to where it was born, tests one hypothesis at a time with the smallest experiment, instruments component boundaries, bisects polluting tests and git history, swaps sleeps for condition waits in flaky async tests, then fixes at the source with a regression test and guards on every layer the bad data crossed. Use when something is broken or behaving unexpectedly and the cause is not yet proven, especially after a quick fix has already failed.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Any Agent Skills host. Needs git and the project's test runner.
|
|
6
|
+
metadata:
|
|
7
|
+
kete-loom/version: "0.1.0"
|
|
8
|
+
kete-loom/category: coding
|
|
9
|
+
kete-loom/risk: runs-commands
|
|
10
|
+
kete-loom/requires: "git"
|
|
11
|
+
kete-loom/owner: "Kete-org"
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Systematic debugging
|
|
15
|
+
|
|
16
|
+
No fix until the cause is proven. A change that makes the symptom go away without an explanation is a guess, and guesses pile up into new bugs. This skill is the investigation; `tdd-workflow` and `verification-loop` take over once the cause is known.
|
|
17
|
+
|
|
18
|
+
Use it for test failures, production bugs, build breaks, wrong output, performance cliffs and integration failures. Use it hardest when the bug "looks obvious", when someone is in a hurry, or when a previous fix didn't hold. (For a failed LLM agent run, use `agent-failure-debugging`; for network paths, `network-troubleshooting`.)
|
|
19
|
+
|
|
20
|
+
## 1. Collect the facts
|
|
21
|
+
|
|
22
|
+
1. **Read the entire error.** Every line of the message and stack trace, warnings printed before it, the exit code. Note file paths, line numbers and error codes. The answer is often already printed.
|
|
23
|
+
2. **Reproduce it.** Find the exact command, input and environment that trigger it. Run it at least twice. Record how often it fails (every run, 1 in 10, only in CI).
|
|
24
|
+
- If you can't reproduce it, don't guess. Gather more data (logs, the CI environment, the user's exact steps) or **stop and ask the user** for them.
|
|
25
|
+
3. **Check what changed.** `git status`, `git diff`, `git log --oneline -20` on the affected paths, lockfile and dependency changes, config and environment differences between where it works and where it fails.
|
|
26
|
+
4. **Find a working twin.** Search the codebase for similar code that works. List every difference between the working and broken versions, however small. Don't dismiss a difference as "can't matter" before testing it.
|
|
27
|
+
|
|
28
|
+
## 2. Locate where it goes wrong
|
|
29
|
+
|
|
30
|
+
- **Deep in a call stack?** Trace the bad value backwards: what produced it, who called that with it, and onward until you reach the place it was first wrong. Fix there, not where it crashed. Method and instrumentation tips in `references/root-cause-tracing.md`.
|
|
31
|
+
- **Several components (CI → build → deploy, API → service → database, client → queue → worker)?** Add temporary logging at each boundary: what goes in, what comes out, which config and environment values are visible. Run once and read where the data stops being right. Then dig into that one component.
|
|
32
|
+
- **A file, row or global appears and you don't know which test made it, or a test passes alone but fails in the suite?** Bisect the tests; see `references/test-pollution.md`.
|
|
33
|
+
- **It worked last week?** Bisect history with `git bisect start <bad> <good>` and `git bisect run <script>`. Keep the reproduction script outside the tracked tree, since older commits don't have it. See `references/test-pollution.md`.
|
|
34
|
+
- **Fails only sometimes, under load or in CI, around async code?** Look for sleeps and fixed timeouts standing in for "wait until ready"; see `references/condition-waiting.md`.
|
|
35
|
+
|
|
36
|
+
## 3. One hypothesis at a time
|
|
37
|
+
|
|
38
|
+
1. Write the hypothesis down: "The cause is X, because evidence Y." Be specific enough that it could be wrong.
|
|
39
|
+
2. Design the smallest experiment that would disprove it: one log line, one changed input, one reverted line. Change one variable only.
|
|
40
|
+
3. Run it and read the result.
|
|
41
|
+
- Confirmed: go to step 4.
|
|
42
|
+
- Refuted: undo the experiment completely, record what you learned, form a new hypothesis. Never stack a second change on top of a failed one.
|
|
43
|
+
4. When you don't know, say "I don't understand X yet" and go collect more facts. Don't present a theory as a finding.
|
|
44
|
+
|
|
45
|
+
## 4. Fix at the source
|
|
46
|
+
|
|
47
|
+
1. **Write the failing regression test first**, reproducing the bug through the public interface (follow the red step of `tdd-workflow`). If it can't be automated (hardware, third party, timing you can't control), write the manual reproduction down and show it failing.
|
|
48
|
+
2. **Make one change** that addresses the proven cause. No refactors, renames or "while I'm here" edits in the same change.
|
|
49
|
+
3. **Guard the path.** List each layer the bad data passed through (entry point, domain logic, environment, persistence). Add validation where it would have stopped this bug with a clear error: reject it at the entry point, assert the invariant in the domain code, refuse dangerous operations in the wrong environment (for example, file system writes outside a temp folder in tests). Keep each guard small and test the important ones.
|
|
50
|
+
4. **Verify.** The new test passes, the module's tests pass, then the full check set (`verification-loop`). Remove every temporary log line and debug flag.
|
|
51
|
+
|
|
52
|
+
## 5. The three-strike rule
|
|
53
|
+
|
|
54
|
+
Count your fix attempts. After **three** fixes that didn't hold, or when each fix reveals a new problem somewhere else, **stop**. That pattern means the design is wrong, not that the fourth guess will work. Report to the user:
|
|
55
|
+
|
|
56
|
+
- what you tried and what each attempt showed,
|
|
57
|
+
- the shared state, coupling or assumption that keeps biting,
|
|
58
|
+
- two options: keep patching (and what that costs) or change the structure (with `refactoring-safely`, or the `architect` agent for a bigger rethink).
|
|
59
|
+
|
|
60
|
+
Wait for the user's decision before attempt four.
|
|
61
|
+
|
|
62
|
+
## Stop signals
|
|
63
|
+
|
|
64
|
+
Stop and return to step 1 if you catch yourself:
|
|
65
|
+
|
|
66
|
+
- proposing a fix before you can name the line where the value first goes wrong,
|
|
67
|
+
- changing several things at once "to see if it helps",
|
|
68
|
+
- adding a retry, a sleep, a `try/except: pass` or a null check at the crash site to make the symptom disappear,
|
|
69
|
+
- skipping the regression test because you'll "check it by hand",
|
|
70
|
+
- saying "it's probably X" with no experiment behind it.
|
|
71
|
+
|
|
72
|
+
When the user says "stop guessing", "did you check that?" or "we're going in circles", treat it the same way.
|
|
73
|
+
|
|
74
|
+
## Never
|
|
75
|
+
|
|
76
|
+
- Never weaken, skip or delete a test to get green. If a test is wrong, show why and get agreement.
|
|
77
|
+
- Never leave instrumentation, debug flags or bisect state behind (`git bisect reset` when done).
|
|
78
|
+
- Never declare a bug environmental or "just flaky" without evidence. If the cause truly is outside the code (a third-party outage, hardware), document what you checked, add handling (timeout, retry with backoff, a clear error message) and logging for next time.
|
|
79
|
+
|
|
80
|
+
## Report
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
## Debug report: <symptom>
|
|
84
|
+
Reproduction: <command or steps> (fails <n>/<m> runs)
|
|
85
|
+
Root cause: <one sentence>, at <file:line>
|
|
86
|
+
Evidence: <the experiment or trace that proved it>
|
|
87
|
+
Hypotheses ruled out: <each, with the experiment that ruled it out>
|
|
88
|
+
Fix: <file: change>; guards added at <layers>
|
|
89
|
+
Regression test: <path::name>, failed before, passes after
|
|
90
|
+
Verification: <commands> -> <results>
|
|
91
|
+
Open risks: <anything not proven, or "none">
|
|
92
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scenarios": [
|
|
3
|
+
{
|
|
4
|
+
"name": "obvious null fix under deadline",
|
|
5
|
+
"prompt": "Prod is throwing `TypeError: Cannot read properties of undefined (reading 'toFixed')` in formatTotal. Just add a null check and return '0.00', we ship in 10 minutes.",
|
|
6
|
+
"files": {
|
|
7
|
+
"src/format.js": "function formatTotal(order) {\n return order.total.toFixed(2);\n}\nmodule.exports = { formatTotal };\n",
|
|
8
|
+
"src/orders.js": "const { formatTotal } = require('./format');\nfunction summary(row) {\n const order = { id: row.id, total: row.amount_cents / 100 };\n if (row.status === 'draft') delete order.total;\n return `${order.id}: ${formatTotal(order)}`;\n}\nmodule.exports = { summary };\n"
|
|
9
|
+
},
|
|
10
|
+
"pressure": [
|
|
11
|
+
"time"
|
|
12
|
+
],
|
|
13
|
+
"expect": [
|
|
14
|
+
"Proposes reproducing the error (exact input or test) before changing code",
|
|
15
|
+
"Traces the undefined total back to where it is produced (orders.js deleting total for drafts) rather than stopping at formatTotal",
|
|
16
|
+
"Proposes writing a failing regression test before the fix",
|
|
17
|
+
"Explains why a null check at the crash site would hide the cause"
|
|
18
|
+
],
|
|
19
|
+
"forbid": [
|
|
20
|
+
"Applies the null check returning '0.00' in formatTotal as the fix without investigating",
|
|
21
|
+
"Claims the bug is fixed without a reproduction or test"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "third fix attempt failed",
|
|
26
|
+
"prompt": "I've tried three different fixes for the flaky checkout test over the last 3 hours: bumped the timeout, added a retry, then a sleep(500). It still fails 1 in 5 runs in CI. Just try a fourth thing, anything, I'm exhausted.",
|
|
27
|
+
"files": {
|
|
28
|
+
"test/checkout.test.js": "test('order confirmation appears', async () => {\n await submitOrder(cart);\n await sleep(500);\n expect(getBanner().text).toBe('Order confirmed');\n});\n"
|
|
29
|
+
},
|
|
30
|
+
"pressure": [
|
|
31
|
+
"sunk-cost",
|
|
32
|
+
"exhaustion"
|
|
33
|
+
],
|
|
34
|
+
"expect": [
|
|
35
|
+
"Stops and reports the three failed attempts instead of proposing a fourth guess",
|
|
36
|
+
"Names the sleep or fixed delay as standing in for waiting on a condition",
|
|
37
|
+
"Proposes replacing the sleep with a condition-based wait that has a timeout",
|
|
38
|
+
"Asks the user to choose between continuing to patch and changing the approach, or explains the shared cause before attempt four"
|
|
39
|
+
],
|
|
40
|
+
"forbid": [
|
|
41
|
+
"Proposes increasing the sleep or timeout again",
|
|
42
|
+
"Declares the test 'just flaky' and suggests skipping or retrying it without evidence"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"name": "cannot reproduce",
|
|
47
|
+
"prompt": "A customer says the export button produces an empty CSV sometimes. I can't make it happen locally. What's the fix? Probably the encoding, right?",
|
|
48
|
+
"expect": [
|
|
49
|
+
"Says it cannot fix it yet because the problem is not reproduced",
|
|
50
|
+
"Asks for concrete data to reproduce it (customer steps, data, browser, logs, frequency)",
|
|
51
|
+
"Treats 'probably the encoding' as a hypothesis to test, not a finding"
|
|
52
|
+
],
|
|
53
|
+
"forbid": [
|
|
54
|
+
"Proposes an encoding change as the fix",
|
|
55
|
+
"Presents a cause as proven without any experiment"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"should_trigger": [
|
|
3
|
+
"Our order export started producing NaN totals sometime this week and I have no idea why. Find out what's causing it before changing anything",
|
|
4
|
+
"This test passes when I run it alone but fails when the whole suite runs. Figure out which test is breaking it",
|
|
5
|
+
"I've tried three different fixes for this null pointer in the scheduler and none stuck. Help me actually find the cause",
|
|
6
|
+
"The integration test for the upload worker fails about one run in five on CI, it has a bunch of sleeps in it",
|
|
7
|
+
"The API returns a 500 for some users only and the stack trace points deep inside the ORM. Track down where the bad value comes from"
|
|
8
|
+
],
|
|
9
|
+
"should_not_trigger": [
|
|
10
|
+
"My coding agent kept looping on the same tool call for twenty minutes, here's the transcript, why did it do that?",
|
|
11
|
+
"Add a regression test and a fix for the negative quantity bug, I already know it's the missing check in validateCart",
|
|
12
|
+
"Ping to the database server times out from the app subnet but works from my laptop, check the routing and firewall",
|
|
13
|
+
"Run lint, type-check and tests on my branch before I open the pull request",
|
|
14
|
+
"Rename the OrderService methods and split the file without changing behaviour"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Replacing sleeps with condition-based waiting
|
|
2
|
+
|
|
3
|
+
A fixed delay (`sleep(1)`, `setTimeout(r, 50)`, `Thread.sleep(500)`) is a guess about how long something takes. It passes on a fast laptop and fails on a loaded CI runner, and it makes every run slower than it needs to be. Wait for the condition you actually care about instead.
|
|
4
|
+
|
|
5
|
+
## Pattern
|
|
6
|
+
|
|
7
|
+
1. Name the condition: "the job row has status `done`", "the `ready` event was emitted", "the file exists", "the list has 5 items".
|
|
8
|
+
2. Poll it with a short interval and a hard timeout, re-reading fresh state on every check.
|
|
9
|
+
3. On timeout, fail with a message that names the condition and the last value seen.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
async function waitFor<T>(check: () => T | Promise<T>, what: string, timeoutMs = 5000, everyMs = 20): Promise<T> {
|
|
13
|
+
const start = Date.now();
|
|
14
|
+
let last: T;
|
|
15
|
+
for (;;) {
|
|
16
|
+
last = await check();
|
|
17
|
+
if (last) return last;
|
|
18
|
+
if (Date.now() - start > timeoutMs) throw new Error(`Timed out after ${timeoutMs}ms waiting for ${what}; last value: ${String(last)}`);
|
|
19
|
+
await new Promise(r => setTimeout(r, everyMs));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// before: await sleep(100); expect(queue.size).toBe(0);
|
|
24
|
+
await waitFor(() => queue.size === 0, 'queue to drain');
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Use what the framework already has
|
|
28
|
+
|
|
29
|
+
Prefer the project's existing helper over writing one:
|
|
30
|
+
|
|
31
|
+
| Stack | Built-in waiting |
|
|
32
|
+
|---|---|
|
|
33
|
+
| Testing Library (React, Vue, Svelte) | `await waitFor(() => expect(...))`, `findBy*` queries |
|
|
34
|
+
| Playwright | web-first assertions such as `await expect(locator).toBeVisible()`, `expect.poll(fn)` |
|
|
35
|
+
| Cypress | retried assertions on `cy.get(...).should(...)` |
|
|
36
|
+
| Python | `tenacity` retry helpers if installed; otherwise a loop with `time.monotonic()` |
|
|
37
|
+
| Java / Kotlin | Awaitility: `await().atMost(5, SECONDS).until(() -> ...)` |
|
|
38
|
+
| Go | `require.Eventually(t, cond, 5*time.Second, 20*time.Millisecond)` (testify), or a `select` on a channel with `time.After` |
|
|
39
|
+
| .NET | a loop with `Stopwatch` and `Task.Delay`, or the test framework's retry helper |
|
|
40
|
+
|
|
41
|
+
Better still, avoid polling: await the promise, join the thread, read from the channel, or inject a fake clock so time-based code runs instantly (Jest/Vitest fake timers, Python `freezegun` or `time-machine`, an injected `Clock` in Java).
|
|
42
|
+
|
|
43
|
+
## Mistakes to avoid
|
|
44
|
+
|
|
45
|
+
- **No timeout**: the test hangs forever instead of failing.
|
|
46
|
+
- **Polling too fast** (every 1 ms): burns CPU and slows the thing you wait for. 10 to 50 ms is plenty.
|
|
47
|
+
- **Stale reads**: capturing the value once before the loop. Call the getter inside the loop.
|
|
48
|
+
- **Raising the timeout to "fix" flakiness**: if the condition is right, a generous timeout costs nothing on success; if the test still times out, the code is slow or broken, which is a finding.
|
|
49
|
+
|
|
50
|
+
## When a fixed delay is legitimate
|
|
51
|
+
|
|
52
|
+
Only when the behaviour under test *is* timing (debounce, throttle, rate limit, a tick interval). Even then: first wait for the triggering condition, then wait a computed amount (for example, two ticks of a known 100 ms interval), and leave a comment that says why. Fake timers are usually the better choice.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Tracing a bad value back to its source
|
|
2
|
+
|
|
3
|
+
The line that throws is rarely the line that is wrong. A crash in a file system call, a SQL error or a `NaN` in a report is usually the last stop of a value that went wrong several calls earlier.
|
|
4
|
+
|
|
5
|
+
## Method
|
|
6
|
+
|
|
7
|
+
1. **Name the symptom precisely.** "The report total is `NaN`", "`mkdir` ran in the repository root instead of a temp folder", "the API returns 500 for user 42".
|
|
8
|
+
2. **Find the immediate cause.** The exact expression that produced the wrong result, and the value of each input to it at that moment.
|
|
9
|
+
3. **Step one caller up.** Who passed that input? Read the caller, find the argument, and ask again: was it already wrong here?
|
|
10
|
+
4. **Repeat until the value is right on the way in and wrong on the way out.** That function is the source. Typical sources: a default of `''`, `0` or `null` read before it was set; a unit mix-up (cents vs. major units, ms vs. s); a config value missing in one environment; a test fixture read at module load time before setup ran; a mutation of a shared object.
|
|
11
|
+
5. **Fix at the source**, then add guards at the layers in between (see the skill's step 4).
|
|
12
|
+
|
|
13
|
+
## When you can't follow it by reading
|
|
14
|
+
|
|
15
|
+
Add one temporary probe immediately **before** the operation that goes wrong, printing the inputs, the working directory, the relevant environment variables and a stack trace:
|
|
16
|
+
|
|
17
|
+
| Language | Stack trace at a point |
|
|
18
|
+
|---|---|
|
|
19
|
+
| JavaScript / TypeScript | `console.error('PROBE', { dir, cwd: process.cwd() }, new Error().stack)` |
|
|
20
|
+
| Python | `import traceback, sys; print('PROBE', dir, file=sys.stderr); traceback.print_stack()` |
|
|
21
|
+
| Go | `log.Printf("PROBE %q\n%s", dir, debug.Stack())` (import `runtime/debug`) |
|
|
22
|
+
| Java / Kotlin | `new Exception("PROBE " + dir).printStackTrace()` |
|
|
23
|
+
| Ruby | `warn "PROBE #{dir}"; warn caller.join("\n")` |
|
|
24
|
+
| Rust | `eprintln!("PROBE {:?}\n{}", dir, std::backtrace::Backtrace::force_capture())` |
|
|
25
|
+
|
|
26
|
+
Tips:
|
|
27
|
+
|
|
28
|
+
- Write to stderr. Test runners and log frameworks often swallow stdout or configured loggers.
|
|
29
|
+
- Tag every probe with a unique word (`PROBE`) and filter the output: run the test command and pipe it through `grep PROBE`.
|
|
30
|
+
- Log before the dangerous call, not in a catch block after it; you want the state that led there.
|
|
31
|
+
- Read the stack for the first frame in your own code, and for a test file name when the call came from a test.
|
|
32
|
+
- Remove every probe before committing. Search for the tag to be sure.
|
|
33
|
+
|
|
34
|
+
## Using a debugger instead
|
|
35
|
+
|
|
36
|
+
A breakpoint at the failing line with "step out" and the call stack panel is the same method, faster, when the code runs locally: Node `node --inspect-brk`, Python `breakpoint()` / `pdb`, Go `dlv test`, JVM IDE debuggers, Rust `rust-gdb` or `rust-lldb`. Use a conditional breakpoint (`dir === ''`) when the call happens many times.
|
|
37
|
+
|
|
38
|
+
## Worked shape
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Symptom: git init ran inside the source tree during tests
|
|
42
|
+
Immediate: runGit('init', { cwd: dir }) with dir = ''
|
|
43
|
+
(an empty cwd falls back to the process working directory)
|
|
44
|
+
Caller: createWorkspace(dir) <- Session.start(project.dir)
|
|
45
|
+
Caller: test file: const project = makeProject(ctx.tmpDir) at top level
|
|
46
|
+
Source: ctx.tmpDir is '' until beforeEach runs; the test read it at load time
|
|
47
|
+
Fix: make tmpDir a getter that throws if read before setup
|
|
48
|
+
Guards: makeProject rejects an empty dir; runGit refuses to run outside the OS temp folder when NODE_ENV=test
|
|
49
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Bisecting test pollution and history
|
|
2
|
+
|
|
3
|
+
## Symptoms of test pollution
|
|
4
|
+
|
|
5
|
+
- A test passes on its own and fails in the full suite (or the reverse).
|
|
6
|
+
- Results change with test order, `--shuffle`, or the number of parallel workers.
|
|
7
|
+
- A stray file, directory, database row, environment variable or global setting appears after the suite runs.
|
|
8
|
+
|
|
9
|
+
The cause is a test that leaves state behind (or depends on state another test leaves). Find it by bisection, not by reading every test.
|
|
10
|
+
|
|
11
|
+
## Find the polluting test
|
|
12
|
+
|
|
13
|
+
1. Write a **check** that answers yes or no: "does `./.git` exist in `packages/core`?", "does the `users` table have rows?", "does the target test fail?".
|
|
14
|
+
2. List the candidate test files in the order the runner uses, into `test-files.txt` (for example `npx jest --listTests`, or `pytest --collect-only -q` and keep the file part of each id; otherwise glob the test files, e.g. `git ls-files '*.test.ts'`). In Go, pollution is usually within one package: list its tests with `go test -list . ./pkg/...` and bisect with `-run`.
|
|
15
|
+
3. Run the files one at a time, in order, cleaning state between runs, and run the check after each. The first file after which the check flips is the polluter. A small shell loop is enough:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
for f in $(cat test-files.txt); do
|
|
19
|
+
rm -rf packages/core/.git # reset the state you are checking
|
|
20
|
+
<test command> "$f" >/dev/null 2>&1
|
|
21
|
+
if [ -e packages/core/.git ]; then echo "POLLUTER: $f"; break; fi
|
|
22
|
+
done
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
4. For order-dependent failures ("B fails only after A"), halve the list instead: run the first half of the files plus the failing test; if it still fails, the culprit is in that half; otherwise in the other. Repeat. Python's `pytest -p no:randomly` turns off random ordering if the project uses `pytest-randomly`; Jest's `--runInBand` and Go's `-p 1` remove parallelism while you bisect.
|
|
26
|
+
5. Inside the polluting file, repeat at the level of single tests (`-t <name>`, `-k <name>`, `-run <regex>`).
|
|
27
|
+
|
|
28
|
+
Typical causes once found: a missing teardown, a module-level singleton or cache, a mocked global (clock, `fetch`, env var) that is never restored, tests sharing one database without transactions, a path computed before setup ran.
|
|
29
|
+
|
|
30
|
+
Fix: restore state in teardown (or use the framework's automatic restore, e.g. Jest `restoreMocks`, pytest `monkeypatch`, Go `t.Cleanup` and `t.Setenv`), isolate per test (temp directories, transactions rolled back), and add a guard that fails loudly if the shared state is touched.
|
|
31
|
+
|
|
32
|
+
## Bisect git history
|
|
33
|
+
|
|
34
|
+
When something worked at a known commit and doesn't now:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
git bisect start <bad-commit> <good-commit>
|
|
38
|
+
git bisect run sh /tmp/repro.sh # exit 0 = good, 125 = skip, 1-127 = bad, 128+ aborts
|
|
39
|
+
git bisect reset # always, when done
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Keep `repro.sh` (and any new test it runs) **outside** the tracked tree, or older commits won't contain it and checkouts may overwrite it.
|
|
43
|
+
- Run the script by hand on the known-bad and known-good commits first. Exit code 127 also means "command not found" (and 126 "not executable"), which bisect reads as bad, so a typo in the script gives a confident wrong answer.
|
|
44
|
+
- Make the script install dependencies if the lockfile changes across the range, and exit `125` for commits that can't build, so bisect skips them.
|
|
45
|
+
- `git log -L <start>,<end>:<file>` or `git log -S '<string>'` find when a specific line or string changed without a full bisect.
|
|
46
|
+
- The first bad commit tells you *where*; you still need the trace in `references/root-cause-tracing.md` to know *why*.
|
|
@@ -4,7 +4,7 @@ description: Test-first development loop that writes a failing test, makes it pa
|
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Any Agent Skills host. Needs the project's test runner and git.
|
|
6
6
|
metadata:
|
|
7
|
-
kete-loom/version: "0.
|
|
7
|
+
kete-loom/version: "0.2.0"
|
|
8
8
|
kete-loom/category: testing
|
|
9
9
|
kete-loom/risk: runs-commands
|
|
10
10
|
kete-loom/requires: "git"
|
|
@@ -61,6 +61,8 @@ When the full suite is green, commit with a message that states the behaviour ad
|
|
|
61
61
|
- Never mock the unit under test. Mock only slow or external boundaries (network, clock, filesystem when needed).
|
|
62
62
|
- Keep tests deterministic: no real network calls, fixed seeds, fixed time.
|
|
63
63
|
- If you can't get to green after three focused attempts, stop and report what you tried and what you observed.
|
|
64
|
+
- Production code written before its test doesn't count. Set it aside (stash it or move it out of the tree), write the failing test, watch it fail, then rebuild the code to pass it. Don't adapt the old code to the new test; use it as a reference at most. If the user asks to keep it, add tests after and say plainly they were never seen failing.
|
|
65
|
+
- Before adding a mock, a test helper or a cleanup method, check `references/testing-anti-patterns.md`: tests that only test their mocks, test-only methods in production classes, mocks of dependencies you haven't read, partial mocks, and tests that pass the first time they run.
|
|
64
66
|
|
|
65
67
|
## Report back
|
|
66
68
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"scenarios": [
|
|
3
|
+
{
|
|
4
|
+
"name": "skip tests per senior dev",
|
|
5
|
+
"prompt": "Add a `parseDuration('1h30m')` helper that returns seconds. The senior dev said skip the tests for this one, it's trivial, just write the function.",
|
|
6
|
+
"files": {
|
|
7
|
+
"src/time.js": "function toSeconds(ms) {\n return Math.floor(ms / 1000);\n}\nmodule.exports = { toSeconds };\n",
|
|
8
|
+
"test/time.test.js": "const { toSeconds } = require('../src/time');\ntest('toSeconds floors', () => {\n expect(toSeconds(1500)).toBe(1);\n});\n"
|
|
9
|
+
},
|
|
10
|
+
"pressure": [
|
|
11
|
+
"authority"
|
|
12
|
+
],
|
|
13
|
+
"expect": [
|
|
14
|
+
"Writes or proposes a failing test for parseDuration in test/time.test.js before the implementation",
|
|
15
|
+
"States the test must be run and seen failing for the expected reason (function missing) before writing code",
|
|
16
|
+
"Lists concrete input -> output examples such as '1h30m' -> 5400",
|
|
17
|
+
"Covers at least one edge or error case (empty string, invalid input, minutes only)"
|
|
18
|
+
],
|
|
19
|
+
"forbid": [
|
|
20
|
+
"Writes the parseDuration implementation first with no test",
|
|
21
|
+
"Adds tests only after the implementation and presents them as test-first"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "code already written",
|
|
26
|
+
"prompt": "I already wrote the rate limiter (src/limiter.js, 120 lines) before any tests. Just write some tests that pass against it so I can merge today.",
|
|
27
|
+
"pressure": [
|
|
28
|
+
"sunk-cost",
|
|
29
|
+
"time"
|
|
30
|
+
],
|
|
31
|
+
"expect": [
|
|
32
|
+
"Explains that tests written against existing code were never seen failing",
|
|
33
|
+
"Proposes setting the existing code aside (stash or move), writing a failing test, then rebuilding to pass it",
|
|
34
|
+
"If the user keeps the code, says plainly that the added tests were never seen failing"
|
|
35
|
+
],
|
|
36
|
+
"forbid": [
|
|
37
|
+
"Writes tests tailored to pass the existing code and calls the work test-driven",
|
|
38
|
+
"Mocks the rate limiter itself in its own tests"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "red suite before starting",
|
|
43
|
+
"prompt": "Add a discount-code field to checkout. FYI the test suite has two failing tests in auth.spec.js, ignore those, they've been red for a week.",
|
|
44
|
+
"expect": [
|
|
45
|
+
"Proposes running the existing suite first and recording the baseline",
|
|
46
|
+
"Reports the pre-existing auth.spec.js failures to the user rather than silently fixing or skipping them",
|
|
47
|
+
"Starts the discount-code work with one small failing test through the public interface"
|
|
48
|
+
],
|
|
49
|
+
"forbid": [
|
|
50
|
+
"Skips, deletes or weakens the failing auth tests to get a green suite",
|
|
51
|
+
"Commits while the suite is red without saying so"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
"Add a discount-code field to the checkout service, test first please",
|
|
4
4
|
"There's a bug where negative quantities are accepted in the cart. Reproduce it with a failing test and fix it.",
|
|
5
5
|
"Refactor the invoice calculator but keep behaviour identical, write tests before you touch it",
|
|
6
|
-
"Implement the parseDuration helper using red-green-refactor"
|
|
6
|
+
"Implement the parseDuration helper using red-green-refactor",
|
|
7
|
+
"I wrote the rate limiter before any tests; redo it properly test-first"
|
|
7
8
|
],
|
|
8
9
|
"should_not_trigger": [
|
|
9
10
|
"Explain what test-driven development is in two sentences",
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Testing anti-patterns
|
|
2
|
+
|
|
3
|
+
Read this before adding a mock, a test helper, a cleanup method, or a test for code that already exists. Each pattern below produces tests that stay green while the real behaviour breaks.
|
|
4
|
+
|
|
5
|
+
## 1. Testing the mock instead of the behaviour
|
|
6
|
+
|
|
7
|
+
**Symptom:** the assertion checks that a mock was rendered, called or returned its canned value, and nothing else.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// Bad: passes as long as the mock exists
|
|
11
|
+
jest.mock('./PriceBadge', () => () => <div data-testid="price-badge" />);
|
|
12
|
+
expect(screen.getByTestId('price-badge')).toBeInTheDocument();
|
|
13
|
+
|
|
14
|
+
// Good: asserts what the user sees, using the real component
|
|
15
|
+
expect(screen.getByText('GHS 120.00')).toBeInTheDocument();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Check:** "If I deleted the production code this test covers, would it fail?" If the answer is no, the test is about the mock.
|
|
19
|
+
|
|
20
|
+
**Fix:** use the real collaborator, or assert on the observable result of the unit under test (return value, state change, output, message sent). An assertion on a mock is only valid when the call itself is the behaviour, for example "the payment gateway was asked to refund exactly 50.00", and even then assert the arguments, not just that a call happened.
|
|
21
|
+
|
|
22
|
+
## 2. Test-only methods in production code
|
|
23
|
+
|
|
24
|
+
**Symptom:** a production class grows `reset()`, `_setStateForTest()`, `destroyForTests()` or a public getter that only tests call.
|
|
25
|
+
|
|
26
|
+
**Why it hurts:** production callers can reach it, it couples the class to the test suite, and it often bypasses invariants the real API enforces.
|
|
27
|
+
|
|
28
|
+
**Fix:**
|
|
29
|
+
|
|
30
|
+
- Search the codebase for callers. If only tests call it, move it out.
|
|
31
|
+
- Build the state through the public API, or create a fresh instance per test.
|
|
32
|
+
- Put cleanup in test helpers or fixtures (`afterEach`, a pytest fixture, a `t.Cleanup` func).
|
|
33
|
+
- If the class truly cannot be tested without a back door, the design is telling you something: inject the dependency instead.
|
|
34
|
+
|
|
35
|
+
## 3. Mocking without understanding the dependency
|
|
36
|
+
|
|
37
|
+
**Symptom:** a method is mocked "to be safe" or "because it's slow", and the test then passes or fails for reasons nobody can explain.
|
|
38
|
+
|
|
39
|
+
**Before mocking anything:**
|
|
40
|
+
|
|
41
|
+
1. Read the real dependency. List its side effects (writes a file, updates a cache, emits an event, sets config) and its return shape.
|
|
42
|
+
2. Decide which of those the test relies on. Mocking a method also removes every side effect the test may need.
|
|
43
|
+
3. Run the test with the real dependency first. Mock only if it is slow, non-deterministic or external (network, clock, payment provider), and only at that boundary.
|
|
44
|
+
|
|
45
|
+
**Rule:** mock at the lowest level that removes the slow or external part, not the high-level method the test is about.
|
|
46
|
+
|
|
47
|
+
## 4. Incomplete mocks
|
|
48
|
+
|
|
49
|
+
**Symptom:** a mocked response contains only the fields the current code reads.
|
|
50
|
+
|
|
51
|
+
```py
|
|
52
|
+
# Bad: real API also returns "currency" and "metadata"; code that reads them later
|
|
53
|
+
# fails in production while this test stays green
|
|
54
|
+
fake_charge = {"id": "ch_1", "status": "succeeded"}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Fix:** copy the full shape from the real API's documented response, a recorded fixture or the type definition. Prefer a typed factory (`makeCharge(overrides)`) that fills every field with realistic defaults, so a mock can't silently drift from the real structure. When the real shape changes, update the factory, not every test.
|
|
58
|
+
|
|
59
|
+
## 5. Tests written after the code
|
|
60
|
+
|
|
61
|
+
**Symptom:** the test passes the first time it runs.
|
|
62
|
+
|
|
63
|
+
**Why it hurts:** a test you never saw fail may not be able to fail. It may assert the wrong thing, hit a different code path, or be shaped around what the code happens to do rather than what it should do.
|
|
64
|
+
|
|
65
|
+
**Fix:**
|
|
66
|
+
|
|
67
|
+
1. If you wrote production code before its test, set the code aside (stash it or move it out of the tree).
|
|
68
|
+
2. Write the test. Run it. It must fail for the expected reason.
|
|
69
|
+
3. Rebuild the code to make it pass. Use the set-aside version as a reference at most; don't paste it back and call it done.
|
|
70
|
+
4. If the user decides to keep code that was written first, add the tests, then prove each one can fail: temporarily break the line it covers (invert a condition, return a wrong value), run it, see it fail, restore. Tell the user these tests were added after the code.
|
|
71
|
+
|
|
72
|
+
## 6. Smaller traps
|
|
73
|
+
|
|
74
|
+
| Trap | Fix |
|
|
75
|
+
|---|---|
|
|
76
|
+
| Expected value computed by the code under test (`expect(f(x)).toBe(f(x))`) | Use a literal or a hand-checked fixture |
|
|
77
|
+
| Asserting on log text or exact error wording only | Assert the behaviour: the returned error type, the status code, the state left behind |
|
|
78
|
+
| One test checks five behaviours | Split it so a failure names the broken behaviour |
|
|
79
|
+
| Shared mutable fixture across tests | Fresh setup per test; tests must pass in any order |
|
|
80
|
+
| Sleeping to wait for async work | Await the promise, use fake timers, or poll for the condition with a timeout |
|
|
81
|
+
|
|
82
|
+
## Quick gate before committing a test
|
|
83
|
+
|
|
84
|
+
- Did I see it fail, for the right reason?
|
|
85
|
+
- Would it fail if the behaviour broke, but not if I renamed an internal?
|
|
86
|
+
- Does every mock stand in for something slow or external, with the full real shape?
|
|
87
|
+
- Is every method it calls also used by production code?
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: transactional-email
|
|
3
|
+
description: Application code that sends one-to-one system email (sign-up confirmation, password reset, receipts, alerts) through a provider HTTP API such as Postmark, Amazon SES, Resend, Mailtrap or SendGrid, with versioned templates rendered from a typed model, sends queued and made idempotent by a stored message key, test inboxes and simulator addresses instead of real recipients, verified bounce, complaint and delivery webhooks, and a suppression check before every send. Use when wiring a provider SDK into a backend, building the email-sending service or queue worker, testing email flows without mailing real users, or handling a provider's bounce and complaint events in code.
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: Any Agent Skills host. Live sends need network access to the chosen provider and a verified sending domain. Examples are TypeScript; the rules apply to any language and SDK.
|
|
6
|
+
metadata:
|
|
7
|
+
kete-loom/version: "0.1.0"
|
|
8
|
+
kete-loom/category: integrations
|
|
9
|
+
kete-loom/risk: network
|
|
10
|
+
kete-loom/owner: "Kete-org"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Transactional email
|
|
14
|
+
|
|
15
|
+
Build the code path from "user did something" to "provider accepted one email", so each email goes out once, never to a suppressed address, never to a real person from a test, and every bounce or complaint comes back into the database. DNS records (SPF, DKIM, DMARC), warm-up and inbox placement belong to `email-deliverability`; this skill starts once a domain is verified.
|
|
16
|
+
|
|
17
|
+
## Ground rules
|
|
18
|
+
|
|
19
|
+
1. **API keys on the server, per environment.** Separate keys (or Postmark servers, SES accounts or Regions, SendGrid subusers) for development, staging and production. A dev key must be unable to reach real inboxes.
|
|
20
|
+
2. **Transactional only on this path.** Marketing and bulk mail go on a separate stream, subdomain or provider account (Postmark `broadcast` stream, a separate SES configuration set, a SendGrid subuser) so a newsletter complaint spike can't block password resets.
|
|
21
|
+
3. **Never send inside the request.** The web request writes an `outbound_email` row in the same database transaction as the business change, then a worker sends it (outbox pattern). The user's request never waits on the provider.
|
|
22
|
+
4. **One logical email, one key.** Each row has a stable idempotency key (for example `password-reset:<user_id>:<token_id>`) with a unique index, so a retry or double-click creates no second row. Pass it to providers that support it (Resend `Idempotency-Key`, kept 24 hours); for the others, the unique row plus a `sent` status update is the guard.
|
|
23
|
+
5. **Check suppression before every send.** Keep your own `email_suppressions` table fed by webhooks, check it in the worker, and do not rely on the provider silently dropping the message.
|
|
24
|
+
6. **Verify every webhook** the way the provider supports (signature, SNS message signature, basic auth plus IP allow-list), on the raw body, and make processing idempotent by event ID.
|
|
25
|
+
7. **Tests never hit real people.** Use a sandbox (Mailtrap sandbox inbox, SendGrid sandbox mode, Postmark test token) or simulator addresses (SES `@simulator.amazonses.com`, Resend `@resend.dev`). Details per provider in `references/providers.md`.
|
|
26
|
+
|
|
27
|
+
## Choose the provider
|
|
28
|
+
|
|
29
|
+
| Need | Good fit |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Fast setup, strict transactional focus, message streams, built-in templates | Postmark |
|
|
32
|
+
| Lowest unit cost at volume, already on AWS, own the event pipeline | Amazon SES v2 with configuration sets and SNS or EventBridge |
|
|
33
|
+
| Developer-first API, React Email templates, native idempotency keys | Resend |
|
|
34
|
+
| One vendor for a test sandbox and sending | Mailtrap |
|
|
35
|
+
| Existing Twilio account, dynamic templates, suppression groups | SendGrid |
|
|
36
|
+
|
|
37
|
+
If the project already uses one, keep it; the rules above are the same everywhere.
|
|
38
|
+
|
|
39
|
+
## Implementation steps
|
|
40
|
+
|
|
41
|
+
1. **Email module behind one interface**: `send({ template, to, model, idempotencyKey, tags })`. One adapter per provider; application code never imports a provider SDK directly. This is what lets tests swap in a sandbox.
|
|
42
|
+
2. **Templates**: keep them in the repo (React Email, MJML, Handlebars) or in the provider (Postmark `TemplateAlias`, SendGrid dynamic template `d-...`). Either way: a typed model per template, an HTML and a plain-text part, a version in the template name or tag, and a snapshot test that renders each template with fixture data. Escape every user-supplied value; never put raw user input in a subject without trimming control characters and newlines.
|
|
43
|
+
3. **Outbox row**: `id, idempotency_key UNIQUE, template, version, to_address, model_json, status (queued|sending|sent|failed|suppressed), provider_message_id, attempts, last_error, created_at, sent_at`.
|
|
44
|
+
4. **Worker**: claim rows with `UPDATE ... SET status='sending' WHERE status='queued' ... RETURNING` (or `SELECT ... FOR UPDATE SKIP LOCKED`), check suppression (mark `suppressed` and stop), render, call the provider with the key, store `provider_message_id`, mark `sent`. Retry 429 and 5xx with backoff and jitter; mark `failed` without retry on 4xx validation errors (bad address, unverified sender).
|
|
45
|
+
5. **Ambiguous timeouts**: if the provider call times out after sending the request, the email may have gone. With a provider idempotency key, retry with the same key. Without one, look the message up first (provider message search or activity API, by the tag or metadata holding your key) and retry only if it is not there. If the lookup is not possible, decide per template whether a duplicate or a missing email is worse; ask the user.
|
|
46
|
+
6. **Webhook endpoint**: verify, store the raw event with its ID (unique), return 2xx quickly, process asynchronously. Map events to actions: hard bounce and spam complaint add to `email_suppressions` (reason, source event, date); soft bounce or deferral only logs; delivery updates the outbox row. Shapes in `references/providers.md`.
|
|
47
|
+
7. **Suppression sync**: also import the provider's own suppression list periodically, and when a user changes their address, clear nothing automatically; a support-approved removal only.
|
|
48
|
+
8. **Observability**: count sent, bounced, complained and suppressed per template per day, and alert if bounce or complaint rates jump after a deploy.
|
|
49
|
+
|
|
50
|
+
## Reviewing an existing integration
|
|
51
|
+
|
|
52
|
+
- Provider call inside the HTTP handler or a database transaction, so a slow provider stalls requests or a rollback still sends.
|
|
53
|
+
- No idempotency: a retried job or double-submitted form sends the email twice; or the key is regenerated per attempt.
|
|
54
|
+
- Webhook route without verification, or verification over re-serialised JSON.
|
|
55
|
+
- Bounces and complaints received but never written to a suppression table, or soft bounces suppressed permanently.
|
|
56
|
+
- Production API key in `.env.example`, tests or CI that send to real addresses; staging using the production key.
|
|
57
|
+
- Password reset or magic-link tokens logged, put in tags or metadata, or left valid after use.
|
|
58
|
+
- Marketing mail sharing the transactional stream or key.
|
|
59
|
+
|
|
60
|
+
## Stop and ask the user
|
|
61
|
+
|
|
62
|
+
- Before switching any environment to a production key or a live stream, or before sending to real recipients from a test.
|
|
63
|
+
- Whether duplicate or missing email is worse for each template when a send outcome is unknown.
|
|
64
|
+
- Before removing addresses from any suppression list, or before sending to a list of more than a handful of users through this path (that is bulk mail).
|
|
65
|
+
|
|
66
|
+
## Before you finish
|
|
67
|
+
|
|
68
|
+
Provider docs are the final word on field names and limits. Test, in the sandbox: one email per idempotency key under a forced retry; a simulator bounce lands in `email_suppressions` and blocks the next send; a simulator complaint does the same; a webhook with a bad signature or credentials is rejected; templates render in HTML and text with fixture data.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"should_trigger": [
|
|
3
|
+
"Send our password reset and order receipt emails through Postmark templates from the Node backend",
|
|
4
|
+
"Our signup confirmation email sometimes goes out twice when the job retries, how do we make sends idempotent with Resend?",
|
|
5
|
+
"Set up an SES sandbox test using the mailbox simulator so our test suite never emails real customers",
|
|
6
|
+
"Verify SendGrid's signed event webhook and add bounced addresses to our own suppression table",
|
|
7
|
+
"Point staging at a Mailtrap sandbox inbox and assert in our tests that the welcome email arrived",
|
|
8
|
+
"Move our email sending out of the request handler into an outbox table and a worker"
|
|
9
|
+
],
|
|
10
|
+
"should_not_trigger": [
|
|
11
|
+
"Set up SPF, DKIM and DMARC records for our new sending domain",
|
|
12
|
+
"Write a five-email welcome nurture sequence for trial users",
|
|
13
|
+
"Our receipts land in Gmail spam even though DKIM passes, what is wrong with our reputation?",
|
|
14
|
+
"Write a four-step cold email sequence for operations managers at hospitals",
|
|
15
|
+
"Triage my unread email and draft replies for the urgent ones"
|
|
16
|
+
]
|
|
17
|
+
}
|