@windyroad/tdd 0.1.2 → 0.1.4-preview.26
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/README.md +71 -0
- package/hooks/hooks.json +2 -1
- package/hooks/lib/tdd-gate.sh +3 -2
- package/hooks/tdd-enforce-edit.sh +6 -2
- package/hooks/tdd-inject.sh +1 -1
- package/hooks/tdd-setup-marker.sh +23 -0
- package/hooks/test/tdd-gate.bats +133 -0
- package/package.json +1 -1
- package/skills/{wr:tdd → setup-tests}/SKILL.md +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @windyroad/tdd
|
|
2
|
+
|
|
3
|
+
**TDD state machine enforcement for Claude Code.** Forces the Red-Green-Refactor cycle so your AI agent writes tests before implementation -- every time.
|
|
4
|
+
|
|
5
|
+
Part of [Windy Road Agent Plugins](../../README.md).
|
|
6
|
+
|
|
7
|
+
## What It Does
|
|
8
|
+
|
|
9
|
+
AI agents love to jump straight to implementation. This plugin stops that. It enforces a strict TDD state machine:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
IDLE ──> RED ──> GREEN ──> RED (next test)
|
|
13
|
+
│
|
|
14
|
+
└──> Refactor (staying GREEN)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **IDLE** -- No test written yet. Implementation file edits are blocked.
|
|
18
|
+
- **RED** -- A failing test exists. Implementation edits are allowed.
|
|
19
|
+
- **GREEN** -- Tests pass. You can refactor or write a new failing test.
|
|
20
|
+
- **BLOCKED** -- Test runner error or timeout. Fix the setup before continuing.
|
|
21
|
+
|
|
22
|
+
The agent must write a failing test first. There are no shortcuts.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @windyroad/tdd
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Restart Claude Code after installing.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
The plugin activates automatically. On first use in a project without a test framework, it directs you to set one up:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
/wr-tdd:setup-tests
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This examines your codebase, recommends a test runner, configures `package.json`, and creates an example test.
|
|
41
|
+
|
|
42
|
+
Once active, the workflow is enforced on every edit:
|
|
43
|
+
|
|
44
|
+
1. Write a test file (`*.test.ts`, `*.spec.ts`, etc.) that describes the desired behaviour
|
|
45
|
+
2. The test must fail (RED state) -- proving the test is meaningful
|
|
46
|
+
3. Write the minimum implementation to make it pass (GREEN state)
|
|
47
|
+
4. Refactor while keeping tests green
|
|
48
|
+
5. Repeat
|
|
49
|
+
|
|
50
|
+
Test files and config/doc files are always writable regardless of state.
|
|
51
|
+
|
|
52
|
+
## How It Works
|
|
53
|
+
|
|
54
|
+
| Hook | Trigger | What it does |
|
|
55
|
+
|------|---------|-------------|
|
|
56
|
+
| `tdd-inject.sh` | Every prompt | Injects the current TDD state into the prompt |
|
|
57
|
+
| `tdd-enforce-edit.sh` | Edit or Write | Blocks implementation edits in IDLE or BLOCKED state |
|
|
58
|
+
| `tdd-post-write.sh` | After edit | Runs tests and transitions the state machine |
|
|
59
|
+
| `tdd-setup-marker.sh` | Skill completes | Marks test setup as done |
|
|
60
|
+
| `tdd-reset.sh` | Session end | Resets the TDD state |
|
|
61
|
+
|
|
62
|
+
## Updating and Uninstalling
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx @windyroad/tdd --update
|
|
66
|
+
npx @windyroad/tdd --uninstall
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Licence
|
|
70
|
+
|
|
71
|
+
[MIT](../../LICENSE)
|
package/hooks/hooks.json
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-enforce-edit.sh" }] }
|
|
8
8
|
],
|
|
9
9
|
"PostToolUse": [
|
|
10
|
-
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-post-write.sh" }] }
|
|
10
|
+
{ "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-post-write.sh" }] },
|
|
11
|
+
{ "matcher": "Skill", "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-setup-marker.sh" }] }
|
|
11
12
|
],
|
|
12
13
|
"Stop": [
|
|
13
14
|
{ "hooks": [{ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/tdd-reset.sh" }] }
|
package/hooks/lib/tdd-gate.sh
CHANGED
|
@@ -27,8 +27,8 @@ tdd_classify_file() {
|
|
|
27
27
|
|
|
28
28
|
# Exempt files (not gated)
|
|
29
29
|
case "$FILE_PATH" in
|
|
30
|
-
# Config files
|
|
31
|
-
*.config.*|*.json|*.yml|*.yaml) echo "exempt"; return ;;
|
|
30
|
+
# Config and setup files (test infrastructure)
|
|
31
|
+
*.config.*|*.setup.*|*.json|*.yml|*.yaml) echo "exempt"; return ;;
|
|
32
32
|
# Module configs (*.mjs, *.cjs are config when at root or named as config)
|
|
33
33
|
*.mjs|*.cjs) echo "exempt"; return ;;
|
|
34
34
|
# Styles
|
|
@@ -158,6 +158,7 @@ tdd_cleanup() {
|
|
|
158
158
|
rm -f "$(_tdd_state_file "$SESSION_ID")"
|
|
159
159
|
rm -f "$(_tdd_test_files_file "$SESSION_ID")"
|
|
160
160
|
rm -f "$(_tdd_test_stdout_file "$SESSION_ID")"
|
|
161
|
+
rm -f "/tmp/tdd-setup-active-${SESSION_ID}"
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
# --- Deny Helper ---
|
|
@@ -21,10 +21,14 @@ if [ "$FILE_TYPE" != "impl" ]; then
|
|
|
21
21
|
exit 0
|
|
22
22
|
fi
|
|
23
23
|
|
|
24
|
-
# If no test script configured,
|
|
24
|
+
# If no test script configured, check if setup skill is running
|
|
25
25
|
if ! tdd_has_test_script; then
|
|
26
|
+
# Allow edits if the setup skill is actively running (chicken-and-egg bypass)
|
|
27
|
+
if [ -n "$SESSION_ID" ] && [ -f "/tmp/tdd-setup-active-${SESSION_ID}" ]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
26
30
|
BASENAME=$(basename "$FILE_PATH")
|
|
27
|
-
tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' because no test script is configured in package.json. Run /wr-tdd:
|
|
31
|
+
tdd_deny_json "BLOCKED: Cannot edit '${BASENAME}' because no test script is configured in package.json. Run /wr-tdd:setup-tests to set up a test framework for this project. TDD enforcement requires a working test runner before implementation code can be written."
|
|
28
32
|
exit 0
|
|
29
33
|
fi
|
|
30
34
|
|
package/hooks/tdd-inject.sh
CHANGED
|
@@ -18,7 +18,7 @@ This project has NO test script configured in package.json. Implementation file
|
|
|
18
18
|
edits (.ts, .tsx, .js, .jsx) are BLOCKED until testing is set up.
|
|
19
19
|
|
|
20
20
|
If the user's task involves writing or editing implementation code, you MUST
|
|
21
|
-
run /wr-tdd:
|
|
21
|
+
run /wr-tdd:setup-tests first to configure a test framework for this project.
|
|
22
22
|
|
|
23
23
|
Test files, config files, docs, and styles are still writable.
|
|
24
24
|
HOOK_OUTPUT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PostToolUse:Skill hook — sets a marker when /wr-tdd:setup-tests is invoked.
|
|
3
|
+
# This marker allows tdd-enforce-edit.sh to permit edits during test setup,
|
|
4
|
+
# avoiding the chicken-and-egg problem where the setup skill needs to write
|
|
5
|
+
# .ts/.js files but the enforce hook blocks them.
|
|
6
|
+
|
|
7
|
+
INPUT=$(cat)
|
|
8
|
+
|
|
9
|
+
SKILL_NAME=$(echo "$INPUT" | jq -r '.tool_input.skill // empty') || true
|
|
10
|
+
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty') || true
|
|
11
|
+
|
|
12
|
+
if [ -z "$SESSION_ID" ] || [ -z "$SKILL_NAME" ]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Match the TDD setup skill (handles both full and short names)
|
|
17
|
+
case "$SKILL_NAME" in
|
|
18
|
+
*tdd*setup*|*setup*test*)
|
|
19
|
+
touch "/tmp/tdd-setup-active-${SESSION_ID}"
|
|
20
|
+
;;
|
|
21
|
+
esac
|
|
22
|
+
|
|
23
|
+
exit 0
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
|
|
3
|
+
# Tests for tdd-gate.sh shared library functions
|
|
4
|
+
|
|
5
|
+
setup() {
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
|
|
7
|
+
source "$SCRIPT_DIR/lib/tdd-gate.sh"
|
|
8
|
+
TEST_SESSION="test-$$-$BATS_TEST_NUMBER"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
teardown() {
|
|
12
|
+
tdd_cleanup "$TEST_SESSION" 2>/dev/null || true
|
|
13
|
+
rm -f "/tmp/tdd-setup-active-${TEST_SESSION}"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# --- tdd_classify_file ---
|
|
17
|
+
|
|
18
|
+
@test "classify_file: .test.ts is test" {
|
|
19
|
+
result=$(tdd_classify_file "src/utils.test.ts")
|
|
20
|
+
[ "$result" = "test" ]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@test "classify_file: .spec.jsx is test" {
|
|
24
|
+
result=$(tdd_classify_file "src/App.spec.jsx")
|
|
25
|
+
[ "$result" = "test" ]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@test "classify_file: __tests__ directory is test" {
|
|
29
|
+
result=$(tdd_classify_file "src/__tests__/utils.ts")
|
|
30
|
+
[ "$result" = "test" ]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@test "classify_file: .config.ts is exempt" {
|
|
34
|
+
result=$(tdd_classify_file "vitest.config.ts")
|
|
35
|
+
[ "$result" = "exempt" ]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "classify_file: .setup.ts is exempt" {
|
|
39
|
+
result=$(tdd_classify_file "vitest.setup.ts")
|
|
40
|
+
[ "$result" = "exempt" ]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@test "classify_file: .json is exempt" {
|
|
44
|
+
result=$(tdd_classify_file "package.json")
|
|
45
|
+
[ "$result" = "exempt" ]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@test "classify_file: .css is exempt" {
|
|
49
|
+
result=$(tdd_classify_file "src/styles.css")
|
|
50
|
+
[ "$result" = "exempt" ]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@test "classify_file: .md is exempt" {
|
|
54
|
+
result=$(tdd_classify_file "README.md")
|
|
55
|
+
[ "$result" = "exempt" ]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@test "classify_file: .sh is exempt" {
|
|
59
|
+
result=$(tdd_classify_file "scripts/build.sh")
|
|
60
|
+
[ "$result" = "exempt" ]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@test "classify_file: .ts is impl" {
|
|
64
|
+
result=$(tdd_classify_file "src/utils.ts")
|
|
65
|
+
[ "$result" = "impl" ]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@test "classify_file: .tsx is impl" {
|
|
69
|
+
result=$(tdd_classify_file "src/App.tsx")
|
|
70
|
+
[ "$result" = "impl" ]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@test "classify_file: .js is impl" {
|
|
74
|
+
result=$(tdd_classify_file "src/index.js")
|
|
75
|
+
[ "$result" = "impl" ]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@test "classify_file: unknown extension is exempt" {
|
|
79
|
+
result=$(tdd_classify_file "Dockerfile")
|
|
80
|
+
[ "$result" = "exempt" ]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# --- tdd_has_test_script ---
|
|
84
|
+
|
|
85
|
+
@test "has_test_script: returns false when no package.json" {
|
|
86
|
+
cd "$(mktemp -d)"
|
|
87
|
+
run tdd_has_test_script
|
|
88
|
+
[ "$status" -ne 0 ]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@test "has_test_script: returns false for default npm test script" {
|
|
92
|
+
local tmpdir=$(mktemp -d)
|
|
93
|
+
echo '{"scripts":{"test":"echo \"Error: no test specified\" && exit 1"}}' > "$tmpdir/package.json"
|
|
94
|
+
cd "$tmpdir"
|
|
95
|
+
run tdd_has_test_script
|
|
96
|
+
[ "$status" -ne 0 ]
|
|
97
|
+
rm -rf "$tmpdir"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@test "has_test_script: returns true for real test script" {
|
|
101
|
+
local tmpdir=$(mktemp -d)
|
|
102
|
+
echo '{"scripts":{"test":"vitest"}}' > "$tmpdir/package.json"
|
|
103
|
+
cd "$tmpdir"
|
|
104
|
+
run tdd_has_test_script
|
|
105
|
+
[ "$status" -eq 0 ]
|
|
106
|
+
rm -rf "$tmpdir"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# --- tdd state ---
|
|
110
|
+
|
|
111
|
+
@test "read_state: returns IDLE when no state file" {
|
|
112
|
+
result=$(tdd_read_state "$TEST_SESSION")
|
|
113
|
+
[ "$result" = "IDLE" ]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@test "write_state and read_state roundtrip" {
|
|
117
|
+
tdd_write_state "$TEST_SESSION" "RED"
|
|
118
|
+
result=$(tdd_read_state "$TEST_SESSION")
|
|
119
|
+
[ "$result" = "RED" ]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@test "cleanup removes state files" {
|
|
123
|
+
tdd_write_state "$TEST_SESSION" "GREEN"
|
|
124
|
+
tdd_cleanup "$TEST_SESSION"
|
|
125
|
+
result=$(tdd_read_state "$TEST_SESSION")
|
|
126
|
+
[ "$result" = "IDLE" ]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@test "cleanup removes setup marker" {
|
|
130
|
+
touch "/tmp/tdd-setup-active-${TEST_SESSION}"
|
|
131
|
+
tdd_cleanup "$TEST_SESSION"
|
|
132
|
+
[ ! -f "/tmp/tdd-setup-active-${TEST_SESSION}" ]
|
|
133
|
+
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: wr:
|
|
2
|
+
name: wr-tdd:setup-tests
|
|
3
3
|
description: Set up a test framework for the project. Examines the codebase, recommends a test runner, configures package.json, and creates an example test.
|
|
4
4
|
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion
|
|
5
5
|
---
|