qualia-framework-v2 2.9.0 → 3.0.0
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/CLAUDE.md +1 -3
- package/README.md +11 -6
- package/agents/planner.md +52 -0
- package/agents/verifier.md +120 -19
- package/bin/cli.js +151 -2
- package/bin/install.js +48 -43
- package/bin/qualia-ui.js +27 -27
- package/bin/state.js +106 -6
- package/bin/statusline.js +53 -3
- package/hooks/auto-update.js +27 -2
- package/hooks/block-env-edit.js +22 -0
- package/hooks/branch-guard.js +23 -2
- package/hooks/migration-guard.js +24 -1
- package/hooks/pre-compact.js +20 -0
- package/hooks/pre-deploy-gate.js +41 -2
- package/hooks/pre-push.js +20 -0
- package/hooks/session-start.js +19 -0
- package/package.json +1 -1
- package/skills/qualia/SKILL.md +1 -0
- package/skills/qualia-build/SKILL.md +18 -0
- package/skills/qualia-design/SKILL.md +1 -1
- package/skills/qualia-learn/SKILL.md +28 -5
- package/skills/qualia-new/SKILL.md +2 -2
- package/skills/qualia-polish/SKILL.md +154 -117
- package/skills/qualia-report/SKILL.md +17 -8
- package/skills/qualia-review/SKILL.md +126 -41
- package/skills/qualia-test/SKILL.md +134 -0
- package/skills/qualia-verify/SKILL.md +1 -1
- package/templates/plan.md +14 -0
- package/tests/bin.test.sh +15 -1
- package/tests/hooks.test.sh +53 -7
- package/tests/state.test.sh +189 -11
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qualia-test
|
|
3
|
+
description: "Generate or run tests for client projects. Trigger on 'write tests', 'add tests', 'test this', 'run tests', 'test coverage', 'need tests for'."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# /qualia-test — Test Generator
|
|
7
|
+
|
|
8
|
+
Generate tests for client project code. Detect framework, classify targets, write tests, run them.
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
|
|
12
|
+
- `/qualia-test` — Generate tests for recently changed files
|
|
13
|
+
- `/qualia-test {file}` — Generate tests for a specific file
|
|
14
|
+
- `/qualia-test --run` — Run existing tests and report
|
|
15
|
+
- `/qualia-test --coverage` — Run with coverage report
|
|
16
|
+
|
|
17
|
+
## Process
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
node ~/.claude/bin/qualia-ui.js banner test
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 1. Detect Test Framework
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
node -e "
|
|
27
|
+
const p=JSON.parse(require('fs').readFileSync('package.json','utf8'));
|
|
28
|
+
const d={...p.dependencies,...p.devDependencies};
|
|
29
|
+
console.log(JSON.stringify({
|
|
30
|
+
vitest: !!d.vitest,
|
|
31
|
+
jest: !!d.jest,
|
|
32
|
+
playwright: !!d['@playwright/test'],
|
|
33
|
+
testing_library: !!d['@testing-library/react']
|
|
34
|
+
}))
|
|
35
|
+
"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If no test framework found, install vitest (lighter than jest for Next.js/Vite):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Add to `vitest.config.ts` if it doesn't exist:
|
|
45
|
+
```typescript
|
|
46
|
+
import { defineConfig } from 'vitest/config'
|
|
47
|
+
import react from '@vitejs/plugin-react'
|
|
48
|
+
|
|
49
|
+
export default defineConfig({
|
|
50
|
+
plugins: [react()],
|
|
51
|
+
test: {
|
|
52
|
+
environment: 'jsdom',
|
|
53
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Find Targets
|
|
59
|
+
|
|
60
|
+
If specific file given → use that.
|
|
61
|
+
If `--run` → skip to step 4.
|
|
62
|
+
Otherwise find recently changed untested files:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Files changed in last 5 commits that don't have tests
|
|
66
|
+
git diff --name-only HEAD~5 --diff-filter=AM -- '*.ts' '*.tsx' 2>/dev/null | grep -v "test\|spec\|__test__\|\.d\.ts" | head -10
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. Generate Tests
|
|
70
|
+
|
|
71
|
+
For each target file, classify it and generate the appropriate test:
|
|
72
|
+
|
|
73
|
+
**API route** (`app/api/**/route.ts`):
|
|
74
|
+
- Test each exported handler (GET, POST, PUT, DELETE)
|
|
75
|
+
- Test with valid input → expected response
|
|
76
|
+
- Test with invalid input → 400 error
|
|
77
|
+
- Test without auth → 401 (if auth is required)
|
|
78
|
+
|
|
79
|
+
**Server action** (has `"use server"`):
|
|
80
|
+
- Test each exported function
|
|
81
|
+
- Test with valid args → expected result
|
|
82
|
+
- Test with invalid args → error handling
|
|
83
|
+
|
|
84
|
+
**React component** (`*.tsx` with JSX):
|
|
85
|
+
- Test rendering without crashing
|
|
86
|
+
- Test interactive elements (clicks, form submissions)
|
|
87
|
+
- Test loading, error, and empty states if they exist
|
|
88
|
+
- Test accessibility (role, aria-label presence)
|
|
89
|
+
|
|
90
|
+
**Utility function** (`lib/*.ts`, `utils/*.ts`):
|
|
91
|
+
- Test each exported function with normal input
|
|
92
|
+
- Test edge cases: empty, null, undefined, boundary values
|
|
93
|
+
- Test error cases: invalid input, missing data
|
|
94
|
+
|
|
95
|
+
Write test file next to the source: `{file}.test.ts` or `{file}.test.tsx`.
|
|
96
|
+
|
|
97
|
+
### 4. Run Tests
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Vitest
|
|
101
|
+
npx vitest run --reporter=verbose 2>&1 | tail -30
|
|
102
|
+
|
|
103
|
+
# Or Jest
|
|
104
|
+
npx jest --verbose 2>&1 | tail -30
|
|
105
|
+
|
|
106
|
+
# Coverage (if --coverage flag)
|
|
107
|
+
npx vitest run --coverage 2>&1 | tail -30
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 5. Report
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
node ~/.claude/bin/qualia-ui.js divider
|
|
114
|
+
node ~/.claude/bin/qualia-ui.js info "Files tested: {N}"
|
|
115
|
+
node ~/.claude/bin/qualia-ui.js ok "Passing: {pass}/{total}"
|
|
116
|
+
node ~/.claude/bin/qualia-ui.js end "TESTS DONE"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If any tests fail, show the failures and offer to fix them.
|
|
120
|
+
|
|
121
|
+
### 6. Commit
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
git add {test files}
|
|
125
|
+
git commit -m "test: add tests for {files}"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Rules
|
|
129
|
+
|
|
130
|
+
1. **Test behavior, not implementation.** Don't test internal state — test what the user/caller sees.
|
|
131
|
+
2. **No snapshot tests.** They're brittle and meaningless.
|
|
132
|
+
3. **No mocking unless necessary.** Test real behavior. Mock only external services (APIs, databases).
|
|
133
|
+
4. **Each test file is self-contained.** No shared mutable state between tests.
|
|
134
|
+
5. **Name tests as sentences.** `it("returns 401 when user is not authenticated")` not `it("test auth")`.
|
|
@@ -33,7 +33,7 @@ node ~/.claude/bin/qualia-ui.js spawn verifier "Goal-backward check..."
|
|
|
33
33
|
Agent(prompt="
|
|
34
34
|
Read your role: @agents/verifier.md
|
|
35
35
|
|
|
36
|
-
Phase plan with success criteria:
|
|
36
|
+
Phase plan with success criteria AND verification contracts:
|
|
37
37
|
@.planning/phase-{N}-plan.md
|
|
38
38
|
|
|
39
39
|
{If re-verification: Previous verification with gaps:}
|
package/templates/plan.md
CHANGED
|
@@ -26,3 +26,17 @@ Goal: {what must be true when done}
|
|
|
26
26
|
- [ ] {truth 1 — what the user can do}
|
|
27
27
|
- [ ] {truth 2}
|
|
28
28
|
- [ ] {truth 3}
|
|
29
|
+
|
|
30
|
+
## Verification Contract
|
|
31
|
+
|
|
32
|
+
### Contract for Task 1 — {title}
|
|
33
|
+
**Check type:** {file-exists | grep-match | command-exit | behavioral}
|
|
34
|
+
**Command:** `{exact command the verifier will run}`
|
|
35
|
+
**Expected:** {what the output should be}
|
|
36
|
+
**Fail if:** {what constitutes failure}
|
|
37
|
+
|
|
38
|
+
### Contract for Task 2 — {title}
|
|
39
|
+
**Check type:** {file-exists | grep-match | command-exit | behavioral}
|
|
40
|
+
**Command:** `{exact command}`
|
|
41
|
+
**Expected:** {expected output}
|
|
42
|
+
**Fail if:** {failure condition}
|
package/tests/bin.test.sh
CHANGED
|
@@ -494,7 +494,21 @@ else
|
|
|
494
494
|
fail_case "settings.json contents"
|
|
495
495
|
fi
|
|
496
496
|
|
|
497
|
-
# 33.
|
|
497
|
+
# 33. settings.json contains all 8 hooks wired correctly
|
|
498
|
+
if grep -q 'block-env-edit.js' "$TMP/.claude/settings.json" \
|
|
499
|
+
&& grep -q 'branch-guard.js' "$TMP/.claude/settings.json" \
|
|
500
|
+
&& grep -q 'migration-guard.js' "$TMP/.claude/settings.json" \
|
|
501
|
+
&& grep -q 'pre-push.js' "$TMP/.claude/settings.json" \
|
|
502
|
+
&& grep -q 'pre-deploy-gate.js' "$TMP/.claude/settings.json" \
|
|
503
|
+
&& grep -q 'auto-update.js' "$TMP/.claude/settings.json" \
|
|
504
|
+
&& grep -q 'session-start.js' "$TMP/.claude/settings.json" \
|
|
505
|
+
&& grep -q 'pre-compact.js' "$TMP/.claude/settings.json"; then
|
|
506
|
+
pass "settings.json has all 8 hooks wired"
|
|
507
|
+
else
|
|
508
|
+
fail_case "settings.json missing hooks"
|
|
509
|
+
fi
|
|
510
|
+
|
|
511
|
+
# 34. Lowercase code works (resolveTeamCode normalizes)
|
|
498
512
|
TMP=$(mktmp)
|
|
499
513
|
echo "qs-fawzi-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
500
514
|
EXIT=$?
|
package/tests/hooks.test.sh
CHANGED
|
@@ -107,11 +107,11 @@ TMP=$(setup_guard_repo main OWNER)
|
|
|
107
107
|
assert_exit "OWNER on main → allowed" 0 $?
|
|
108
108
|
rm -rf "$TMP"
|
|
109
109
|
|
|
110
|
-
# EMPLOYEE on main → blocked (exit
|
|
110
|
+
# EMPLOYEE on main → blocked (exit 2)
|
|
111
111
|
TMP=$(setup_guard_repo main EMPLOYEE)
|
|
112
112
|
OUT=$(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" 2>&1)
|
|
113
113
|
RC=$?
|
|
114
|
-
if [ "$RC" -eq
|
|
114
|
+
if [ "$RC" -eq 2 ] && echo "$OUT" | grep -q "BLOCKED" && echo "$OUT" | grep -q "main"; then
|
|
115
115
|
echo " ✓ EMPLOYEE on main → blocked (BLOCKED in stdout)"
|
|
116
116
|
PASS=$((PASS + 1))
|
|
117
117
|
else
|
|
@@ -123,7 +123,7 @@ rm -rf "$TMP"
|
|
|
123
123
|
# EMPLOYEE on master → blocked
|
|
124
124
|
TMP=$(setup_guard_repo master EMPLOYEE)
|
|
125
125
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
126
|
-
assert_exit "EMPLOYEE on master → blocked"
|
|
126
|
+
assert_exit "EMPLOYEE on master → blocked" 2 $?
|
|
127
127
|
rm -rf "$TMP"
|
|
128
128
|
|
|
129
129
|
# EMPLOYEE on feature branch → allowed
|
|
@@ -138,13 +138,13 @@ TMP=$(setup_guard_repo feature/xyz OWNER)
|
|
|
138
138
|
assert_exit "OWNER on feature/xyz → allowed" 0 $?
|
|
139
139
|
rm -rf "$TMP"
|
|
140
140
|
|
|
141
|
-
# Missing config → fails closed (block, exit
|
|
141
|
+
# Missing config → fails closed (block, exit 2)
|
|
142
142
|
TMP=$(mktemp -d)
|
|
143
143
|
mkdir -p "$TMP/proj"
|
|
144
144
|
(cd "$TMP/proj" && git init -q && git checkout -b feature/x -q 2>/dev/null)
|
|
145
145
|
# NO .claude/.qualia-config.json
|
|
146
146
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
147
|
-
assert_exit "missing config → blocked (fails closed)"
|
|
147
|
+
assert_exit "missing config → blocked (fails closed)" 2 $?
|
|
148
148
|
rm -rf "$TMP"
|
|
149
149
|
|
|
150
150
|
# Malformed config JSON → fails closed
|
|
@@ -153,7 +153,7 @@ mkdir -p "$TMP/proj" "$TMP/.claude"
|
|
|
153
153
|
(cd "$TMP/proj" && git init -q && git checkout -b feature/x -q 2>/dev/null)
|
|
154
154
|
echo 'not json{' > "$TMP/.claude/.qualia-config.json"
|
|
155
155
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
156
|
-
assert_exit "malformed config JSON → blocked"
|
|
156
|
+
assert_exit "malformed config JSON → blocked" 2 $?
|
|
157
157
|
rm -rf "$TMP"
|
|
158
158
|
|
|
159
159
|
# Empty role field → fails closed
|
|
@@ -162,7 +162,7 @@ mkdir -p "$TMP/proj" "$TMP/.claude"
|
|
|
162
162
|
(cd "$TMP/proj" && git init -q && git checkout -b feature/x -q 2>/dev/null)
|
|
163
163
|
echo '{"role":""}' > "$TMP/.claude/.qualia-config.json"
|
|
164
164
|
(cd "$TMP/proj" && HOME="$TMP" $NODE "$HOOKS_DIR/branch-guard.js" >/dev/null 2>&1)
|
|
165
|
-
assert_exit "empty role field → blocked"
|
|
165
|
+
assert_exit "empty role field → blocked" 2 $?
|
|
166
166
|
rm -rf "$TMP"
|
|
167
167
|
|
|
168
168
|
# --- pre-push.js ---
|
|
@@ -286,6 +286,52 @@ else
|
|
|
286
286
|
fi
|
|
287
287
|
rm -rf "$TMP"
|
|
288
288
|
|
|
289
|
+
# --- pre-deploy-gate: Server Component / route handler exemptions ---
|
|
290
|
+
|
|
291
|
+
# route.ts with service_role → exempt (always server-side)
|
|
292
|
+
TMP=$(mktemp -d)
|
|
293
|
+
mkdir -p "$TMP/app/api/auth"
|
|
294
|
+
echo 'const key = process.env.SUPABASE_SERVICE_ROLE_KEY; export async function POST() {}' > "$TMP/app/api/auth/route.ts"
|
|
295
|
+
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
296
|
+
RC=$?
|
|
297
|
+
assert_exit "route.ts with service_role → exempt (exit 0)" 0 $RC
|
|
298
|
+
rm -rf "$TMP"
|
|
299
|
+
|
|
300
|
+
# middleware.ts with service_role → exempt (always server-side)
|
|
301
|
+
TMP=$(mktemp -d)
|
|
302
|
+
echo 'import { service_role } from "./config"; export function middleware() {}' > "$TMP/middleware.ts"
|
|
303
|
+
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
304
|
+
RC=$?
|
|
305
|
+
assert_exit "middleware.ts with service_role → exempt (exit 0)" 0 $RC
|
|
306
|
+
rm -rf "$TMP"
|
|
307
|
+
|
|
308
|
+
# File in app/api/ with service_role → exempt
|
|
309
|
+
TMP=$(mktemp -d)
|
|
310
|
+
mkdir -p "$TMP/app/api/webhook"
|
|
311
|
+
echo 'const sr = "service_role"; export async function GET() { return new Response(sr); }' > "$TMP/app/api/webhook/route.js"
|
|
312
|
+
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
313
|
+
RC=$?
|
|
314
|
+
assert_exit "app/api/ file with service_role → exempt (exit 0)" 0 $RC
|
|
315
|
+
rm -rf "$TMP"
|
|
316
|
+
|
|
317
|
+
# File with "use server" directive + service_role → exempt
|
|
318
|
+
TMP=$(mktemp -d)
|
|
319
|
+
mkdir -p "$TMP/app/admin"
|
|
320
|
+
printf '"use server"\nconst key = process.env.SUPABASE_SERVICE_ROLE_KEY;\nexport async function deleteUser() {}\n' > "$TMP/app/admin/actions.ts"
|
|
321
|
+
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
322
|
+
RC=$?
|
|
323
|
+
assert_exit "\"use server\" file with service_role → exempt (exit 0)" 0 $RC
|
|
324
|
+
rm -rf "$TMP"
|
|
325
|
+
|
|
326
|
+
# Regular app/page.tsx WITHOUT directive + service_role → still blocks
|
|
327
|
+
TMP=$(mktemp -d)
|
|
328
|
+
mkdir -p "$TMP/app/admin"
|
|
329
|
+
echo 'const key = "service_role"; export default function Page() { return <div>{key}</div>; }' > "$TMP/app/admin/page.tsx"
|
|
330
|
+
OUT=$( (cd "$TMP" && $NODE "$HOOKS_DIR/pre-deploy-gate.js") 2>&1 )
|
|
331
|
+
RC=$?
|
|
332
|
+
assert_exit "regular page.tsx with service_role → blocked (exit 1)" 1 $RC
|
|
333
|
+
rm -rf "$TMP"
|
|
334
|
+
|
|
289
335
|
# --- session-start.js — must exit 0 always ---
|
|
290
336
|
echo ""
|
|
291
337
|
echo "session-start:"
|
package/tests/state.test.sh
CHANGED
|
@@ -45,6 +45,34 @@ fail_case() {
|
|
|
45
45
|
FAIL=$((FAIL + 1))
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
# Write a minimal valid plan file (passes content validation).
|
|
49
|
+
# Usage: make_valid_plan "$TMP" 1
|
|
50
|
+
make_valid_plan() {
|
|
51
|
+
local dir="$1"
|
|
52
|
+
local phase="${2:-1}"
|
|
53
|
+
cat > "$dir/.planning/phase-${phase}-plan.md" <<'PLAN'
|
|
54
|
+
---
|
|
55
|
+
phase: 1
|
|
56
|
+
goal: "Test goal"
|
|
57
|
+
tasks: 1
|
|
58
|
+
waves: 1
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
# Phase 1: Test
|
|
62
|
+
|
|
63
|
+
Goal: Test goal
|
|
64
|
+
|
|
65
|
+
## Task 1 — Test task
|
|
66
|
+
**Wave:** 1
|
|
67
|
+
**Files:** src/test.ts
|
|
68
|
+
**Action:** Create test file
|
|
69
|
+
**Done when:** File exists
|
|
70
|
+
|
|
71
|
+
## Success Criteria
|
|
72
|
+
- [ ] Test passes
|
|
73
|
+
PLAN
|
|
74
|
+
}
|
|
75
|
+
|
|
48
76
|
echo "=== state.js Behavioral Tests ==="
|
|
49
77
|
echo ""
|
|
50
78
|
|
|
@@ -126,7 +154,7 @@ echo "happy path transitions:"
|
|
|
126
154
|
|
|
127
155
|
# 4. setup → planned (with plan file)
|
|
128
156
|
TMP=$(make_project)
|
|
129
|
-
|
|
157
|
+
make_valid_plan "$TMP" 1
|
|
130
158
|
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
131
159
|
EXIT=$?
|
|
132
160
|
if [ "$EXIT" -eq 0 ] \
|
|
@@ -166,7 +194,7 @@ fi
|
|
|
166
194
|
|
|
167
195
|
# 7. built → verified(fail) stays on phase 1, records verification=fail
|
|
168
196
|
TMP=$(make_project)
|
|
169
|
-
|
|
197
|
+
make_valid_plan "$TMP" 1
|
|
170
198
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
171
199
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 3 --tasks-total 5 >/dev/null 2>&1)
|
|
172
200
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
@@ -201,7 +229,7 @@ fi
|
|
|
201
229
|
|
|
202
230
|
# 9. planned → verified fails (requires status=built)
|
|
203
231
|
TMP=$(make_project)
|
|
204
|
-
|
|
232
|
+
make_valid_plan "$TMP" 1
|
|
205
233
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
206
234
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
207
235
|
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
@@ -229,7 +257,7 @@ fi
|
|
|
229
257
|
|
|
230
258
|
# 11. built → verified with missing verification file → MISSING_FILE
|
|
231
259
|
TMP=$(make_project)
|
|
232
|
-
|
|
260
|
+
make_valid_plan "$TMP" 1
|
|
233
261
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
234
262
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
235
263
|
# NO verification file
|
|
@@ -245,7 +273,7 @@ fi
|
|
|
245
273
|
|
|
246
274
|
# 12. built → verified without --verification → MISSING_ARG
|
|
247
275
|
TMP=$(make_project)
|
|
248
|
-
|
|
276
|
+
make_valid_plan "$TMP" 1
|
|
249
277
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
250
278
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
251
279
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
@@ -262,13 +290,13 @@ fi
|
|
|
262
290
|
# 13. → shipped without --deployed-url → MISSING_ARG
|
|
263
291
|
# Must go through polished first, so fabricate state by transitioning through the full path.
|
|
264
292
|
TMP=$(make_project)
|
|
265
|
-
|
|
293
|
+
make_valid_plan "$TMP" 1
|
|
266
294
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
267
295
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
268
296
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
269
297
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
270
298
|
# Now on phase 2, status=setup. Run phase 2 to completion.
|
|
271
|
-
|
|
299
|
+
make_valid_plan "$TMP" 2
|
|
272
300
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
273
301
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
274
302
|
touch "$TMP/.planning/phase-2-verification.md"
|
|
@@ -303,7 +331,7 @@ echo "gap cycle circuit breaker:"
|
|
|
303
331
|
|
|
304
332
|
# 15. First gap closure: verified(fail) → planned, gap_cycles[1]=1
|
|
305
333
|
TMP=$(make_project)
|
|
306
|
-
|
|
334
|
+
make_valid_plan "$TMP" 1
|
|
307
335
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
308
336
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
309
337
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
@@ -346,7 +374,7 @@ fi
|
|
|
346
374
|
# 18. verified(pass) resets gap_cycles[1] to 0
|
|
347
375
|
# Set up a fresh project, do ONE failed cycle, then pass on the next attempt.
|
|
348
376
|
TMP=$(make_project)
|
|
349
|
-
|
|
377
|
+
make_valid_plan "$TMP" 1
|
|
350
378
|
touch "$TMP/.planning/phase-1-verification.md"
|
|
351
379
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
352
380
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
@@ -465,7 +493,7 @@ fi
|
|
|
465
493
|
|
|
466
494
|
# 26. Transition refuses on severity=error (missing Phase: header)
|
|
467
495
|
TMP=$(make_project)
|
|
468
|
-
|
|
496
|
+
make_valid_plan "$TMP" 1
|
|
469
497
|
sed -i.bak '/^Phase:/d' "$TMP/.planning/STATE.md"
|
|
470
498
|
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
471
499
|
EXIT=$?
|
|
@@ -513,7 +541,7 @@ fi
|
|
|
513
541
|
|
|
514
542
|
# 29. After fix, transition that was previously blocked now works
|
|
515
543
|
TMP=$(make_project)
|
|
516
|
-
|
|
544
|
+
make_valid_plan "$TMP" 1
|
|
517
545
|
sed -i.bak '/^Phase:/d' "$TMP/.planning/STATE.md"
|
|
518
546
|
# Blocked before fix
|
|
519
547
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1 | grep -q STATE_SCHEMA_ERROR) || \
|
|
@@ -529,6 +557,156 @@ else
|
|
|
529
557
|
fail_case "after fix transition" "exit=$EXIT out=$OUT"
|
|
530
558
|
fi
|
|
531
559
|
|
|
560
|
+
# ─── Configurable gap cycle limit ────────────────────────
|
|
561
|
+
echo ""
|
|
562
|
+
echo "configurable gap cycle limit:"
|
|
563
|
+
|
|
564
|
+
# 30. gap_cycle_limit=5 allows 3rd gap closure (would fail at default 2)
|
|
565
|
+
TMP=$(make_project)
|
|
566
|
+
make_valid_plan "$TMP" 1
|
|
567
|
+
touch "$TMP/.planning/phase-1-verification.md"
|
|
568
|
+
# Set custom limit in tracking.json
|
|
569
|
+
TRACKING=$(cat "$TMP/.planning/tracking.json")
|
|
570
|
+
echo "$TRACKING" | $NODE -e "
|
|
571
|
+
const t = JSON.parse(require('fs').readFileSync(0,'utf8'));
|
|
572
|
+
t.gap_cycle_limit = 5;
|
|
573
|
+
process.stdout.write(JSON.stringify(t, null, 2));
|
|
574
|
+
" > "$TMP/.planning/tracking.json"
|
|
575
|
+
# Do 3 gap closure cycles
|
|
576
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
577
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
578
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
579
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
580
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
581
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification fail >/dev/null 2>&1)
|
|
582
|
+
# 3rd closure should succeed (limit is 5, we're at 2)
|
|
583
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
584
|
+
EXIT=$?
|
|
585
|
+
if [ "$EXIT" -eq 0 ] \
|
|
586
|
+
&& echo "$OUT" | grep -q '"ok": true'; then
|
|
587
|
+
pass "gap_cycle_limit=5 allows 3rd closure (default would block)"
|
|
588
|
+
else
|
|
589
|
+
fail_case "custom gap limit" "exit=$EXIT out=$OUT"
|
|
590
|
+
fi
|
|
591
|
+
|
|
592
|
+
# 31. cmdCheck includes gap_cycle_limit in output
|
|
593
|
+
TMP=$(make_project)
|
|
594
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
595
|
+
if echo "$OUT" | grep -q '"gap_cycle_limit":'; then
|
|
596
|
+
pass "cmdCheck includes gap_cycle_limit in output"
|
|
597
|
+
else
|
|
598
|
+
fail_case "gap_cycle_limit in check" "out=$OUT"
|
|
599
|
+
fi
|
|
600
|
+
|
|
601
|
+
# ─── Plan content validation ────────────────────────────
|
|
602
|
+
echo ""
|
|
603
|
+
echo "plan content validation:"
|
|
604
|
+
|
|
605
|
+
# 32. validate-plan accepts well-formed plan
|
|
606
|
+
TMP=$(make_project)
|
|
607
|
+
make_valid_plan "$TMP" 1
|
|
608
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 2>&1)
|
|
609
|
+
EXIT=$?
|
|
610
|
+
if [ "$EXIT" -eq 0 ] \
|
|
611
|
+
&& echo "$OUT" | grep -q '"action": "validate-plan"' \
|
|
612
|
+
&& echo "$OUT" | grep -q '"task_count": 1'; then
|
|
613
|
+
pass "validate-plan accepts well-formed plan"
|
|
614
|
+
else
|
|
615
|
+
fail_case "validate well-formed plan" "exit=$EXIT out=$OUT"
|
|
616
|
+
fi
|
|
617
|
+
|
|
618
|
+
# 33. validate-plan rejects empty plan
|
|
619
|
+
TMP=$(make_project)
|
|
620
|
+
echo "" > "$TMP/.planning/phase-1-plan.md"
|
|
621
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 2>&1)
|
|
622
|
+
EXIT=$?
|
|
623
|
+
if [ "$EXIT" -eq 1 ] \
|
|
624
|
+
&& echo "$OUT" | grep -q '"error": "PLAN_VALIDATION_FAILED"'; then
|
|
625
|
+
pass "validate-plan rejects empty plan"
|
|
626
|
+
else
|
|
627
|
+
fail_case "validate empty plan" "exit=$EXIT out=$OUT"
|
|
628
|
+
fi
|
|
629
|
+
|
|
630
|
+
# 34. validate-plan rejects plan missing Done when
|
|
631
|
+
TMP=$(make_project)
|
|
632
|
+
cat > "$TMP/.planning/phase-1-plan.md" <<'EOF'
|
|
633
|
+
---
|
|
634
|
+
phase: 1
|
|
635
|
+
goal: "Test"
|
|
636
|
+
tasks: 1
|
|
637
|
+
waves: 1
|
|
638
|
+
---
|
|
639
|
+
## Task 1 — Incomplete
|
|
640
|
+
**Wave:** 1
|
|
641
|
+
**Files:** test.ts
|
|
642
|
+
**Action:** Do something
|
|
643
|
+
|
|
644
|
+
## Success Criteria
|
|
645
|
+
- [ ] Works
|
|
646
|
+
EOF
|
|
647
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 2>&1)
|
|
648
|
+
EXIT=$?
|
|
649
|
+
if [ "$EXIT" -eq 1 ] \
|
|
650
|
+
&& echo "$OUT" | grep -q "PLAN_VALIDATION_FAILED" \
|
|
651
|
+
&& echo "$OUT" | grep -q "Done when"; then
|
|
652
|
+
pass "validate-plan rejects plan missing 'Done when'"
|
|
653
|
+
else
|
|
654
|
+
fail_case "validate missing done-when" "exit=$EXIT out=$OUT"
|
|
655
|
+
fi
|
|
656
|
+
|
|
657
|
+
# 35. Transition to planned with invalid plan content → INVALID_PLAN
|
|
658
|
+
TMP=$(make_project)
|
|
659
|
+
echo "# Empty plan with no tasks" > "$TMP/.planning/phase-1-plan.md"
|
|
660
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned 2>&1)
|
|
661
|
+
EXIT=$?
|
|
662
|
+
if [ "$EXIT" -eq 1 ] \
|
|
663
|
+
&& echo "$OUT" | grep -q '"error": "INVALID_PLAN"'; then
|
|
664
|
+
pass "transition → planned with invalid plan → INVALID_PLAN"
|
|
665
|
+
else
|
|
666
|
+
fail_case "transition invalid plan" "exit=$EXIT out=$OUT"
|
|
667
|
+
fi
|
|
668
|
+
|
|
669
|
+
# ─── Force flag ──────────────────────────────────────────
|
|
670
|
+
echo ""
|
|
671
|
+
echo "force flag:"
|
|
672
|
+
|
|
673
|
+
# 36. --force bypasses precondition failure
|
|
674
|
+
TMP=$(make_project)
|
|
675
|
+
# setup → built should fail (requires planned first)
|
|
676
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to built --force 2>&1)
|
|
677
|
+
EXIT=$?
|
|
678
|
+
if [ "$EXIT" -eq 0 ] \
|
|
679
|
+
&& echo "$OUT" | grep -q '"ok": true' \
|
|
680
|
+
&& echo "$OUT" | grep -q '"status": "built"'; then
|
|
681
|
+
pass "--force bypasses precondition (setup → built)"
|
|
682
|
+
else
|
|
683
|
+
fail_case "force flag" "exit=$EXIT out=$OUT"
|
|
684
|
+
fi
|
|
685
|
+
|
|
686
|
+
# 37. --force does NOT bypass MISSING_FILE (planned without plan file)
|
|
687
|
+
TMP=$(make_project)
|
|
688
|
+
# No plan file exists — force should NOT help
|
|
689
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned --force 2>&1)
|
|
690
|
+
EXIT=$?
|
|
691
|
+
if [ "$EXIT" -eq 1 ] \
|
|
692
|
+
&& echo "$OUT" | grep -q '"error": "MISSING_FILE"'; then
|
|
693
|
+
pass "--force does NOT bypass MISSING_FILE"
|
|
694
|
+
else
|
|
695
|
+
fail_case "force vs MISSING_FILE" "exit=$EXIT out=$OUT"
|
|
696
|
+
fi
|
|
697
|
+
|
|
698
|
+
# 38. --force does NOT bypass INVALID_PLAN
|
|
699
|
+
TMP=$(make_project)
|
|
700
|
+
echo "# No tasks here" > "$TMP/.planning/phase-1-plan.md"
|
|
701
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to planned --force 2>&1)
|
|
702
|
+
EXIT=$?
|
|
703
|
+
if [ "$EXIT" -eq 1 ] \
|
|
704
|
+
&& echo "$OUT" | grep -q '"error": "INVALID_PLAN"'; then
|
|
705
|
+
pass "--force does NOT bypass INVALID_PLAN"
|
|
706
|
+
else
|
|
707
|
+
fail_case "force vs INVALID_PLAN" "exit=$EXIT out=$OUT"
|
|
708
|
+
fi
|
|
709
|
+
|
|
532
710
|
# ─── Summary ─────────────────────────────────────────────
|
|
533
711
|
echo ""
|
|
534
712
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|