@windyroad/tdd 0.3.0-preview.215 → 0.3.1-preview.217

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.0",
3
+ "version": "0.3.1",
4
4
  "description": "TDD state machine enforcement (IDLE/RED/GREEN/BLOCKED) for Claude Code"
5
5
  }
@@ -72,6 +72,13 @@ fi
72
72
  # Write new state for this specific test file
73
73
  tdd_write_state "$SESSION_ID" "$TEST_FILE" "$NEW_STATE"
74
74
 
75
+ # P096 Phase 2 — silent-on-GREEN-unchanged: when both old and new state
76
+ # are GREEN, the assistant already knows the file passes; emit nothing.
77
+ # Transitions and any non-GREEN state still emit the full block below.
78
+ if [ "$OLD_STATE" = "GREEN" ] && [ "$NEW_STATE" = "GREEN" ]; then
79
+ exit 0
80
+ fi
81
+
75
82
  # Read last test output for context
76
83
  STDOUT_FILE="/tmp/tdd-test-stdout-${SESSION_ID}"
77
84
  TEST_OUTPUT=""
@@ -95,21 +102,35 @@ File written: ${FILE_PATH} (${FILE_TYPE})
95
102
  Test result: exit code ${TEST_EXIT}
96
103
  EOF
97
104
 
105
+ # P096 Phase 2 — dedupe RED test output: when consecutive RED edits on
106
+ # the same test file produce identical last-50-lines output, only the
107
+ # first emission carries the body; subsequent emissions skip the test
108
+ # output block. Hash file is keyed by session + encoded test path.
98
109
  if [ $TEST_EXIT -ne 0 ] && [ -n "$TEST_OUTPUT" ]; then
99
- echo ""
100
- echo "Test output (last 50 lines):"
101
- echo "$TEST_OUTPUT"
110
+ ENCODED_TEST=$(echo "$TEST_FILE" | sed 's|/|__|g')
111
+ HASH_FILE="/tmp/tdd-stdout-hash-${SESSION_ID}-${ENCODED_TEST}"
112
+ NEW_HASH=$(printf '%s' "$TEST_OUTPUT" | shasum 2>/dev/null | awk '{print $1}')
113
+ PREV_HASH=""
114
+ [ -f "$HASH_FILE" ] && PREV_HASH=$(cat "$HASH_FILE" 2>/dev/null)
115
+ if [ -n "$NEW_HASH" ] && [ "$NEW_HASH" = "$PREV_HASH" ]; then
116
+ echo ""
117
+ echo "Test output unchanged from previous emission (hash match)."
118
+ else
119
+ echo ""
120
+ echo "Test output (last 50 lines):"
121
+ echo "$TEST_OUTPUT"
122
+ [ -n "$NEW_HASH" ] && echo "$NEW_HASH" > "$HASH_FILE"
123
+ fi
102
124
  fi
103
125
 
126
+ # P096 Phase 2 — GREEN ACTION line dropped (standing prose the assistant
127
+ # already knows; the STATE UPDATE block above carries the transition
128
+ # signal). RED and BLOCKED keep their actionable next-step ACTION line.
104
129
  case "$NEW_STATE" in
105
130
  RED)
106
131
  echo ""
107
132
  echo "ACTION: Tests are failing for ${TEST_FILE}. Write implementation code to make them pass."
108
133
  ;;
109
- GREEN)
110
- echo ""
111
- echo "ACTION: Tests are passing for ${TEST_FILE}. You may refactor or write a new failing test for the next behavior."
112
- ;;
113
134
  BLOCKED)
114
135
  echo ""
