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
|
@@ -75,17 +75,17 @@ Fixed scope for every project. Do not reassign these elsewhere.
|
|
|
75
75
|
|
|
76
76
|
## Post-Handoff (v2)
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
**Only** capabilities the client EXPLICITLY deferred in discovery §8 (Out of Scope) belong here. This is NOT an overflow bucket for a milestone cap — there is no cap; everything in the capability inventory (discovery §9) must be a REQ-ID mapped to a milestone above. If a capability is agreed but absent from the arc, that's a roadmap bug, not a v2 item.
|
|
79
79
|
|
|
80
80
|
### {Category}
|
|
81
81
|
|
|
82
|
-
- **{CAT}-XX**: {capability}
|
|
82
|
+
- **{CAT}-XX**: {capability} — deferred by client (discovery §8)
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
86
86
|
## Out of Scope
|
|
87
87
|
|
|
88
|
-
Explicit exclusions with reasoning. Prevents scope creep.
|
|
88
|
+
Explicit exclusions with reasoning, drawn from discovery §8. Prevents scope creep.
|
|
89
89
|
|
|
90
90
|
| Feature | Reason |
|
|
91
91
|
|---------|--------|
|
|
@@ -95,16 +95,16 @@ Explicit exclusions with reasoning. Prevents scope creep.
|
|
|
95
95
|
|
|
96
96
|
## Traceability
|
|
97
97
|
|
|
98
|
-
Populated during roadmap creation. Every
|
|
98
|
+
Populated during roadmap creation. Every capability from discovery §9 maps to exactly one milestone + phase. Coverage of the full inventory — not a v1 slice — is the gate.
|
|
99
99
|
|
|
100
100
|
| Requirement | Milestone | Phase | Status |
|
|
101
101
|
|-------------|-----------|-------|--------|
|
|
102
102
|
| {CAT}-01 | M1: {name} | Phase {N} | Pending |
|
|
103
103
|
|
|
104
|
-
**Coverage:**
|
|
105
|
-
-
|
|
104
|
+
**Coverage (must be 100% of the §9 capability inventory):**
|
|
105
|
+
- Agreed capabilities (discovery §9, whole project): {X} total
|
|
106
106
|
- Mapped to milestones + phases: {Y}
|
|
107
|
-
- Unmapped: {Z}
|
|
107
|
+
- Unmapped: {Z} ← MUST be 0 before the journey-approval gate passes
|
|
108
108
|
|
|
109
109
|
---
|
|
110
110
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# agent-status.test.sh — bin/agent-status.js (per-task status + fan-in barrier)
|
|
3
|
+
# Run: bash tests/agent-status.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
AS="$BIN_DIR/agent-status.js"
|
|
10
|
+
|
|
11
|
+
assert_exit() {
|
|
12
|
+
local name="$1" expected="$2" actual="$3"
|
|
13
|
+
if [ "$expected" = "$actual" ]; then
|
|
14
|
+
echo " ✓ $name"; PASS=$((PASS + 1))
|
|
15
|
+
else
|
|
16
|
+
echo " ✗ $name (expected exit $expected, got $actual)"; FAIL=$((FAIL + 1))
|
|
17
|
+
fi
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
assert_contains() {
|
|
21
|
+
local name="$1" haystack="$2" needle="$3"
|
|
22
|
+
if echo "$haystack" | grep -q "$needle"; then
|
|
23
|
+
echo " ✓ $name"; PASS=$((PASS + 1))
|
|
24
|
+
else
|
|
25
|
+
echo " ✗ $name (missing '$needle' in: $haystack)"; FAIL=$((FAIL + 1))
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# A minimal contract with two tasks in wave 1, one in wave 2.
|
|
30
|
+
write_contract() {
|
|
31
|
+
cat > "$1" <<'EOF'
|
|
32
|
+
{
|
|
33
|
+
"version": 1,
|
|
34
|
+
"tasks": [
|
|
35
|
+
{ "id": "T1", "wave": 1 },
|
|
36
|
+
{ "id": "T2", "wave": 1 },
|
|
37
|
+
{ "id": "T3", "wave": 2 }
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
EOF
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
echo "agent-status.test.sh — bin/agent-status.js"
|
|
44
|
+
echo ""
|
|
45
|
+
|
|
46
|
+
# syntax
|
|
47
|
+
$NODE -c "$AS" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
|
|
48
|
+
|
|
49
|
+
# --- write / read round-trip ---
|
|
50
|
+
TMP=$(mktemp -d)
|
|
51
|
+
$NODE "$AS" write T1 RUNNING --phase 3 --wave 1 --cwd "$TMP" >/dev/null 2>&1
|
|
52
|
+
assert_exit "write RUNNING → exit 0" 0 $?
|
|
53
|
+
[ -f "$TMP/.agent-status/T1.json" ] && { echo " ✓ status file created"; PASS=$((PASS+1)); } || { echo " ✗ status file missing"; FAIL=$((FAIL+1)); }
|
|
54
|
+
OUT=$($NODE "$AS" read T1 --cwd "$TMP" --json 2>&1)
|
|
55
|
+
assert_contains "read shows RUNNING" "$OUT" '"status":"RUNNING"'
|
|
56
|
+
$NODE "$AS" write T1 DONE --commit abc1234 --cwd "$TMP" >/dev/null 2>&1
|
|
57
|
+
OUT=$($NODE "$AS" read T1 --cwd "$TMP" --json 2>&1)
|
|
58
|
+
assert_contains "overwrite to DONE" "$OUT" '"status":"DONE"'
|
|
59
|
+
assert_contains "records commit hash" "$OUT" 'abc1234'
|
|
60
|
+
rm -rf "$TMP"
|
|
61
|
+
|
|
62
|
+
# --- validation: bad task id and bad status are rejected (exit 2) ---
|
|
63
|
+
TMP=$(mktemp -d)
|
|
64
|
+
$NODE "$AS" write nope DONE --cwd "$TMP" >/dev/null 2>&1
|
|
65
|
+
assert_exit "invalid task id → exit 2" 2 $?
|
|
66
|
+
$NODE "$AS" write T1 FINISHED --cwd "$TMP" >/dev/null 2>&1
|
|
67
|
+
assert_exit "invalid status → exit 2" 2 $?
|
|
68
|
+
# traversal attempt in task id is rejected (no file written outside dir)
|
|
69
|
+
$NODE "$AS" write ../evil DONE --cwd "$TMP" >/dev/null 2>&1
|
|
70
|
+
assert_exit "path-traversal task id → exit 2" 2 $?
|
|
71
|
+
rm -rf "$TMP"
|
|
72
|
+
|
|
73
|
+
# --- barrier: holds until all expected tasks in the wave are DONE ---
|
|
74
|
+
TMP=$(mktemp -d)
|
|
75
|
+
write_contract "$TMP/contract.json"
|
|
76
|
+
|
|
77
|
+
# Nothing written yet → wave 1 barrier HOLDS (exit 1), T1+T2 MISSING
|
|
78
|
+
OUT=$($NODE "$AS" barrier "$TMP/contract.json" --wave 1 --cwd "$TMP" 2>&1)
|
|
79
|
+
RC=$?
|
|
80
|
+
assert_exit "empty wave-1 barrier holds" 1 $RC
|
|
81
|
+
assert_contains "barrier reports HOLD" "$OUT" "BARRIER HOLD"
|
|
82
|
+
|
|
83
|
+
# One DONE, one RUNNING → still holds
|
|
84
|
+
$NODE "$AS" write T1 DONE --commit aaa --cwd "$TMP" >/dev/null 2>&1
|
|
85
|
+
$NODE "$AS" write T2 RUNNING --cwd "$TMP" >/dev/null 2>&1
|
|
86
|
+
$NODE "$AS" barrier "$TMP/contract.json" --wave 1 --cwd "$TMP" >/dev/null 2>&1
|
|
87
|
+
assert_exit "partial wave-1 barrier holds" 1 $?
|
|
88
|
+
|
|
89
|
+
# Both DONE → wave 1 barrier PASSES (exit 0)
|
|
90
|
+
$NODE "$AS" write T2 DONE --commit bbb --cwd "$TMP" >/dev/null 2>&1
|
|
91
|
+
OUT=$($NODE "$AS" barrier "$TMP/contract.json" --wave 1 --cwd "$TMP" 2>&1)
|
|
92
|
+
RC=$?
|
|
93
|
+
assert_exit "complete wave-1 barrier passes" 0 $RC
|
|
94
|
+
assert_contains "barrier reports PASS" "$OUT" "BARRIER PASS"
|
|
95
|
+
|
|
96
|
+
# Wave 2 still has T3 MISSING → holds independently
|
|
97
|
+
$NODE "$AS" barrier "$TMP/contract.json" --wave 2 --cwd "$TMP" >/dev/null 2>&1
|
|
98
|
+
assert_exit "wave-2 barrier still holds (T3 missing)" 1 $?
|
|
99
|
+
|
|
100
|
+
# Whole-phase barrier (no --wave) holds because T3 not done
|
|
101
|
+
$NODE "$AS" barrier "$TMP/contract.json" --cwd "$TMP" >/dev/null 2>&1
|
|
102
|
+
assert_exit "phase barrier holds while T3 open" 1 $?
|
|
103
|
+
|
|
104
|
+
# Finish T3 → phase barrier passes
|
|
105
|
+
$NODE "$AS" write T3 DONE --commit ccc --cwd "$TMP" >/dev/null 2>&1
|
|
106
|
+
$NODE "$AS" barrier "$TMP/contract.json" --cwd "$TMP" >/dev/null 2>&1
|
|
107
|
+
assert_exit "phase barrier passes when all done" 0 $?
|
|
108
|
+
|
|
109
|
+
# A BLOCKED task holds the barrier (BLOCKED != DONE)
|
|
110
|
+
$NODE "$AS" write T3 BLOCKED --note "missing dep" --cwd "$TMP" >/dev/null 2>&1
|
|
111
|
+
OUT=$($NODE "$AS" barrier "$TMP/contract.json" --cwd "$TMP" --json 2>&1)
|
|
112
|
+
RC=$?
|
|
113
|
+
assert_exit "BLOCKED task holds barrier" 1 $RC
|
|
114
|
+
assert_contains "barrier json counts blocked" "$OUT" '"blocked": 1'
|
|
115
|
+
rm -rf "$TMP"
|
|
116
|
+
|
|
117
|
+
# --- barrier --tasks (explicit batch gate, no contract needed; R16 wave-plan) ---
|
|
118
|
+
TMP=$(mktemp -d)
|
|
119
|
+
$NODE "$AS" write T1 DONE --commit a --cwd "$TMP" >/dev/null 2>&1
|
|
120
|
+
$NODE "$AS" write T2 RUNNING --cwd "$TMP" >/dev/null 2>&1
|
|
121
|
+
$NODE "$AS" write T5 DONE --commit b --cwd "$TMP" >/dev/null 2>&1
|
|
122
|
+
# batch {T1,T5} both DONE → pass, with no contract file present
|
|
123
|
+
$NODE "$AS" barrier --tasks T1,T5 --cwd "$TMP" >/dev/null 2>&1
|
|
124
|
+
assert_exit "barrier --tasks all DONE → pass (no contract)" 0 $?
|
|
125
|
+
# batch {T1,T2} → T2 RUNNING → hold
|
|
126
|
+
$NODE "$AS" barrier --tasks T1,T2 --cwd "$TMP" >/dev/null 2>&1
|
|
127
|
+
assert_exit "barrier --tasks with a RUNNING member → hold" 1 $?
|
|
128
|
+
OUT=$($NODE "$AS" barrier --tasks T1,T2 --cwd "$TMP" 2>&1)
|
|
129
|
+
assert_contains "barrier --tasks scope label" "$OUT" "batch T1,T2"
|
|
130
|
+
rm -rf "$TMP"
|
|
131
|
+
|
|
132
|
+
# --- list + clear ---
|
|
133
|
+
TMP=$(mktemp -d)
|
|
134
|
+
$NODE "$AS" write T1 DONE --cwd "$TMP" >/dev/null 2>&1
|
|
135
|
+
$NODE "$AS" write T2 RUNNING --cwd "$TMP" >/dev/null 2>&1
|
|
136
|
+
OUT=$($NODE "$AS" list --cwd "$TMP" 2>&1)
|
|
137
|
+
assert_contains "list shows T1" "$OUT" "T1"
|
|
138
|
+
assert_contains "list shows T2" "$OUT" "T2"
|
|
139
|
+
$NODE "$AS" clear --cwd "$TMP" >/dev/null 2>&1
|
|
140
|
+
[ ! -d "$TMP/.agent-status" ] && { echo " ✓ clear removes status dir"; PASS=$((PASS+1)); } || { echo " ✗ clear left status dir"; FAIL=$((FAIL+1)); }
|
|
141
|
+
rm -rf "$TMP"
|
|
142
|
+
|
|
143
|
+
# --- buildActive library signal (used by R1's pre-write guard) ---
|
|
144
|
+
TMP=$(mktemp -d)
|
|
145
|
+
ACTIVE=$($NODE -e "const a=require('$AS'); a.writeStatus('$TMP',{task:'T1',status:'RUNNING'}); console.log(a.buildActive('$TMP'))" 2>&1)
|
|
146
|
+
assert_contains "buildActive true while RUNNING" "$ACTIVE" "true"
|
|
147
|
+
IDLE=$($NODE -e "const a=require('$AS'); a.writeStatus('$TMP',{task:'T1',status:'DONE'}); console.log(a.buildActive('$TMP'))" 2>&1)
|
|
148
|
+
assert_contains "buildActive false when none RUNNING" "$IDLE" "false"
|
|
149
|
+
rm -rf "$TMP"
|
|
150
|
+
|
|
151
|
+
echo ""
|
|
152
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
153
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# analyze-gate.test.sh — bin/analyze-gate.js (cross-artifact scope↔plan gate)
|
|
3
|
+
# Run: bash tests/analyze-gate.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
AG="$BIN_DIR/analyze-gate.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 -q "$needle"; then echo " ✓ $name"; PASS=$((PASS+1));
|
|
19
|
+
else echo " ✗ $name (missing '$needle' in: $hay)"; FAIL=$((FAIL+1)); fi
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# A contract whose tasks cover "checkout" + "stripe webhook" + "email receipt".
|
|
23
|
+
write_good_contract() {
|
|
24
|
+
cat > "$1" <<'EOF'
|
|
25
|
+
{
|
|
26
|
+
"version": 1,
|
|
27
|
+
"phase": 2,
|
|
28
|
+
"goal": "Customers can pay for an order and receive a receipt",
|
|
29
|
+
"why": "revenue",
|
|
30
|
+
"success_criteria": ["Checkout charges the card via Stripe", "Customer receives an email receipt"],
|
|
31
|
+
"tasks": [
|
|
32
|
+
{ "id": "T1", "title": "Stripe checkout charge", "action": "Implement the checkout endpoint that charges the card through the Stripe webhook handler", "acceptance_criteria": ["Checkout charges card via Stripe successfully"] },
|
|
33
|
+
{ "id": "T2", "title": "Email receipt", "action": "Send an email receipt to the customer after a successful charge", "acceptance_criteria": ["Customer receives an email receipt after payment"] }
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
EOF
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
write_scope() {
|
|
40
|
+
cat > "$1" <<'EOF'
|
|
41
|
+
---
|
|
42
|
+
phase: 2
|
|
43
|
+
---
|
|
44
|
+
# Phase 2 Context: Payments
|
|
45
|
+
|
|
46
|
+
## Acceptance Criteria (testable)
|
|
47
|
+
- AC1 — Checkout charges the card through Stripe
|
|
48
|
+
- AC2 — Customer receives an email receipt
|
|
49
|
+
- AC3 — Failed payments display a retry banner with a refund option
|
|
50
|
+
|
|
51
|
+
## DoD closure
|
|
52
|
+
- Security: resolved
|
|
53
|
+
EOF
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
echo "analyze-gate.test.sh — bin/analyze-gate.js"
|
|
57
|
+
echo ""
|
|
58
|
+
|
|
59
|
+
# syntax
|
|
60
|
+
$NODE -c "$AG" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
|
|
61
|
+
|
|
62
|
+
# --- clean: every scope AC covered, every success criterion has a task ---
|
|
63
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
64
|
+
write_good_contract "$TMP/.planning/phase-2-contract.json"
|
|
65
|
+
# scope with only the two covered ACs
|
|
66
|
+
cat > "$TMP/.planning/phase-2-context.md" <<'EOF'
|
|
67
|
+
## Acceptance Criteria (testable)
|
|
68
|
+
- AC1 — Checkout charges the card through Stripe
|
|
69
|
+
- AC2 — Customer receives an email receipt
|
|
70
|
+
EOF
|
|
71
|
+
$NODE "$AG" 2 --cwd "$TMP" >/dev/null 2>&1
|
|
72
|
+
assert_exit "all covered → ANALYZE PASS (exit 0)" 0 $?
|
|
73
|
+
OUT=$($NODE "$AG" 2 --cwd "$TMP" 2>&1)
|
|
74
|
+
assert_contains "reports PASS" "$OUT" "ANALYZE PASS"
|
|
75
|
+
rm -rf "$TMP"
|
|
76
|
+
|
|
77
|
+
# --- under-covered scope AC (the retry/refund banner is in scope, not in plan) ---
|
|
78
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
79
|
+
write_good_contract "$TMP/.planning/phase-2-contract.json"
|
|
80
|
+
write_scope "$TMP/.planning/phase-2-context.md"
|
|
81
|
+
OUT=$($NODE "$AG" 2 --cwd "$TMP" 2>&1)
|
|
82
|
+
RC=$?
|
|
83
|
+
assert_exit "uncovered scope AC → findings (exit 1)" 1 $RC
|
|
84
|
+
assert_contains "flags the uncovered AC" "$OUT" "under-covered"
|
|
85
|
+
assert_contains "names the refund/retry requirement" "$OUT" "retry banner"
|
|
86
|
+
# JSON shape carries the finding type
|
|
87
|
+
OUT=$($NODE "$AG" 2 --cwd "$TMP" --json 2>&1)
|
|
88
|
+
assert_contains "json finding type" "$OUT" '"uncovered-scope-ac"'
|
|
89
|
+
assert_contains "json severity HIGH" "$OUT" '"severity": "HIGH"'
|
|
90
|
+
rm -rf "$TMP"
|
|
91
|
+
|
|
92
|
+
# --- orphan success criterion (no task covers it) ---
|
|
93
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
94
|
+
cat > "$TMP/.planning/phase-3-contract.json" <<'EOF'
|
|
95
|
+
{
|
|
96
|
+
"version": 1,
|
|
97
|
+
"phase": 3,
|
|
98
|
+
"goal": "g",
|
|
99
|
+
"why": "w",
|
|
100
|
+
"success_criteria": ["Dashboard exports analytics to CSV"],
|
|
101
|
+
"tasks": [
|
|
102
|
+
{ "id": "T1", "title": "Login form", "action": "Build the login form with password reset", "acceptance_criteria": ["User can log in"] }
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
EOF
|
|
106
|
+
OUT=$($NODE "$AG" 3 --cwd "$TMP" --json 2>&1)
|
|
107
|
+
RC=$?
|
|
108
|
+
assert_exit "orphan success criterion → exit 1" 1 $RC
|
|
109
|
+
assert_contains "flags orphan success criterion" "$OUT" '"uncovered-success-criterion"'
|
|
110
|
+
rm -rf "$TMP"
|
|
111
|
+
|
|
112
|
+
# --- glossary violation: plan uses a banned alias ---
|
|
113
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
114
|
+
cat > "$TMP/.planning/phase-1-contract.json" <<'EOF'
|
|
115
|
+
{
|
|
116
|
+
"version": 1,
|
|
117
|
+
"phase": 1,
|
|
118
|
+
"goal": "Manage the AuthUser session lifecycle",
|
|
119
|
+
"why": "w",
|
|
120
|
+
"success_criteria": ["Sessions expire after 24h"],
|
|
121
|
+
"tasks": [
|
|
122
|
+
{ "id": "T1", "title": "Session expiry", "action": "Expire AuthUser sessions after twentyfour hours of inactivity", "acceptance_criteria": ["Sessions expire after the configured window"] }
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
EOF
|
|
126
|
+
cat > "$TMP/.planning/CONTEXT.md" <<'EOF'
|
|
127
|
+
# Glossary
|
|
128
|
+
## Language
|
|
129
|
+
### Customer
|
|
130
|
+
The paying account holder.
|
|
131
|
+
**Avoid:** AuthUser vs Customer (unless disambiguated)
|
|
132
|
+
EOF
|
|
133
|
+
OUT=$($NODE "$AG" 1 --cwd "$TMP" --json 2>&1)
|
|
134
|
+
RC=$?
|
|
135
|
+
assert_exit "banned glossary alias → exit 1" 1 $RC
|
|
136
|
+
assert_contains "flags glossary violation" "$OUT" '"glossary-violation"'
|
|
137
|
+
assert_contains "names the banned term" "$OUT" "AuthUser"
|
|
138
|
+
rm -rf "$TMP"
|
|
139
|
+
|
|
140
|
+
# --- no scope file → scope-coverage skipped, not a failure ---
|
|
141
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
142
|
+
write_good_contract "$TMP/.planning/phase-2-contract.json"
|
|
143
|
+
# no phase-2-context.md, no CONTEXT.md
|
|
144
|
+
OUT=$($NODE "$AG" 2 --cwd "$TMP" 2>&1)
|
|
145
|
+
RC=$?
|
|
146
|
+
assert_exit "no scope file → exit 0 (skipped)" 0 $RC
|
|
147
|
+
assert_contains "notes the skip" "$OUT" "scope-coverage check skipped"
|
|
148
|
+
rm -rf "$TMP"
|
|
149
|
+
|
|
150
|
+
# --- missing contract → invocation error (exit 2) ---
|
|
151
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
152
|
+
$NODE "$AG" 9 --cwd "$TMP" >/dev/null 2>&1
|
|
153
|
+
assert_exit "missing contract → exit 2" 2 $?
|
|
154
|
+
rm -rf "$TMP"
|
|
155
|
+
|
|
156
|
+
# --- library: coverage() unit behavior ---
|
|
157
|
+
COV=$($NODE -e "const a=require('$AG'); const t=a.tokenize('Checkout charges the card via Stripe'); const set=new Set(t); console.log(a.coverage('Checkout charges card Stripe', set).covered)" 2>&1)
|
|
158
|
+
assert_contains "coverage true when terms overlap" "$COV" "true"
|
|
159
|
+
COV=$($NODE -e "const a=require('$AG'); console.log(a.coverage('Quantum teleportation export pipeline', new Set(['login','password'])).covered)" 2>&1)
|
|
160
|
+
assert_contains "coverage false when disjoint" "$COV" "false"
|
|
161
|
+
|
|
162
|
+
# --- library: scope AC parser strips the AC label ---
|
|
163
|
+
ACS=$($NODE -e "const a=require('$AG'); console.log(JSON.stringify(a.parseScopeAcceptanceCriteria('## Acceptance Criteria (testable)\n- AC1 — Checkout works\n- AC2 — Receipt sent\n## Next\n- ignored')))" 2>&1)
|
|
164
|
+
assert_contains "parses AC1 without label" "$ACS" "Checkout works"
|
|
165
|
+
assert_contains "stops at next heading" "$ACS" "Receipt sent"
|
|
166
|
+
if echo "$ACS" | grep -q "ignored"; then echo " ✗ parser leaked past section"; FAIL=$((FAIL+1)); else echo " ✓ parser stops at next ## heading"; PASS=$((PASS+1)); fi
|
|
167
|
+
|
|
168
|
+
echo ""
|
|
169
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
170
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
package/tests/bin.test.sh
CHANGED
|
@@ -487,15 +487,16 @@ else
|
|
|
487
487
|
fail_case "CLAUDE.md role substitution"
|
|
488
488
|
fi
|
|
489
489
|
|
|
490
|
-
# 31. All
|
|
490
|
+
# 31. All 15 hooks installed (block-env-edit removed in v3.2.0;
|
|
491
491
|
# git-guardrails + stop-session-log added in v4.2.0;
|
|
492
492
|
# vercel-account-guard + env-empty-guard + supabase-destructive-guard added in v5.0.0;
|
|
493
493
|
# fawzi-approval-guard added in v6.2.11; pre-compact removed in v6.2.0 and
|
|
494
494
|
# REINTRODUCED in v6.3.2 with sidecar-snapshot mechanism;
|
|
495
|
-
# usage-capture added in v6.9.1 — UserPromptSubmit telemetry capture
|
|
495
|
+
# usage-capture added in v6.9.1 — UserPromptSubmit telemetry capture;
|
|
496
|
+
# task-write-guard added in v6.13 — R1 runtime plan-contract file-scope guard)
|
|
496
497
|
HOOK_COUNT=$(ls "$TMP/.claude/hooks/"*.js 2>/dev/null | wc -l)
|
|
497
|
-
if [ "$HOOK_COUNT" -eq
|
|
498
|
-
pass "
|
|
498
|
+
if [ "$HOOK_COUNT" -eq 15 ]; then
|
|
499
|
+
pass "15 hooks installed in hooks/ (incl. task-write-guard v6.13)"
|
|
499
500
|
else
|
|
500
501
|
fail_case "hook count" "got $HOOK_COUNT"
|
|
501
502
|
fi
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# branch-hygiene.test.sh — bin/branch-hygiene.js (clock-out stranded-branch sweep)
|
|
3
|
+
# Run: bash tests/branch-hygiene.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
BH="$BIN_DIR/branch-hygiene.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
|
+
|
|
22
|
+
# fresh repo on main with one commit; prints the dir (caller rm -rf)
|
|
23
|
+
setup_repo() {
|
|
24
|
+
local tmp
|
|
25
|
+
tmp=$(mktemp -d)
|
|
26
|
+
(cd "$tmp" \
|
|
27
|
+
&& git init -q \
|
|
28
|
+
&& git checkout -q -b main 2>/dev/null \
|
|
29
|
+
&& git config user.email t@t.com && git config user.name T \
|
|
30
|
+
&& echo seed > seed.txt && git add seed.txt && git commit -q -m seed)
|
|
31
|
+
echo "$tmp"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
echo "branch-hygiene.test.sh — bin/branch-hygiene.js"
|
|
35
|
+
echo ""
|
|
36
|
+
|
|
37
|
+
$NODE -c "$BH" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
|
|
38
|
+
|
|
39
|
+
# --- not a git repo → exit 2 ---
|
|
40
|
+
TMP=$(mktemp -d)
|
|
41
|
+
(cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
|
|
42
|
+
assert_exit "not a git repo → exit 2" 2 $?
|
|
43
|
+
rm -rf "$TMP"
|
|
44
|
+
|
|
45
|
+
# --- clean: only main → exit 0 ---
|
|
46
|
+
TMP=$(setup_repo)
|
|
47
|
+
(cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
|
|
48
|
+
assert_exit "clean repo (main only) → exit 0" 0 $?
|
|
49
|
+
OUT=$(cd "$TMP" && $NODE "$BH" 2>&1)
|
|
50
|
+
assert_contains "reports clean" "$OUT" "clean"
|
|
51
|
+
rm -rf "$TMP"
|
|
52
|
+
|
|
53
|
+
# --- stranded feature branch ahead of main → exit 1, listed ---
|
|
54
|
+
TMP=$(setup_repo)
|
|
55
|
+
(cd "$TMP" && git checkout -q -b feat/stranded && echo work > w.txt && git add w.txt && git commit -q -m "wip work")
|
|
56
|
+
(cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
|
|
57
|
+
assert_exit "branch ahead of main → exit 1" 1 $?
|
|
58
|
+
OUT=$(cd "$TMP" && $NODE "$BH" 2>&1)
|
|
59
|
+
assert_contains "lists the stranded branch" "$OUT" "feat/stranded"
|
|
60
|
+
assert_contains "shows commits ahead" "$OUT" "+1 commit"
|
|
61
|
+
# json shape
|
|
62
|
+
OUT=$(cd "$TMP" && $NODE "$BH" --json 2>&1)
|
|
63
|
+
assert_contains "json stranded entry" "$OUT" '"branch": "feat/stranded"'
|
|
64
|
+
assert_contains "json ahead count" "$OUT" '"ahead": 1'
|
|
65
|
+
rm -rf "$TMP"
|
|
66
|
+
|
|
67
|
+
# --- once integrated (ff-merged) into main → no longer stranded → exit 0 ---
|
|
68
|
+
TMP=$(setup_repo)
|
|
69
|
+
(cd "$TMP" && git checkout -q -b feat/done && echo x > x.txt && git add x.txt && git commit -q -m "done work")
|
|
70
|
+
(cd "$TMP" && git checkout -q main && git merge -q --ff-only feat/done)
|
|
71
|
+
(cd "$TMP" && $NODE "$BH" >/dev/null 2>&1)
|
|
72
|
+
assert_exit "ff-merged branch no longer stranded → exit 0" 0 $?
|
|
73
|
+
rm -rf "$TMP"
|
|
74
|
+
|
|
75
|
+
# --- master as the base branch is detected ---
|
|
76
|
+
TMP=$(mktemp -d)
|
|
77
|
+
(cd "$TMP" && git init -q && git checkout -q -b master 2>/dev/null && git config user.email t@t.com && git config user.name T && echo s > s.txt && git add s.txt && git commit -q -m s)
|
|
78
|
+
(cd "$TMP" && git checkout -q -b feature && echo y > y.txt && git add y.txt && git commit -q -m y)
|
|
79
|
+
OUT=$(cd "$TMP" && $NODE "$BH" --json 2>&1)
|
|
80
|
+
assert_contains "detects master as base" "$OUT" '"base": "master"'
|
|
81
|
+
assert_contains "stranded vs master" "$OUT" '"branch": "feature"'
|
|
82
|
+
rm -rf "$TMP"
|
|
83
|
+
|
|
84
|
+
# --- library: analyze() returns structured result ---
|
|
85
|
+
TMP=$(setup_repo)
|
|
86
|
+
(cd "$TMP" && git checkout -q -b b1 && echo a>a && git add a && git commit -q -m a)
|
|
87
|
+
RES=$($NODE -e "console.log(JSON.stringify(require('$BH').analyze('$TMP').stranded.length))" 2>&1)
|
|
88
|
+
assert_contains "analyze() finds 1 stranded" "$RES" "1"
|
|
89
|
+
rm -rf "$TMP"
|
|
90
|
+
|
|
91
|
+
echo ""
|
|
92
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
93
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# eval-runner.test.sh — bin/eval-runner.js (layered AI-feature eval, R7)
|
|
3
|
+
# Run: bash tests/eval-runner.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
BIN_DIR="$(cd "$(dirname "$0")/../bin" && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
ER="$BIN_DIR/eval-runner.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
|
+
|
|
22
|
+
echo "eval-runner.test.sh — bin/eval-runner.js"
|
|
23
|
+
echo ""
|
|
24
|
+
|
|
25
|
+
$NODE -c "$ER" 2>/dev/null && { echo " ✓ syntax valid"; PASS=$((PASS+1)); } || { echo " ✗ syntax invalid"; FAIL=$((FAIL+1)); }
|
|
26
|
+
|
|
27
|
+
# --- all deterministic assertions pass → PASS ---
|
|
28
|
+
TMP=$(mktemp -d)
|
|
29
|
+
cat > "$TMP/s.json" <<'EOF'
|
|
30
|
+
{ "feature":"support-chat", "cases":[
|
|
31
|
+
{ "name":"refund", "output":"We refund within 30 days.", "latency_ms":1200, "cost_usd":0.008,
|
|
32
|
+
"assert":[
|
|
33
|
+
{"type":"contains","value":"30 days"},
|
|
34
|
+
{"type":"not_contains","value":"I cannot help"},
|
|
35
|
+
{"type":"regex","value":"\\b30\\b"},
|
|
36
|
+
{"type":"min_length","value":5},
|
|
37
|
+
{"type":"max_latency_ms","value":2000},
|
|
38
|
+
{"type":"max_cost_usd","value":0.02}
|
|
39
|
+
] } ] }
|
|
40
|
+
EOF
|
|
41
|
+
$NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
|
|
42
|
+
assert_exit "all deterministic pass → exit 0" 0 $?
|
|
43
|
+
OUT=$($NODE "$ER" "$TMP/s.json" 2>&1)
|
|
44
|
+
assert_contains "reports EVAL PASS" "$OUT" "EVAL PASS"
|
|
45
|
+
rm -rf "$TMP"
|
|
46
|
+
|
|
47
|
+
# --- a failing contains → FAIL with the failing assertion shown ---
|
|
48
|
+
TMP=$(mktemp -d)
|
|
49
|
+
cat > "$TMP/s.json" <<'EOF'
|
|
50
|
+
{ "feature":"chat", "cases":[
|
|
51
|
+
{ "name":"refusal", "output":"I cannot help with that.",
|
|
52
|
+
"assert":[ {"type":"contains","value":"30 days"}, {"type":"not_contains","value":"I cannot help"} ] } ] }
|
|
53
|
+
EOF
|
|
54
|
+
$NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
|
|
55
|
+
assert_exit "failing assertion → exit 1" 1 $?
|
|
56
|
+
OUT=$($NODE "$ER" "$TMP/s.json" 2>&1)
|
|
57
|
+
assert_contains "shows failing case" "$OUT" "refusal"
|
|
58
|
+
rm -rf "$TMP"
|
|
59
|
+
|
|
60
|
+
# --- latency over budget → FAIL ---
|
|
61
|
+
TMP=$(mktemp -d)
|
|
62
|
+
cat > "$TMP/s.json" <<'EOF'
|
|
63
|
+
{ "feature":"chat", "cases":[
|
|
64
|
+
{ "name":"slow", "output":"ok", "latency_ms":5000, "assert":[ {"type":"max_latency_ms","value":2000} ] } ] }
|
|
65
|
+
EOF
|
|
66
|
+
$NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
|
|
67
|
+
assert_exit "latency over budget → exit 1" 1 $?
|
|
68
|
+
# missing latency metric but asserted → fail (no silent pass)
|
|
69
|
+
cat > "$TMP/s2.json" <<'EOF'
|
|
70
|
+
{ "feature":"chat", "cases":[ { "name":"no-metric", "output":"ok", "assert":[ {"type":"max_latency_ms","value":2000} ] } ] }
|
|
71
|
+
EOF
|
|
72
|
+
$NODE "$ER" "$TMP/s2.json" >/dev/null 2>&1
|
|
73
|
+
assert_exit "asserting latency with none recorded → exit 1" 1 $?
|
|
74
|
+
rm -rf "$TMP"
|
|
75
|
+
|
|
76
|
+
# --- json_path + json_valid ---
|
|
77
|
+
TMP=$(mktemp -d)
|
|
78
|
+
cat > "$TMP/s.json" <<'EOF'
|
|
79
|
+
{ "feature":"rag", "cases":[
|
|
80
|
+
{ "name":"structured", "output":"{\"answer\":\"yes\",\"sources\":[{\"id\":\"doc1\"}]}",
|
|
81
|
+
"assert":[
|
|
82
|
+
{"type":"json_valid"},
|
|
83
|
+
{"type":"json_path","path":"answer","equals":"yes"},
|
|
84
|
+
{"type":"json_path","path":"sources.0.id","contains":"doc"}
|
|
85
|
+
] } ] }
|
|
86
|
+
EOF
|
|
87
|
+
$NODE "$ER" "$TMP/s.json" >/dev/null 2>&1
|
|
88
|
+
assert_exit "json_path + json_valid pass → exit 0" 0 $?
|
|
89
|
+
# invalid json under json_valid → fail
|
|
90
|
+
cat > "$TMP/bad.json" <<'EOF'
|
|
91
|
+
{ "feature":"rag", "cases":[ { "name":"notjson", "output":"not json", "assert":[ {"type":"json_valid"} ] } ] }
|
|
92
|
+
EOF
|
|
93
|
+
$NODE "$ER" "$TMP/bad.json" >/dev/null 2>&1
|
|
94
|
+
assert_exit "json_valid on non-json → exit 1" 1 $?
|
|
95
|
+
rm -rf "$TMP"
|
|
96
|
+
|
|
97
|
+
# --- llm_rubric: verdict pass / fail / pending ---
|
|
98
|
+
TMP=$(mktemp -d)
|
|
99
|
+
cat > "$TMP/pass.json" <<'EOF'
|
|
100
|
+
{ "feature":"chat", "cases":[ { "name":"r", "output":"grounded answer", "assert":[ {"type":"llm_rubric","rubric":"grounded","verdict":"pass"} ] } ] }
|
|
101
|
+
EOF
|
|
102
|
+
$NODE "$ER" "$TMP/pass.json" >/dev/null 2>&1
|
|
103
|
+
assert_exit "llm_rubric verdict=pass → exit 0" 0 $?
|
|
104
|
+
cat > "$TMP/fail.json" <<'EOF'
|
|
105
|
+
{ "feature":"chat", "cases":[ { "name":"r", "output":"hallucination", "assert":[ {"type":"llm_rubric","rubric":"grounded","verdict":"fail"} ] } ] }
|
|
106
|
+
EOF
|
|
107
|
+
$NODE "$ER" "$TMP/fail.json" >/dev/null 2>&1
|
|
108
|
+
assert_exit "llm_rubric verdict=fail → exit 1" 1 $?
|
|
109
|
+
cat > "$TMP/pending.json" <<'EOF'
|
|
110
|
+
{ "feature":"chat", "cases":[ { "name":"r", "output":"x", "assert":[ {"type":"llm_rubric","rubric":"grounded"} ] } ] }
|
|
111
|
+
EOF
|
|
112
|
+
$NODE "$ER" "$TMP/pending.json" >/dev/null 2>&1
|
|
113
|
+
assert_exit "unjudged llm_rubric → PENDING → exit 1 (no silent pass)" 1 $?
|
|
114
|
+
OUT=$($NODE "$ER" "$TMP/pending.json" 2>&1)
|
|
115
|
+
assert_contains "reports unjudged rubric count" "$OUT" "unjudged"
|
|
116
|
+
rm -rf "$TMP"
|
|
117
|
+
|
|
118
|
+
# --- output_file resolution + --write artifact ---
|
|
119
|
+
TMP=$(mktemp -d); mkdir -p "$TMP/.planning"
|
|
120
|
+
echo -n "the answer is 42" > "$TMP/out.txt"
|
|
121
|
+
cat > "$TMP/s.json" <<'EOF'
|
|
122
|
+
{ "feature":"file-feature", "cases":[ { "name":"fromfile", "output_file":"out.txt", "assert":[ {"type":"contains","value":"42"} ] } ] }
|
|
123
|
+
EOF
|
|
124
|
+
(cd "$TMP" && $NODE "$ER" s.json --write >/dev/null 2>&1)
|
|
125
|
+
assert_exit "output_file case passes → exit 0" 0 $?
|
|
126
|
+
[ -f "$TMP/.planning/evals/eval-file-feature.json" ] && { echo " ✓ --write emits eval artifact"; PASS=$((PASS+1)); } || { echo " ✗ no eval artifact"; FAIL=$((FAIL+1)); }
|
|
127
|
+
# missing output_file → fail, not crash
|
|
128
|
+
cat > "$TMP/miss.json" <<'EOF'
|
|
129
|
+
{ "feature":"x", "cases":[ { "name":"gone", "output_file":"nope.txt", "assert":[ {"type":"contains","value":"y"} ] } ] }
|
|
130
|
+
EOF
|
|
131
|
+
(cd "$TMP" && $NODE "$ER" miss.json >/dev/null 2>&1)
|
|
132
|
+
assert_exit "missing output_file → exit 1 (graceful)" 1 $?
|
|
133
|
+
rm -rf "$TMP"
|
|
134
|
+
|
|
135
|
+
# --- library: runAssertion unit ---
|
|
136
|
+
U=$($NODE -e "const e=require('$ER'); console.log(e.runAssertion({type:'contains',value:'hi'},{output:'oh hi there'}).ok)" 2>&1)
|
|
137
|
+
assert_contains "runAssertion contains true" "$U" "true"
|
|
138
|
+
U=$($NODE -e "const e=require('$ER'); console.log(e.getPath({a:{b:[{c:9}]}},'a.b.0.c'))" 2>&1)
|
|
139
|
+
assert_contains "getPath nested+index" "$U" "9"
|
|
140
|
+
|
|
141
|
+
# --- malformed suite → exit 2 ---
|
|
142
|
+
$NODE "$ER" /nonexistent-suite.json >/dev/null 2>&1
|
|
143
|
+
assert_exit "missing suite → exit 2" 2 $?
|
|
144
|
+
|
|
145
|
+
echo ""
|
|
146
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
147
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|