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.
@@ -1,158 +1,141 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Global PostToolUse hook: gh pr create
3
+ * Global PostToolUse guardrail for the PR draft → ready lifecycle.
4
4
  *
5
- * Injects a conditional multi-agent review request after a PR is created.
6
- * Generic and project-agnostic: no version bump, no React Doctor, no project paths.
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
- * The review subagents are CONDITIONAL — only the ones relevant to the diff run.
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 (single channel per runtime, so the bridge never duplicates it):
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 formatGitRef(value) {
40
- const ref = String(value);
41
- return /^[A-Za-z0-9._/-]+$/.test(ref) ? ref : `'${ref.replace(/'/g, "'\\''")}'`;
42
- }
43
-
44
- function resolveBaseBranch(cwd) {
45
- try {
46
- const base = execSync('gh pr view --json baseRefName --jq .baseRefName', {
47
- cwd,
48
- encoding: 'utf8',
49
- stdio: ['ignore', 'pipe', 'ignore'],
50
- timeout: 8_000,
51
- }).trim();
52
- if (base) return { branch: base, source: BASE_BRANCH_SOURCE.GH };
53
- } catch {
54
- // PR not yet discoverable; fall back below.
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
- try {
58
- const originHead = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
59
- cwd,
60
- encoding: 'utf8',
61
- stdio: ['ignore', 'pipe', 'ignore'],
62
- timeout: 10_000,
63
- }).trim();
64
- if (originHead) {
65
- return {
66
- branch: originHead.replace(/^refs\/remotes\/origin\//, ''),
67
- source: BASE_BRANCH_SOURCE.ORIGIN_HEAD,
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
- return { branch: DEFAULT_BASE_BRANCH, source: BASE_BRANCH_SOURCE.DEFAULT };
87
+ const action = tokens[index]?.toLowerCase();
88
+ return action === "create" || action === "ready";
75
89
  }
76
90
 
77
- let raw = '';
78
- process.stdin.setEncoding('utf8');
79
- process.stdin.on('data', (chunk) => (raw += chunk));
80
- process.stdin.on('end', () => {
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('invalid JSON payload; skipping review hook.');
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 || '').toLowerCase();
90
- const SHELL_TOOLS = ['bash', 'shell', 'local_shell', 'powershell'];
91
- const commandValue = data?.tool_input?.command ?? data?.args?.command ?? '';
92
- // Codex passes the shell command as an argv array; the rest as a string.
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 + '\n');
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: { hookEventName: 'PostToolUse', additionalContext: message },
155
- }) + '\n',
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 | faltan tests, hay tests rotos por un cambio de contrato, o hay que verificar comportamiento |
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 gaps de cobertura → `tester` (que los escribe)
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: Test-driven development with red-green-refactor loop. Use when user wants to build features or fix bugs using TDD, mentions "red-green-refactor", wants integration tests, or asks for test-first development.
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
- ## Philosophy
8
+ ## Core principle
9
9
 
10
- **Core principle**: Tests should verify behavior through public interfaces, not implementation details. Code can change entirely; tests shouldn't.
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
- **Good tests** are integration-style: they exercise real code paths through public APIs. They describe _what_ the system does, not _how_ it does it. A good test reads like a specification - "user can checkout with valid cart" tells you exactly what capability exists. These tests survive refactors because they don't care about internal structure.
12
+ For every change, establish:
13
13
 
14
- **Bad tests** are coupled to implementation. They mock internal collaborators, test private methods, or verify through external means (like querying a database directly instead of using the interface). The warning sign: your test breaks when you refactor, but behavior hasn't changed. If you rename an internal function and tests fail, those tests were testing implementation, not behavior.
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
- See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for mocking guidelines.
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
- ## Anti-Pattern: Horizontal Slices
22
+ See [tests.md](tests.md) for examples and [mocking.md](mocking.md) for boundary-double guidance.
19
23
 
20
- **DO NOT write all tests first, then all implementation.** This is "horizontal slicing" - treating RED as "write all tests" and GREEN as "write all code."
24
+ ## When to use TDD
21
25
 
22
- This produces **crap tests**:
26
+ Use red-green-refactor when the change defines or repairs:
23
27
 
24
- - Tests written in bulk test _imagined_ behavior, not _actual_ behavior
25
- - You end up testing the _shape_ of things (data structures, function signatures) rather than user-facing behavior
26
- - Tests become insensitive to real changes - they pass when behavior breaks, fail when behavior is fine
27
- - You outrun your headlights, committing to test structure before understanding the implementation
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
- **Correct approach**: Vertical slices via tracer bullets. One test one implementation repeat. Each test responds to what you learned from the previous cycle. Because you just wrote the code, you know exactly what behavior matters and how to verify it.
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
- ### 1. Planning
39
+ Use the cheapest seam that can fail for the real regression:
46
40
 
47
- When exploring the codebase, use the project's domain glossary so that test names and interface vocabulary match the project's language, and respect ADRs in the area you're touching.
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
- Before writing any code:
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
- - [ ] Confirm with user what interface changes are needed
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
- Ask: "What should the public interface look like? Which behaviors are most important to test?"
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
- **You can't test everything.** Confirm with the user exactly which behaviors matter most. Focus testing effort on critical paths and complex logic, not every possible edge case.
54
+ Use vertical tracer bullets for each behavior that merits new protection:
61
55
 
62
- ### 2. Tracer Bullet
63
-
64
- Write ONE test that confirms ONE thing about the system:
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
- This is your tracer bullet - proves the path works end-to-end.
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
- ### 3. Incremental Loop
64
+ ## Workflow
74
65
 
75
- For each remaining behavior:
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
- Rules:
70
+ ### 2. RED, when new protection is warranted
83
71
 
84
- - One test at a time
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
- ### 4. Refactor
74
+ ### 3. GREEN
90
75
 
91
- After all tests pass, look for [refactor candidates](refactoring.md):
76
+ Write only enough production code to satisfy the behavior. Do not anticipate speculative cases.
92
77
 
93
- - [ ] Extract duplication
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
- **Never refactor while RED.** Get to GREEN first.
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 Per Cycle
82
+ ## Checklist
102
83
 
103
- ```
104
- [ ] Test describes behavior, not implementation
105
- [ ] Test uses public interface only
106
- [ ] Test would survive internal refactor
107
- [ ] Code is minimal for this test
108
- [ ] No speculative features added
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
- # When to Mock
1
+ # Boundary Doubles and Mocks
2
2
 
3
- Mock at **system boundaries** only:
3
+ Mocks are a cost/risk tradeoff, not a goal or a categorical ban.
4
4
 
5
- - External APIs (payment, email, etc.)
6
- - Databases (sometimes - prefer test DB)
7
- - Time/randomness
8
- - File system (sometimes)
5
+ ## Prefer real behavior when practical
9
6
 
10
- Don't mock:
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
- - Your own classes/modules
13
- - Internal collaborators
14
- - Anything you control
9
+ Prefer real test infrastructure when the risk lives there:
15
10
 
16
- ## Designing for Mockability
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
- At system boundaries, design interfaces that are easy to mock:
16
+ ## Use a boundary double when it is the reliable seam
19
17
 
20
- **1. Use dependency injection**
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
- Pass external dependencies in rather than creating them internally:
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
- ```typescript
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
- // Hard to mock
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
- **2. Prefer SDK-style interfaces over generic fetchers**
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
- Create specific functions for each external operation instead of one generic function with conditional logic:
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
- # Good and Bad Tests
1
+ # Choosing Valuable Tests
2
2
 
3
- ## Good Tests
3
+ ## One behavior, one authoritative seam
4
4
 
5
- **Integration-style**: Test through real interfaces, not mocks of internal parts.
5
+ Choose the seam from the regression you need to catch.
6
6
 
7
7
  ```typescript
8
- // GOOD: Tests observable behavior
9
- test("user can checkout with valid cart", async () => {
10
- const cart = createCart();
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
- Characteristics:
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
- - Tests behavior users/callers care about
20
- - Uses public API only
21
- - Survives internal refactors
22
- - Describes WHAT, not HOW
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
- ## Bad Tests
27
+ Characteristics of valuable tests:
26
28
 
27
- **Implementation-detail tests**: Coupled to internal structure.
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
- ```typescript
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
- Red flags:
37
+ Avoid tests whose only purpose is to assert:
39
38
 
40
- - Mocking internal collaborators
41
- - Testing private methods
42
- - Asserting on call counts/order
43
- - Test breaks when refactoring without behavior change
44
- - Test name describes HOW not WHAT
45
- - Verifying through external means instead of interface
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: Bypasses interface to verify
49
- test("createUser saves to database", async () => {
50
- await createUser({ name: "Alice" });
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
- // GOOD: Verifies through interface
56
- test("createUser makes user retrievable", async () => {
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.