jorgex-stack 1.0.28 → 1.0.30
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 +17 -9
- package/dist/cli.js +28 -3
- package/package.json +1 -1
- package/stack/agents/orchestrator.md +33 -8
- package/stack/agents/test-analyzer.md +26 -62
- package/stack/agents/tester.md +52 -27
- package/stack/commands/claude-code/xreview.md +5 -0
- package/stack/commands/opencode/xreview.md +5 -0
- package/stack/hooks/hooks.json +1 -1
- package/stack/scripts/post-pr-review.cjs +103 -120
- package/stack/skills/agent-delegation/SKILL.md +2 -2
- package/stack/skills/tdd/SKILL.md +54 -72
- package/stack/skills/tdd/mocking.md +22 -47
- package/stack/skills/tdd/tests.md +42 -44
- package/stack/skills/to-prd/SKILL.md +10 -4
- package/stack/skills/work-lifecycle/SKILL.md +12 -0
- package/stack/skills/work-lifecycle/references/plan-template.md +12 -0
- package/stack/{commands/xreview.md → skills/xreview/SKILL.md} +9 -8
- package/stack/system-prompt/AGENTS.md +11 -4
- package/upstreams.json +2 -1
|
@@ -1,158 +1,141 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Global PostToolUse
|
|
3
|
+
* Global PostToolUse guardrail for the PR draft → ready lifecycle.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The historical filename is intentionally preserved so sync can migrate the
|
|
6
|
+
* existing hook entry instead of leaving an orphan in user configuration.
|
|
7
|
+
* The hook never infers success or PR state from command text: PostToolUse
|
|
8
|
+
* payloads are not consistent enough across runtimes to prove either.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
* Mirrors the `/xreview` command logic so both stay aligned: comment-fixer first
|
|
10
|
-
* (committed before the analysts), then the read-only analysts in parallel. 4R
|
|
11
|
-
* stays internal (Reliability / Resilience / Readability / Risk), not a separate
|
|
12
|
-
* report section, taxonomy, or extra agents.
|
|
13
|
-
*
|
|
14
|
-
* Payload compatibility (stdin JSON), so the same script works on every runtime:
|
|
10
|
+
* Payload compatibility (stdin JSON):
|
|
15
11
|
* - Claude Code hooks: { tool_name: "Bash", tool_input: { command: "..." }, cwd }
|
|
16
12
|
* - Codex hooks: { tool_name: "shell", tool_input: { command: [...] }, cwd }
|
|
17
13
|
* - OpenCode bridge: { tool: "bash", args: { command: "..." }, directory }
|
|
18
14
|
*
|
|
19
|
-
* Output
|
|
20
|
-
* - OpenCode bridge payload → plain message on stderr.
|
|
21
|
-
* - Claude Code / Codex payload → JSON additionalContext on stdout.
|
|
22
|
-
* Exit 0 always.
|
|
15
|
+
* Output uses one channel per runtime and the script always exits 0.
|
|
23
16
|
*/
|
|
24
17
|
|
|
25
|
-
const { execSync } = require('child_process');
|
|
26
|
-
const path = require('path');
|
|
27
|
-
|
|
28
|
-
const DEFAULT_BASE_BRANCH = 'main';
|
|
29
|
-
const BASE_BRANCH_SOURCE = Object.freeze({
|
|
30
|
-
GH: 'gh',
|
|
31
|
-
ORIGIN_HEAD: 'origin-head',
|
|
32
|
-
DEFAULT: 'default',
|
|
33
|
-
});
|
|
34
|
-
|
|
35
18
|
function writeWarning(message) {
|
|
36
19
|
process.stderr.write(`post-pr-review: warning: ${message}\n`);
|
|
37
20
|
}
|
|
38
21
|
|
|
39
|
-
function
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
22
|
+
function shellCommandSegments(command) {
|
|
23
|
+
const segments = [];
|
|
24
|
+
let tokens = [];
|
|
25
|
+
let token = "";
|
|
26
|
+
let quote = null;
|
|
27
|
+
let started = false;
|
|
28
|
+
|
|
29
|
+
const pushToken = () => {
|
|
30
|
+
if (!started) return;
|
|
31
|
+
tokens.push(token);
|
|
32
|
+
token = "";
|
|
33
|
+
started = false;
|
|
34
|
+
};
|
|
35
|
+
const pushSegment = () => {
|
|
36
|
+
pushToken();
|
|
37
|
+
if (tokens.length > 0) segments.push(tokens);
|
|
38
|
+
tokens = [];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (const character of command) {
|
|
42
|
+
if (quote !== null) {
|
|
43
|
+
if (character === quote) quote = null;
|
|
44
|
+
else token += character;
|
|
45
|
+
started = true;
|
|
46
|
+
} else if (character === '"' || character === "'") {
|
|
47
|
+
quote = character;
|
|
48
|
+
started = true;
|
|
49
|
+
} else if (character === "\r" || character === "\n") {
|
|
50
|
+
pushSegment();
|
|
51
|
+
} else if (/\s/.test(character)) {
|
|
52
|
+
pushToken();
|
|
53
|
+
} else if (character === ";" || character === "&" || character === "|") {
|
|
54
|
+
pushSegment();
|
|
55
|
+
} else {
|
|
56
|
+
token += character;
|
|
57
|
+
started = true;
|
|
58
|
+
}
|
|
55
59
|
}
|
|
60
|
+
pushSegment();
|
|
61
|
+
return segments;
|
|
62
|
+
}
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
};
|
|
64
|
+
function skipRepoOptions(tokens, start) {
|
|
65
|
+
let index = start;
|
|
66
|
+
while (index < tokens.length) {
|
|
67
|
+
const option = tokens[index];
|
|
68
|
+
if (option === "-R" || option === "--repo") {
|
|
69
|
+
if (tokens[index + 1] === undefined) return false;
|
|
70
|
+
index += 2;
|
|
71
|
+
} else if (/^(?:-R|--repo=).+/i.test(option)) {
|
|
72
|
+
index += 1;
|
|
73
|
+
} else {
|
|
74
|
+
break;
|
|
69
75
|
}
|
|
70
|
-
} catch {
|
|
71
|
-
// ignore
|
|
72
76
|
}
|
|
77
|
+
return index;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isLifecycleSegment(tokens) {
|
|
81
|
+
if (!/(?:^|[\\/])gh(?:\.exe)?$/i.test(tokens[0] ?? "")) return false;
|
|
82
|
+
|
|
83
|
+
let index = skipRepoOptions(tokens, 1);
|
|
84
|
+
if (tokens[index]?.toLowerCase() !== "pr") return false;
|
|
85
|
+
index = skipRepoOptions(tokens, index + 1);
|
|
73
86
|
|
|
74
|
-
|
|
87
|
+
const action = tokens[index]?.toLowerCase();
|
|
88
|
+
return action === "create" || action === "ready";
|
|
75
89
|
}
|
|
76
90
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
function isPrLifecycleCommand(command) {
|
|
92
|
+
const segments = Array.isArray(command)
|
|
93
|
+
? [command.map(String)]
|
|
94
|
+
: shellCommandSegments(String(command));
|
|
95
|
+
return segments.some(isLifecycleSegment);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const message = `<pr-lifecycle-state-required>
|
|
99
|
+
A \`gh pr create\` or \`gh pr ready\` command was attempted. Do not infer success or PR state from the command text. Resolve the current PR and run \`gh pr view --json number,isDraft,headRefOid\` before the next action.
|
|
100
|
+
|
|
101
|
+
- If the PR should still be under development, it must be draft. If it is ready, run \`gh pr ready --undo <number>\` before any change or push.
|
|
102
|
+
- While draft, finish code, the applicable version bump, local tests, \`pnpm qa:quality\` when defined, Vercel preview review when applicable, final diff inspection, and the full review on the candidate SHA.
|
|
103
|
+
- If the PR is actually ready, do not push. If the project has PR checks configured, wait for the complete Quality Gates, run \`gh pr checks <number>\`, and verify the checked headRefOid is the candidate SHA.
|
|
104
|
+
- If no PR checks are configured, confirm that from project configuration such as workflows, rulesets or integrations, and record it; their absence does not block the merge. An empty \`gh pr checks\` result immediately after ready is not evidence that no checks are configured.
|
|
105
|
+
- Immediately before reporting or merging, compare \`gh pr view --json headRefOid\` with the recorded candidate SHA. Merge still requires explicit user approval.
|
|
106
|
+
</pr-lifecycle-state-required>`;
|
|
107
|
+
|
|
108
|
+
let raw = "";
|
|
109
|
+
process.stdin.setEncoding("utf8");
|
|
110
|
+
process.stdin.on("data", (chunk) => (raw += chunk));
|
|
111
|
+
process.stdin.on("end", () => {
|
|
81
112
|
let data = {};
|
|
82
113
|
try {
|
|
83
|
-
data = JSON.parse(raw ||
|
|
114
|
+
data = JSON.parse(raw || "{}");
|
|
84
115
|
} catch {
|
|
85
|
-
writeWarning(
|
|
116
|
+
writeWarning("invalid JSON payload; skipping PR lifecycle hook.");
|
|
86
117
|
process.exit(0);
|
|
87
118
|
}
|
|
88
119
|
|
|
89
|
-
const toolName = String(data.tool_name || data.tool ||
|
|
90
|
-
const
|
|
91
|
-
const commandValue = data?.tool_input?.command ?? data?.args?.command ??
|
|
92
|
-
|
|
93
|
-
const rawToolCommand = Array.isArray(commandValue) ? commandValue.join(' ') : String(commandValue);
|
|
94
|
-
const toolCommand = rawToolCommand.toLowerCase();
|
|
95
|
-
|
|
96
|
-
if (!SHELL_TOOLS.includes(toolName) || !toolCommand.includes('gh pr create')) {
|
|
120
|
+
const toolName = String(data.tool_name || data.tool || "").toLowerCase();
|
|
121
|
+
const shellTools = ["bash", "shell", "local_shell", "powershell"];
|
|
122
|
+
const commandValue = data?.tool_input?.command ?? data?.args?.command ?? "";
|
|
123
|
+
if (!shellTools.includes(toolName) || !isPrLifecycleCommand(commandValue)) {
|
|
97
124
|
process.exit(0);
|
|
98
125
|
}
|
|
99
126
|
|
|
100
|
-
const scriptDir = __dirname;
|
|
101
|
-
const projectDir = data.cwd || data.directory || path.resolve(scriptDir, '..');
|
|
102
|
-
const resolution = resolveBaseBranch(projectDir);
|
|
103
|
-
const baseBranch = resolution.branch;
|
|
104
|
-
const baseRef = formatGitRef(baseBranch);
|
|
105
|
-
const diffScope = `git diff ${baseRef}...HEAD`;
|
|
106
|
-
const isConfirmed = resolution.source === BASE_BRANCH_SOURCE.GH;
|
|
107
|
-
const baseSummary = isConfirmed
|
|
108
|
-
? `BASE (PR target, confirmed via gh pr view): ${baseBranch}`
|
|
109
|
-
: `BASE (PR target, NOT confirmed — fallback guess): ${baseBranch}. Before anything else, re-resolve it yourself: run \`gh pr view --json baseRefName --jq .baseRefName\` (the PR exists now; this hook may have raced its creation) and use THAT as BASE. Keep ${baseBranch} only if it still fails. Work is often done in sub-branches whose PR does NOT target the default branch — reviewing against the wrong BASE produces a huge, useless diff.`;
|
|
110
|
-
|
|
111
|
-
const message = `<post-pr-review-required>
|
|
112
|
-
A PR was just created. Run a conditional multi-agent review BEFORE reporting back to the user.
|
|
113
|
-
|
|
114
|
-
${baseSummary}
|
|
115
|
-
HEAD: the current branch / worktree (resolve with \`git rev-parse --abbrev-ref HEAD\`).
|
|
116
|
-
|
|
117
|
-
1. Routing only (lightweight): list changed file NAMES with \`${diffScope} --name-only\` to decide which subagents apply. Do NOT load the full diff into your own context.
|
|
118
|
-
|
|
119
|
-
Sanity check: if that list is far larger than the work just done (hundreds of files, unrelated areas), BASE is almost certainly wrong — STOP, re-resolve the PR base with \`gh pr view\`, and only continue when the diff matches the actual work.
|
|
120
|
-
|
|
121
|
-
2. Comment pass FIRST (conditional): if the diff adds or changes comments/docstrings, run comment-fixer ALONE before the analysts — it edits comments in place (comments only, never code). If it changed anything, comment-fixer itself never commits — YOU commit its fixes to the PR branch BEFORE launching the analysts, staging ONLY the files it touched (never -a/-A: don't sweep unrelated working-tree changes), so the diff they fetch is already clean of comment noise; push them together with whatever the review produces, or on their own if nothing else needs fixing. If the commit can't be made, leave the edits uncommitted and say so in the report. If the diff touches no comments, skip it.
|
|
122
|
-
|
|
123
|
-
3. The remaining subagents are CONDITIONAL, read-only, and each fetches its OWN diff. Launch in PARALLEL (with your runtime's delegation mechanism) ONLY the relevant ones, passing each EXACTLY the BASE and HEAD branches and the instruction: review only \`${diffScope}\` — never assume \`main\`, use the BASE/HEAD given.
|
|
124
|
-
- test-analyzer — only if the diff touches tests or code that should be tested
|
|
125
|
-
- silent-failure-hunter — only if the diff includes error handling, try/catch, fallbacks, or async flows
|
|
126
|
-
- type-design-analyzer — only if the diff changes types, interfaces, schemas, or public contracts
|
|
127
|
-
- code-reviewer — for general code quality whenever non-trivial source code changed
|
|
128
|
-
- code-simplifier — only if the diff introduces complexity worth simplifying; this is the lean/anti-bloat pass for diffs and PRs
|
|
129
|
-
- security-auditor — only if the diff touches auth, authorization, permissions, secrets/credentials, sensitive data, input validation, webhooks, or other security-critical flows
|
|
130
|
-
|
|
131
|
-
If none of a subagent's triggers are present, skip it. Always state which subagents ran and which were skipped and why.
|
|
132
|
-
|
|
133
|
-
4. After the relevant subagents complete, synthesize a unified report:
|
|
134
|
-
Use 4R internally (Reliability / Resilience / Readability / Risk) as a checklist while synthesizing; do not add a separate 4R section or taxonomy to the final report.
|
|
135
|
-
- BASE and HEAD used
|
|
136
|
-
- Subagents run vs skipped (with reason)
|
|
137
|
-
- Critical Issues (must fix)
|
|
138
|
-
- Important Improvements (should fix)
|
|
139
|
-
- Suggestions (nice to have)
|
|
140
|
-
- Changes already applied (comment fixes: committed to the PR branch, or left uncommitted for working-tree reviews)
|
|
141
|
-
- Positive Findings
|
|
142
|
-
</post-pr-review-required>`;
|
|
143
|
-
|
|
144
|
-
// Un solo canal por runtime: el bridge de OpenCode recoge stdout Y stderr,
|
|
145
|
-
// así que emitir por ambos duplicaría el mensaje.
|
|
146
127
|
const isOpenCodeBridge = data.tool !== undefined && data.tool_name === undefined;
|
|
147
128
|
if (isOpenCodeBridge) {
|
|
148
|
-
process.stderr.write(message
|
|
129
|
+
process.stderr.write(`${message}\n`);
|
|
149
130
|
} else {
|
|
150
|
-
// Claude Code / Codex PostToolUse leen additionalContext del stdout JSON.
|
|
151
131
|
process.stdout.write(
|
|
152
|
-
JSON.stringify({
|
|
132
|
+
`${JSON.stringify({
|
|
153
133
|
additionalContext: message,
|
|
154
|
-
hookSpecificOutput: {
|
|
155
|
-
|
|
134
|
+
hookSpecificOutput: {
|
|
135
|
+
hookEventName: "PostToolUse",
|
|
136
|
+
additionalContext: message,
|
|
137
|
+
},
|
|
138
|
+
})}\n`,
|
|
156
139
|
);
|
|
157
140
|
}
|
|
158
141
|
process.exit(0);
|
|
@@ -23,7 +23,7 @@ Importante sobre el mecanismo:
|
|
|
23
23
|
| Agente | Scope | Delega aquí cuando aparezca... |
|
|
24
24
|
|---|---|---|
|
|
25
25
|
| `implementer` | escribe código de producción | falta código para que algo funcione; hay que implementar el cambio real |
|
|
26
|
-
| `tester` | escribe/ejecuta tests |
|
|
26
|
+
| `tester` | decide/escribe/ejecuta tests según riesgo | hay que decidir la protección adecuada, falta un test valioso, hay tests rotos por un cambio de contrato, o hay que verificar comportamiento |
|
|
27
27
|
| `translator` | traducciones, locales, multiidioma | strings hardcodeadas visibles, locales desincronizados, copy en varios idiomas |
|
|
28
28
|
| `docs-maintainer` | documentación (/docs y docs site público) | el cambio deja docs desactualizadas o requiere nueva documentación |
|
|
29
29
|
| `backend-analyst` | análisis backend (read-only) | hace falta mapear servicios, DB, APIs o riesgos backend antes de actuar |
|
|
@@ -54,7 +54,7 @@ Una línea por delegación, al final de tu output:
|
|
|
54
54
|
- `tester` detecta que falta código de producción → `implementer`
|
|
55
55
|
- cualquier agente detecta auth, permisos o datos sensibles → `security-auditor`
|
|
56
56
|
- cualquier agente detecta cambio documental relevante → `docs-maintainer`
|
|
57
|
-
- `test-analyzer` detecta
|
|
57
|
+
- `test-analyzer` detecta un gap de riesgo concreto → `tester` (que decide si añade, actualiza o reutiliza cobertura)
|
|
58
58
|
|
|
59
59
|
## Regla de conflicto
|
|
60
60
|
|
|
@@ -1,109 +1,91 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tdd
|
|
3
|
-
description:
|
|
3
|
+
description: Risk-based test-driven development with a red-green-refactor loop. Use for business rules, bugs/regressions, public contracts, invariants, or explicit test-first work; not automatically for styles, wiring, or mechanical changes.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Test-Driven Development
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Core principle
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Tests protect behavior and risk, not files, layers, or coverage percentages. A change needs a **testing decision**, not automatically a new test.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
For every change, establish:
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
1. **Risk** — what meaningful failure could this change introduce?
|
|
15
|
+
2. **Existing protection** — which existing test already catches it, if any?
|
|
16
|
+
3. **New behavior** — what changed contract or regression needs new protection?
|
|
17
|
+
4. **Seam** — what is the strongest test closest to that risk?
|
|
18
|
+
5. **Decision** — add/update a test, reuse existing coverage, or add no test with a concrete reason.
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
One behavior should normally have one authoritative test. Test it again at another layer only when that layer protects a distinct contract.
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for boundary-double guidance.
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
## When to use TDD
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
Use red-green-refactor when the change defines or repairs:
|
|
23
27
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
+
- Business rules, calculations, validation, dates, or time zones
|
|
29
|
+
- A real bug/regression
|
|
30
|
+
- Public API, event, schema, or protocol contracts
|
|
31
|
+
- Authentication, authorization, RLS, tenant separation, billing, privacy, or data integrity
|
|
32
|
+
- Destructive, concurrent, atomic, or idempotent behavior
|
|
33
|
+
- Important accessibility or user interactions
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
Do not impose TDD merely because a file changed. Styling, decorative DOM, wiring, aliases, wrappers, generated code, mechanical refactors, and trivial callbacks usually need existing verification or no new test unless they change meaningful behavior.
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
WRONG (horizontal):
|
|
33
|
-
RED: test1, test2, test3, test4, test5
|
|
34
|
-
GREEN: impl1, impl2, impl3, impl4, impl5
|
|
35
|
-
|
|
36
|
-
RIGHT (vertical):
|
|
37
|
-
RED→GREEN: test1→impl1
|
|
38
|
-
RED→GREEN: test2→impl2
|
|
39
|
-
RED→GREEN: test3→impl3
|
|
40
|
-
...
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Workflow
|
|
37
|
+
## Choose the seam from the risk
|
|
44
38
|
|
|
45
|
-
|
|
39
|
+
Use the cheapest seam that can fail for the real regression:
|
|
46
40
|
|
|
47
|
-
|
|
41
|
+
- Pure rule or calculation → focused unit/module test
|
|
42
|
+
- Component interaction or accessibility contract → component/browser test through stable semantics
|
|
43
|
+
- Persistence, SQL, RLS, migration, or data-transaction atomicity → real database/integration test
|
|
44
|
+
- Other concurrency or atomicity → execute at the implicated filesystem, queue, process, or shared-state boundary
|
|
45
|
+
- Public endpoint or privileged function → contract/integration test at that boundary
|
|
46
|
+
- Critical cross-system user journey → end-to-end test
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
“Integration-style” is not inherently stronger. A broad test full of mocks may be weaker than a focused rule test, while a regex over SQL text is weaker than executing the database behavior it claims to protect.
|
|
50
49
|
|
|
51
|
-
-
|
|
52
|
-
- [ ] Confirm with user which behaviors to test (prioritize)
|
|
53
|
-
- [ ] Identify opportunities for [deep modules](deep-modules.md) (small interface, deep implementation)
|
|
54
|
-
- [ ] Design interfaces for [testability](interface-design.md)
|
|
55
|
-
- [ ] List the behaviors to test (not implementation steps)
|
|
56
|
-
- [ ] Get user approval on the plan
|
|
50
|
+
## Anti-pattern: horizontal slices
|
|
57
51
|
|
|
58
|
-
|
|
52
|
+
Do not write all tests first and then all implementation. This outruns what has been learned and encourages tests of imagined shapes.
|
|
59
53
|
|
|
60
|
-
|
|
54
|
+
Use vertical tracer bullets for each behavior that merits new protection:
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
RED: Write test for first behavior → test fails
|
|
68
|
-
GREEN: Write minimal code to pass → test passes
|
|
56
|
+
```text
|
|
57
|
+
RED → one test fails for the intended behavioral reason
|
|
58
|
+
GREEN → minimal production change makes it pass
|
|
59
|
+
REFACTOR → improve structure while behavior stays green
|
|
69
60
|
```
|
|
70
61
|
|
|
71
|
-
|
|
62
|
+
Then repeat for the next distinct behavior. Do not create separate tests merely to split assertions that describe one coherent outcome.
|
|
72
63
|
|
|
73
|
-
|
|
64
|
+
## Workflow
|
|
74
65
|
|
|
75
|
-
|
|
66
|
+
### 1. Make the testing decision
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
RED: Write next test → fails
|
|
79
|
-
GREEN: Minimal code to pass → passes
|
|
80
|
-
```
|
|
68
|
+
Complete the five-part decision under **Core principle**. If no new protection is warranted, record the reason and run the cheapest sufficient verification; otherwise continue to RED.
|
|
81
69
|
|
|
82
|
-
|
|
70
|
+
### 2. RED, when new protection is warranted
|
|
83
71
|
|
|
84
|
-
|
|
85
|
-
- Only enough code to pass current test
|
|
86
|
-
- Don't anticipate future tests
|
|
87
|
-
- Keep tests focused on observable behavior
|
|
72
|
+
Write one test that fails because the behavior is missing or broken—not because setup, mocks, or fixtures are wrong.
|
|
88
73
|
|
|
89
|
-
###
|
|
74
|
+
### 3. GREEN
|
|
90
75
|
|
|
91
|
-
|
|
76
|
+
Write only enough production code to satisfy the behavior. Do not anticipate speculative cases.
|
|
92
77
|
|
|
93
|
-
|
|
94
|
-
- [ ] Deepen modules (move complexity behind simple interfaces)
|
|
95
|
-
- [ ] Apply SOLID principles where natural
|
|
96
|
-
- [ ] Consider what new code reveals about existing code
|
|
97
|
-
- [ ] Run tests after each refactor step
|
|
78
|
+
### 4. Refactor
|
|
98
79
|
|
|
99
|
-
|
|
80
|
+
Refactor only while green. Remove duplication in production and tests, and delete lower-value tests when a stronger test now protects the same behavior.
|
|
100
81
|
|
|
101
|
-
## Checklist
|
|
82
|
+
## Checklist
|
|
102
83
|
|
|
103
|
-
```
|
|
104
|
-
[ ]
|
|
105
|
-
[ ]
|
|
106
|
-
[ ]
|
|
107
|
-
[ ]
|
|
108
|
-
[ ] No
|
|
84
|
+
```text
|
|
85
|
+
[ ] When new protection is warranted, RED fails for the intended behavioral reason
|
|
86
|
+
[ ] The chosen seam observes behavior or the real boundary at risk
|
|
87
|
+
[ ] Another layer would protect a distinct contract, not duplicate this one
|
|
88
|
+
[ ] Mocks do not encode internal call choreography
|
|
89
|
+
[ ] No-test decisions have a concrete trivial/mechanical/already-covered reason
|
|
90
|
+
[ ] Production code is minimal and non-speculative
|
|
109
91
|
```
|
|
@@ -1,59 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Boundary Doubles and Mocks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Mocks are a cost/risk tradeoff, not a goal or a categorical ban.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- Databases (sometimes - prefer test DB)
|
|
7
|
-
- Time/randomness
|
|
8
|
-
- File system (sometimes)
|
|
5
|
+
## Prefer real behavior when practical
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
Use real owned code when it is fast, deterministic, safe, and easy to set up. Mocking internal collaborators just to assert call choreography couples the test to implementation and can let broken behavior pass.
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
- Internal collaborators
|
|
14
|
-
- Anything you control
|
|
9
|
+
Prefer real test infrastructure when the risk lives there:
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
- RLS, SQL, migrations, transactions, and data-transaction atomicity → test database
|
|
12
|
+
- Other concurrency or atomicity → the actual filesystem, queue, process, or shared-state boundary
|
|
13
|
+
- Filesystem semantics → isolated temp directory
|
|
14
|
+
- Serialization/protocol parsing → real encoder/decoder
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
## Use a boundary double when it is the reliable seam
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
A fake, stub, or mock is appropriate for a boundary that is unavailable, expensive, nondeterministic, destructive, or controlled by a third party:
|
|
21
19
|
|
|
22
|
-
|
|
20
|
+
- Payment, email, identity, or other external APIs
|
|
21
|
+
- Time, randomness, process execution, or network failures
|
|
22
|
+
- A slow service when its protocol—not its implementation—is the contract under test
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
// Easy to mock
|
|
26
|
-
function processPayment(order, paymentClient) {
|
|
27
|
-
return paymentClient.charge(order.total);
|
|
28
|
-
}
|
|
24
|
+
Assert only the boundary contract needed by the behavior: payload, headers, idempotency key, returned error mapping, or observable result. Avoid exhaustive call counts and ordering unless the external protocol requires them.
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
function processPayment(order) {
|
|
32
|
-
const client = new StripeClient(process.env.STRIPE_KEY);
|
|
33
|
-
return client.charge(order.total);
|
|
34
|
-
}
|
|
35
|
-
```
|
|
26
|
+
## Keep doubles simple
|
|
36
27
|
|
|
37
|
-
|
|
28
|
+
- Inject the narrow boundary instead of mocking a large internal module graph.
|
|
29
|
+
- Return one explicit shape per scenario.
|
|
30
|
+
- Do not rebuild production branching logic inside the mock.
|
|
31
|
+
- If every important collaborator is mocked, do not call the suite integration testing.
|
|
32
|
+
- Prefer a reusable fake only after repeated real need; do not create abstraction for a single test.
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
// GOOD: Each function is independently mockable
|
|
43
|
-
const api = {
|
|
44
|
-
getUser: (id) => fetch(`/users/${id}`),
|
|
45
|
-
getOrders: (userId) => fetch(`/users/${userId}/orders`),
|
|
46
|
-
createOrder: (data) => fetch('/orders', { method: 'POST', body: data }),
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// BAD: Mocking requires conditional logic inside the mock
|
|
50
|
-
const api = {
|
|
51
|
-
fetch: (endpoint, options) => fetch(endpoint, options),
|
|
52
|
-
};
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
The SDK approach means:
|
|
56
|
-
- Each mock returns one specific shape
|
|
57
|
-
- No conditional logic in test setup
|
|
58
|
-
- Easier to see which endpoints a test exercises
|
|
59
|
-
- Type safety per endpoint
|
|
34
|
+
The question is not “can this be mocked?” It is “which setup gives the strongest evidence for this risk at acceptable cost?”
|
|
@@ -1,61 +1,59 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Choosing Valuable Tests
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## One behavior, one authoritative seam
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Choose the seam from the regression you need to catch.
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
|
-
//
|
|
9
|
-
test("
|
|
10
|
-
|
|
11
|
-
cart.add(product);
|
|
12
|
-
const result = await checkout(cart, paymentMethod);
|
|
13
|
-
expect(result.status).toBe("confirmed");
|
|
8
|
+
// Pure pricing rule: a focused module test is closest to the risk.
|
|
9
|
+
test("applies the reduced tax rate to eligible items", () => {
|
|
10
|
+
expect(calculateTax(eligibleItem)).toBe(4.2);
|
|
14
11
|
});
|
|
15
12
|
```
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
```typescript
|
|
15
|
+
// User interaction: verify the accessible outcome, not DOM decoration.
|
|
16
|
+
test("submits a valid checkout", async () => {
|
|
17
|
+
await user.click(screen.getByRole("button", { name: "Pay" }));
|
|
18
|
+
expect(await screen.findByText("Payment confirmed")).toBeVisible();
|
|
19
|
+
});
|
|
20
|
+
```
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- One logical assertion per test
|
|
22
|
+
```sql
|
|
23
|
+
-- Tenant isolation: execute against a real test database with two users.
|
|
24
|
+
-- Regex matching a CREATE POLICY statement does not prove RLS behavior.
|
|
25
|
+
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Characteristics of valuable tests:
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
- Catch a concrete user, business, security, data, or contract regression
|
|
30
|
+
- Observe a public interface or the real boundary at risk
|
|
31
|
+
- Survive an internal refactor
|
|
32
|
+
- Use the narrowest reliable setup
|
|
33
|
+
- Add a second layer only for a different contract
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
// BAD: Tests implementation details
|
|
31
|
-
test("checkout calls paymentService.process", async () => {
|
|
32
|
-
const mockPayment = jest.mock(paymentService);
|
|
33
|
-
await checkout(cart, payment);
|
|
34
|
-
expect(mockPayment.process).toHaveBeenCalledWith(cart.total);
|
|
35
|
-
});
|
|
36
|
-
```
|
|
35
|
+
## Low-value and redundant tests
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
Avoid tests whose only purpose is to assert:
|
|
39
38
|
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
-
|
|
39
|
+
- Tailwind classes, decorative DOM, or incidental markup
|
|
40
|
+
- That a wrapper, alias, constant, callback, or function exists
|
|
41
|
+
- Exact internal call counts/order when the observable result is what matters
|
|
42
|
+
- The same behavior already protected at a stronger seam
|
|
43
|
+
- SQL policy or migration correctness exclusively through text/regex shape
|
|
44
|
+
- “Integration” while every important collaborator is mocked
|
|
46
45
|
|
|
47
46
|
```typescript
|
|
48
|
-
// BAD:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const row = await db.query("SELECT * FROM users WHERE name = ?", ["Alice"]);
|
|
52
|
-
expect(row).toBeDefined();
|
|
53
|
-
});
|
|
47
|
+
// BAD: locks internal choreography.
|
|
48
|
+
expect(paymentService.charge).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(emailService.send).toHaveBeenCalledAfter(paymentService.charge);
|
|
54
50
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
const user = await createUser({ name: "Alice" });
|
|
58
|
-
const retrieved = await getUser(user.id);
|
|
59
|
-
expect(retrieved.name).toBe("Alice");
|
|
60
|
-
});
|
|
51
|
+
// BETTER: assert the contract callers rely on.
|
|
52
|
+
expect(result).toMatchObject({ status: "confirmed", receiptId: expect.any(String) });
|
|
61
53
|
```
|
|
54
|
+
|
|
55
|
+
Exact calls are valid only when the call itself is the external contract—for example, the precise payload sent to a payment provider or an idempotency key required by its protocol.
|
|
56
|
+
|
|
57
|
+
## Valid no-new-test decisions
|
|
58
|
+
|
|
59
|
+
A change may need no new test when it is styling-only, mechanical, generated, already covered by an authoritative test, or has no meaningful behavioral branch. State the reason and run the cheapest existing verification that could catch an accidental break.
|