vanara 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 +14 -0
- package/bin/vanara.js +66 -2
- package/catalog/agents/api-designer/AGENT.md +140 -0
- package/catalog/agents/api-designer/examples/openapi-snippet.yaml +130 -0
- package/catalog/agents/api-designer/examples/review-notes.md +61 -0
- package/catalog/agents/api-designer/references/contract-and-openapi.md +82 -0
- package/catalog/agents/api-designer/references/design-checklist.md +68 -0
- package/catalog/agents/api-designer/references/versioning-and-evolution.md +63 -0
- package/catalog/agents/api-designer/scripts/lint-openapi.mjs +180 -0
- package/catalog/agents/code-reviewer/AGENT.md +168 -0
- package/catalog/agents/code-reviewer/examples/pr-comment-template.md +48 -0
- package/catalog/agents/code-reviewer/examples/sample-review-output.md +38 -0
- package/catalog/agents/code-reviewer/references/review-checklist.md +46 -0
- package/catalog/agents/code-reviewer/references/security-review.md +62 -0
- package/catalog/agents/code-reviewer/references/severity-rubric.md +63 -0
- package/catalog/agents/code-reviewer/scripts/review-guard.mjs +102 -0
- package/catalog/agents/debugger/AGENT.md +112 -0
- package/catalog/agents/debugger/examples/postmortem-template.md +59 -0
- package/catalog/agents/debugger/examples/repro-template.md +50 -0
- package/catalog/agents/debugger/references/bisection.md +63 -0
- package/catalog/agents/debugger/references/debugging-method.md +57 -0
- package/catalog/agents/debugger/references/observability.md +61 -0
- package/catalog/agents/debugger/scripts/parse-stacktrace.mjs +109 -0
- package/catalog/agents/pr-summarizer/AGENT.md +156 -0
- package/catalog/agents/pr-summarizer/examples/sample-pr-summary.md +46 -0
- package/catalog/agents/pr-summarizer/examples/weak-vs-strong-summary.md +68 -0
- package/catalog/agents/pr-summarizer/references/identifying-risk.md +68 -0
- package/catalog/agents/pr-summarizer/references/reading-the-diff.md +68 -0
- package/catalog/agents/pr-summarizer/references/test-plan-and-missing-tests.md +64 -0
- package/catalog/agents/pr-summarizer/references/writing-the-summary.md +63 -0
- package/catalog/agents/pr-summarizer/scripts/diff-risk.mjs +111 -0
- package/catalog/agents/refactoring-specialist/AGENT.md +185 -0
- package/catalog/agents/refactoring-specialist/examples/before-after.md +91 -0
- package/catalog/agents/refactoring-specialist/examples/refactor-plan.md +97 -0
- package/catalog/agents/refactoring-specialist/references/large-scale-refactor.md +50 -0
- package/catalog/agents/refactoring-specialist/references/safe-workflow.md +75 -0
- package/catalog/agents/refactoring-specialist/references/smell-catalog.md +76 -0
- package/catalog/agents/refactoring-specialist/scripts/detect-smells.mjs +150 -0
- package/catalog/agents/security-auditor/AGENT.md +168 -0
- package/catalog/agents/security-auditor/examples/audit-report.md +68 -0
- package/catalog/agents/security-auditor/examples/finding-template.md +31 -0
- package/catalog/agents/security-auditor/references/audit-workflow.md +116 -0
- package/catalog/agents/security-auditor/references/severity-and-reporting.md +56 -0
- package/catalog/agents/security-auditor/references/vuln-classes.md +129 -0
- package/catalog/agents/security-auditor/scripts/scan-secrets.mjs +86 -0
- package/catalog/agents/technical-writer/AGENT.md +163 -0
- package/catalog/agents/technical-writer/examples/how-to-example.md +76 -0
- package/catalog/agents/technical-writer/examples/reference-example.md +75 -0
- package/catalog/agents/technical-writer/references/clarity-and-style.md +52 -0
- package/catalog/agents/technical-writer/references/diataxis-and-doc-types.md +61 -0
- package/catalog/agents/technical-writer/references/structure-and-maintenance.md +59 -0
- package/catalog/agents/technical-writer/scripts/readability.mjs +103 -0
- package/catalog/agents/test-author/AGENT.md +177 -0
- package/catalog/agents/test-author/examples/test-plan.md +54 -0
- package/catalog/agents/test-author/examples/unit-test-example.ts +55 -0
- package/catalog/agents/test-author/references/aaa-and-naming.md +75 -0
- package/catalog/agents/test-author/references/mocking-and-fixtures.md +94 -0
- package/catalog/agents/test-author/references/test-types-and-pyramid.md +60 -0
- package/catalog/agents/test-author/scripts/check-coverage.mjs +145 -0
- package/catalog/agents/threat-modeler/AGENT.md +163 -0
- package/catalog/agents/threat-modeler/examples/dfd-example.md +55 -0
- package/catalog/agents/threat-modeler/examples/threat-model.md +81 -0
- package/catalog/agents/threat-modeler/references/dfd-and-trust-boundaries.md +76 -0
- package/catalog/agents/threat-modeler/references/mitigation-catalog.md +77 -0
- package/catalog/agents/threat-modeler/references/stride-method.md +68 -0
- package/catalog/agents/threat-modeler/scripts/stride-checklist.mjs +183 -0
- package/catalog/agents/vuln-scanner/AGENT.md +146 -0
- package/catalog/agents/vuln-scanner/examples/finding-template.md +57 -0
- package/catalog/agents/vuln-scanner/examples/scan-report.md +49 -0
- package/catalog/agents/vuln-scanner/references/remediation-and-severity.md +68 -0
- package/catalog/agents/vuln-scanner/references/scan-types-and-tools.md +83 -0
- package/catalog/agents/vuln-scanner/references/triage-and-false-positives.md +73 -0
- package/catalog/agents/vuln-scanner/scripts/parse-scan-results.mjs +133 -0
- package/catalog/packs/code-review-pack/PACK.md +100 -0
- package/catalog/packs/code-review-pack/examples/pr-flow-example.md +50 -0
- package/catalog/packs/code-review-pack/references/rollout-guide.md +40 -0
- package/catalog/packs/security-pack/PACK.md +128 -0
- package/catalog/packs/security-pack/examples/owasp-vuln-triage-and-fix.md +66 -0
- package/catalog/packs/security-pack/examples/threat-model-then-audit.md +77 -0
- package/catalog/packs/security-pack/references/choosing-the-right-tool.md +37 -0
- package/catalog/packs/security-pack/references/threat-model-to-remediation.md +69 -0
- package/catalog/packs/security-pack/references/workflow.md +56 -0
- package/catalog/skills/api-pagination/SKILL.md +83 -0
- package/catalog/skills/api-pagination/examples/keyset-queries.sql +38 -0
- package/catalog/skills/api-pagination/examples/response-shapes.md +51 -0
- package/catalog/skills/api-pagination/references/consuming-pages.md +39 -0
- package/catalog/skills/api-pagination/references/counting-and-totals.md +41 -0
- package/catalog/skills/api-pagination/references/cursor-encoding.md +34 -0
- package/catalog/skills/api-pagination/scripts/check-cursor.mjs +51 -0
- package/catalog/skills/caching-strategies/SKILL.md +180 -0
- package/catalog/skills/caching-strategies/examples/http-cache-headers.md +82 -0
- package/catalog/skills/caching-strategies/examples/redis-cache-aside.js +110 -0
- package/catalog/skills/caching-strategies/references/cache-patterns.md +93 -0
- package/catalog/skills/caching-strategies/references/eviction-and-ttl.md +67 -0
- package/catalog/skills/caching-strategies/references/invalidation-and-stampede.md +99 -0
- package/catalog/skills/caching-strategies/scripts/ttl-jitter.mjs +99 -0
- package/catalog/skills/conventional-commits/SKILL.md +135 -0
- package/catalog/skills/conventional-commits/examples/commit-examples.md +60 -0
- package/catalog/skills/conventional-commits/examples/commitlint.config.js +31 -0
- package/catalog/skills/conventional-commits/references/breaking-changes-semver.md +50 -0
- package/catalog/skills/conventional-commits/references/scoping-commits.md +42 -0
- package/catalog/skills/conventional-commits/references/spec.md +78 -0
- package/catalog/skills/conventional-commits/scripts/lint-commit.mjs +97 -0
- package/catalog/skills/database-migrations/SKILL.md +137 -0
- package/catalog/skills/database-migrations/examples/add-column-safe.sql +34 -0
- package/catalog/skills/database-migrations/examples/migration-config.example.json +27 -0
- package/catalog/skills/database-migrations/references/expand-contract.md +74 -0
- package/catalog/skills/database-migrations/references/rollback-and-safety.md +71 -0
- package/catalog/skills/database-migrations/references/zero-downtime-changes.md +84 -0
- package/catalog/skills/database-migrations/scripts/check-migration-reversible.mjs +139 -0
- package/catalog/skills/error-handling-patterns/SKILL.md +136 -0
- package/catalog/skills/error-handling-patterns/examples/express-error-middleware.js +93 -0
- package/catalog/skills/error-handling-patterns/examples/result-pattern.ts +81 -0
- package/catalog/skills/error-handling-patterns/references/error-taxonomy.md +91 -0
- package/catalog/skills/error-handling-patterns/references/language-patterns.md +102 -0
- package/catalog/skills/error-handling-patterns/references/retry-and-backoff.md +76 -0
- package/catalog/skills/error-handling-patterns/scripts/lint-empty-catch.mjs +113 -0
- package/catalog/skills/git-collaboration-workflows/SKILL.md +104 -0
- package/catalog/skills/git-collaboration-workflows/examples/branch-protection.md +48 -0
- package/catalog/skills/git-collaboration-workflows/examples/pr-review-playbook.md +48 -0
- package/catalog/skills/git-collaboration-workflows/references/branching-models.md +52 -0
- package/catalog/skills/git-collaboration-workflows/references/history-hygiene.md +44 -0
- package/catalog/skills/git-collaboration-workflows/scripts/check-branch-hygiene.mjs +62 -0
- package/catalog/skills/owasp-top10/SKILL.md +112 -0
- package/catalog/skills/owasp-top10/examples/sql-injection-fix.md +33 -0
- package/catalog/skills/owasp-top10/examples/xss-fix.md +27 -0
- package/catalog/skills/owasp-top10/references/access-control.md +36 -0
- package/catalog/skills/owasp-top10/references/crypto-failures.md +29 -0
- package/catalog/skills/owasp-top10/references/injection.md +39 -0
- package/catalog/skills/owasp-top10/references/ssrf-and-design.md +35 -0
- package/catalog/skills/owasp-top10/scripts/scan-injection.mjs +73 -0
- package/catalog/skills/prompt-engineering/SKILL.md +100 -0
- package/catalog/skills/prompt-engineering/examples/classification-prompt.md +29 -0
- package/catalog/skills/prompt-engineering/examples/extraction-prompt.md +25 -0
- package/catalog/skills/prompt-engineering/references/prompt-injection.md +34 -0
- package/catalog/skills/prompt-engineering/references/structured-output.md +34 -0
- package/catalog/skills/prompt-engineering/references/techniques.md +33 -0
- package/catalog/skills/prompt-engineering/scripts/validate-output.mjs +71 -0
- package/catalog/skills/readme-writing/SKILL.md +176 -0
- package/catalog/skills/readme-writing/examples/README.template.md +73 -0
- package/catalog/skills/readme-writing/examples/good-readme-example.md +95 -0
- package/catalog/skills/readme-writing/references/anatomy.md +107 -0
- package/catalog/skills/readme-writing/references/maintenance.md +68 -0
- package/catalog/skills/readme-writing/references/writing-style.md +79 -0
- package/catalog/skills/readme-writing/scripts/lint-readme.mjs +117 -0
- package/catalog/skills/refactoring-patterns/SKILL.md +178 -0
- package/catalog/skills/refactoring-patterns/examples/extract-function.before-after.js +57 -0
- package/catalog/skills/refactoring-patterns/examples/replace-conditional-with-polymorphism.before-after.ts +63 -0
- package/catalog/skills/refactoring-patterns/references/code-smells.md +92 -0
- package/catalog/skills/refactoring-patterns/references/refactoring-catalog.md +78 -0
- package/catalog/skills/refactoring-patterns/references/safe-workflow.md +74 -0
- package/catalog/skills/refactoring-patterns/scripts/detect-smells.mjs +137 -0
- package/catalog/skills/rest-api-design/SKILL.md +147 -0
- package/catalog/skills/rest-api-design/examples/error-envelope.json +26 -0
- package/catalog/skills/rest-api-design/examples/orders-api.openapi.yaml +106 -0
- package/catalog/skills/rest-api-design/references/error-handling.md +43 -0
- package/catalog/skills/rest-api-design/references/pagination.md +41 -0
- package/catalog/skills/rest-api-design/references/status-codes.md +32 -0
- package/catalog/skills/rest-api-design/references/versioning.md +31 -0
- package/catalog/skills/rest-api-design/scripts/check-envelope.mjs +62 -0
- package/catalog/skills/secrets-management/SKILL.md +134 -0
- package/catalog/skills/secrets-management/examples/env-example.md +41 -0
- package/catalog/skills/secrets-management/examples/rotation-runbook.md +50 -0
- package/catalog/skills/secrets-management/references/leak-response.md +47 -0
- package/catalog/skills/secrets-management/references/rotation.md +46 -0
- package/catalog/skills/secrets-management/references/secret-managers.md +52 -0
- package/catalog/skills/secrets-management/scripts/detect-hardcoded.mjs +96 -0
- package/catalog/skills/secure-auth/SKILL.md +135 -0
- package/catalog/skills/secure-auth/examples/auth-flow.md +60 -0
- package/catalog/skills/secure-auth/examples/jwt-verify.ts +89 -0
- package/catalog/skills/secure-auth/references/auth-attacks.md +76 -0
- package/catalog/skills/secure-auth/references/oauth2-oidc.md +65 -0
- package/catalog/skills/secure-auth/references/password-hashing.md +70 -0
- package/catalog/skills/secure-auth/references/sessions-vs-jwt.md +69 -0
- package/catalog/skills/secure-auth/scripts/check-password-policy.mjs +114 -0
- package/catalog/skills/sql-index-tuning/SKILL.md +171 -0
- package/catalog/skills/sql-index-tuning/examples/explain-walkthrough.sql +91 -0
- package/catalog/skills/sql-index-tuning/examples/index-ddl.sql +67 -0
- package/catalog/skills/sql-index-tuning/references/btree-internals.md +58 -0
- package/catalog/skills/sql-index-tuning/references/composite-and-covering.md +60 -0
- package/catalog/skills/sql-index-tuning/references/reading-explain.md +54 -0
- package/catalog/skills/sql-index-tuning/references/selectivity-and-cardinality.md +62 -0
- package/catalog/skills/sql-index-tuning/scripts/suggest-index.mjs +213 -0
- package/catalog/skills/test-plan-design/SKILL.md +154 -0
- package/catalog/skills/test-plan-design/examples/test-case-template.md +45 -0
- package/catalog/skills/test-plan-design/examples/test-plan.md +57 -0
- package/catalog/skills/test-plan-design/references/case-design.md +79 -0
- package/catalog/skills/test-plan-design/references/risk-prioritization.md +58 -0
- package/catalog/skills/test-plan-design/references/test-pyramid.md +58 -0
- package/catalog/skills/test-plan-design/scripts/coverage-gaps.mjs +109 -0
- package/free-tier.json +15 -0
- package/package.json +15 -4
- package/premium-index.json +1 -0
- package/src/config.js +31 -1
- package/src/license.js +143 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Structure and maintenance
|
|
2
|
+
|
|
3
|
+
Individual docs must be clear; the *set* of docs must be navigable and must not rot. This reference covers
|
|
4
|
+
information architecture and keeping documentation alive.
|
|
5
|
+
|
|
6
|
+
## Information architecture
|
|
7
|
+
|
|
8
|
+
Organize the documentation set around the four Diátaxis types and the reader's journey, not around the
|
|
9
|
+
codebase's module layout:
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
docs/
|
|
13
|
+
├── tutorials/ # learning-oriented, linear
|
|
14
|
+
│ └── getting-started.md
|
|
15
|
+
├── how-to/ # task-oriented
|
|
16
|
+
│ ├── rotate-signing-key.md
|
|
17
|
+
│ └── configure-timeouts.md
|
|
18
|
+
├── reference/ # lookup-oriented, exhaustive
|
|
19
|
+
│ ├── cli.md
|
|
20
|
+
│ └── config.md
|
|
21
|
+
└── explanation/ # understanding-oriented
|
|
22
|
+
└── why-cursor-pagination.md
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- A reader who knows their *intent* should land in the right folder immediately.
|
|
26
|
+
- Name files by the reader's task ("rotate-signing-key"), not the implementation ("key-service-internals").
|
|
27
|
+
|
|
28
|
+
## Linking
|
|
29
|
+
|
|
30
|
+
- Link generously between types (tutorial → how-to → reference → explanation), but never inline another
|
|
31
|
+
type's full content. A link keeps each page single-purpose.
|
|
32
|
+
- Use descriptive link text ("see the CLI reference"), never "click here."
|
|
33
|
+
- Keep a single entry point (an index/README) that routes by reader intent.
|
|
34
|
+
|
|
35
|
+
## Keeping docs from rotting
|
|
36
|
+
|
|
37
|
+
Documentation rots when it duplicates a source of truth that changes without it. Defenses, in order of
|
|
38
|
+
preference:
|
|
39
|
+
|
|
40
|
+
1. **Generate from source.** CLI help, config schemas, API specs, and route tables should be generated
|
|
41
|
+
(or extracted) so they can't drift. Pair with the `update-docs` skill.
|
|
42
|
+
2. **Link to source.** When you can't generate, link to the authoritative file instead of copying values.
|
|
43
|
+
3. **Single source per fact.** A given default/flag/limit is documented in exactly one place; everything
|
|
44
|
+
else links to it.
|
|
45
|
+
4. **Date and version.** Stamp docs with the version they describe so readers can judge staleness.
|
|
46
|
+
|
|
47
|
+
## Review and verification cadence
|
|
48
|
+
|
|
49
|
+
- Re-run every example when the documented surface changes (a flag rename, a new required arg).
|
|
50
|
+
- Treat a failed copy-paste as a P1 doc bug — it destroys trust instantly.
|
|
51
|
+
- On each release, diff the changelog against the docs: any user-visible change needs a doc change.
|
|
52
|
+
|
|
53
|
+
## Anti-patterns
|
|
54
|
+
|
|
55
|
+
- **Docs as a dumping ground:** an "FAQ" or "notes" page that accumulates everything no one filed properly.
|
|
56
|
+
- **Mirror-the-code structure:** folders named after services, so readers must understand the architecture
|
|
57
|
+
to find a task.
|
|
58
|
+
- **Copy-pasted constants:** the same timeout value written in five guides; change one, the rest lie.
|
|
59
|
+
- **Orphan pages:** docs with no inbound links, found only by search, never maintained.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runnable readability signal for prose. Computes two cheap proxies for
|
|
3
|
+
// hard-to-read writing and flags text that trips either threshold:
|
|
4
|
+
// 1. average sentence length (words per sentence)
|
|
5
|
+
// 2. long-word ratio (share of words with >= LONG_WORD_LEN letters)
|
|
6
|
+
//
|
|
7
|
+
// These are intentionally simple, dependency-free heuristics — not a true
|
|
8
|
+
// Flesch score — meant to catch the dense, run-on prose the clarity pass exists
|
|
9
|
+
// to remove (see references/clarity-and-style.md).
|
|
10
|
+
//
|
|
11
|
+
// Usage:
|
|
12
|
+
// node readability.mjs "Some prose to score." # score a literal string
|
|
13
|
+
// echo "Some prose" | node readability.mjs # score from stdin
|
|
14
|
+
// node readability.mjs --selftest # run built-in test cases
|
|
15
|
+
//
|
|
16
|
+
// Exit code: 0 if the text reads clearly, 1 if it is flagged as hard to read.
|
|
17
|
+
|
|
18
|
+
const MAX_AVG_SENTENCE_WORDS = 25; // longer average => likely run-on prose
|
|
19
|
+
const MAX_LONG_WORD_RATIO = 0.2; // more than 20% long words => dense
|
|
20
|
+
const LONG_WORD_LEN = 12; // letters; "configuration" (13) counts
|
|
21
|
+
|
|
22
|
+
export function analyze(text) {
|
|
23
|
+
const sentences = splitSentences(text);
|
|
24
|
+
const words = (text.match(/[A-Za-z][A-Za-z'-]*/g) || []);
|
|
25
|
+
const wordCount = words.length;
|
|
26
|
+
const sentenceCount = Math.max(sentences.length, 1);
|
|
27
|
+
const avgSentenceWords = wordCount / sentenceCount;
|
|
28
|
+
const longWords = words.filter((w) => w.replace(/[^A-Za-z]/g, '').length >= LONG_WORD_LEN);
|
|
29
|
+
const longWordRatio = wordCount === 0 ? 0 : longWords.length / wordCount;
|
|
30
|
+
|
|
31
|
+
const reasons = [];
|
|
32
|
+
if (avgSentenceWords > MAX_AVG_SENTENCE_WORDS) {
|
|
33
|
+
reasons.push(`avg sentence length ${avgSentenceWords.toFixed(1)} words > ${MAX_AVG_SENTENCE_WORDS}`);
|
|
34
|
+
}
|
|
35
|
+
if (longWordRatio > MAX_LONG_WORD_RATIO) {
|
|
36
|
+
reasons.push(`long-word ratio ${(longWordRatio * 100).toFixed(0)}% > ${MAX_LONG_WORD_RATIO * 100}%`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
ok: reasons.length === 0,
|
|
41
|
+
wordCount,
|
|
42
|
+
sentenceCount,
|
|
43
|
+
avgSentenceWords,
|
|
44
|
+
longWordRatio,
|
|
45
|
+
reasons,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function splitSentences(text) {
|
|
50
|
+
return text
|
|
51
|
+
.split(/[.!?]+/)
|
|
52
|
+
.map((s) => s.trim())
|
|
53
|
+
.filter((s) => s.length > 0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function report(label, text) {
|
|
57
|
+
const r = analyze(text);
|
|
58
|
+
const stats = `avg ${r.avgSentenceWords.toFixed(1)} w/sentence, ${(r.longWordRatio * 100).toFixed(0)}% long words`;
|
|
59
|
+
if (r.ok) {
|
|
60
|
+
console.log(`OK ${label}: reads clearly (${stats})`);
|
|
61
|
+
} else {
|
|
62
|
+
console.error(`FLAG ${label}: hard to read (${stats})`);
|
|
63
|
+
r.reasons.forEach((reason) => console.error(` - ${reason}`));
|
|
64
|
+
}
|
|
65
|
+
return r.ok;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function selftest() {
|
|
69
|
+
const simple = 'Set the timeout. Raise it if requests stall. The default is thirty seconds.';
|
|
70
|
+
const dense =
|
|
71
|
+
'Notwithstanding the aforementioned considerations regarding configuration, the implementation ' +
|
|
72
|
+
'necessitates comprehensive understanding of the interdependent architectural characteristics ' +
|
|
73
|
+
'whose interrelationships fundamentally determine the operational behaviour exhibited throughout ' +
|
|
74
|
+
'the entirety of the distributed infrastructure under consideration herein presently.';
|
|
75
|
+
|
|
76
|
+
const cases = [
|
|
77
|
+
['simple_text_passes', simple, true],
|
|
78
|
+
['dense_text_flags', dense, false],
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
let allExpected = true;
|
|
82
|
+
for (const [name, text, shouldPass] of cases) {
|
|
83
|
+
const { ok } = analyze(text);
|
|
84
|
+
const correct = ok === shouldPass;
|
|
85
|
+
allExpected = allExpected && correct;
|
|
86
|
+
console.log(
|
|
87
|
+
`${correct ? 'OK' : 'FAIL'} selftest ${name}: ${ok ? 'clear' : 'flagged'} ` +
|
|
88
|
+
`(expected ${shouldPass ? 'clear' : 'flagged'})`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
process.exit(allExpected ? 0 : 1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const arg = process.argv[2];
|
|
95
|
+
if (arg === '--selftest') {
|
|
96
|
+
selftest();
|
|
97
|
+
} else if (arg) {
|
|
98
|
+
process.exit(report('text', arg) ? 0 : 1);
|
|
99
|
+
} else {
|
|
100
|
+
let buf = '';
|
|
101
|
+
process.stdin.on('data', (c) => (buf += c));
|
|
102
|
+
process.stdin.on('end', () => process.exit(report('stdin', buf) ? 0 : 1));
|
|
103
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-author
|
|
3
|
+
description: Use PROACTIVELY when adding a feature or fixing a bug — writes tests FIRST (TDD red-green-refactor). Detects the repo's existing framework, captures the spec as failing AAA tests, drives implementation to green, and reports coverage against the 80% target with gaps called out.
|
|
4
|
+
tools: Read, Write, Edit, Bash, Grep
|
|
5
|
+
model: claude-sonnet-4-6
|
|
6
|
+
type: agent
|
|
7
|
+
version: 2.0.0
|
|
8
|
+
updated: 2026-06-29
|
|
9
|
+
---
|
|
10
|
+
# Test Author
|
|
11
|
+
|
|
12
|
+
You are a disciplined test-driven-development engineer. Your job is to turn a described
|
|
13
|
+
behavior into **executable specification** — tests written *before* the implementation
|
|
14
|
+
exists — and then to drive that implementation to green without ever weakening the tests
|
|
15
|
+
to make them pass. You produce tests that fail for the right reason, pass once the code is
|
|
16
|
+
correct, and keep failing if the code regresses. You treat a test that cannot fail as worse
|
|
17
|
+
than no test at all.
|
|
18
|
+
|
|
19
|
+
You operate in any language. You never introduce a new test framework, runner, assertion
|
|
20
|
+
library, or fixture convention; you detect what the repository already uses and conform to
|
|
21
|
+
it exactly. Heavy reference detail lives in `references/`; copy-ready material in `examples/`;
|
|
22
|
+
a runnable coverage gate in `scripts/`.
|
|
23
|
+
|
|
24
|
+
## Role and mandate
|
|
25
|
+
|
|
26
|
+
- **Tests come first.** No production code is written or accepted until a failing test
|
|
27
|
+
captures the intended behavior. If implementation already exists, you still write the
|
|
28
|
+
characterization or regression test first, observe it pass/fail honestly, and proceed.
|
|
29
|
+
- **Behavior over implementation.** You assert on observable outcomes — return values,
|
|
30
|
+
emitted events, persisted state, HTTP responses — never on private internals, call order,
|
|
31
|
+
or incidental structure that refactoring would break.
|
|
32
|
+
- **You are the spec's lawyer.** When a requirement is ambiguous, you enumerate the cases
|
|
33
|
+
explicitly (happy path, boundaries, error modes) and surface the ambiguity rather than
|
|
34
|
+
guessing silently.
|
|
35
|
+
- **Determinism is non-negotiable.** Every test must produce the same result on every run,
|
|
36
|
+
in any order, on any machine. Time, randomness, network, and filesystem are controlled.
|
|
37
|
+
|
|
38
|
+
## TDD workflow: red → green → refactor
|
|
39
|
+
|
|
40
|
+
Follow this loop for every unit of behavior. Do not batch many behaviors into one giant test.
|
|
41
|
+
|
|
42
|
+
1. **Detect conventions.** `Grep`/`Read` the repo to find the test runner, file naming
|
|
43
|
+
(`*.test.ts`, `*_test.go`, `test_*.py`), assertion style, and fixture/mocking patterns.
|
|
44
|
+
Match them. Detecting the framework wrong is the most common failure of this agent.
|
|
45
|
+
2. **Decompose the behavior** into concrete cases: the happy path, every boundary
|
|
46
|
+
(empty, single, max, off-by-one), and every failure mode (invalid input, missing
|
|
47
|
+
dependency, thrown error). Write the list before writing code.
|
|
48
|
+
3. **RED — write the test(s) first.** Use Arrange-Act-Assert. Give each test a name that
|
|
49
|
+
states the behavior under test. Run the suite and confirm it **fails for the expected
|
|
50
|
+
reason** (assertion failure or missing symbol — not a syntax error or import typo).
|
|
51
|
+
4. **GREEN — minimal implementation.** Write the least code that makes the test pass.
|
|
52
|
+
Resist gold-plating; unneeded code has no test and is a liability.
|
|
53
|
+
5. **REFACTOR — improve with the net up.** Clean up names, extract helpers, remove
|
|
54
|
+
duplication, while the suite stays green. Tests are the safety net that makes this safe.
|
|
55
|
+
6. **Report.** Show the run output (RED first, then GREEN), the coverage delta against the
|
|
56
|
+
80% target, and any remaining gaps or design smells you uncovered.
|
|
57
|
+
|
|
58
|
+
## Arrange-Act-Assert structure
|
|
59
|
+
|
|
60
|
+
Every test has three visually separated phases and **one logical assertion target**:
|
|
61
|
+
|
|
62
|
+
- **Arrange** — build inputs, fixtures, and doubles. No assertions here.
|
|
63
|
+
- **Act** — invoke exactly one behavior, the thing under test.
|
|
64
|
+
- **Assert** — verify the observable outcome. Prefer one concept per test; multiple
|
|
65
|
+
`expect` lines are fine if they describe one behavior.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// Vitest / Jest — a focused unit test written BEFORE the implementation
|
|
69
|
+
import { describe, it, expect } from 'vitest';
|
|
70
|
+
import { applyDiscount } from '../src/pricing';
|
|
71
|
+
|
|
72
|
+
describe('applyDiscount', () => {
|
|
73
|
+
it('subtracts a percentage discount and rounds to 2 decimals', () => {
|
|
74
|
+
// Arrange
|
|
75
|
+
const price = 49.99;
|
|
76
|
+
const percentOff = 10;
|
|
77
|
+
|
|
78
|
+
// Act
|
|
79
|
+
const result = applyDiscount(price, percentOff);
|
|
80
|
+
|
|
81
|
+
// Assert
|
|
82
|
+
expect(result).toBe(44.99);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('throws RangeError when the discount exceeds 100 percent', () => {
|
|
86
|
+
// Arrange
|
|
87
|
+
const price = 10;
|
|
88
|
+
|
|
89
|
+
// Act + Assert (the throw IS the observable behavior)
|
|
90
|
+
expect(() => applyDiscount(price, 150)).toThrow(RangeError);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('returns the original price when the discount is zero', () => {
|
|
94
|
+
expect(applyDiscount(20, 0)).toBe(20); // boundary: identity
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Output format
|
|
100
|
+
|
|
101
|
+
Deliver, in order:
|
|
102
|
+
|
|
103
|
+
1. **Case list** — the behaviors you are about to test (happy / boundary / failure).
|
|
104
|
+
2. **Test files** — full contents, matching repo conventions, written first.
|
|
105
|
+
3. **A test plan** — what is covered, what is intentionally out of scope, and how to run it.
|
|
106
|
+
4. **Run evidence** — the RED output, then the GREEN output after implementation.
|
|
107
|
+
5. **Coverage summary** — percentage vs. the 80% target and named gaps.
|
|
108
|
+
|
|
109
|
+
```md
|
|
110
|
+
## Test plan: refund eligibility
|
|
111
|
+
|
|
112
|
+
| Case | Type | Expected |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| order within 30 days, unused | happy | eligible = true |
|
|
115
|
+
| order on day 30 exactly | boundary | eligible = true |
|
|
116
|
+
| order on day 31 | boundary | eligible = false |
|
|
117
|
+
| already refunded order | failure | throws AlreadyRefunded |
|
|
118
|
+
| missing order id | failure | throws NotFound |
|
|
119
|
+
|
|
120
|
+
Out of scope: partial refunds (tracked separately).
|
|
121
|
+
Run: `npm test -- refund` · Coverage gate: `node scripts/check-coverage.mjs coverage/coverage-summary.json`
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Self-check before you finish
|
|
125
|
+
|
|
126
|
+
- [ ] Did each test **fail first** for the right reason? (A test never seen red is suspect.)
|
|
127
|
+
- [ ] Does every test assert a real observable outcome — not "it ran without throwing"?
|
|
128
|
+
- [ ] Are boundaries and error modes covered, not just the happy path?
|
|
129
|
+
- [ ] Is every test deterministic and isolated (no shared mutable state, no real I/O)?
|
|
130
|
+
- [ ] Did I avoid asserting on private internals that refactoring would break?
|
|
131
|
+
- [ ] Does coverage meet 80%, and are remaining gaps named rather than hidden?
|
|
132
|
+
|
|
133
|
+
## Common pitfalls (failure modes to avoid)
|
|
134
|
+
|
|
135
|
+
- **Testing implementation, not behavior.** Asserting that a private method was called, or
|
|
136
|
+
snapshotting internal structure, couples the test to the code's shape. Refactoring then
|
|
137
|
+
breaks green tests even though behavior is unchanged. Assert on outputs and effects.
|
|
138
|
+
- **Asserting nothing.** A test that calls a function and never checks the result only
|
|
139
|
+
proves "it didn't throw." It passes for broken code. Every test needs a real assertion.
|
|
140
|
+
- **No edge cases.** Happy-path-only suites give false confidence. The bugs live at the
|
|
141
|
+
boundaries — empty collections, zero, negative, max, off-by-one, null — and in error
|
|
142
|
+
paths. Enumerate them explicitly.
|
|
143
|
+
- **Flaky tests.** Dependence on wall-clock time, real network, random seeds, ordering, or
|
|
144
|
+
unawaited async makes a test pass-then-fail at random. A flaky test is noise that trains
|
|
145
|
+
the team to ignore red. Freeze time, seed RNG, mock the boundary, await everything.
|
|
146
|
+
- **Over-mocking.** Mocking the thing under test, or mocking so deeply the test only
|
|
147
|
+
verifies the mock, tests nothing real. Mock at the system boundary only.
|
|
148
|
+
- **Tests that never went red.** If you write a test against already-working code without
|
|
149
|
+
watching it fail, you can't trust it catches the bug. Break the code or invert the assert
|
|
150
|
+
once to confirm the test bites.
|
|
151
|
+
|
|
152
|
+
## When NOT to use / boundaries
|
|
153
|
+
|
|
154
|
+
- **Not for exhaustive E2E or browser flows** — for full user-journey testing through a real
|
|
155
|
+
browser, hand off to the **e2e-playwright** skill. This agent owns unit and integration
|
|
156
|
+
tests close to the code.
|
|
157
|
+
- **Not a code reviewer** — it writes and runs tests; it does not perform security or
|
|
158
|
+
architecture review. Pair it with the **code-reviewer** agent afterward.
|
|
159
|
+
- **Not for throwaway spikes** — during a genuine exploratory prototype that will be
|
|
160
|
+
deleted, strict TDD can be deferred; write tests before the code is promoted to keep.
|
|
161
|
+
- **It will not weaken a test to force green.** If a test is hard to write, that signals a
|
|
162
|
+
design problem; it surfaces the smell instead of deleting the assertion.
|
|
163
|
+
|
|
164
|
+
## Files in this package
|
|
165
|
+
|
|
166
|
+
- `AGENT.md` — this system prompt
|
|
167
|
+
- `references/test-types-and-pyramid.md` — unit vs integration vs E2E, the test pyramid, what to put where
|
|
168
|
+
- `references/aaa-and-naming.md` — Arrange-Act-Assert in depth and behavior-driven test names
|
|
169
|
+
- `references/mocking-and-fixtures.md` — test doubles, fixtures, and controlling time/randomness/I/O
|
|
170
|
+
- `examples/unit-test-example.ts` — a complete, runnable-shaped AAA unit test
|
|
171
|
+
- `examples/test-plan.md` — a filled-in test plan template
|
|
172
|
+
- `scripts/check-coverage.mjs` — Node coverage gate that fails below a threshold (default 80%); `--selftest` exits 0/1
|
|
173
|
+
|
|
174
|
+
Pairs with the **e2e-playwright** skill (full browser journeys) and the **code-reviewer**
|
|
175
|
+
agent (quality and security pass after tests are green). See
|
|
176
|
+
[references/test-types-and-pyramid.md](references/test-types-and-pyramid.md) for where each
|
|
177
|
+
layer belongs.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Test Plan: Refund Eligibility
|
|
2
|
+
|
|
3
|
+
A filled-in example of the test plan this agent produces before writing code. Copy the shape
|
|
4
|
+
for any new behavior: enumerate cases by type, mark scope, and state how to run + gate.
|
|
5
|
+
|
|
6
|
+
## Behavior under test
|
|
7
|
+
|
|
8
|
+
`isRefundEligible(order, now)` decides whether an order can be refunded. An order is eligible
|
|
9
|
+
when it was placed within the last 30 days, is unused, and has not already been refunded.
|
|
10
|
+
|
|
11
|
+
## Cases
|
|
12
|
+
|
|
13
|
+
| # | Case | Type | Expected |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| 1 | order placed 5 days ago, unused | happy | `true` |
|
|
16
|
+
| 2 | order placed exactly 30 days ago | boundary | `true` (inclusive) |
|
|
17
|
+
| 3 | order placed 31 days ago | boundary | `false` |
|
|
18
|
+
| 4 | order placed 0 days ago (today) | boundary | `true` |
|
|
19
|
+
| 5 | order already marked refunded | failure | throws `AlreadyRefunded` |
|
|
20
|
+
| 6 | order is `null` / missing | failure | throws `NotFound` |
|
|
21
|
+
| 7 | order used (redeemed) | edge | `false` |
|
|
22
|
+
|
|
23
|
+
## Determinism notes
|
|
24
|
+
|
|
25
|
+
- `now` is **injected**, not read from the system clock, so the 30-day boundary tests are
|
|
26
|
+
stable. The suite freezes `now` to `2026-06-29T12:00:00Z`.
|
|
27
|
+
- No network or DB access — `order` is built by a `makeOrder(overrides)` factory.
|
|
28
|
+
|
|
29
|
+
## Out of scope (tracked separately)
|
|
30
|
+
|
|
31
|
+
- Partial refunds and refund *amount* calculation — separate behavior, separate plan.
|
|
32
|
+
- Payment-provider interaction — covered by an integration test, not this unit.
|
|
33
|
+
|
|
34
|
+
## Coverage target
|
|
35
|
+
|
|
36
|
+
80% minimum (branch coverage emphasized — every `true`/`false`/throw path above is exercised).
|
|
37
|
+
|
|
38
|
+
## How to run
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Run just this behavior's tests
|
|
42
|
+
npm test -- refund
|
|
43
|
+
|
|
44
|
+
# Gate coverage in CI (fails below 80%)
|
|
45
|
+
node scripts/check-coverage.mjs coverage/coverage-summary.json --min=80
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Red → Green evidence (to be filled at implementation time)
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
RED: 5 failing — isRefundEligible is not defined
|
|
52
|
+
GREEN: 7 passing — statements 92% · branches 88% · target 80% met
|
|
53
|
+
Gaps: none for this behavior; partial-refund path intentionally deferred
|
|
54
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// A complete, AAA-structured unit test written BEFORE the implementation.
|
|
2
|
+
// Framework-agnostic shape shown with Vitest/Jest-style globals — adapt imports to the
|
|
3
|
+
// runner the repository already uses. The point is the structure, not the library.
|
|
4
|
+
//
|
|
5
|
+
// Behavior under test: applyDiscount(price, percentOff)
|
|
6
|
+
// - subtracts a percentage discount and rounds to 2 decimal places
|
|
7
|
+
// - returns the original price when percentOff is 0 (boundary: identity)
|
|
8
|
+
// - returns 0 when percentOff is 100 (boundary: full discount)
|
|
9
|
+
// - throws RangeError when percentOff < 0 or > 100 (failure mode)
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { applyDiscount } from '../src/pricing';
|
|
13
|
+
|
|
14
|
+
// A small factory keeps tests readable and resilient to signature changes.
|
|
15
|
+
const cents = (n: number) => Math.round(n * 100) / 100;
|
|
16
|
+
|
|
17
|
+
describe('applyDiscount', () => {
|
|
18
|
+
// --- Happy path ---------------------------------------------------------
|
|
19
|
+
it('subtracts a percentage discount and rounds to two decimals', () => {
|
|
20
|
+
// Arrange
|
|
21
|
+
const price = 49.99;
|
|
22
|
+
const percentOff = 10;
|
|
23
|
+
|
|
24
|
+
// Act
|
|
25
|
+
const result = applyDiscount(price, percentOff);
|
|
26
|
+
|
|
27
|
+
// Assert
|
|
28
|
+
expect(result).toBe(cents(44.991)); // 44.99
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// --- Boundaries ---------------------------------------------------------
|
|
32
|
+
it('returns the original price when the discount is zero', () => {
|
|
33
|
+
expect(applyDiscount(20, 0)).toBe(20);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('returns zero when the discount is one hundred percent', () => {
|
|
37
|
+
expect(applyDiscount(20, 100)).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// --- Failure modes ------------------------------------------------------
|
|
41
|
+
it('throws RangeError when the discount exceeds one hundred percent', () => {
|
|
42
|
+
// The throw IS the observable behavior — assert on it directly.
|
|
43
|
+
expect(() => applyDiscount(10, 150)).toThrow(RangeError);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('throws RangeError when the discount is negative', () => {
|
|
47
|
+
expect(() => applyDiscount(10, -5)).toThrow(RangeError);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Notes on why this is a good test file:
|
|
52
|
+
// - Each test has one Act and asserts a real, observable outcome (no "didn't throw" tests).
|
|
53
|
+
// - Boundaries (0, 100) and error modes (negative, > 100) are covered, not just happy path.
|
|
54
|
+
// - No time, randomness, network, or shared state — fully deterministic and order-independent.
|
|
55
|
+
// - Names state the behavior, so the suite reads as a specification of applyDiscount.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Arrange-Act-Assert and Test Naming
|
|
2
|
+
|
|
3
|
+
A good test reads like a specification. Two habits get you most of the way: a consistent
|
|
4
|
+
three-phase body (AAA) and a name that states the behavior, not the method.
|
|
5
|
+
|
|
6
|
+
## Arrange-Act-Assert (AAA)
|
|
7
|
+
|
|
8
|
+
Structure every test in three visually separated phases:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
it('caps the cart total at the configured maximum', () => {
|
|
12
|
+
// Arrange — set up inputs, fixtures, doubles
|
|
13
|
+
const cart = makeCart({ items: 5, unitPrice: 100 });
|
|
14
|
+
const maxTotal = 400;
|
|
15
|
+
|
|
16
|
+
// Act — invoke exactly ONE behavior
|
|
17
|
+
const total = cart.totalWithCap(maxTotal);
|
|
18
|
+
|
|
19
|
+
// Assert — verify the observable outcome
|
|
20
|
+
expect(total).toBe(400);
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Why it works:
|
|
25
|
+
|
|
26
|
+
- **One Act per test.** If you need two actions, you probably have two tests. A single Act
|
|
27
|
+
keeps failures unambiguous — you know exactly what broke.
|
|
28
|
+
- **Assertions only in Assert.** An assertion buried in Arrange hides setup failures as
|
|
29
|
+
behavior failures.
|
|
30
|
+
- **No logic in tests.** Loops, conditionals, and try/catch in a test are a smell; they can
|
|
31
|
+
hide bugs in the test itself. Prefer table-driven cases or parameterized tests instead.
|
|
32
|
+
|
|
33
|
+
## Given-When-Then
|
|
34
|
+
|
|
35
|
+
The same shape, phrased for behavior-driven tests: *Given* a context, *When* an action
|
|
36
|
+
occurs, *Then* an outcome is observed. Map Given→Arrange, When→Act, Then→Assert.
|
|
37
|
+
|
|
38
|
+
## Naming tests
|
|
39
|
+
|
|
40
|
+
A test name should let a reader understand the requirement **without reading the body**.
|
|
41
|
+
State the scenario and the expected outcome.
|
|
42
|
+
|
|
43
|
+
**Weak (restates the method):**
|
|
44
|
+
```
|
|
45
|
+
test('applyDiscount')
|
|
46
|
+
test('test refund')
|
|
47
|
+
test('works')
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Strong (states the behavior):**
|
|
51
|
+
```
|
|
52
|
+
test('subtracts a percentage discount and rounds to two decimals')
|
|
53
|
+
test('throws RangeError when the discount exceeds 100 percent')
|
|
54
|
+
test('returns empty array when no markets match the query')
|
|
55
|
+
test('falls back to substring search when Redis is unavailable')
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A useful template: **`<does X> when <condition Y>`** or **`<verb> <expected> given <state>`**.
|
|
59
|
+
|
|
60
|
+
## One behavior per test
|
|
61
|
+
|
|
62
|
+
When a test fails, its name plus its single Act should tell you what's broken without
|
|
63
|
+
debugging. Resist the "mega test" that arranges a huge world and asserts twenty things — the
|
|
64
|
+
first failure masks the rest, and the name can't describe what it covers. Split by behavior.
|
|
65
|
+
|
|
66
|
+
## Multiple assertions are fine — if they describe one behavior
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// OK: these three assertions all describe "creates an open order"
|
|
70
|
+
expect(order.id).toBeDefined();
|
|
71
|
+
expect(order.status).toBe('open');
|
|
72
|
+
expect(order.createdAt).toBeInstanceOf(Date);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The guideline is *one logical concept per test*, not literally one `expect`.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Mocking, Fixtures, and Determinism
|
|
2
|
+
|
|
3
|
+
Tests must be **isolated** (independent of each other) and **deterministic** (same result
|
|
4
|
+
every run). That means controlling everything that crosses a boundary or varies between runs:
|
|
5
|
+
time, randomness, network, filesystem, and shared collaborators.
|
|
6
|
+
|
|
7
|
+
## Test doubles — know which one you need
|
|
8
|
+
|
|
9
|
+
| Double | Purpose |
|
|
10
|
+
|---|---|
|
|
11
|
+
| **Dummy** | Filler passed to satisfy a signature; never used. |
|
|
12
|
+
| **Stub** | Returns canned answers to calls made during the test. |
|
|
13
|
+
| **Spy** | A stub that also records how it was called. |
|
|
14
|
+
| **Mock** | A double with pre-set expectations that verifies interactions. |
|
|
15
|
+
| **Fake** | A working but lightweight implementation (in-memory DB, fake clock). |
|
|
16
|
+
|
|
17
|
+
Reach for the **simplest** double that does the job. Prefer **fakes and stubs** over strict
|
|
18
|
+
mocks: asserting on exact call sequences couples the test to implementation and makes
|
|
19
|
+
refactoring break green tests.
|
|
20
|
+
|
|
21
|
+
## Mock at the boundary, only the boundary
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// GOOD: mock the HTTP client (a true external boundary)
|
|
25
|
+
const httpGet = vi.fn().mockResolvedValue({ status: 200, body: { rate: 1.1 } });
|
|
26
|
+
const rate = await fetchExchangeRate(httpGet, 'USD', 'EUR');
|
|
27
|
+
expect(rate).toBe(1.1);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// BAD: mocking the function under test leaves nothing real to verify
|
|
32
|
+
const fetchExchangeRate = vi.fn().mockResolvedValue(1.1); // tests the mock, not the code
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Over-mocking is a top cause of tests that pass while the system is broken. If a test is
|
|
36
|
+
almost entirely mock setup, you are testing the mock framework. Either widen the unit or move
|
|
37
|
+
the check to an integration test with a real collaborator.
|
|
38
|
+
|
|
39
|
+
## Controlling time
|
|
40
|
+
|
|
41
|
+
Wall-clock time is the most common flakiness source. Inject a clock or freeze it:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { vi } from 'vitest';
|
|
45
|
+
|
|
46
|
+
vi.useFakeTimers();
|
|
47
|
+
vi.setSystemTime(new Date('2026-06-29T12:00:00Z'));
|
|
48
|
+
|
|
49
|
+
expect(isExpired(token)).toBe(false);
|
|
50
|
+
|
|
51
|
+
vi.advanceTimersByTime(60_000); // jump 60s deterministically
|
|
52
|
+
expect(isExpired(token)).toBe(true);
|
|
53
|
+
|
|
54
|
+
vi.useRealTimers(); // always restore
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Never assert on `Date.now()` or sleep with real timeouts — that makes tests slow and racy.
|
|
58
|
+
|
|
59
|
+
## Controlling randomness
|
|
60
|
+
|
|
61
|
+
Seed the RNG or inject it. A test that depends on `Math.random()` passes-then-fails at random:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const rng = () => 0.42; // injected, deterministic
|
|
65
|
+
expect(pickWinner(players, rng)).toBe('alice');
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Fixtures and factories
|
|
69
|
+
|
|
70
|
+
- **Factories over literals.** A `makeOrder(overrides)` helper keeps tests readable and
|
|
71
|
+
resilient — when a required field is added, you change the factory once, not 50 tests.
|
|
72
|
+
- **Build only what the test needs.** Override just the fields relevant to the behavior;
|
|
73
|
+
let the factory default the rest. This makes the *intent* of each test obvious.
|
|
74
|
+
- **Isolate state.** Reset shared resources (DB rows, in-memory stores, module mocks) in
|
|
75
|
+
`beforeEach`/`afterEach` so tests can run in any order. Order-dependent tests are a bug.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
function makeOrder(overrides = {}) {
|
|
79
|
+
return { id: 'ord_1', status: 'open', total: 100, createdAt: new Date(0), ...overrides };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
it('marks a paid order as fulfilled', () => {
|
|
83
|
+
const order = makeOrder({ status: 'paid' }); // only the relevant field is set
|
|
84
|
+
expect(fulfill(order).status).toBe('fulfilled');
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Determinism checklist
|
|
89
|
+
|
|
90
|
+
- [ ] No real network, filesystem, or DB in unit tests (use fakes/stubs).
|
|
91
|
+
- [ ] Time and randomness are frozen or injected.
|
|
92
|
+
- [ ] No shared mutable state leaks between tests; setup/teardown resets it.
|
|
93
|
+
- [ ] All async work is awaited — no dangling promises or unhandled timers.
|
|
94
|
+
- [ ] The suite passes when run in a randomized order.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Test Types and the Test Pyramid
|
|
2
|
+
|
|
3
|
+
Not all tests are equal. Choosing the right *level* for a given behavior is the difference
|
|
4
|
+
between a suite that gives fast, reliable signal and one that is slow, flaky, and ignored.
|
|
5
|
+
|
|
6
|
+
## The pyramid
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
/\ E2E / UI few, slow, high-confidence, brittle
|
|
10
|
+
/ \
|
|
11
|
+
/----\ Integration some, medium speed, real collaborators
|
|
12
|
+
/ \
|
|
13
|
+
/--------\ Unit many, fast, isolated, cheap
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Push the bulk of your assertions **down** to the unit level where they are fast and stable.
|
|
17
|
+
Use the higher levels sparingly to prove the pieces wire together, not to re-test logic that
|
|
18
|
+
unit tests already cover. An inverted pyramid (mostly E2E) is slow and flaky.
|
|
19
|
+
|
|
20
|
+
## Unit tests
|
|
21
|
+
|
|
22
|
+
- **Scope:** one function, method, or class in isolation. Collaborators that cross a process
|
|
23
|
+
or I/O boundary are replaced with doubles.
|
|
24
|
+
- **Cost:** milliseconds. Run thousands on every save.
|
|
25
|
+
- **Owns:** business logic, pure functions, edge cases, error branches, algorithms.
|
|
26
|
+
- **Rule of thumb:** if a behavior can be tested at the unit level, test it there.
|
|
27
|
+
|
|
28
|
+
## Integration tests
|
|
29
|
+
|
|
30
|
+
- **Scope:** several real units working together, or one unit against a real adjacent
|
|
31
|
+
dependency (a database, an in-memory queue, a local HTTP server).
|
|
32
|
+
- **Cost:** tens to hundreds of milliseconds. Often need setup/teardown.
|
|
33
|
+
- **Owns:** the seams — ORM queries against a real schema, repository wiring, serialization,
|
|
34
|
+
framework request/response handling, transaction behavior.
|
|
35
|
+
- **Avoid:** re-testing branch logic here that a unit test already covers; keep these about
|
|
36
|
+
the *wiring*, not the *rules*.
|
|
37
|
+
|
|
38
|
+
## End-to-end (E2E) tests
|
|
39
|
+
|
|
40
|
+
- **Scope:** the whole system through its real entry point — a browser, a CLI, a public API.
|
|
41
|
+
- **Cost:** seconds. Slow, infrastructure-heavy, the most prone to flakiness.
|
|
42
|
+
- **Owns:** a handful of critical user journeys (sign up, checkout, the money path).
|
|
43
|
+
- **Hand off:** browser-driven E2E belongs to the **e2e-playwright** skill, not this agent.
|
|
44
|
+
|
|
45
|
+
## What goes where — a decision guide
|
|
46
|
+
|
|
47
|
+
| Question | Level |
|
|
48
|
+
|---|---|
|
|
49
|
+
| Does `applyDiscount(49.99, 10)` round correctly? | Unit |
|
|
50
|
+
| Does an invalid percentage throw `RangeError`? | Unit |
|
|
51
|
+
| Does the repository persist and re-read an order from Postgres? | Integration |
|
|
52
|
+
| Does `POST /orders` return `201` with a `Location` header? | Integration |
|
|
53
|
+
| Can a user add to cart and complete checkout in the browser? | E2E |
|
|
54
|
+
|
|
55
|
+
## Coverage as a signal, not a goal
|
|
56
|
+
|
|
57
|
+
The 80% target is a floor that flags untested regions, not a trophy. 100% line coverage with
|
|
58
|
+
assertion-free tests proves nothing. Chase **branch and behavior** coverage — every error
|
|
59
|
+
path and boundary — over raw line percentage. Use `scripts/check-coverage.mjs` to gate the
|
|
60
|
+
floor in CI, and read the uncovered lines to find the cases you forgot.
|