@windyroad/tdd 0.1.4-preview.27 → 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.
- package/hooks/lib/tdd-gate.sh +150 -30
- package/hooks/tdd-enforce-edit.sh +9 -7
- package/hooks/tdd-inject.sh +29 -18
- package/hooks/tdd-post-write.sh +34 -29
- package/hooks/test/tdd-gate.bats +152 -9
- package/package.json +1 -1
package/hooks/lib/tdd-gate.sh
CHANGED
|
@@ -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,
|
|
5
|
-
# tdd_add_test_file, tdd_get_test_files,
|
|
6
|
-
#
|
|
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
|
-
#
|
|
63
|
-
|
|
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
|
-
#
|
|
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
|
|
71
|
-
|
|
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
|
|
83
|
-
local
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
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 "$
|
|
123
|
-
echo "No test
|
|
124
|
-
return 0
|
|
246
|
+
if [ -z "$TEST_FILE" ]; then
|
|
247
|
+
echo "No test file specified" > "$STDOUT_FILE"
|
|
248
|
+
return 0
|
|
125
249
|
fi
|
|
126
250
|
|
|
127
|
-
#
|
|
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 $
|
|
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
|
-
|
|
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
|
|
42
|
-
STATE=$(
|
|
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:
|
|
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.
|
|
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
|
|
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
|
package/hooks/tdd-inject.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# TDD - UserPromptSubmit hook
|
|
3
|
-
# Injects TDD instructions and
|
|
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
|
-
|
|
28
|
+
# Collect per-file states
|
|
29
|
+
ALL_STATES=""
|
|
29
30
|
if [ -n "$SESSION_ID" ]; then
|
|
30
|
-
|
|
31
|
+
ALL_STATES=$(tdd_get_all_states "$SESSION_ID")
|
|
31
32
|
fi
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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: **${
|
|
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
|
|
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
|
|
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
|
|
63
|
-
- The hook runs
|
|
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 "$
|
|
76
|
+
if [ -n "$ALL_STATES" ]; then
|
|
68
77
|
echo ""
|
|
69
78
|
echo "TRACKED TEST FILES THIS SESSION:"
|
|
70
|
-
echo "$
|
|
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
|
package/hooks/tdd-post-write.sh
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# TDD - PostToolUse hook (Edit|Write)
|
|
3
|
-
# Runs
|
|
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
|
-
#
|
|
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
|
|
37
|
-
|
|
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
|
|
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
|
|
50
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
package/hooks/test/tdd-gate.bats
CHANGED
|
@@ -106,23 +106,46 @@ teardown() {
|
|
|
106
106
|
rm -rf "$tmpdir"
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
# ---
|
|
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 "
|
|
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
|
-
|
|
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
|
+
}
|