@windyroad/tdd 0.1.4 → 0.2.0-preview.29

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,9 +1,10 @@
1
1
  #!/bin/bash
2
2
  # TDD Gate - shared library for TDD enforcement hooks.
3
3
  # Sourced by tdd-inject.sh, tdd-enforce-edit.sh, tdd-post-write.sh, tdd-reset.sh.
4
- # Provides: tdd_classify_file, tdd_read_state, tdd_write_state, tdd_run_tests,
5
- # tdd_add_test_file, tdd_get_test_files, tdd_cleanup, tdd_has_test_script,
6
- # tdd_deny_json
4
+ # Provides: tdd_classify_file, tdd_read_state, tdd_write_state, tdd_run_test_file,
5
+ # tdd_add_test_file, tdd_get_test_files, tdd_find_test_for_impl,
6
+ # tdd_read_state_for_impl, tdd_get_all_states, tdd_cleanup,
7
+ # tdd_has_test_script, tdd_deny_json
7
8
 
8
9
  # --- Configuration ---
9
10
  TDD_TEST_CMD="${TDD_TEST_CMD:-npm test --}"
@@ -58,17 +59,28 @@ tdd_classify_file() {
58
59
  echo "exempt"
59
60
  }
60
61
 
61
- # --- State Management ---
62
- # Marker files use session ID to isolate concurrent sessions
63
- _tdd_state_file() { echo "/tmp/tdd-state-${1}"; }
62
+ # --- State Management (per-test-file) ---
63
+ # State is stored per test file in a session-scoped directory.
64
+ # Each test file gets its own state: IDLE, RED, GREEN, or BLOCKED.
65
+
66
+ _tdd_state_dir() { echo "/tmp/tdd-state-${1}"; }
64
67
  _tdd_test_files_file() { echo "/tmp/tdd-test-files-${1}"; }
65
68
  _tdd_test_stdout_file() { echo "/tmp/tdd-test-stdout-${1}"; }
66
69
 
