@windyroad/tdd 0.3.1 → 0.4.0-preview.267

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,5 +1,5 @@
1
1
  {
2
2
  "name": "wr-tdd",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "TDD state machine enforcement (IDLE/RED/GREEN/BLOCKED) for Claude Code"
5
5
  }
@@ -0,0 +1,171 @@
1
+ ---
2
+ name: review-test
3
+ description: Classifies a test file as STRUCTURAL (asserts source content of
4
+ SKILL.md / agent.md / ADR / hook / policy prose) or BEHAVIOURAL (exercises
5
+ the target and asserts on its outputs / side-effects / tool-calls). Returns
6
+ a structured verdict with evidence, behavioural-alternative suggestion, and
7
+ harness-gap citation. Use after a test file is added or modified, or on
8
+ demand. Multi-framework — bats, vitest, jest, mocha, cucumber/.feature,
9
+ pytest. Runs as mechanical / silent classification — never calls
10
+ AskUserQuestion.
11
+ tools:
12
+ - Read
13
+ - Glob
14
+ - Grep
15
+ model: inherit
16
+ ---
17
+
18
+ You are the Test Reviewer. You classify a test file as structural or behavioural per [ADR-052](../../../docs/decisions/052-behavioural-tests-default-for-skill-testing.proposed.md). You are a reviewer, not an editor.
19
+
20
+ ## Your Role
21
+
22
+ 1. Read the test file path(s) given in the prompt.
23
+ 2. For each test case (`@test` / `it(...)` / `Scenario:` / `def test_...`), identify the assertion target and classify it as STRUCTURAL or BEHAVIOURAL.
24
+ 3. Emit a structured verdict (JSON-in-fenced-block) with evidence, suggestion, and harness_gap fields.
25
+ 4. Run silently — you are in a mechanical classification stage and MUST NOT call `AskUserQuestion` regardless of edge-case ambiguity.
26
+
27
+ ## What is structural vs behavioural
28
+
29
+ A **behavioural** test asserts what the target **does** when invoked: its tool-call sequence, its final artefact state, its output text, its exit code, its side-effects on the filesystem.
30
+
31
+ A **structural** test asserts what the target's source **says**: that a string appears in `SKILL.md`, that a frontmatter field has a particular value, that a section heading is present.
32
+
33
+ Behavioural is the default per ADR-052. Structural is permitted only with documented justification (Surface 1: env-var skip; Surface 2: in-file justification comment).
34
+
35
+ ## Detection method
36
+
37
+ Read the full test source. For each test case:
38
+
39
+ 1. Identify the assertion target (the `run` invocation, the `expect(...)`, the `assert ...`, the `Then` step).
40
+ 2. Trace the target back to its data source.
41
+ 3. Classify:
42
+ - **STRUCTURAL** — assertion's data source reduces to "string X appears in (or is absent from) prose document Y" where Y is `SKILL.md` / `agent.md` / `*.proposed.md` / `*.accepted.md` / `*.superseded.md` / `RISK-POLICY.md` / `CLAUDE.md` / similar prose contracts.
43
+ - **BEHAVIOURAL** — assertion observes target invocation outputs (stdout / stderr / return value / promise resolution), exit codes, written artefacts (final filesystem state), captured tool-calls (mock invocation parameters), or final state of an externally-observable system.
44
+ - **STRUCTURAL-PERMITTED** — assertion is structural BUT the target is one of ADR-005's preserved permitted exceptions: `hooks.json` content checks, file-existence / file-removed checks, hook-script safety-construct presence (e.g. `set -euo pipefail`).
45
+
46
+ If the test file contains the comment `tdd-review: structural-permitted (justification: …)` (any case), treat ALL its structural assertions as STRUCTURAL-JUSTIFIED. Recognise both `# tdd-review: …` (bash / pytest / cucumber) and `// tdd-review: …` (vitest / jest / mocha).
47
+
48
+ If a single test file mixes structural and behavioural test cases without a justification comment, the file-level verdict is MIXED. Per-test-case classification appears in the evidence array.
49
+
50
+ If the file's intent is genuinely unclear (e.g. test cases that read a config file but assert on the parsed result rather than the raw text), emit `verdict: "unclear"` rather than guessing. Populate evidence and suggestion fields so a reader can resolve the ambiguity.
51
+
52
+ ## Per-framework exemplars
53
+
54
+ You will be classifying test files written in bats, vitest, jest, mocha, cucumber/.feature, and pytest. Recognise structural and behavioural shapes across all of them.
55
+
56
+ ### bats
57
+
58
+ ```bash
59
+ # STRUCTURAL — asserts SKILL.md prose
60
+ @test "skill cites P081" {
61
+ run grep -F "P081" "$SKILL_MD"
62
+ [ "$status" -eq 0 ]
63
+ }
64
+
65
+ # BEHAVIOURAL — exercises the hook with mock JSON, asserts exit + output
66
+ @test "hook denies edit when gate fresh and verdict is REJECT" {
67
+ echo '{"tool_input":{"file_path":"x.ts"},"session_id":"t"}' \
68
+ | bash "$HOOK"
69
+ [ "$status" -eq 2 ]
70
+ [[ "$output" == *"BLOCKED"* ]]
71
+ }
72
+
73
+ # STRUCTURAL-PERMITTED — hook safety-construct on executable bash
74
+ @test "hook prologue sets euo pipefail" {
75
+ run grep -nE '^set -[eo]+u?[eo]*' "$HOOK"
76
+ [ "$status" -eq 0 ]
77
+ }
78
+ ```
79
+
80
+ ### vitest / jest / mocha
81
+
82
+ ```js
83
+ // STRUCTURAL — asserts SKILL.md prose
84
+ expect(readFileSync('SKILL.md', 'utf8')).toContain('Step 5');
85
+
86
+ // BEHAVIOURAL — exercises the skill, asserts on result
87
+ const result = await runSkill({ args: 'baz' });
88
+ expect(result.toolCalls).toMatchObject([
89
+ { name: 'Skill', input: { skill: 'wr-itil:manage-problem' } },
90
+ ]);
91
+ ```
92
+
93
+ ### cucumber / .feature
94
+
95
+ ```gherkin
96
+ # STRUCTURAL — Then-step that greps a doc
97
+ Then the SKILL.md should contain "Step 4a Verification"
98
+
99
+ # BEHAVIOURAL — Then-step asserting on captured world state
100
+ Then the skill should call AskUserQuestion with options ["amend", "supersede", "one-time"]
101
+ ```
102
+
103
+ ### pytest
104
+
105
+ ```python
106
+ # STRUCTURAL — reads prose document
107
+ assert "Step 5" in open("SKILL.md").read()
108
+
109
+ # BEHAVIOURAL — exercises target, asserts on artefact
110
+ result = run_skill(args)
111
+ assert result.artefact_state == expected_tree
112
+ ```
113
+
114
+ ### Cross-framework heuristics
115
+
116
+ - **STRUCTURAL signals**: assertion data flow `read_file(prose_doc)` → `contains(...)`; `readFileSync` / `cat` / `grep -F` / `grep -nE` against a `*.md` / `*.proposed.md` / `agent.md` / `SKILL.md` path.
117
+ - **BEHAVIOURAL signals**: subprocess invocation (`bash`, `node`, `python -m`); function call returning a captured tool-call sequence; assertions on `status` / `exit_code` / `stdout` / `stderr` / `output` / `artefact_state` / `result.toolCalls` / `world.lastOutput` / mock call counts.
118
+ - **STRUCTURAL-PERMITTED signals**: target is `hooks.json` content; file-existence / removal checks (`[ -f ... ]` / `[ ! -f ... ]` / `existsSync` / `os.path.exists`); shebang / safety-construct prologue greps on executable bash files (paths under `hooks/` ending `.sh`).
119
+ - **STRUCTURAL-JUSTIFIED signals**: in-file comment `tdd-review: structural-permitted (justification: …)` linking a P012-descendant ticket ID.
120
+
121
+ ## Verdict shape
122
+
123
+ Emit your verdict as a JSON object inside a fenced code block at the end of your output:
124
+
125
+ ```json
126
+ {
127
+ "verdict": "structural" | "behavioural" | "mixed" | "structural-permitted" | "structural-justified" | "unclear",
128
+ "evidence": [
129
+ { "test_name": "skill cites P081", "line": 12, "why": "asserts grep -F on SKILL.md prose" }
130
+ ],
131
+ "suggestion": "Replace with behavioural assertion: invoke the hook with mock JSON for the documented case and assert the resulting exit code and output text. Example: ...",
132
+ "harness_gap": "P012" | null
133
+ }
134
+ ```
135
+
136
+ ### Field rules
137
+
138
+ - **verdict** — one of the six enum values. The file-level verdict; per-test-case classifications belong in evidence.
139
+ - **evidence** — array of `{test_name, line, why}` objects, one per non-trivial classification. For BEHAVIOURAL files this may be empty or omit per-case detail.
140
+ - **suggestion** — a behavioural alternative the test author can adapt. Concrete (name a specific assertion shape, not "write better tests"). Empty string when verdict is BEHAVIOURAL.
141
+ - **harness_gap** — the ticket ID (`P012` / `P081-followup` / a new `PNNN`) of the harness primitive whose absence forces the structural assertion. Per [ADR-026](../../../docs/decisions/026-agent-output-grounding.proposed.md) grounding rules, this MUST be either a specific ticket ID OR `null`. **Never emit free-text speculation** (e.g. `"a Skill-tool interceptor would help"`) without a ticket citation. If you can't cite a ticket, emit `null`.
142
+
143
+ ### When the file has no test cases
144
+
145
+ If the file is empty or contains only setup/teardown, emit `verdict: "unclear"` with `evidence: []` and `suggestion: "File contains no test cases — add @test / it() / Scenario: / def test_..."`. Do not classify as structural-by-default.
146
+
147
+ ## Escape-hatch recognition
148
+
149
+ When the file contains the comment `tdd-review: structural-permitted (justification: …)` (or `// tdd-review: …`), emit `verdict: "structural-justified"` and report the cited ticket in `harness_gap` (parse the ticket ID from the justification text). The agent does not second-guess the justification — surfacing the verdict is the job.
150
+
151
+ If the justification text does NOT cite a ticket ID (e.g. the comment is `tdd-review: structural-permitted (justification: TODO)`), emit `verdict: "structural-justified"` with `harness_gap: null` AND populate `suggestion` with a reminder to link a specific ticket per ADR-052's grounding requirement. Do not auto-promote to STRUCTURAL — the comment is the operator's deviation approval; the agent's role is to surface the missing citation, not to override the deviation.
152
+
153
+ ## Input handling
154
+
155
+ You will be given a test file path (or paths). Read the full file before classifying. If the prompt names a target source-under-test, also read it briefly to ground the suggestion (e.g. "for skill X delegating via Skill tool: simulate invocation and assert the Skill-tool call carries the expected target + arguments"). Do not load broader package context — JTBD-001 60-second budget applies.
156
+
157
+ ## Output formatting
158
+
159
+ Per [ADR-013](../../../docs/decisions/013-structured-user-interaction-for-governance-decisions.proposed.md):
160
+
161
+ - **Rule 1 (interactive default)** — emit a brief prose summary (1-3 sentences) describing what you classified and why, followed by the JSON verdict block. The prose summary is the human-readable context; the JSON is the machine-readable verdict.
162
+ - **Rule 6 (non-interactive fail-safe)** — if any read fails or the file is unparseable, emit `verdict: "unclear"` with evidence describing the failure and suggestion proposing a corrective action. Never crash; never block; never call AskUserQuestion.
163
+
164
+ ## Constraints
165
+
166
+ - You are read-only. You do not edit files.
167
+ - You run as a mechanical / silent classification stage per the project CLAUDE.md (P132 inverse-P078 carve-out). You MUST NOT call `AskUserQuestion` even when classification is genuinely ambiguous; emit `verdict: "unclear"` and let the main agent escalate at retro time.
168
+ - You classify across frameworks: bats, vitest, jest, mocha, cucumber/.feature, pytest. Recognise the shape of each.
169
+ - You ground every `harness_gap` claim in a specific ticket ID per ADR-026, OR emit `null`. Free-text harness-gap speculation is forbidden.
170
+ - You respect ADR-005's preserved permitted exceptions: `hooks.json` content checks, file-existence / file-removed checks, and hook-script safety-construct presence on executable bash. Classify these as STRUCTURAL-PERMITTED, not STRUCTURAL.
171
+ - You respect the in-file justification comment as a per-file deviation approval (ADR-044 category 2). Surface it as STRUCTURAL-JUSTIFIED; do not override.
package/hooks/hooks.json CHANGED
@@ -8,6 +8,7 @@
8
8
  ],