115
136
  echo "ACTION: Test runner timed out for ${TEST_FILE} (exit code ${TEST_EXIT}). Fix the test setup before continuing."
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env bats
2
+
3
+ # P096 Phase 2: tdd-post-write.sh injection trims.
4
+ #
5
+ # Three behaviours covered:
6
+ # 1. Silent on GREEN-unchanged (OLD=GREEN, NEW=GREEN -> exit 0 with no output).
7
+ # 2. Hash-based dedupe of RED test output across consecutive RED edits with
8
+ # identical last-50-lines output.
9
+ # 3. GREEN ACTION line dropped (the standing prose "Tests are passing... You
10
+ # may refactor..." is no longer emitted on GREEN transitions).
11
+ #
12
+ # Per ADR-038 progressive-disclosure pattern: dynamic state on transition
13
+ # stays; standing prose / repeated content is suppressed.
14
+
15
+ setup() {
16
+ REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
17
+ HOOK="$REPO_ROOT/packages/tdd/hooks/tdd-post-write.sh"
18
+
19
+ WORKDIR="$(mktemp -d)"
20
+ # Use relative paths in the hook input so dirname/basename matching in
21
+ # tdd_find_test_for_impl behaves predictably across mktemp prefixes.
22
+ TEST_FILE="src/widget.test.ts"
23
+ IMPL_FILE="src/widget.ts"
24
+ mkdir -p "$WORKDIR/src"
25
+ : > "$WORKDIR/$TEST_FILE"
26
+ : > "$WORKDIR/$IMPL_FILE"
27
+
28
+ SID="tdd-post-write-phase2-$$-$RANDOM"
29
+ }
30
+
31
+ teardown() {
32
+ rm -rf "/tmp/tdd-state-${SID}" \
33
+ "/tmp/tdd-test-files-${SID}" \
34
+ "/tmp/tdd-test-stdout-${SID}" \
35
+ "/tmp/tdd-stdout-hash-${SID}-"*
36
+ rm -rf "$WORKDIR"
37
+ }
38
+
39
+ # Write a package.json whose `test` script prints fixed stdout then exits
40
+ # 0 (pass) or 1 (fail). Uses `true`/`false` as the exit primitive so the
41
+ # trailing test-file argument injected by `npm test -- <file>` is absorbed
42
+ # without triggering "exit: too many arguments".
43
+ write_pkg_json() {
44
+ local exit_code="${1:-0}"
45
+ local stdout_text="${2:-}"
46
+ local exit_cmd="true"
47
+ [ "$exit_code" -ne 0 ] && exit_cmd="false"
48
+ cat > "$WORKDIR/package.json" <<JSON
49
+ {
50
+ "name": "test-tdd-post-write-phase2",
51
+ "version": "0.0.0",
52
+ "scripts": {
53
+ "test": "printf '%s\\n' '${stdout_text}' && ${exit_cmd}"
54
+ }
55
+ }
56
+ JSON
57
+ }
58
+
59
+ run_hook_for_impl() {
60
+ local sid="$1"
61
+ local impl="$2"
62
+ (cd "$WORKDIR" && \
63
+ echo "{\"session_id\":\"$sid\",\"tool_input\":{\"file_path\":\"$impl\"}}" | \
64
+ bash "$HOOK")
65
+ }
66
+
67
+ run_hook_for_test() {
68
+ local sid="$1"
69
+ local test_file="$2"
70
+ (cd "$WORKDIR" && \
71
+ echo "{\"session_id\":\"$sid\",\"tool_input\":{\"file_path\":\"$test_file\"}}" | \
72
+ bash "$HOOK")
73
+ }
74
+
75
+ # Register the test file in the tracked set by firing the hook on it once.
76
+ # This is the prerequisite for tdd_find_test_for_impl to associate the impl
77
+ # with the test on subsequent impl-file invocations.
78
+ register_test_file() {
79
+ local sid="$1"
80
+ local test_file="$2"
81
+ run_hook_for_test "$sid" "$test_file" >/dev/null
82
+ }
83
+
84
+ # --- Behaviour 1: silent on GREEN-unchanged ---
85
+
86
+ @test "tdd-post-write: GREEN -> GREEN unchanged emits nothing" {
87
+ write_pkg_json 0 "all good"
88
+ # First invocation: writing the test file enters tracked state, runs
89
+ # the test, classifies as GREEN. Output expected (we discard it).
90
+ register_test_file "$SID" "$TEST_FILE"
91
+
92
+ # Second invocation: edit impl. Test still passes. OLD == NEW == GREEN.
93
+ run run_hook_for_impl "$SID" "$IMPL_FILE"
94
+ [ "$status" -eq 0 ]
95
+ [ -z "$output" ]
96
+ }
97
+
98
+ @test "tdd-post-write: GREEN -> GREEN unchanged exits 0 with no STATE UPDATE" {
99
+ write_pkg_json 0 "all good"
100
+ register_test_file "$SID" "$TEST_FILE"
101
+
102
+ run run_hook_for_impl "$SID" "$IMPL_FILE"
103
+ [[ "$output" != *"TDD STATE UPDATE"* ]]
104
+ [[ "$output" != *"State unchanged"* ]]
105
+ }
106
+
107
+ # --- Behaviour 2: dedupe RED test output across consecutive identical RED edits ---
108
+
109
+ @test "tdd-post-write: consecutive RED with identical output suppresses second emit's test-output block" {
110
+ write_pkg_json 1 "FAIL_assertion_mismatch_line_7"
111
+ register_test_file "$SID" "$TEST_FILE"
112
+
113
+ # Second invocation: same impl, same failing test, same output. Still
114
+ # RED. Hash matches the previous emission, so the output block is
115
+ # suppressed (replaced with the "unchanged" marker).
116
+ run run_hook_for_impl "$SID" "$IMPL_FILE"
117
+ [[ "$output" == *"TDD STATE UPDATE"* ]]
118
+ [[ "$output" == *"Test output unchanged from previous emission"* ]]
119
+ [[ "$output" != *"Test output (last 50 lines):"* ]]
120
+ }
121
+
122
+ @test "tdd-post-write: RED with changed output emits the full body" {
123
+ write_pkg_json 1 "FAIL_assertion_mismatch_line_7"
124
+ register_test_file "$SID" "$TEST_FILE"
125
+ run_hook_for_impl "$SID" "$IMPL_FILE" >/dev/null
126
+
127
+ # Different output: re-run with a different stdout. Hash mismatches,
128
+ # full output block re-emits.
129
+ write_pkg_json 1 "FAIL_assertion_mismatch_line_9"
130
+ run run_hook_for_impl "$SID" "$IMPL_FILE"
131
+ [[ "$output" == *"Test output (last 50 lines):"* ]]
132
+ [[ "$output" == *"FAIL_assertion_mismatch_line_9"* ]]
133
+ [[ "$output" != *"Test output unchanged"* ]]
134
+ }
135
+
136
+ # --- Behaviour 3: GREEN ACTION line dropped ---
137
+
138
+ @test "tdd-post-write: RED -> GREEN transition emits STATE UPDATE but no GREEN ACTION line" {
139
+ # Drive RED first.
140
+ write_pkg_json 1 "FAIL"
141
+ register_test_file "$SID" "$TEST_FILE"
142
+ # Now transition to GREEN.
143
+ write_pkg_json 0 "ok"
144
+ run run_hook_for_impl "$SID" "$IMPL_FILE"
145
+ [[ "$output" == *"TDD STATE UPDATE"* ]]
146
+ [[ "$output" == *"State transition: RED -> GREEN"* ]]
147
+ [[ "$output" != *"You may refactor"* ]]
148
+ [[ "$output" != *"ACTION: Tests are passing"* ]]
149
+ }
150
+
151
+ # --- Negative: RED keeps its actionable ACTION line ---
152
+
153
+ @test "tdd-post-write: IDLE -> RED still emits ACTION: Tests are failing" {
154
+ write_pkg_json 1 "FAIL"
155
+ run run_hook_for_test "$SID" "$TEST_FILE"
156
+ [[ "$output" == *"ACTION: Tests are failing"* ]]
157
+ }
158
+
159
+ # --- Negative: empty session_id falls through cleanly ---
160
+
161
+ @test "tdd-post-write: empty session_id exits 0 without crashing" {
162
+ write_pkg_json 0 "ok"
163
+ run bash -c "cd '$WORKDIR' && echo '{\"session_id\":\"\",\"tool_input\":{\"file_path\":\"$IMPL_FILE\"}}' | bash '$HOOK'"
164
+ [ "$status" -eq 0 ]
165
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/tdd",
3
- "version": "0.3.0-preview.215",
3
+ "version": "0.3.1-preview.217",
4
4
  "description": "TDD state machine enforcement (Red-Green-Refactor cycle)",
5
5
  "bin": {
6
6
  "windyroad-tdd": "./bin/install.mjs"