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.
@@ -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. Lowercase code works (resolveTeamCode normalizes)
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=$?
@@ -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 1)
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 1 ] && echo "$OUT" | grep -q "BLOCKED" && echo "$OUT" | grep -q "main"; then
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" 1 $?
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 1)
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)" 1 $?
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" 1 $?
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" 1 $?
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:"
@@ -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
- touch "$TMP/.planning/phase-1-plan.md"
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
- touch "$TMP/.planning/phase-1-plan.md"
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
- touch "$TMP/.planning/phase-1-plan.md"
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
- touch "$TMP/.planning/phase-1-plan.md"
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
- touch "$TMP/.planning/phase-1-plan.md"
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
- touch "$TMP/.planning/phase-1-plan.md"
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
- touch "$TMP/.planning/phase-2-plan.md"
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
- touch "$TMP/.planning/phase-1-plan.md"
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
- touch "$TMP/.planning/phase-1-plan.md"
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
- touch "$TMP/.planning/phase-1-plan.md"
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
- touch "$TMP/.planning/phase-1-plan.md"
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 ==="