67
- # Read current state. Returns: IDLE, RED, GREEN, or BLOCKED
70
+ # Encode a file path for use as a filename (replace / with __)
71
+ _tdd_encode_path() {
72
+ echo "$1" | sed 's|/|__|g'
73
+ }
74
+
75
+ # Read state for a specific test file. Returns: IDLE, RED, GREEN, or BLOCKED
68
76
  tdd_read_state() {
69
77
  local SESSION_ID="$1"
70
- local STATE_FILE
71
- STATE_FILE=$(_tdd_state_file "$SESSION_ID")
78
+ local TEST_FILE="$2"
79
+ local STATE_DIR
80
+ STATE_DIR=$(_tdd_state_dir "$SESSION_ID")
81
+ local ENCODED
82
+ ENCODED=$(_tdd_encode_path "$TEST_FILE")
83
+ local STATE_FILE="${STATE_DIR}/${ENCODED}"
72
84
  if [ -f "$STATE_FILE" ]; then
73
85
  cat "$STATE_FILE"
74
86
  else
@@ -76,13 +88,17 @@ tdd_read_state() {
76
88
  fi
77
89
  }
78
90
 
79
- # Write state
91
+ # Write state for a specific test file
80
92
  tdd_write_state() {
81
93
  local SESSION_ID="$1"
82
- local NEW_STATE="$2"
83
- local STATE_FILE
84
- STATE_FILE=$(_tdd_state_file "$SESSION_ID")
85
- echo "$NEW_STATE" > "$STATE_FILE"
94
+ local TEST_FILE="$2"
95
+ local NEW_STATE="$3"
96
+ local STATE_DIR
97
+ STATE_DIR=$(_tdd_state_dir "$SESSION_ID")
98
+ mkdir -p "$STATE_DIR"
99
+ local ENCODED
100
+ ENCODED=$(_tdd_encode_path "$TEST_FILE")
101
+ echo "$NEW_STATE" > "${STATE_DIR}/${ENCODED}"
86
102
  }
87
103
 
88
104
  # Track test files touched this session
@@ -108,31 +124,133 @@ tdd_get_test_files() {
108
124
  fi
109
125
  }
110
126
 
127
+ # --- Impl-to-Test Association ---
128
+ # Given an implementation file, find its associated test file from tracked tests.
129
+ # Convention: Hero.tsx ↔ Hero.test.tsx or Hero.spec.tsx (same dir or __tests__/)
130
+ # Returns the first matching tracked test file, or empty string if none.
131
+ tdd_find_test_for_impl() {
132
+ local SESSION_ID="$1"
133
+ local IMPL_PATH="$2"
134
+ local DIR BASENAME STEM EXT
135
+ DIR=$(dirname "$IMPL_PATH")
136
+ BASENAME=$(basename "$IMPL_PATH")
137
+
138
+ # Strip extension to get stem (e.g., "Hero" from "Hero.tsx")
139
+ # Handle .ts, .tsx, .js, .jsx
140
+ case "$BASENAME" in
141
+ *.tsx) STEM="${BASENAME%.tsx}"; EXT="tsx" ;;
142
+ *.ts) STEM="${BASENAME%.ts}"; EXT="ts" ;;
143
+ *.jsx) STEM="${BASENAME%.jsx}"; EXT="jsx" ;;
144
+ *.js) STEM="${BASENAME%.js}"; EXT="js" ;;
145
+ *) STEM="$BASENAME"; EXT="" ;;
146
+ esac
147
+
148
+ local TEST_FILES
149
+ TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
150
+ if [ -z "$TEST_FILES" ]; then
151
+ echo ""
152
+ return
153
+ fi
154
+
155
+ # Check tracked test files for a match
156
+ # Priority: exact match in tracked files (any convention)
157
+ while IFS= read -r tracked; do
158
+ local tracked_dir tracked_base
159
+ tracked_dir=$(dirname "$tracked")
160
+ tracked_base=$(basename "$tracked")
161
+
162
+ # Same directory: Hero.test.tsx, Hero.spec.tsx, etc.
163
+ if [ "$tracked_dir" = "$DIR" ]; then
164
+ case "$tracked_base" in
165
+ "${STEM}.test."*|"${STEM}.spec."*) echo "$tracked"; return ;;
166
+ esac
167
+ fi
168
+
169
+ # __tests__ subdirectory: src/__tests__/Hero.test.tsx for src/Hero.tsx
170
+ if [ "$tracked_dir" = "${DIR}/__tests__" ]; then
171
+ case "$tracked_base" in
172
+ "${STEM}.test."*|"${STEM}.spec."*) echo "$tracked"; return ;;
173
+ esac
174
+ fi
175
+
176
+ # Parent __tests__: src/__tests__/Hero.test.tsx for src/components/Hero.tsx
177
+ # (only if dir contains __tests__)
178
+ case "$tracked" in
179
+ */__tests__/*)
180
+ case "$tracked_base" in
181
+ "${STEM}.test."*|"${STEM}.spec."*) echo "$tracked"; return ;;
182
+ esac
183
+ ;;
184
+ esac
185
+ done <<< "$TEST_FILES"
186
+
187
+ # No tracked test found
188
+ echo ""
189
+ }
190
+
191
+ # Suggest a test file path for an impl file using naming convention.
192
+ # Used for user-facing messages (e.g., "write src/Hero.test.tsx first").
193
+ tdd_suggest_test_path() {
194
+ local IMPL_PATH="$1"
195
+ local DIR BASENAME STEM EXT
196
+ DIR=$(dirname "$IMPL_PATH")
197
+ BASENAME=$(basename "$IMPL_PATH")
198
+ case "$BASENAME" in
199
+ *.tsx) STEM="${BASENAME%.tsx}"; EXT="tsx" ;;
200
+ *.ts) STEM="${BASENAME%.ts}"; EXT="ts" ;;
201
+ *.jsx) STEM="${BASENAME%.jsx}"; EXT="jsx" ;;
202
+ *.js) STEM="${BASENAME%.js}"; EXT="js" ;;
203
+ *) echo ""; return ;;
204
+ esac
205
+ echo "${DIR}/${STEM}.test.${EXT}"
206
+ }
207
+
208
+ # Read state for an implementation file by looking up its associated test
209
+ tdd_read_state_for_impl() {
210
+ local SESSION_ID="$1"
211
+ local IMPL_PATH="$2"
212
+ local TEST_FILE
213
+ TEST_FILE=$(tdd_find_test_for_impl "$SESSION_ID" "$IMPL_PATH")
214
+ if [ -z "$TEST_FILE" ]; then
215
+ echo "IDLE"
216
+ return
217
+ fi
218
+ tdd_read_state "$SESSION_ID" "$TEST_FILE"
219
+ }
220
+
221
+ # Get all tracked test files with their states (format: "path:STATE" per line)
222
+ tdd_get_all_states() {
223
+ local SESSION_ID="$1"
224
+ local TEST_FILES
225
+ TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
226
+ if [ -z "$TEST_FILES" ]; then
227
+ return
228
+ fi
229
+ while IFS= read -r test_file; do
230
+ local state
231
+ state=$(tdd_read_state "$SESSION_ID" "$test_file")
232
+ echo "${test_file}:${state}"
233
+ done <<< "$TEST_FILES"
234
+ }
235
+
111
236
  # --- Test Runner ---
112
- # Runs tests for the session's tracked test files.
237
+ # Runs a single test file.
113
238
  # Returns: 0=pass, 1=fail, 124=timeout, other=error
114
239
  # Saves stdout to marker file for debugging.
115
- tdd_run_tests() {
240
+ tdd_run_test_file() {
116
241
  local SESSION_ID="$1"
242
+ local TEST_FILE="$2"
117
243
  local STDOUT_FILE
118
244
  STDOUT_FILE=$(_tdd_test_stdout_file "$SESSION_ID")
119
- local TEST_FILES
120
- TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
121
245
 
122
- if [ -z "$TEST_FILES" ]; then
123
- echo "No test files tracked" > "$STDOUT_FILE"
124
- return 0 # No tests to run = pass (no-op)
246
+ if [ -z "$TEST_FILE" ]; then
247
+ echo "No test file specified" > "$STDOUT_FILE"
248
+ return 0
125
249
  fi
126
250
 
127
- # Build argument list from tracked test files
128
- local ARGS=""
129
- while IFS= read -r f; do
130
- ARGS="$ARGS $f"
131
- done <<< "$TEST_FILES"
132
-
133
- # Run with timeout
251
+ # Run with timeout only the specified test file
134
252
  local EXIT_CODE
135
- timeout "$TDD_TEST_TIMEOUT" bash -c "$TDD_TEST_CMD $ARGS" > "$STDOUT_FILE" 2>&1
253
+ timeout "$TDD_TEST_TIMEOUT" bash -c "$TDD_TEST_CMD $TEST_FILE" > "$STDOUT_FILE" 2>&1
136
254
  EXIT_CODE=$?
137
255
 
138
256
  return $EXIT_CODE
@@ -155,7 +273,9 @@ tdd_has_test_script() {
155
273
  # --- Cleanup ---
156
274
  tdd_cleanup() {
157
275
  local SESSION_ID="$1"
158
- rm -f "$(_tdd_state_file "$SESSION_ID")"
276
+ local STATE_DIR
277
+ STATE_DIR=$(_tdd_state_dir "$SESSION_ID")
278
+ rm -rf "$STATE_DIR"
159
279
  rm -f "$(_tdd_test_files_file "$SESSION_ID")"
160
280
  rm -f "$(_tdd_test_stdout_file "$SESSION_ID")"
161
281
  rm -f "/tmp/tdd-setup-active-${SESSION_ID}"
@@ -1,7 +1,8 @@
1
1
  #!/bin/bash
2
2
  # TDD - PreToolUse enforcement hook (Edit|Write)
3
- # Blocks implementation file edits unless state is RED or GREEN.
3
+ # Blocks implementation file edits unless the associated test's state is RED or GREEN.
4
4
  # Test files and exempt files are always allowed.
5
+ # Per-file state: a failing Countdown test does NOT block editing Hero.
5
6
 
6
7
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
8
  source "$SCRIPT_DIR/lib/tdd-gate.sh"
@@ -38,25 +39,26 @@ if [ -z "$SESSION_ID" ]; then
38
39
  exit 0
39
40
  fi
40
41
 
41
- # Check state for implementation files
42
- STATE=$(tdd_read_state "$SESSION_ID")
42
+ # Check state for THIS implementation file's associated test
43
+ STATE=$(tdd_read_state_for_impl "$SESSION_ID" "$FILE_PATH")
43
44
  BASENAME=$(basename "$FILE_PATH")
45
+ SUGGESTED_TEST=$(tdd_suggest_test_path "$FILE_PATH")
44
46
 
45
47
  case "$STATE" in
46
48
  RED|GREEN)
47
- # Allowed: agent is in the TDD cycle
49
+ # Allowed: this file's test is in the TDD cycle
48
50
  exit 0
49
51
  ;;
50
52
  IDLE)
51
- tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- no tests written yet. TDD state is IDLE. You MUST write a failing test first (*.test.ts or *.spec.ts) before editing implementation files. The test should describe the behavior you want to implement."
53
+ tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- no tests written for this file yet. TDD state is IDLE. Write a failing test first (e.g., ${SUGGESTED_TEST}) before editing this implementation file."
52
54
  exit 0
53
55
  ;;
54
56
  BLOCKED)
55
- tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- test runner is in error state. TDD state is BLOCKED. Fix the test setup first (check test configuration, fix syntax errors in test files, or verify the test command works)."
57
+ tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- its test runner timed out. TDD state is BLOCKED. Fix the test setup for this file before continuing."
56
58
  exit 0
57
59
  ;;
58
60
  *)
59
- tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- unknown TDD state '${STATE}'. Write a failing test first."
61
+ tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' -- unknown TDD state '${STATE}'. Write a failing test first (e.g., ${SUGGESTED_TEST})."
60
62
  exit 0
61
63
  ;;
62
64
  esac
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # TDD - UserPromptSubmit hook
3
- # Injects TDD instructions and current state into every prompt.
3
+ # Injects TDD instructions and per-file state into every prompt.
4
4
  # Only active when a test script is configured in package.json.
5
5
 
6
6
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -25,49 +25,60 @@ HOOK_OUTPUT
25
25
  exit 0
26
26
  fi
27
27
 
28
- STATE="IDLE"
28
+ # Collect per-file states
29
+ ALL_STATES=""
29
30
  if [ -n "$SESSION_ID" ]; then
30
- STATE=$(tdd_read_state "$SESSION_ID")
31
+ ALL_STATES=$(tdd_get_all_states "$SESSION_ID")
31
32
  fi
32
33
 
33
- TEST_FILES=""
34
- if [ -n "$SESSION_ID" ]; then
35
- TEST_FILES=$(tdd_get_test_files "$SESSION_ID")
34
+ # Determine overall status for the header
35
+ OVERALL="IDLE"
36
+ if [ -n "$ALL_STATES" ]; then
37
+ if echo "$ALL_STATES" | grep -q ":BLOCKED$"; then
38
+ OVERALL="BLOCKED (some tests)"
39
+ elif echo "$ALL_STATES" | grep -q ":RED$"; then
40
+ OVERALL="RED (some tests failing)"
41
+ elif echo "$ALL_STATES" | grep -q ":GREEN$"; then
42
+ OVERALL="GREEN"
43
+ fi
36
44
  fi
37
45
 
38
46
  cat <<HOOK_OUTPUT
39
47
  INSTRUCTION: MANDATORY TDD ENFORCEMENT. YOU MUST FOLLOW THIS.
40
48
 
41
- This project enforces Red-Green-Refactor via hooks. Your current TDD state is: **${STATE}**
49
+ This project enforces Red-Green-Refactor via hooks. Your current TDD state is: **${OVERALL}**
42
50
 
43
- STATE RULES:
51
+ STATE RULES (per test file — each component has independent state):
44
52
  - IDLE: You MUST write a failing test FIRST before any implementation code.
45
- Implementation file edits (.ts, .tsx, .js, .jsx) are BLOCKED until you write a test.
53
+ Implementation file edits (.ts, .tsx, .js, .jsx) are BLOCKED until you write a test for that file.
46
54
  - RED: Tests are failing. Write implementation code to make them pass.
47
- Implementation file edits are ALLOWED.
55
+ Implementation file edits are ALLOWED for files whose associated test is RED.
48
56
  - GREEN: Tests are passing. You may refactor or write a new failing test.
49
- Implementation file edits are ALLOWED.
50
- - BLOCKED: Test runner error or timeout. Fix the test setup before continuing.
51
- Implementation file edits are BLOCKED.
57
+ Implementation file edits are ALLOWED for files whose associated test is GREEN.
58
+ - BLOCKED: Test runner timed out. Fix the test setup before continuing.
59
+ Implementation file edits are BLOCKED for files whose associated test is BLOCKED.
52
60
 
53
61
  WORKFLOW:
54
- 1. Write a test file (*.test.ts, *.spec.ts, etc.) that describes the desired behavior
62
+ 1. Write a test file (*.test.ts or *.spec.ts) that describes the desired behavior
55
63
  2. The test MUST fail (RED state) -- this proves the test is meaningful
56
64
  3. Write the minimum implementation to make the test pass (GREEN state)
57
65
  4. Refactor while keeping tests green
58
66
  5. Repeat for the next behavior
59
67
 
60
68
  IMPORTANT:
69
+ - State is tracked PER TEST FILE — a failing Countdown test does NOT block editing Hero
61
70
  - Test files and config/doc/style files are ALWAYS writable regardless of state
62
- - Implementation files are ONLY writable in RED or GREEN states
63
- - The hook runs your tests automatically after every file write
71
+ - Implementation files are ONLY writable when their associated test is RED or GREEN
72
+ - The hook runs only the relevant test after each file write (not the full suite)
64
73
  - To refactor existing code, touch the relevant test file first to enter the cycle
65
74
  HOOK_OUTPUT
66
75
 
67
- if [ -n "$TEST_FILES" ]; then
76
+ if [ -n "$ALL_STATES" ]; then
68
77
  echo ""
69
78
  echo "TRACKED TEST FILES THIS SESSION:"
70
- echo "$TEST_FILES" | sed 's/^/ - /'
79
+ echo "$ALL_STATES" | while IFS=: read -r file state; do
80
+ echo " - ${file} [${state}]"
81
+ done
71
82
  fi
72
83
 
73
84
  exit 0
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
  # TDD - PostToolUse hook (Edit|Write)
3
- # Runs tests after file writes and transitions state.
3
+ # Runs only the relevant test after file writes and transitions per-file state.
4
4
  # Emits additionalContext with the current TDD state.
5
5
 
6
6
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
@@ -28,45 +28,49 @@ if [ "$FILE_TYPE" = "exempt" ]; then
28
28
  exit 0
29
29
  fi
30
30
 
31
- # Track test files
31
+ # Determine which test file to run
32
+ TEST_FILE=""
33
+
32
34
  if [ "$FILE_TYPE" = "test" ]; then
35
+ # Written a test file — track it and run it
33
36
  tdd_add_test_file "$SESSION_ID" "$FILE_PATH"
37
+ TEST_FILE="$FILE_PATH"
38
+ elif [ "$FILE_TYPE" = "impl" ]; then
39
+ # Written an impl file — find and run its associated test
40
+ TEST_FILE=$(tdd_find_test_for_impl "$SESSION_ID" "$FILE_PATH")
41
+ fi
42
+
43
+ # If no test file to run, nothing to do
44
+ if [ -z "$TEST_FILE" ]; then
45
+ exit 0
34
46
  fi
35
47
 
36
- # Run tests
37
- tdd_run_tests "$SESSION_ID"
48
+ # Run only the relevant test file
49
+ tdd_run_test_file "$SESSION_ID" "$TEST_FILE"
38
50
  TEST_EXIT=$?
39
51
 
40
- # Read current state
41
- OLD_STATE=$(tdd_read_state "$SESSION_ID")
52
+ # Read current state for this specific test file
53
+ OLD_STATE=$(tdd_read_state "$SESSION_ID" "$TEST_FILE")
42
54
 
43
- # Transition state based on file type + test result
55
+ # Transition state based on test result
56
+ # Only timeout (124) → BLOCKED. All other failures → RED.
44
57
  NEW_STATE="$OLD_STATE"
45
58
 
46
59
  if [ $TEST_EXIT -eq 0 ]; then
47
60
  # Tests pass
48
61
  NEW_STATE="GREEN"
49
- elif [ $TEST_EXIT -eq 1 ]; then
50
- # Tests fail
51
- case "$FILE_TYPE" in
52
- test)
53
- NEW_STATE="RED"
54
- ;;
55
- impl)
56
- case "$OLD_STATE" in
57
- RED) NEW_STATE="RED" ;; # still working
58
- GREEN) NEW_STATE="RED" ;; # broke something
59
- *) NEW_STATE="RED" ;;
60
- esac
61
- ;;
62
- esac
63
- else
64
- # Timeout (124) or other error
62
+ elif [ $TEST_EXIT -eq 124 ]; then
63
+ # Timeout — genuinely broken setup
65
64
  NEW_STATE="BLOCKED"
65
+ else
66
+ # Tests fail (exit code 1, 2, or any other non-zero)
67
+ # This includes import errors, syntax errors, assertion failures.
68
+ # All of these are RED — the user should write impl to fix them.
69
+ NEW_STATE="RED"
66
70
  fi
67
71
 
68
- # Write new state
69
- tdd_write_state "$SESSION_ID" "$NEW_STATE"
72
+ # Write new state for this specific test file
73
+ tdd_write_state "$SESSION_ID" "$TEST_FILE" "$NEW_STATE"
70
74
 
71
75
  # Read last test output for context
72
76
  STDOUT_FILE="/tmp/tdd-test-stdout-${SESSION_ID}"
@@ -85,7 +89,8 @@ fi
85
89
 
86
90
  cat <<EOF
87
91
  TDD STATE UPDATE: ${TRANSITION}
88
- Current state: ${NEW_STATE}
92
+ Test file: ${TEST_FILE}
93
+ State: ${NEW_STATE}
89
94
  File written: ${FILE_PATH} (${FILE_TYPE})
90
95
  Test result: exit code ${TEST_EXIT}
91
96
  EOF
@@ -99,15 +104,15 @@ fi
99
104
  case "$NEW_STATE" in
100
105
  RED)
101
106
  echo ""
102
- echo "ACTION: Tests are failing. Write implementation code to make them pass."
107
+ echo "ACTION: Tests are failing for ${TEST_FILE}. Write implementation code to make them pass."
103
108
  ;;
104
109
  GREEN)
105
110
  echo ""
106
- echo "ACTION: Tests are passing. You may refactor or write a new failing test for the next behavior."
111
+ echo "ACTION: Tests are passing for ${TEST_FILE}. You may refactor or write a new failing test for the next behavior."
107
112
  ;;
108
113
  BLOCKED)
109
114
  echo ""
110
- echo "ACTION: Test runner error (exit code ${TEST_EXIT}). Fix the test setup before continuing."
115
+ echo "ACTION: Test runner timed out for ${TEST_FILE} (exit code ${TEST_EXIT}). Fix the test setup before continuing."
111
116
  ;;
112
117
  esac
113
118
 
@@ -106,23 +106,46 @@ teardown() {
106
106
  rm -rf "$tmpdir"
107
107
  }
108
108
 
109
- # --- tdd state ---
109
+ # --- Per-test-file state ---
110
110
 
111
- @test "read_state: returns IDLE when no state file" {
112
- result=$(tdd_read_state "$TEST_SESSION")
111
+ @test "read_state: returns IDLE when no state exists for test file" {
112
+ result=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
113
113
  [ "$result" = "IDLE" ]
114
114
  }
115
115
 
116
- @test "write_state and read_state roundtrip" {
117
- tdd_write_state "$TEST_SESSION" "RED"
118
- result=$(tdd_read_state "$TEST_SESSION")
116
+ @test "write_state and read_state roundtrip for a specific test file" {
117
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "RED"
118
+ result=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
119
119
  [ "$result" = "RED" ]
120
120
  }
121
121
 
122
- @test "cleanup removes state files" {
123
- tdd_write_state "$TEST_SESSION" "GREEN"
122
+ @test "per-file state: different test files have independent states" {
123
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "GREEN"
124
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "RED"
125
+
126
+ hero_state=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
127
+ countdown_state=$(tdd_read_state "$TEST_SESSION" "src/Countdown.test.tsx")
128
+
129
+ [ "$hero_state" = "GREEN" ]
130
+ [ "$countdown_state" = "RED" ]
131
+ }
132
+
133
+ @test "per-file state: BLOCKED on one test does not affect another" {
134
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "GREEN"
135
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "BLOCKED"
136
+
137
+ hero_state=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
138
+ [ "$hero_state" = "GREEN" ]
139
+ }
140
+
141
+ @test "cleanup removes all per-file state" {
142
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "RED"
143
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "GREEN"
124
144
  tdd_cleanup "$TEST_SESSION"
125
- result=$(tdd_read_state "$TEST_SESSION")
145
+
146
+ result=$(tdd_read_state "$TEST_SESSION" "src/Hero.test.tsx")
147
+ [ "$result" = "IDLE" ]
148
+ result=$(tdd_read_state "$TEST_SESSION" "src/Countdown.test.tsx")
126
149
  [ "$result" = "IDLE" ]
127
150
  }
128
151
 
@@ -131,3 +154,123 @@ teardown() {
131
154
  tdd_cleanup "$TEST_SESSION"
132
155
  [ ! -f "/tmp/tdd-setup-active-${TEST_SESSION}" ]
133
156
  }
157
+
158
+ # --- Test file tracking ---
159
+
160
+ @test "add_test_file and get_test_files roundtrip" {
161
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
162
+ result=$(tdd_get_test_files "$TEST_SESSION")
163
+ [ "$result" = "src/Hero.test.tsx" ]
164
+ }
165
+
166
+ @test "add_test_file deduplicates" {
167
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
168
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
169
+ local count
170
+ count=$(tdd_get_test_files "$TEST_SESSION" | wc -l | tr -d ' ')
171
+ [ "$count" -eq 1 ]
172
+ }
173
+
174
+ # --- Impl-to-test association ---
175
+
176
+ @test "find_test_for_impl: Hero.tsx associates with tracked Hero.test.tsx" {
177
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
178
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/Hero.tsx")
179
+ [ "$result" = "src/Hero.test.tsx" ]
180
+ }
181
+
182
+ @test "find_test_for_impl: utils.ts associates with tracked utils.test.ts" {
183
+ tdd_add_test_file "$TEST_SESSION" "src/utils.test.ts"
184
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/utils.ts")
185
+ [ "$result" = "src/utils.test.ts" ]
186
+ }
187
+
188
+ @test "suggest_test_path: Hero.tsx suggests Hero.test.tsx" {
189
+ result=$(tdd_suggest_test_path "src/Hero.tsx")
190
+ [ "$result" = "src/Hero.test.tsx" ]
191
+ }
192
+
193
+ @test "suggest_test_path: utils.ts suggests utils.test.ts" {
194
+ result=$(tdd_suggest_test_path "src/utils.ts")
195
+ [ "$result" = "src/utils.test.ts" ]
196
+ }
197
+
198
+ @test "find_test_for_impl: prefers tracked .test over .spec" {
199
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.spec.tsx"
200
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/Hero.tsx")
201
+ [ "$result" = "src/Hero.spec.tsx" ]
202
+ }
203
+
204
+ @test "find_test_for_impl: finds __tests__ variant when tracked" {
205
+ tdd_add_test_file "$TEST_SESSION" "src/__tests__/Hero.test.tsx"
206
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/Hero.tsx")
207
+ [ "$result" = "src/__tests__/Hero.test.tsx" ]
208
+ }
209
+
210
+ @test "find_test_for_impl: returns empty when no test tracked" {
211
+ result=$(tdd_find_test_for_impl "$TEST_SESSION" "src/Unknown.tsx")
212
+ [ -z "$result" ]
213
+ }
214
+
215
+ # --- State for impl files (via association) ---
216
+
217
+ @test "read_state_for_impl: returns IDLE when no associated test" {
218
+ result=$(tdd_read_state_for_impl "$TEST_SESSION" "src/Hero.tsx")
219
+ [ "$result" = "IDLE" ]
220
+ }
221
+
222
+ @test "read_state_for_impl: returns associated test's state" {
223
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
224
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "RED"
225
+ result=$(tdd_read_state_for_impl "$TEST_SESSION" "src/Hero.tsx")
226
+ [ "$result" = "RED" ]
227
+ }
228
+
229
+ @test "read_state_for_impl: Hero GREEN while Countdown BLOCKED" {
230
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
231
+ tdd_add_test_file "$TEST_SESSION" "src/Countdown.test.tsx"
232
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "GREEN"
233
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "BLOCKED"
234
+
235
+ hero=$(tdd_read_state_for_impl "$TEST_SESSION" "src/Hero.tsx")
236
+ countdown=$(tdd_read_state_for_impl "$TEST_SESSION" "src/Countdown.tsx")
237
+
238
+ [ "$hero" = "GREEN" ]
239
+ [ "$countdown" = "BLOCKED" ]
240
+ }
241
+
242
+ # --- get_all_states ---
243
+
244
+ @test "get_all_states: lists all test files with their states" {
245
+ tdd_add_test_file "$TEST_SESSION" "src/Hero.test.tsx"
246
+ tdd_add_test_file "$TEST_SESSION" "src/Countdown.test.tsx"
247
+ tdd_write_state "$TEST_SESSION" "src/Hero.test.tsx" "GREEN"
248
+ tdd_write_state "$TEST_SESSION" "src/Countdown.test.tsx" "RED"
249
+
250
+ result=$(tdd_get_all_states "$TEST_SESSION")
251
+ echo "$result" | grep -q "src/Hero.test.tsx:GREEN"
252
+ echo "$result" | grep -q "src/Countdown.test.tsx:RED"
253
+ }
254
+
255
+ # --- Scoped test runner ---
256
+
257
+ @test "run_test_file: runs only the specified test file" {
258
+ local tmpdir=$(mktemp -d)
259
+ cd "$tmpdir"
260
+
261
+ TDD_TEST_CMD="echo"
262
+ tdd_run_test_file "$TEST_SESSION" "src/Hero.test.tsx"
263
+ local exit_code=$?
264
+ [ "$exit_code" -eq 0 ]
265
+
266
+ # The stdout file should contain just the one file path (from echo)
267
+ local stdout_file="/tmp/tdd-test-stdout-${TEST_SESSION}"
268
+ [ -f "$stdout_file" ]
269
+ grep -q "src/Hero.test.tsx" "$stdout_file"
270
+ # Should NOT contain any other test file
271
+ local line_count
272
+ line_count=$(wc -l < "$stdout_file" | tr -d ' ')
273
+ [ "$line_count" -eq 1 ]
274
+
275
+ rm -rf "$tmpdir"
276
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/tdd",
3
- "version": "0.1.4",
3
+ "version": "0.2.0-preview.29",
4
4
  "description": "TDD state machine enforcement (Red-Green-Refactor cycle)",
5
5
  "bin": {
6
6
  "windyroad-tdd": "./bin/install.mjs"