9
9
  "PostToolUse": [
10
10
  { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-post-write.sh" }] },
11
+ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-review-test.sh" }] },
11
12
  { "matcher": "Skill", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-setup-marker.sh" }] }
12
13
  ],
13
14
  "Stop": [
@@ -0,0 +1,106 @@
1
+ #!/bin/bash
2
+ # TDD - PostToolUse hook (Edit|Write) - review-test advisory
3
+ # Per ADR-052 (Behavioural-tests-default for skill testing).
4
+ #
5
+ # When a test-shaped file is written, emit additionalContext directing the
6
+ # assistant to invoke the review-test agent. The hook never blocks; it is
7
+ # advisory-only in this Phase. Phase-2 promotion to PreToolUse blocking is
8
+ # tracked in ADR-052 reassessment criteria.
9
+ #
10
+ # Returns silent on:
11
+ # - Non-test file extension (.sh, .ts impl, .md, etc.)
12
+ # - Path outside $PWD (avoids classifying tests in node_modules, vendored libs)
13
+ # - Env var WR_TDD_REVIEW_TEST=skip set (ADR-044 category 3 override)
14
+ # - File contains `tdd-review: structural-permitted` comment (ADR-044 category 2)
15
+ # - File does not yet exist (Edit on a path that hasn't been written)
16
+
17
+ set -euo pipefail
18
+
19
+ INPUT=$(cat)
20
+
21
+ FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty') || true
22
+
23
+ # No file path → nothing to classify.
24
+ if [ -z "$FILE_PATH" ]; then
25
+ exit 0
26
+ fi
27
+
28
+ # Surface 1 escape hatch — env-var skip per ADR-044 category 3.
29
+ if [ "${WR_TDD_REVIEW_TEST:-}" = "skip" ]; then
30
+ exit 0
31
+ fi
32
+
33
+ # Outside-PWD check — avoid classifying vendored / node_modules tests.
34
+ case "$FILE_PATH" in
35
+ "$PWD"/*) ;;
36
+ *) exit 0 ;;
37
+ esac
38
+
39
+ # Test-shape file extension recognition.
40
+ # Recognised shapes: bats, vitest/jest/mocha (.test.* / .spec.*), cucumber
41
+ # (.feature), pytest (test_*.py / *_test.py), go (*_test.go), ruby
42
+ # (*_test.rb / *_spec.rb).
43
+ is_test_file() {
44
+ local p="$1"
45
+ local base
46
+ base=$(basename "$p")
47
+ case "$base" in
48
+ *.bats) return 0 ;;
49
+ *.feature) return 0 ;;
50
+ *.test.ts|*.test.tsx|*.test.js|*.test.jsx|*.test.mjs|*.test.cjs) return 0 ;;
51
+ *.spec.ts|*.spec.tsx|*.spec.js|*.spec.jsx|*.spec.mjs|*.spec.cjs) return 0 ;;
52
+ *.test.py|*.spec.py) return 0 ;;
53
+ test_*.py|*_test.py) return 0 ;;
54
+ *_test.go) return 0 ;;
55
+ *_test.rb|*_spec.rb) return 0 ;;
56
+ *) return 1 ;;
57
+ esac
58
+ }
59
+
60
+ if ! is_test_file "$FILE_PATH"; then
61
+ exit 0
62
+ fi
63
+
64
+ # File-not-yet-on-disk → bail. The PreToolUse-Edit-on-new-file case is rare
65
+ # but the agent has nothing to read until the Write completes.
66
+ if [ ! -f "$FILE_PATH" ]; then
67
+ exit 0
68
+ fi
69
+
70
+ # Surface 2 escape hatch — in-file justification comment per ADR-044 category 2.
71
+ # Recognise both `# tdd-review: …` (bash / pytest / cucumber) and
72
+ # `// tdd-review: …` (vitest / jest / mocha / TypeScript).
73
+ if grep -qE '^[[:space:]]*(#|//)[[:space:]]*tdd-review:[[:space:]]*structural-permitted' "$FILE_PATH"; then
74
+ exit 0
75
+ fi
76
+
77
+ # Emit advisory directive — the assistant should invoke the review-test agent.
78
+ cat <<EOF
79
+ TDD REVIEW-TEST ADVISORY (per ADR-052):
80
+
81
+ A test file was just written:
82
+ ${FILE_PATH}
83
+
84
+ Before continuing, invoke the review-test agent to classify the test as
85
+ behavioural or structural:
86
+
87
+ Use the Agent tool with subagent_type 'wr-tdd:review-test' (or the
88
+ equivalent agent invocation surface for your harness) and pass the
89
+ test file path.
90
+
91
+ The agent will return a JSON verdict with fields {verdict, evidence,
92
+ suggestion, harness_gap}. If the verdict is 'structural', either:
93
+
94
+ 1. Replace the structural assertions with behavioural ones using the
95
+ suggestion as a starting point, OR
96
+ 2. Add a comment like:
97
+ # tdd-review: structural-permitted (justification: <ticket-ID>
98
+ <reason>)
99
+ citing a specific P012-descendant harness-gap ticket per ADR-052
100
+ Surface 2.
101
+
102
+ To skip review for this session (ADR-044 category 3 strategic override),
103
+ set WR_TDD_REVIEW_TEST=skip in the environment.
104
+ EOF
105
+
106
+ exit 0
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # Behavioural fixture for tdd-review-test.sh per ADR-052.
4
+ # Dogfood: this file MUST be behavioural — no greps of tdd-review-test.sh
5
+ # source or review-test.md agent source. We exercise the hook with mock
6
+ # JSON tool input and assert on its emitted output (or its silence).
7
+ #
8
+ # Coverage per ADR-052 Confirmation:
9
+ # (a) test file → emits advisory directive
10
+ # (b) non-test file → silent
11
+ # (c) WR_TDD_REVIEW_TEST=skip → silent
12
+ # (d) tdd-review: structural-permitted comment → silent
13
+ # (e) outside-PWD path → silent
14
+ # (f) file does not exist on disk → silent
15
+ #
16
+ # @problem P081
17
+
18
+ setup() {
19
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
20
+ HOOK="$REPO_ROOT/packages/tdd/hooks/tdd-review-test.sh"
21
+
22
+ WORKDIR="$(mktemp -d)"
23
+ ORIG_PWD="$PWD"
24
+ cd "$WORKDIR"
25
+
26
+ # Sample test file content — a structural assertion the agent should flag.
27
+ STRUCTURAL_BATS_BODY='@test "skill cites P081" {
28
+ run grep -F "P081" "$SKILL_MD"
29
+ [ "$status" -eq 0 ]
30
+ }'
31
+
32
+ # Sample test file content with the in-file justification comment.
33
+ JUSTIFIED_BATS_BODY='# tdd-review: structural-permitted (justification: P012 harness primitive not yet implemented)
34
+ @test "skill cites P081" {
35
+ run grep -F "P081" "$SKILL_MD"
36
+ [ "$status" -eq 0 ]
37
+ }'
38
+
39
+ # Sample TS test file with the // form of the justification comment.
40
+ JUSTIFIED_TS_BODY='// tdd-review: structural-permitted (justification: P012 vitest harness)
41
+ import { expect, test } from "vitest";
42
+ test("skill cites P081", () => {
43
+ expect(true).toBe(true);
44
+ });'
45
+
46
+ # Non-test file body.
47
+ IMPL_BODY='function foo() { return 1; }'
48
+ }
49
+
50
+ teardown() {
51
+ cd "$ORIG_PWD"
52
+ rm -rf "$WORKDIR"
53
+ }
54
+
55
+ # Helper — invoke the hook with a tool_input.file_path and capture output.
56
+ run_hook_with_path() {
57
+ local path="$1"
58
+ local json
59
+ json=$(jq -nc --arg p "$path" '{tool_input: {file_path: $p}, session_id: "rt-test"}')
60
+ printf '%s' "$json" | bash "$HOOK"
61
+ }
62
+
63
+ # --- (a) test file → emits advisory directive ---
64
+
65
+ @test "emits advisory when a .bats file is written" {
66
+ local f="$WORKDIR/foo.bats"
67
+ printf '%s\n' "$STRUCTURAL_BATS_BODY" > "$f"
68
+
69
+ run run_hook_with_path "$f"
70
+
71
+ [ "$status" -eq 0 ]
72
+ [[ "$output" == *"TDD REVIEW-TEST ADVISORY"* ]]
73
+ [[ "$output" == *"$f"* ]]
74
+ [[ "$output" == *"review-test"* ]]
75
+ }
76
+
77
+ @test "emits advisory when a vitest .test.ts file is written" {
78
+ local f="$WORKDIR/foo.test.ts"
79
+ printf 'test("foo", () => {});\n' > "$f"
80
+
81
+ run run_hook_with_path "$f"
82
+
83
+ [ "$status" -eq 0 ]
84
+ [[ "$output" == *"TDD REVIEW-TEST ADVISORY"* ]]
85
+ }
86
+
87
+ @test "emits advisory when a pytest test_*.py file is written" {
88
+ local f="$WORKDIR/test_foo.py"
89
+ printf 'def test_foo():\n assert True\n' > "$f"
90
+
91
+ run run_hook_with_path "$f"
92
+
93
+ [ "$status" -eq 0 ]
94
+ [[ "$output" == *"TDD REVIEW-TEST ADVISORY"* ]]
95
+ }
96
+
97
+ @test "emits advisory when a cucumber .feature file is written" {
98
+ local f="$WORKDIR/checkout.feature"
99
+ printf 'Feature: foo\n Scenario: bar\n Given x\n' > "$f"
100
+
101
+ run run_hook_with_path "$f"
102
+
103
+ [ "$status" -eq 0 ]
104
+ [[ "$output" == *"TDD REVIEW-TEST ADVISORY"* ]]
105
+ }
106
+
107
+ # --- (b) non-test file → silent ---
108
+
109
+ @test "silent on a plain .ts implementation file" {
110
+ local f="$WORKDIR/foo.ts"
111
+ printf '%s\n' "$IMPL_BODY" > "$f"
112
+
113
+ run run_hook_with_path "$f"
114
+
115
+ [ "$status" -eq 0 ]
116
+ [ -z "$output" ]
117
+ }
118
+
119
+ @test "silent on a hook .sh file" {
120
+ local f="$WORKDIR/some-hook.sh"
121
+ printf '#!/bin/bash\necho hi\n' > "$f"
122
+
123
+ run run_hook_with_path "$f"
124
+
125
+ [ "$status" -eq 0 ]
126
+ [ -z "$output" ]
127
+ }
128
+
129
+ @test "silent on a .md prose file" {
130
+ local f="$WORKDIR/README.md"
131
+ printf '# Heading\n' > "$f"
132
+
133
+ run run_hook_with_path "$f"
134
+
135
+ [ "$status" -eq 0 ]
136
+ [ -z "$output" ]
137
+ }
138
+
139
+ # --- (c) WR_TDD_REVIEW_TEST=skip → silent ---
140
+
141
+ @test "silent when WR_TDD_REVIEW_TEST=skip is set" {
142
+ local f="$WORKDIR/foo.bats"
143
+ printf '%s\n' "$STRUCTURAL_BATS_BODY" > "$f"
144
+
145
+ WR_TDD_REVIEW_TEST=skip run run_hook_with_path "$f"
146
+
147
+ [ "$status" -eq 0 ]
148
+ [ -z "$output" ]
149
+ }
150
+
151
+ # --- (d) tdd-review: structural-permitted comment → silent ---
152
+
153
+ @test "silent when bash # tdd-review justification comment present" {
154
+ local f="$WORKDIR/foo.bats"
155
+ printf '%s\n' "$JUSTIFIED_BATS_BODY" > "$f"
156
+
157
+ run run_hook_with_path "$f"
158
+
159
+ [ "$status" -eq 0 ]
160
+ [ -z "$output" ]
161
+ }
162
+
163
+ @test "silent when TS // tdd-review justification comment present" {
164
+ local f="$WORKDIR/foo.test.ts"
165
+ printf '%s\n' "$JUSTIFIED_TS_BODY" > "$f"
166
+
167
+ run run_hook_with_path "$f"
168
+
169
+ [ "$status" -eq 0 ]
170
+ [ -z "$output" ]
171
+ }
172
+
173
+ # --- (e) outside-PWD path → silent ---
174
+
175
+ @test "silent when file path is outside PWD" {
176
+ # Create the file in a different temp dir so it exists but is not under PWD.
177
+ local outside
178
+ outside="$(mktemp -d)"
179
+ local f="$outside/foo.bats"
180
+ printf '%s\n' "$STRUCTURAL_BATS_BODY" > "$f"
181
+
182
+ run run_hook_with_path "$f"
183
+
184
+ [ "$status" -eq 0 ]
185
+ [ -z "$output" ]
186
+
187
+ rm -rf "$outside"
188
+ }
189
+
190
+ # --- (f) file does not exist on disk → silent ---
191
+
192
+ @test "silent when file path does not exist on disk" {
193
+ local f="$WORKDIR/nonexistent.bats"
194
+ # Deliberately do NOT create the file.
195
+
196
+ run run_hook_with_path "$f"
197
+
198
+ [ "$status" -eq 0 ]
199
+ [ -z "$output" ]
200
+ }
201
+
202
+ # --- Additional behavioural assertions ---
203
+
204
+ @test "advisory text mentions ADR-052" {
205
+ local f="$WORKDIR/foo.bats"
206
+ printf '%s\n' "$STRUCTURAL_BATS_BODY" > "$f"
207
+
208
+ run run_hook_with_path "$f"
209
+
210
+ [ "$status" -eq 0 ]
211
+ [[ "$output" == *"ADR-052"* ]]
212
+ }
213
+
214
+ @test "advisory text mentions both escape hatches" {
215
+ local f="$WORKDIR/foo.bats"
216
+ printf '%s\n' "$STRUCTURAL_BATS_BODY" > "$f"
217
+
218
+ run run_hook_with_path "$f"
219
+
220
+ [ "$status" -eq 0 ]
221
+ [[ "$output" == *"WR_TDD_REVIEW_TEST=skip"* ]]
222
+ [[ "$output" == *"structural-permitted"* ]]
223
+ }
224
+
225
+ @test "exit status is always 0 (advisory, never blocking)" {
226
+ local f="$WORKDIR/foo.bats"
227
+ printf '%s\n' "$STRUCTURAL_BATS_BODY" > "$f"
228
+
229
+ run run_hook_with_path "$f"
230
+ [ "$status" -eq 0 ]
231
+
232
+ printf '%s\n' "$JUSTIFIED_BATS_BODY" > "$f"
233
+ run run_hook_with_path "$f"
234
+ [ "$status" -eq 0 ]
235
+
236
+ rm "$f"
237
+ run run_hook_with_path "$f"
238
+ [ "$status" -eq 0 ]
239
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/tdd",
3
- "version": "0.3.1",
3
+ "version": "0.4.0-preview.267",
4
4
  "description": "TDD state machine enforcement (Red-Green-Refactor cycle)",
5
5
  "bin": {
6
6
  "windyroad-tdd": "./bin/install.mjs"