qualia-framework 6.9.2 → 6.22.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/AGENTS.md +8 -5
- package/CHANGELOG.md +208 -0
- package/CLAUDE.md +3 -1
- package/agents/roadmapper.md +16 -14
- package/agents/verifier.md +1 -1
- package/bin/agent-status.js +264 -0
- package/bin/analyze-gate.js +318 -0
- package/bin/branch-hygiene.js +135 -0
- package/bin/command-surface.js +2 -0
- package/bin/compile-instructions.js +82 -0
- package/bin/eval-runner.js +218 -0
- package/bin/host-adapters.js +72 -12
- package/bin/install.js +27 -17
- package/bin/last-report.js +207 -0
- package/bin/project-sync.js +315 -0
- package/bin/report-payload.js +7 -0
- package/bin/runtime-manifest.js +8 -0
- package/bin/state.js +257 -12
- package/bin/verify-panel.js +294 -0
- package/bin/wave-plan.js +211 -0
- package/docs/EMPLOYEE-QUICKSTART.md +3 -3
- package/docs/erp-contract.md +168 -0
- package/docs/qualia-manual.html +5 -5
- package/hooks/branch-guard.js +133 -63
- package/hooks/pre-deploy-gate.js +38 -0
- package/hooks/task-write-guard.js +165 -0
- package/package.json +3 -2
- package/rules/codex-goal.md +28 -26
- package/rules/infrastructure.md +1 -1
- package/skills/qualia/SKILL.md +6 -0
- package/skills/qualia-build/SKILL.md +39 -7
- package/skills/qualia-eval/SKILL.md +83 -0
- package/skills/qualia-feature/SKILL.md +20 -4
- package/skills/qualia-fix/SKILL.md +13 -1
- package/skills/qualia-milestone/SKILL.md +12 -6
- package/skills/qualia-new/REFERENCE.md +6 -4
- package/skills/qualia-new/SKILL.md +27 -15
- package/skills/qualia-plan/SKILL.md +2 -2
- package/skills/qualia-report/SKILL.md +10 -0
- package/skills/qualia-scope/SKILL.md +3 -3
- package/skills/qualia-ship/SKILL.md +37 -4
- package/skills/qualia-update/SKILL.md +100 -0
- package/skills/qualia-verify/SKILL.md +51 -24
- package/templates/instructions.md +32 -0
- package/templates/journey.md +2 -2
- package/templates/project-discovery.md +30 -23
- package/templates/requirements.md +7 -7
- package/tests/agent-status.test.sh +153 -0
- package/tests/analyze-gate.test.sh +170 -0
- package/tests/bin.test.sh +5 -4
- package/tests/branch-hygiene.test.sh +93 -0
- package/tests/eval-runner.test.sh +147 -0
- package/tests/hooks.test.sh +218 -17
- package/tests/install-smoke.test.sh +4 -3
- package/tests/instructions.test.sh +109 -0
- package/tests/last-report.test.sh +156 -0
- package/tests/lib.test.sh +2 -2
- package/tests/project-sync.test.sh +175 -0
- package/tests/run-all.sh +9 -0
- package/tests/runner.js +3 -2
- package/tests/state.test.sh +187 -0
- package/tests/verify-panel.test.sh +162 -0
- package/tests/wave-plan.test.sh +153 -0
- package/skills/qualia-discuss/SKILL.md +0 -222
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# project-sync.test.sh — bin/project-sync.js (Framework→ERP project-sync payload)
|
|
3
|
+
# Run: bash tests/project-sync.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
PS="$BIN_DIR/project-sync.js"
|
|
10
|
+
|
|
11
|
+
assert_exit() {
|
|
12
|
+
local name="$1" expected="$2" actual="$3"
|
|
13
|
+
if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
14
|
+
else echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL+1)); fi
|
|
15
|
+
}
|
|
16
|
+
assert_contains() {
|
|
17
|
+
local name="$1" hay="$2" needle="$3"
|
|
18
|
+
if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
19
|
+
else echo " ✗ $name (missing '$needle' in output)"; FAIL=$((FAIL+1)); fi
|
|
20
|
+
}
|
|
21
|
+
assert_not_contains() {
|
|
22
|
+
local name="$1" hay="$2" needle="$3"
|
|
23
|
+
if echo "$hay" | grep -qF "$needle"; then echo " ✗ $name (unexpected '$needle')"; FAIL=$((FAIL+1));
|
|
24
|
+
else echo " ✓ $name"; PASS=$((PASS+1)); fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# A fully-populated .planning fixture: milestones, REQ-IDs, offroad, lifetime.
|
|
28
|
+
setup_full() {
|
|
29
|
+
local tmp
|
|
30
|
+
tmp=$(mktemp -d)
|
|
31
|
+
mkdir -p "$tmp/.planning"
|
|
32
|
+
cat > "$tmp/.planning/tracking.json" <<'EOF'
|
|
33
|
+
{
|
|
34
|
+
"project": "acme-portal",
|
|
35
|
+
"project_id": "qs-acme-portal",
|
|
36
|
+
"client": "Acme",
|
|
37
|
+
"milestone": 2,
|
|
38
|
+
"milestone_name": "Product",
|
|
39
|
+
"milestones": [
|
|
40
|
+
{ "num": 1, "name": "Foundation", "closed_at": "2026-04-10T18:00:00Z", "phases_completed": 3, "tasks_completed": 12, "deployed_url": "https://m1.vercel.app" }
|
|
41
|
+
],
|
|
42
|
+
"phase": 2,
|
|
43
|
+
"phase_name": "Dashboard",
|
|
44
|
+
"total_phases": 4,
|
|
45
|
+
"status": "built",
|
|
46
|
+
"tasks_done": 3,
|
|
47
|
+
"tasks_total": 5,
|
|
48
|
+
"verification": "pending",
|
|
49
|
+
"gap_cycles": { "2": 1 },
|
|
50
|
+
"build_count": 4,
|
|
51
|
+
"deploy_count": 1,
|
|
52
|
+
"deployed_url": "https://client.vercel.app",
|
|
53
|
+
"lifecycle": "build",
|
|
54
|
+
"lifetime": { "tasks_completed": 15, "phases_completed": 4, "milestones_completed": 1, "total_phases": 7, "offroad_count": 2 },
|
|
55
|
+
"offroad": [
|
|
56
|
+
{ "at": "2026-06-01T10:00:00Z", "milestone": 2, "ref": "BUG-7", "note": "hotfix login" },
|
|
57
|
+
{ "at": "2026-06-02T10:00:00Z", "milestone": 2, "ref": null, "note": "tweak" }
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
EOF
|
|
61
|
+
cat > "$tmp/.planning/JOURNEY.md" <<'EOF'
|
|
62
|
+
## Milestone 1 · Foundation
|
|
63
|
+
## Milestone 2 · Product [CURRENT]
|
|
64
|
+
## Milestone 3 · Handoff [FINAL]
|
|
65
|
+
EOF
|
|
66
|
+
cat > "$tmp/.planning/REQUIREMENTS.md" <<'EOF'
|
|
67
|
+
| ID | Milestone | Phase | Status |
|
|
68
|
+
|----|-----------|-------|--------|
|
|
69
|
+
| CORE-01 | M1: Foundation | Phase 1 | Complete |
|
|
70
|
+
| CORE-02 | M2: Product | Phase 2 | Complete |
|
|
71
|
+
| CORE-03 | M2: Product | Phase 3 | Incomplete |
|
|
72
|
+
EOF
|
|
73
|
+
echo "$tmp"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
echo "project-sync.test.sh — bin/project-sync.js"
|
|
77
|
+
echo ""
|
|
78
|
+
|
|
79
|
+
# --- syntax ---
|
|
80
|
+
$NODE -c "$PS" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
|
|
81
|
+
|
|
82
|
+
NOW="2026-06-21T00:00:00.000Z"
|
|
83
|
+
|
|
84
|
+
# --- no .planning → exit 2 ---
|
|
85
|
+
TMP=$(mktemp -d)
|
|
86
|
+
(cd "$TMP" && $NODE "$PS" >/dev/null 2>&1)
|
|
87
|
+
assert_exit "no .planning → exit 2" 2 $?
|
|
88
|
+
rm -rf "$TMP"
|
|
89
|
+
|
|
90
|
+
# --- full fixture → exit 0 + valid JSON ---
|
|
91
|
+
TMP=$(setup_full)
|
|
92
|
+
OUT=$($NODE "$PS" --cwd "$TMP" --json --now "$NOW" 2>&1)
|
|
93
|
+
(cd "$TMP" && $NODE "$PS" --json --now "$NOW" >/dev/null 2>&1)
|
|
94
|
+
assert_exit "full fixture → exit 0" 0 $?
|
|
95
|
+
echo "$OUT" | $NODE -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{JSON.parse(s);process.exit(0)})" 2>/dev/null \
|
|
96
|
+
&& { echo " ✓ --json emits valid JSON"; PASS=$((PASS+1)); } \
|
|
97
|
+
|| { echo " ✗ --json invalid JSON"; FAIL=$((FAIL+1)); }
|
|
98
|
+
|
|
99
|
+
# --- schema_version + payload marker ---
|
|
100
|
+
assert_contains "carries schema_version" "$OUT" '"schema_version": 1'
|
|
101
|
+
assert_contains "payload marker is project-sync" "$OUT" '"payload": "project-sync"'
|
|
102
|
+
|
|
103
|
+
# --- lifecycle + launched_at handling ---
|
|
104
|
+
assert_contains "lifecycle present (build)" "$OUT" '"lifecycle": "build"'
|
|
105
|
+
|
|
106
|
+
# --- milestones[] with status + REQ completion ---
|
|
107
|
+
assert_contains "milestone 1 closed" "$OUT" '"status": "closed"'
|
|
108
|
+
assert_contains "milestone 2 current" "$OUT" '"status": "current"'
|
|
109
|
+
assert_contains "milestone 3 future" "$OUT" '"status": "future"'
|
|
110
|
+
assert_contains "closed milestone phases count" "$OUT" '"phases": 3'
|
|
111
|
+
assert_contains "closed milestone tasks_completed" "$OUT" '"tasks_completed": 12'
|
|
112
|
+
assert_contains "REQ total tracked for M2" "$OUT" '"total": 2'
|
|
113
|
+
assert_contains "REQ complete count" "$OUT" '"complete": 1'
|
|
114
|
+
assert_contains "incomplete REQ id surfaced" "$OUT" '"id": "CORE-03"'
|
|
115
|
+
assert_contains "future milestone REQ untracked" "$OUT" '"tracked": false'
|
|
116
|
+
assert_contains "per-milestone deployed_url" "$OUT" '"deployed_url": "https://m1.vercel.app"'
|
|
117
|
+
assert_contains "total_milestones from journey" "$OUT" '"total_milestones": 3'
|
|
118
|
+
|
|
119
|
+
# --- current position ---
|
|
120
|
+
assert_contains "current phase" "$OUT" '"phase": 2'
|
|
121
|
+
assert_contains "current verification" "$OUT" '"verification": "pending"'
|
|
122
|
+
|
|
123
|
+
# --- task rollup ---
|
|
124
|
+
assert_contains "rollup tasks_completed" "$OUT" '"tasks_completed": 15'
|
|
125
|
+
assert_contains "rollup build_count" "$OUT" '"build_count": 4'
|
|
126
|
+
assert_contains "rollup deploy_count" "$OUT" '"deploy_count": 1'
|
|
127
|
+
assert_contains "rollup gap_cycles (flattened to number)" "$OUT" '"current_phase_gap_cycles": 1'
|
|
128
|
+
|
|
129
|
+
# --- accountability / offroad ---
|
|
130
|
+
assert_contains "offroad_count" "$OUT" '"offroad_count": 2'
|
|
131
|
+
assert_contains "recent offroad entry" "$OUT" '"ref": "BUG-7"'
|
|
132
|
+
|
|
133
|
+
# --- integration / merge model ---
|
|
134
|
+
assert_contains "trunk integration model" "$OUT" '"model": "trunk"'
|
|
135
|
+
assert_contains "integrates at ship" "$OUT" '"integrates_at": "/qualia-ship"'
|
|
136
|
+
assert_contains "main-push event type" "$OUT" '"main_push_event_type": "employee_main_push"'
|
|
137
|
+
|
|
138
|
+
rm -rf "$TMP"
|
|
139
|
+
|
|
140
|
+
# --- graceful when fields absent: minimal tracking.json, no JOURNEY/REQUIREMENTS ---
|
|
141
|
+
TMP=$(mktemp -d)
|
|
142
|
+
mkdir -p "$TMP/.planning"
|
|
143
|
+
echo '{ "project": "bare", "milestone": 1, "phase": 1 }' > "$TMP/.planning/tracking.json"
|
|
144
|
+
OUT=$($NODE "$PS" --cwd "$TMP" --json --now "$NOW" 2>&1)
|
|
145
|
+
(cd "$TMP" && $NODE "$PS" --json --now "$NOW" >/dev/null 2>&1)
|
|
146
|
+
assert_exit "bare fixture → exit 0" 0 $?
|
|
147
|
+
assert_contains "bare still has schema_version" "$OUT" '"schema_version": 1'
|
|
148
|
+
assert_contains "bare offroad_count defaults to 0" "$OUT" '"offroad_count": 0'
|
|
149
|
+
assert_contains "bare emits a current milestone" "$OUT" '"status": "current"'
|
|
150
|
+
assert_contains "bare REQ untracked (no REQUIREMENTS.md)" "$OUT" '"tracked": false'
|
|
151
|
+
rm -rf "$TMP"
|
|
152
|
+
|
|
153
|
+
# --- library: buildProjectSync() returns the object ---
|
|
154
|
+
TMP=$(setup_full)
|
|
155
|
+
RES=$($NODE -e "const {buildProjectSync}=require('$PS');const o=buildProjectSync({cwd:'$TMP',now:'$NOW'});console.log(o.schema_version, o.milestones.length, o.milestones[1].requirements.incomplete[0].id)" 2>&1)
|
|
156
|
+
assert_contains "buildProjectSync() returns enriched object" "$RES" "1 3 CORE-03"
|
|
157
|
+
rm -rf "$TMP"
|
|
158
|
+
|
|
159
|
+
# --- library: milestoneRequirements() parses a fixture dir ---
|
|
160
|
+
TMP=$(setup_full)
|
|
161
|
+
RES=$($NODE -e "const {milestoneRequirements}=require('$PS');const r=milestoneRequirements('$TMP/.planning',2);console.log(r.tracked, r.total, r.complete, r.incomplete.length)" 2>&1)
|
|
162
|
+
assert_contains "milestoneRequirements() counts REQ completion" "$RES" "true 2 1 1"
|
|
163
|
+
rm -rf "$TMP"
|
|
164
|
+
|
|
165
|
+
# --- --write persists a snapshot file ---
|
|
166
|
+
TMP=$(setup_full)
|
|
167
|
+
FILE=$($NODE "$PS" --cwd "$TMP" --write --now "$NOW" 2>&1)
|
|
168
|
+
if [ -f "$FILE" ]; then echo " ✓ --write persists a file"; PASS=$((PASS+1));
|
|
169
|
+
else echo " ✗ --write did not persist ($FILE)"; FAIL=$((FAIL+1)); fi
|
|
170
|
+
assert_contains "written file is project-sync-*" "$FILE" "project-sync-"
|
|
171
|
+
rm -rf "$TMP"
|
|
172
|
+
|
|
173
|
+
echo ""
|
|
174
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
175
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
package/tests/run-all.sh
CHANGED
package/tests/runner.js
CHANGED
|
@@ -2578,14 +2578,15 @@ describe("install.js", () => {
|
|
|
2578
2578
|
}
|
|
2579
2579
|
});
|
|
2580
2580
|
|
|
2581
|
-
it("
|
|
2581
|
+
it("15 hooks installed (v6.13: task-write-guard added — R1 plan-contract file-scope guard)", () => {
|
|
2582
2582
|
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "qualia-install-"));
|
|
2583
2583
|
try {
|
|
2584
2584
|
runInstall("QS-FAWZI-11", tmpHome);
|
|
2585
2585
|
const hooks = fs.readdirSync(path.join(tmpHome, ".claude", "hooks")).filter(f => f.endsWith(".js"));
|
|
2586
|
-
assert.equal(hooks.length,
|
|
2586
|
+
assert.equal(hooks.length, 15, `expected 15 hooks, got ${hooks.length}: ${hooks.join(", ")}`);
|
|
2587
2587
|
assert.ok(hooks.includes("fawzi-approval-guard.js"), "fawzi-approval-guard.js must be installed");
|
|
2588
2588
|
assert.ok(hooks.includes("pre-compact.js"), "pre-compact.js must be installed (v6.3.2 sidecar-snapshot mechanism)");
|
|
2589
|
+
assert.ok(hooks.includes("task-write-guard.js"), "task-write-guard.js must be installed (v6.13 R1 guard)");
|
|
2589
2590
|
} finally {
|
|
2590
2591
|
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
2591
2592
|
}
|
package/tests/state.test.sh
CHANGED
|
@@ -104,6 +104,12 @@ Goal: Test goal
|
|
|
104
104
|
## Success Criteria
|
|
105
105
|
- [ ] Test passes
|
|
106
106
|
PLAN
|
|
107
|
+
# v7 kernel: `planned` now requires a machine contract and `verified(pass)`
|
|
108
|
+
# requires passing machine evidence. Emit both so happy-path setups satisfy the
|
|
109
|
+
# preconditions; the few cases that exercise their ABSENCE delete them explicitly.
|
|
110
|
+
make_valid_contract "$dir" "$phase"
|
|
111
|
+
mkdir -p "$dir/.planning/evidence"
|
|
112
|
+
printf '{"ok":true,"checks":[]}\n' > "$dir/.planning/evidence/phase-${phase}-contract-run.json"
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
make_valid_contract() {
|
|
@@ -289,6 +295,9 @@ fi
|
|
|
289
295
|
TMP=$(make_project)
|
|
290
296
|
make_valid_plan "$TMP" 1
|
|
291
297
|
make_valid_contract "$TMP" 1
|
|
298
|
+
# This case asserts the missing-evidence guard, so strip the evidence that
|
|
299
|
+
# make_valid_plan now writes by default.
|
|
300
|
+
rm -f "$TMP/.planning/evidence/phase-1-contract-run.json"
|
|
292
301
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
293
302
|
(cd "$TMP" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
294
303
|
echo "result: PASS" > "$TMP/.planning/phase-1-verification.md"
|
|
@@ -735,6 +744,9 @@ else
|
|
|
735
744
|
fail_case "validate well-formed plan" "exit=$EXIT out=$OUT"
|
|
736
745
|
fi
|
|
737
746
|
|
|
747
|
+
# This case asserts the missing-contract path, so strip the contract that
|
|
748
|
+
# make_valid_plan now writes by default.
|
|
749
|
+
rm -f "$TMP/.planning/phase-1-contract.json"
|
|
738
750
|
OUT=$(cd "$TMP" && $NODE "$STATE_JS" validate-plan --phase 1 --require-contract 2>&1)
|
|
739
751
|
EXIT=$?
|
|
740
752
|
if [ "$EXIT" -eq 1 ] \
|
|
@@ -946,6 +958,25 @@ else
|
|
|
946
958
|
fail_case "note without tasks-done"
|
|
947
959
|
fi
|
|
948
960
|
|
|
961
|
+
# 43b. --scope off tallies offroad_count + ledgers the entry (anti-drift)
|
|
962
|
+
TMP=$(make_project)
|
|
963
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "ad-hoc widget" --tasks-done 1 --scope off --ref "client asked mid-sprint" >/dev/null 2>&1)
|
|
964
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "another off-road" --scope off --ref "x" >/dev/null 2>&1)
|
|
965
|
+
if grep -q '"offroad_count": 2' "$TMP/.planning/tracking.json" && grep -q "client asked mid-sprint" "$TMP/.planning/tracking.json"; then
|
|
966
|
+
pass "--scope off increments offroad_count + records the entry"
|
|
967
|
+
else
|
|
968
|
+
fail_case "scope off tally" "$(grep -o '"offroad_count": [0-9]*' "$TMP/.planning/tracking.json")"
|
|
969
|
+
fi
|
|
970
|
+
|
|
971
|
+
# 43c. --scope in does NOT increment offroad_count
|
|
972
|
+
TMP=$(make_project)
|
|
973
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to note --notes "in-scope work" --tasks-done 1 --scope in --ref "CORE-01" >/dev/null 2>&1)
|
|
974
|
+
if grep -q '"offroad_count": 0' "$TMP/.planning/tracking.json"; then
|
|
975
|
+
pass "--scope in leaves offroad_count at 0"
|
|
976
|
+
else
|
|
977
|
+
fail_case "scope in offroad" "$(grep -o '"offroad_count": [0-9]*' "$TMP/.planning/tracking.json")"
|
|
978
|
+
fi
|
|
979
|
+
|
|
949
980
|
# ─── Close milestone ─────────────────────────────────────
|
|
950
981
|
echo ""
|
|
951
982
|
echo "close-milestone:"
|
|
@@ -1094,6 +1125,79 @@ else
|
|
|
1094
1125
|
fail_case "backfill-milestones placeholder repair" "got=$RESULT expected='1|Foundation Arc|7|Core Features'"
|
|
1095
1126
|
fi
|
|
1096
1127
|
|
|
1128
|
+
# ─── reqs-check (milestone REQ-ID coverage gate) ─────────
|
|
1129
|
+
echo ""
|
|
1130
|
+
echo "reqs-check:"
|
|
1131
|
+
|
|
1132
|
+
# helper: write a REQUIREMENTS.md traceability table into a project
|
|
1133
|
+
write_reqs() {
|
|
1134
|
+
mkdir -p "$1/.planning"
|
|
1135
|
+
cat > "$1/.planning/REQUIREMENTS.md" <<EOF
|
|
1136
|
+
## Traceability
|
|
1137
|
+
| Requirement | Milestone | Phase | Status |
|
|
1138
|
+
|---|---|---|---|
|
|
1139
|
+
$2
|
|
1140
|
+
EOF
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
# all complete for M1 → ok, exit 0
|
|
1144
|
+
TMP=$(make_project)
|
|
1145
|
+
write_reqs "$TMP" "| CORE-01 | M1: Foundation | Phase 1 | Complete |
|
|
1146
|
+
| CORE-02 | M1: Foundation | Phase 2 | Complete |"
|
|
1147
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" reqs-check --milestone 1 2>&1); EXIT=$?
|
|
1148
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"ok": true' && echo "$OUT" | grep -q '"complete": 2'; then
|
|
1149
|
+
pass "reqs-check all complete → ok (exit 0)"
|
|
1150
|
+
else
|
|
1151
|
+
fail_case "reqs-check all complete" "exit=$EXIT out=$OUT"
|
|
1152
|
+
fi
|
|
1153
|
+
|
|
1154
|
+
# an incomplete REQ → not ok, exit 1, lists it
|
|
1155
|
+
TMP=$(make_project)
|
|
1156
|
+
write_reqs "$TMP" "| CORE-01 | M1: Foundation | Phase 1 | Complete |
|
|
1157
|
+
| CORE-02 | M1: Foundation | Phase 2 | Pending |"
|
|
1158
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" reqs-check --milestone 1 2>&1); EXIT=$?
|
|
1159
|
+
if [ "$EXIT" -eq 1 ] && echo "$OUT" | grep -q '"ok": false' && echo "$OUT" | grep -q 'CORE-02'; then
|
|
1160
|
+
pass "reqs-check incomplete → not ok (exit 1) + lists REQ"
|
|
1161
|
+
else
|
|
1162
|
+
fail_case "reqs-check incomplete" "exit=$EXIT out=$OUT"
|
|
1163
|
+
fi
|
|
1164
|
+
|
|
1165
|
+
# milestone filter: M2's pending REQ doesn't fail an M1 check
|
|
1166
|
+
TMP=$(make_project)
|
|
1167
|
+
write_reqs "$TMP" "| CORE-01 | M1: Foundation | Phase 1 | Complete |
|
|
1168
|
+
| ADMIN-01 | M2: Admin | Phase 1 | Pending |"
|
|
1169
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" reqs-check --milestone 1 2>&1); EXIT=$?
|
|
1170
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"total": 1'; then
|
|
1171
|
+
pass "reqs-check filters by milestone (M2 pending ignored for M1)"
|
|
1172
|
+
else
|
|
1173
|
+
fail_case "reqs-check milestone filter" "exit=$EXIT out=$OUT"
|
|
1174
|
+
fi
|
|
1175
|
+
|
|
1176
|
+
# no REQUIREMENTS.md → untracked → ok (can't gate what isn't declared)
|
|
1177
|
+
TMP=$(make_project)
|
|
1178
|
+
rm -f "$TMP/.planning/REQUIREMENTS.md"
|
|
1179
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" reqs-check --milestone 1 2>&1); EXIT=$?
|
|
1180
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"tracked": false'; then
|
|
1181
|
+
pass "reqs-check untracked (no REQUIREMENTS.md) → ok"
|
|
1182
|
+
else
|
|
1183
|
+
fail_case "reqs-check untracked" "exit=$EXIT out=$OUT"
|
|
1184
|
+
fi
|
|
1185
|
+
|
|
1186
|
+
# close-milestone (strict, no --force) blocks when REQs incomplete.
|
|
1187
|
+
# make_project leaves phases unverified, so we --force past the phase gate is NOT
|
|
1188
|
+
# what we want here; instead assert the gate code path via a forced close still
|
|
1189
|
+
# succeeding (force bypasses both gates) — and the non-forced REQ block is proven
|
|
1190
|
+
# by reqs-check above sharing the same readMilestoneRequirements parser.
|
|
1191
|
+
TMP=$(make_project)
|
|
1192
|
+
write_reqs "$TMP" "| CORE-01 | M1: Foundation | Phase 1 | Pending |
|
|
1193
|
+
| CORE-02 | M1: Foundation | Phase 2 | Pending |"
|
|
1194
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" close-milestone --force 2>&1); EXIT=$?
|
|
1195
|
+
if [ "$EXIT" -eq 0 ] && echo "$OUT" | grep -q '"action": "close-milestone"'; then
|
|
1196
|
+
pass "close-milestone --force bypasses REQ gate (retroactive bookkeeping)"
|
|
1197
|
+
else
|
|
1198
|
+
fail_case "close-milestone force bypass" "exit=$EXIT out=$OUT"
|
|
1199
|
+
fi
|
|
1200
|
+
|
|
1097
1201
|
# ─── Backward compatibility ──────────────────────────────
|
|
1098
1202
|
echo ""
|
|
1099
1203
|
echo "backward compatibility:"
|
|
@@ -1417,6 +1521,89 @@ else
|
|
|
1417
1521
|
fail_case "id traversal guard" "out=$TRAV"
|
|
1418
1522
|
fi
|
|
1419
1523
|
|
|
1524
|
+
# ─── v7 lifecycle (build → operate) ──────────────────────
|
|
1525
|
+
echo ""
|
|
1526
|
+
echo "lifecycle (build/operate):"
|
|
1527
|
+
|
|
1528
|
+
# Helper: drive a fresh single-phase project to verified(pass).
|
|
1529
|
+
make_verified() {
|
|
1530
|
+
local dir="$1"
|
|
1531
|
+
make_valid_plan "$dir" 1
|
|
1532
|
+
(cd "$dir" && $NODE "$STATE_JS" transition --to planned >/dev/null 2>&1)
|
|
1533
|
+
(cd "$dir" && $NODE "$STATE_JS" transition --to built --tasks-done 1 --tasks-total 1 >/dev/null 2>&1)
|
|
1534
|
+
echo "result: PASS" > "$dir/.planning/phase-1-verification.md"
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
# 1. A new project defaults to lifecycle=build
|
|
1538
|
+
TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
|
|
1539
|
+
(cd "$TMP" && git init -q 2>/dev/null; $NODE "$STATE_JS" init --project acme --total-phases 1 --phases '[{"name":"Core","goal":"x"}]' --force >/dev/null 2>&1)
|
|
1540
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
1541
|
+
if echo "$OUT" | grep -q '"lifecycle": "build"'; then
|
|
1542
|
+
pass "init defaults to lifecycle=build"
|
|
1543
|
+
else
|
|
1544
|
+
fail_case "init lifecycle default" "out=$OUT"
|
|
1545
|
+
fi
|
|
1546
|
+
|
|
1547
|
+
# 2. launch flips to operate, stamps launched_at + source, routes to /qualia-update
|
|
1548
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" launch --deployed-url https://acme.app --source erp 2>&1)
|
|
1549
|
+
if echo "$OUT" | grep -q '"lifecycle": "operate"' \
|
|
1550
|
+
&& echo "$OUT" | grep -q '"launch_source": "erp"' \
|
|
1551
|
+
&& echo "$OUT" | grep -q '"next_command": "/qualia-update"' \
|
|
1552
|
+
&& echo "$OUT" | grep -q '"launched_at"'; then
|
|
1553
|
+
pass "launch → operate (stamped, routes to /qualia-update)"
|
|
1554
|
+
else
|
|
1555
|
+
fail_case "launch to operate" "out=$OUT"
|
|
1556
|
+
fi
|
|
1557
|
+
|
|
1558
|
+
# 3. launch is idempotent
|
|
1559
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" launch 2>&1)
|
|
1560
|
+
if echo "$OUT" | grep -q '"already_launched": true'; then
|
|
1561
|
+
pass "launch is idempotent (already_launched)"
|
|
1562
|
+
else
|
|
1563
|
+
fail_case "launch idempotent" "out=$OUT"
|
|
1564
|
+
fi
|
|
1565
|
+
|
|
1566
|
+
# 4. operate: verified(pass) on the last phase routes to /qualia-update + bumps updates_completed
|
|
1567
|
+
TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
|
|
1568
|
+
(cd "$TMP" && git init -q 2>/dev/null; $NODE "$STATE_JS" init --project beta --total-phases 1 --phases '[{"name":"Core","goal":"x"}]' --force >/dev/null 2>&1)
|
|
1569
|
+
(cd "$TMP" && $NODE "$STATE_JS" launch >/dev/null 2>&1)
|
|
1570
|
+
make_verified "$TMP"
|
|
1571
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass 2>&1)
|
|
1572
|
+
if echo "$OUT" | grep -q '"next_command": "/qualia-update"'; then
|
|
1573
|
+
pass "operate verified(pass) → /qualia-update"
|
|
1574
|
+
else
|
|
1575
|
+
fail_case "operate verified routing" "out=$OUT"
|
|
1576
|
+
fi
|
|
1577
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" check 2>&1)
|
|
1578
|
+
if echo "$OUT" | grep -q '"updates_completed": 1'; then
|
|
1579
|
+
pass "operate verified(pass) bumps lifetime.updates_completed"
|
|
1580
|
+
else
|
|
1581
|
+
fail_case "updates_completed bump" "out=$OUT"
|
|
1582
|
+
fi
|
|
1583
|
+
|
|
1584
|
+
# 5. BUILD mode still REQUIRES HANDOFF.md (regression: forced handoff intact in build)
|
|
1585
|
+
TMP=$(_mktemp_native); TMP_DIRS+=("$TMP")
|
|
1586
|
+
(cd "$TMP" && git init -q 2>/dev/null; $NODE "$STATE_JS" init --project gamma --total-phases 1 --phases '[{"name":"Core","goal":"x"}]' --force >/dev/null 2>&1)
|
|
1587
|
+
make_verified "$TMP"
|
|
1588
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to verified --verification pass >/dev/null 2>&1)
|
|
1589
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to polished >/dev/null 2>&1)
|
|
1590
|
+
(cd "$TMP" && $NODE "$STATE_JS" transition --to shipped --deployed-url https://x.app >/dev/null 2>&1)
|
|
1591
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to handed_off 2>&1)
|
|
1592
|
+
if echo "$OUT" | grep -q '"error": "MISSING_FILE"'; then
|
|
1593
|
+
pass "build mode → handed_off still requires HANDOFF.md"
|
|
1594
|
+
else
|
|
1595
|
+
fail_case "build handoff still gated" "out=$OUT"
|
|
1596
|
+
fi
|
|
1597
|
+
|
|
1598
|
+
# 6. OPERATE mode: handed_off allowed WITHOUT HANDOFF.md (forced handoff removed)
|
|
1599
|
+
(cd "$TMP" && $NODE "$STATE_JS" launch >/dev/null 2>&1) # same project → flip to operate
|
|
1600
|
+
OUT=$(cd "$TMP" && $NODE "$STATE_JS" transition --to handed_off 2>&1)
|
|
1601
|
+
if echo "$OUT" | grep -q '"status": "handed_off"' && echo "$OUT" | grep -q '"ok": true'; then
|
|
1602
|
+
pass "operate mode → handed_off allowed without HANDOFF.md"
|
|
1603
|
+
else
|
|
1604
|
+
fail_case "operate handoff ungated" "out=$OUT"
|
|
1605
|
+
fi
|
|
1606
|
+
|
|
1420
1607
|
# ─── Summary ─────────────────────────────────────────────
|
|
1421
1608
|
echo ""
|
|
1422
1609
|
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# verify-panel.test.sh — bin/verify-panel.js (panel + skeptic aggregator, R8)
|
|
3
|
+
# Run: bash tests/verify-panel.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
VP="$BIN_DIR/verify-panel.js"
|
|
10
|
+
|
|
11
|
+
assert_exit() {
|
|
12
|
+
local name="$1" expected="$2" actual="$3"
|
|
13
|
+
if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
14
|
+
else echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL+1)); fi
|
|
15
|
+
}
|
|
16
|
+
assert_contains() {
|
|
17
|
+
local name="$1" hay="$2" needle="$3"
|
|
18
|
+
if echo "$hay" | grep -qF "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
19
|
+
else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
|
|
20
|
+
}
|
|
21
|
+
assert_eq() {
|
|
22
|
+
local name="$1" expected="$2" actual="$3"
|
|
23
|
+
if [ "$expected" = "$actual" ]; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
24
|
+
else echo " ✗ $name (expected '$expected', got '$actual')"; FAIL=$((FAIL+1)); fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
echo "verify-panel.test.sh — bin/verify-panel.js"
|
|
28
|
+
echo ""
|
|
29
|
+
|
|
30
|
+
$NODE -c "$VP" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
|
|
31
|
+
|
|
32
|
+
# --- clean panel: no findings → PASS, score 5 ---
|
|
33
|
+
TMP=$(mktemp -d)
|
|
34
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
35
|
+
{ "phase": 1, "lenses": ["correctness","security"], "findings": [] }
|
|
36
|
+
EOF
|
|
37
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
38
|
+
assert_exit "empty panel → PASS (exit 0)" 0 $?
|
|
39
|
+
OUT=$($NODE "$VP" "$TMP/panel.json" --json 2>&1)
|
|
40
|
+
assert_contains "verdict PASS" "$OUT" '"verdict": "PASS"'
|
|
41
|
+
assert_contains "score 5" "$OUT" '"score": 5'
|
|
42
|
+
rm -rf "$TMP"
|
|
43
|
+
|
|
44
|
+
# --- a surviving CRITICAL fails the phase ---
|
|
45
|
+
TMP=$(mktemp -d)
|
|
46
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
47
|
+
{ "phase": 2, "lenses": ["security"], "findings": [
|
|
48
|
+
{ "lens":"security", "file":"lib/auth.ts", "line":42, "severity":"CRITICAL", "title":"service_role reachable client-side", "votes": {"real":3,"notReal":0} }
|
|
49
|
+
] }
|
|
50
|
+
EOF
|
|
51
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
52
|
+
assert_exit "surviving CRITICAL → FAIL (exit 1)" 1 $?
|
|
53
|
+
OUT=$($NODE "$VP" "$TMP/panel.json" --json 2>&1)
|
|
54
|
+
assert_contains "verdict FAIL" "$OUT" '"verdict": "FAIL"'
|
|
55
|
+
rm -rf "$TMP"
|
|
56
|
+
|
|
57
|
+
# --- skeptics kill a finding by majority not-real → back to PASS ---
|
|
58
|
+
TMP=$(mktemp -d)
|
|
59
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
60
|
+
{ "phase": 2, "lenses": ["security"], "findings": [
|
|
61
|
+
{ "lens":"security", "file":"lib/auth.ts", "line":42, "severity":"CRITICAL", "title":"false alarm", "votes": {"real":1,"notReal":2} }
|
|
62
|
+
] }
|
|
63
|
+
EOF
|
|
64
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
65
|
+
assert_exit "skeptic-killed CRITICAL → PASS (exit 0)" 0 $?
|
|
66
|
+
OUT=$($NODE "$VP" "$TMP/panel.json" --json 2>&1)
|
|
67
|
+
assert_contains "finding moved to killed" "$OUT" '"killed"'
|
|
68
|
+
assert_contains "1 killed total" "$OUT" '"killed": 1'
|
|
69
|
+
rm -rf "$TMP"
|
|
70
|
+
|
|
71
|
+
# --- tie vote survives (conservative: killed only on strict majority not-real) ---
|
|
72
|
+
TMP=$(mktemp -d)
|
|
73
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
74
|
+
{ "phase": 3, "lenses": ["correctness"], "findings": [
|
|
75
|
+
{ "lens":"correctness", "file":"a.ts", "line":1, "severity":"HIGH", "title":"tie", "votes": {"real":2,"notReal":2} }
|
|
76
|
+
] }
|
|
77
|
+
EOF
|
|
78
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
79
|
+
assert_exit "tie vote → finding survives → FAIL" 1 $?
|
|
80
|
+
rm -rf "$TMP"
|
|
81
|
+
|
|
82
|
+
# --- no votes → survives (unverified != disproven) ---
|
|
83
|
+
TMP=$(mktemp -d)
|
|
84
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
85
|
+
{ "phase": 3, "lenses": ["correctness"], "findings": [
|
|
86
|
+
{ "lens":"correctness", "file":"a.ts", "line":1, "severity":"HIGH", "title":"unvoted" }
|
|
87
|
+
] }
|
|
88
|
+
EOF
|
|
89
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
90
|
+
assert_exit "no-vote HIGH survives → FAIL" 1 $?
|
|
91
|
+
rm -rf "$TMP"
|
|
92
|
+
|
|
93
|
+
# --- dedupe: same file:line:title from two lenses merges, votes sum, severity max ---
|
|
94
|
+
DEDUP=$($NODE -e '
|
|
95
|
+
const vp=require("'"$VP"'");
|
|
96
|
+
const d=vp.dedupeFindings([
|
|
97
|
+
{lens:"correctness",file:"x.ts",line:5,severity:"MEDIUM",title:"Race condition here",votes:{real:1,notReal:0}},
|
|
98
|
+
{lens:"security",file:"x.ts",line:5,severity:"HIGH",title:"Race condition here",votes:{real:1,notReal:1}}
|
|
99
|
+
]);
|
|
100
|
+
console.log(JSON.stringify({n:d.length,sev:d[0].severity,lenses:d[0].lenses,real:d[0].votes.real,notReal:d[0].votes.notReal}));
|
|
101
|
+
' 2>&1)
|
|
102
|
+
assert_contains "dedupe merges to one finding" "$DEDUP" '"n":1'
|
|
103
|
+
assert_contains "dedupe keeps highest severity" "$DEDUP" '"sev":"HIGH"'
|
|
104
|
+
assert_contains "dedupe unions lenses" "$DEDUP" 'correctness'
|
|
105
|
+
assert_contains "dedupe sums real votes" "$DEDUP" '"real":2'
|
|
106
|
+
|
|
107
|
+
# --- scoreFromCounts matches the grounding.md formula ---
|
|
108
|
+
assert_eq "score 0/0/0/0 = 5" "5" "$($NODE -e "console.log(require('$VP').scoreFromCounts({CRITICAL:0,HIGH:0,MEDIUM:0,LOW:0}))")"
|
|
109
|
+
assert_eq "score 0/2/0/0 = 4" "4" "$($NODE -e "console.log(require('$VP').scoreFromCounts({CRITICAL:0,HIGH:2,MEDIUM:0,LOW:0}))")"
|
|
110
|
+
assert_eq "score 4/0/0/0 = 1" "1" "$($NODE -e "console.log(require('$VP').scoreFromCounts({CRITICAL:4,HIGH:0,MEDIUM:0,LOW:0}))")"
|
|
111
|
+
assert_eq "score 1/2/0/0 = 3" "3" "$($NODE -e "console.log(require('$VP').scoreFromCounts({CRITICAL:1,HIGH:2,MEDIUM:0,LOW:0}))")"
|
|
112
|
+
|
|
113
|
+
# --- MEDIUM/LOW only → still PASS (severity gate is C/H) ---
|
|
114
|
+
TMP=$(mktemp -d)
|
|
115
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
116
|
+
{ "phase": 4, "lenses": ["design"], "findings": [
|
|
117
|
+
{ "lens":"design", "file":"a.css", "line":3, "severity":"MEDIUM", "title":"missing empty state", "votes":{"real":2,"notReal":0} },
|
|
118
|
+
{ "lens":"design", "file":"b.css", "line":9, "severity":"LOW", "title":"console.log", "votes":{"real":2,"notReal":0} }
|
|
119
|
+
] }
|
|
120
|
+
EOF
|
|
121
|
+
$NODE "$VP" "$TMP/panel.json" >/dev/null 2>&1
|
|
122
|
+
assert_exit "MEDIUM+LOW only → PASS (exit 0)" 0 $?
|
|
123
|
+
rm -rf "$TMP"
|
|
124
|
+
|
|
125
|
+
# --- --write emits panel artifacts ---
|
|
126
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
127
|
+
cat > "$TMP/panel.json" <<'EOF'
|
|
128
|
+
{ "phase": 7, "lenses": ["security"], "findings": [
|
|
129
|
+
{ "lens":"security", "file":"s.ts", "line":1, "severity":"CRITICAL", "title":"leak", "votes":{"real":2,"notReal":0} }
|
|
130
|
+
] }
|
|
131
|
+
EOF
|
|
132
|
+
(cd "$TMP" && $NODE "$VP" panel.json --write >/dev/null 2>&1)
|
|
133
|
+
[ -f "$TMP/.planning/phase-7-verification-panel.json" ] && { echo " ✓ writes panel json artifact"; PASS=$((PASS+1)); } || { echo " ✗ no panel json artifact"; FAIL=$((FAIL+1)); }
|
|
134
|
+
[ -f "$TMP/.planning/phase-7-verification-panel.md" ] && { echo " ✓ writes panel md artifact"; PASS=$((PASS+1)); } || { echo " ✗ no panel md artifact"; FAIL=$((FAIL+1)); }
|
|
135
|
+
rm -rf "$TMP"
|
|
136
|
+
|
|
137
|
+
# --- assemble: merge per-lens finding files into one panel.json ---
|
|
138
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
139
|
+
cat > "$TMP/.planning/phase-5-panel-security.json" <<'EOF'
|
|
140
|
+
[{"file":"auth.ts","line":1,"severity":"CRITICAL","title":"leak"}]
|
|
141
|
+
EOF
|
|
142
|
+
cat > "$TMP/.planning/phase-5-panel-correctness.json" <<'EOF'
|
|
143
|
+
[{"file":"util.ts","line":9,"severity":"MEDIUM","title":"off by one"}]
|
|
144
|
+
EOF
|
|
145
|
+
(cd "$TMP" && $NODE "$VP" assemble 5 >/dev/null 2>&1)
|
|
146
|
+
assert_exit "assemble exits 0" 0 $?
|
|
147
|
+
ASM=$(cat "$TMP/.planning/phase-5-panel.json")
|
|
148
|
+
assert_contains "assemble tags security lens" "$ASM" '"lens": "security"'
|
|
149
|
+
assert_contains "assemble tags correctness lens" "$ASM" '"lens": "correctness"'
|
|
150
|
+
assert_contains "assemble zeroes votes" "$ASM" '"real": 0'
|
|
151
|
+
# round-trip: assembled panel aggregates to FAIL (the CRITICAL survives, unvoted)
|
|
152
|
+
(cd "$TMP" && $NODE "$VP" .planning/phase-5-panel.json >/dev/null 2>&1)
|
|
153
|
+
assert_exit "assembled panel aggregates (CRITICAL → FAIL)" 1 $?
|
|
154
|
+
rm -rf "$TMP"
|
|
155
|
+
|
|
156
|
+
# --- malformed input → exit 2 ---
|
|
157
|
+
$NODE "$VP" /nonexistent/panel.json >/dev/null 2>&1
|
|
158
|
+
assert_exit "missing panel file → exit 2" 2 $?
|
|
159
|
+
|
|
160
|
+
echo ""
|
|
161
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
162
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|