qualia-framework-v2 2.8.0 → 2.9.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/README.md +5 -0
- package/bin/cli.js +267 -5
- package/bin/install.js +99 -6
- package/bin/state.js +136 -2
- package/package.json +4 -4
- package/tests/bin.test.sh +673 -0
- package/tests/hooks.test.sh +155 -25
- package/tests/state.test.sh +137 -0
- package/tests/statusline.test.sh +243 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Qualia Framework v2 — bin/ file behavioral tests (install.js, cli.js, qualia-ui.js)
|
|
3
|
+
# Run: bash tests/bin.test.sh
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
FRAMEWORK_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
+
NODE="${NODE:-node}"
|
|
9
|
+
CLI_JS="$FRAMEWORK_DIR/bin/cli.js"
|
|
10
|
+
UI_JS="$FRAMEWORK_DIR/bin/qualia-ui.js"
|
|
11
|
+
INSTALL_JS="$FRAMEWORK_DIR/bin/install.js"
|
|
12
|
+
PKG_VERSION=$($NODE -e 'console.log(require("'"$FRAMEWORK_DIR"'/package.json").version)')
|
|
13
|
+
|
|
14
|
+
# Track tmp dirs we create so we can clean them up on exit
|
|
15
|
+
TMP_DIRS=()
|
|
16
|
+
cleanup() {
|
|
17
|
+
for d in "${TMP_DIRS[@]}"; do
|
|
18
|
+
[ -d "$d" ] && rm -rf "$d"
|
|
19
|
+
done
|
|
20
|
+
}
|
|
21
|
+
trap cleanup EXIT
|
|
22
|
+
|
|
23
|
+
mktmp() {
|
|
24
|
+
local t
|
|
25
|
+
t=$(mktemp -d)
|
|
26
|
+
TMP_DIRS+=("$t")
|
|
27
|
+
echo "$t"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
assert_exit() {
|
|
31
|
+
local name="$1" expected="$2" actual="$3"
|
|
32
|
+
if [ "$expected" = "$actual" ]; then
|
|
33
|
+
echo " ✓ $name"
|
|
34
|
+
PASS=$((PASS + 1))
|
|
35
|
+
else
|
|
36
|
+
echo " ✗ $name (expected exit $expected, got $actual)"
|
|
37
|
+
FAIL=$((FAIL + 1))
|
|
38
|
+
fi
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pass() {
|
|
42
|
+
echo " ✓ $1"
|
|
43
|
+
PASS=$((PASS + 1))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fail_case() {
|
|
47
|
+
echo " ✗ $1${2:+ — $2}"
|
|
48
|
+
FAIL=$((FAIL + 1))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Strip ANSI escape codes for cleaner matching against expected strings.
|
|
52
|
+
strip_ansi() {
|
|
53
|
+
sed 's/\x1b\[[0-9;]*m//g'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
echo "=== bin/ Behavioral Tests ==="
|
|
57
|
+
echo ""
|
|
58
|
+
|
|
59
|
+
# Sanity checks
|
|
60
|
+
for f in "$CLI_JS" "$UI_JS" "$INSTALL_JS"; do
|
|
61
|
+
if [ ! -f "$f" ]; then
|
|
62
|
+
echo "FATAL: $f not found"
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
# ─── cli.js ───────────────────────────────────────────────
|
|
68
|
+
echo "cli.js:"
|
|
69
|
+
|
|
70
|
+
# 1. No args prints help banner
|
|
71
|
+
TMP=$(mktmp)
|
|
72
|
+
OUT=$(HOME="$TMP" $NODE "$CLI_JS" 2>&1)
|
|
73
|
+
EXIT=$?
|
|
74
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
75
|
+
if [ "$EXIT" -eq 0 ] \
|
|
76
|
+
&& echo "$CLEAN" | grep -q "Qualia Framework" \
|
|
77
|
+
&& echo "$CLEAN" | grep -q "Commands:"; then
|
|
78
|
+
pass "no args → help banner, exit 0"
|
|
79
|
+
else
|
|
80
|
+
fail_case "no args help" "exit=$EXIT"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# 2. `help` (unknown subcommand) falls through to help
|
|
84
|
+
TMP=$(mktmp)
|
|
85
|
+
OUT=$(HOME="$TMP" $NODE "$CLI_JS" help 2>&1)
|
|
86
|
+
EXIT=$?
|
|
87
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
88
|
+
if [ "$EXIT" -eq 0 ] \
|
|
89
|
+
&& echo "$CLEAN" | grep -q "/qualia-new"; then
|
|
90
|
+
pass "help arg → shows /qualia-new, /qualia-plan list"
|
|
91
|
+
else
|
|
92
|
+
fail_case "help arg" "exit=$EXIT"
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# 3. Unknown subcommand falls through to help
|
|
96
|
+
TMP=$(mktmp)
|
|
97
|
+
OUT=$(HOME="$TMP" $NODE "$CLI_JS" frobnicate 2>&1)
|
|
98
|
+
EXIT=$?
|
|
99
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
100
|
+
if [ "$EXIT" -eq 0 ] \
|
|
101
|
+
&& echo "$CLEAN" | grep -q "Qualia Framework"; then
|
|
102
|
+
pass "unknown command → help banner, exit 0"
|
|
103
|
+
else
|
|
104
|
+
fail_case "unknown command" "exit=$EXIT"
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# 4. `version` without config — shows version, no User line, offline fallback
|
|
108
|
+
TMP=$(mktmp)
|
|
109
|
+
OUT=$(HOME="$TMP" npm_config_registry="http://127.0.0.1:1/" $NODE "$CLI_JS" version 2>&1)
|
|
110
|
+
EXIT=$?
|
|
111
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
112
|
+
if [ "$EXIT" -eq 0 ] \
|
|
113
|
+
&& echo "$CLEAN" | grep -q "Installed:" \
|
|
114
|
+
&& echo "$CLEAN" | grep -q "$PKG_VERSION" \
|
|
115
|
+
&& ! echo "$CLEAN" | grep -q "User:"; then
|
|
116
|
+
pass "version without config → shows version, no User line"
|
|
117
|
+
else
|
|
118
|
+
fail_case "version without config" "exit=$EXIT"
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# 5. `version` exits cleanly regardless of network path
|
|
122
|
+
# The update check branches on execSync of `npm view ...`: it prints "Latest:"
|
|
123
|
+
# with the version (up-to-date), "Update available:", "(offline — couldn't check)",
|
|
124
|
+
# or nothing if npm returns an empty string. All of these are acceptable — the
|
|
125
|
+
# only real regression would be a crash or stderr on stdout.
|
|
126
|
+
if [ "$EXIT" -eq 0 ] \
|
|
127
|
+
&& ! echo "$CLEAN" | grep -qi "error" \
|
|
128
|
+
&& ! echo "$CLEAN" | grep -qi "traceback"; then
|
|
129
|
+
pass "version update-check branch → clean exit, no errors"
|
|
130
|
+
else
|
|
131
|
+
fail_case "version update-check branch" "exit=$EXIT"
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# 6. `version` with saved config — shows User line
|
|
135
|
+
TMP=$(mktmp)
|
|
136
|
+
mkdir -p "$TMP/.claude"
|
|
137
|
+
cat > "$TMP/.claude/.qualia-config.json" <<'EOF'
|
|
138
|
+
{
|
|
139
|
+
"code": "QS-FAWZI-01",
|
|
140
|
+
"installed_by": "Fawzi Goussous",
|
|
141
|
+
"role": "OWNER",
|
|
142
|
+
"version": "2.8.1",
|
|
143
|
+
"installed_at": "2026-04-10"
|
|
144
|
+
}
|
|
145
|
+
EOF
|
|
146
|
+
OUT=$(HOME="$TMP" npm_config_registry="http://127.0.0.1:1/" $NODE "$CLI_JS" version 2>&1)
|
|
147
|
+
EXIT=$?
|
|
148
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
149
|
+
if [ "$EXIT" -eq 0 ] \
|
|
150
|
+
&& echo "$CLEAN" | grep -q "User:" \
|
|
151
|
+
&& echo "$CLEAN" | grep -q "Fawzi Goussous" \
|
|
152
|
+
&& echo "$CLEAN" | grep -q "OWNER" \
|
|
153
|
+
&& echo "$CLEAN" | grep -q "2026-04-10"; then
|
|
154
|
+
pass "version with config → shows User, role, date"
|
|
155
|
+
else
|
|
156
|
+
fail_case "version with config" "exit=$EXIT"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
# 7. `-v` alias works the same as version
|
|
160
|
+
TMP=$(mktmp)
|
|
161
|
+
OUT=$(HOME="$TMP" npm_config_registry="http://127.0.0.1:1/" $NODE "$CLI_JS" -v 2>&1)
|
|
162
|
+
EXIT=$?
|
|
163
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
164
|
+
if [ "$EXIT" -eq 0 ] \
|
|
165
|
+
&& echo "$CLEAN" | grep -q "Installed:" \
|
|
166
|
+
&& echo "$CLEAN" | grep -q "$PKG_VERSION"; then
|
|
167
|
+
pass "-v alias → shows version"
|
|
168
|
+
else
|
|
169
|
+
fail_case "-v alias" "exit=$EXIT"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
# 8. `--version` alias works
|
|
173
|
+
TMP=$(mktmp)
|
|
174
|
+
OUT=$(HOME="$TMP" npm_config_registry="http://127.0.0.1:1/" $NODE "$CLI_JS" --version 2>&1)
|
|
175
|
+
EXIT=$?
|
|
176
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
177
|
+
if [ "$EXIT" -eq 0 ] \
|
|
178
|
+
&& echo "$CLEAN" | grep -q "Installed:" \
|
|
179
|
+
&& echo "$CLEAN" | grep -q "$PKG_VERSION"; then
|
|
180
|
+
pass "--version alias → shows version"
|
|
181
|
+
else
|
|
182
|
+
fail_case "--version alias" "exit=$EXIT"
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# 9. `update` without saved config → exits 1 cleanly with clear message
|
|
186
|
+
TMP=$(mktmp)
|
|
187
|
+
OUT=$(HOME="$TMP" $NODE "$CLI_JS" update 2>&1)
|
|
188
|
+
EXIT=$?
|
|
189
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
190
|
+
if [ "$EXIT" -eq 1 ] \
|
|
191
|
+
&& echo "$CLEAN" | grep -q "No install code saved"; then
|
|
192
|
+
pass "update without config → exit 1, 'No install code saved'"
|
|
193
|
+
else
|
|
194
|
+
fail_case "update without config" "exit=$EXIT"
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
# 10. `upgrade` alias behaves same as update (no config → exit 1)
|
|
198
|
+
TMP=$(mktmp)
|
|
199
|
+
OUT=$(HOME="$TMP" $NODE "$CLI_JS" upgrade 2>&1)
|
|
200
|
+
EXIT=$?
|
|
201
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
202
|
+
if [ "$EXIT" -eq 1 ] \
|
|
203
|
+
&& echo "$CLEAN" | grep -q "No install code saved"; then
|
|
204
|
+
pass "upgrade alias → exit 1, 'No install code saved'"
|
|
205
|
+
else
|
|
206
|
+
fail_case "upgrade alias" "exit=$EXIT"
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
echo ""
|
|
210
|
+
|
|
211
|
+
# ─── qualia-ui.js ─────────────────────────────────────────
|
|
212
|
+
echo "qualia-ui.js:"
|
|
213
|
+
|
|
214
|
+
# 11. banner router (no state) — exit 0, renders QUALIA + SMART ROUTER
|
|
215
|
+
TMP=$(mktmp)
|
|
216
|
+
OUT=$(cd "$TMP" && HOME="$TMP" $NODE "$UI_JS" banner router 2>&1)
|
|
217
|
+
EXIT=$?
|
|
218
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
219
|
+
if [ "$EXIT" -eq 0 ] \
|
|
220
|
+
&& echo "$CLEAN" | grep -q "QUALIA" \
|
|
221
|
+
&& echo "$CLEAN" | grep -q "SMART ROUTER"; then
|
|
222
|
+
pass "banner router → 'QUALIA' + 'SMART ROUTER'"
|
|
223
|
+
else
|
|
224
|
+
fail_case "banner router" "exit=$EXIT"
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
# 12. banner plan 1 foundation — includes PLANNING + Phase 1 + subtitle
|
|
228
|
+
TMP=$(mktmp)
|
|
229
|
+
OUT=$(cd "$TMP" && HOME="$TMP" $NODE "$UI_JS" banner plan 1 foundation 2>&1)
|
|
230
|
+
EXIT=$?
|
|
231
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
232
|
+
if [ "$EXIT" -eq 0 ] \
|
|
233
|
+
&& echo "$CLEAN" | grep -q "PLANNING" \
|
|
234
|
+
&& echo "$CLEAN" | grep -q "Phase 1"; then
|
|
235
|
+
pass "banner plan 1 foundation → 'PLANNING' + 'Phase 1'"
|
|
236
|
+
else
|
|
237
|
+
fail_case "banner plan 1 foundation" "exit=$EXIT"
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
# 13. banner with unknown action — falls back to uppercased action label
|
|
241
|
+
TMP=$(mktmp)
|
|
242
|
+
OUT=$(cd "$TMP" && HOME="$TMP" $NODE "$UI_JS" banner frobnicate 2>&1)
|
|
243
|
+
EXIT=$?
|
|
244
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
245
|
+
if [ "$EXIT" -eq 0 ] \
|
|
246
|
+
&& echo "$CLEAN" | grep -q "QUALIA" \
|
|
247
|
+
&& echo "$CLEAN" | grep -q "FROBNICATE"; then
|
|
248
|
+
pass "banner unknown action → uppercased fallback label"
|
|
249
|
+
else
|
|
250
|
+
fail_case "banner unknown action" "exit=$EXIT"
|
|
251
|
+
fi
|
|
252
|
+
|
|
253
|
+
# 14. context (no project) — exit 0, shows 'Project' and 'No project detected' hint
|
|
254
|
+
TMP=$(mktmp)
|
|
255
|
+
OUT=$(cd "$TMP" && HOME="$TMP" $NODE "$UI_JS" context 2>&1)
|
|
256
|
+
EXIT=$?
|
|
257
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
258
|
+
if [ "$EXIT" -eq 0 ] \
|
|
259
|
+
&& echo "$CLEAN" | grep -q "Project" \
|
|
260
|
+
&& echo "$CLEAN" | grep -q "No project detected"; then
|
|
261
|
+
pass "context without project → 'Project' + 'No project detected'"
|
|
262
|
+
else
|
|
263
|
+
fail_case "context without project" "exit=$EXIT"
|
|
264
|
+
fi
|
|
265
|
+
|
|
266
|
+
# 15. ok <message>
|
|
267
|
+
TMP=$(mktmp)
|
|
268
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" ok "hello world" 2>&1)
|
|
269
|
+
EXIT=$?
|
|
270
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
271
|
+
if [ "$EXIT" -eq 0 ] \
|
|
272
|
+
&& echo "$CLEAN" | grep -q "hello world" \
|
|
273
|
+
&& echo "$OUT" | grep -q "✓"; then
|
|
274
|
+
pass "ok → '✓' + message"
|
|
275
|
+
else
|
|
276
|
+
fail_case "ok message" "exit=$EXIT"
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
# 16. fail <message>
|
|
280
|
+
TMP=$(mktmp)
|
|
281
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" fail "nope nope" 2>&1)
|
|
282
|
+
EXIT=$?
|
|
283
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
284
|
+
if [ "$EXIT" -eq 0 ] \
|
|
285
|
+
&& echo "$CLEAN" | grep -q "nope nope" \
|
|
286
|
+
&& echo "$OUT" | grep -q "✗"; then
|
|
287
|
+
pass "fail → '✗' + message"
|
|
288
|
+
else
|
|
289
|
+
fail_case "fail message" "exit=$EXIT"
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
# 17. warn <message>
|
|
293
|
+
TMP=$(mktmp)
|
|
294
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" warn "careful" 2>&1)
|
|
295
|
+
EXIT=$?
|
|
296
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
297
|
+
if [ "$EXIT" -eq 0 ] \
|
|
298
|
+
&& echo "$CLEAN" | grep -q "careful"; then
|
|
299
|
+
pass "warn → message rendered"
|
|
300
|
+
else
|
|
301
|
+
fail_case "warn message" "exit=$EXIT"
|
|
302
|
+
fi
|
|
303
|
+
|
|
304
|
+
# 18. info <message>
|
|
305
|
+
TMP=$(mktmp)
|
|
306
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" info "just fyi" 2>&1)
|
|
307
|
+
EXIT=$?
|
|
308
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
309
|
+
if [ "$EXIT" -eq 0 ] \
|
|
310
|
+
&& echo "$CLEAN" | grep -q "just fyi"; then
|
|
311
|
+
pass "info → message rendered"
|
|
312
|
+
else
|
|
313
|
+
fail_case "info message" "exit=$EXIT"
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
# 19. divider renders a horizontal rule with '━' character
|
|
317
|
+
TMP=$(mktmp)
|
|
318
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" divider 2>&1)
|
|
319
|
+
EXIT=$?
|
|
320
|
+
if [ "$EXIT" -eq 0 ] \
|
|
321
|
+
&& echo "$OUT" | grep -q "━"; then
|
|
322
|
+
pass "divider → '━' rule"
|
|
323
|
+
else
|
|
324
|
+
fail_case "divider" "exit=$EXIT"
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
# 20. spawn agent description
|
|
328
|
+
TMP=$(mktmp)
|
|
329
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" spawn builder "task 3" 2>&1)
|
|
330
|
+
EXIT=$?
|
|
331
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
332
|
+
if [ "$EXIT" -eq 0 ] \
|
|
333
|
+
&& echo "$CLEAN" | grep -q "Spawning" \
|
|
334
|
+
&& echo "$CLEAN" | grep -q "builder" \
|
|
335
|
+
&& echo "$CLEAN" | grep -q "task 3"; then
|
|
336
|
+
pass "spawn builder → 'Spawning builder — task 3'"
|
|
337
|
+
else
|
|
338
|
+
fail_case "spawn" "exit=$EXIT"
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
# 21. wave 1 3 5 — renders wave header with task count
|
|
342
|
+
TMP=$(mktmp)
|
|
343
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" wave 1 3 5 2>&1)
|
|
344
|
+
EXIT=$?
|
|
345
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
346
|
+
if [ "$EXIT" -eq 0 ] \
|
|
347
|
+
&& echo "$CLEAN" | grep -q "Wave 1/3" \
|
|
348
|
+
&& echo "$CLEAN" | grep -q "5 tasks"; then
|
|
349
|
+
pass "wave 1 3 5 → 'Wave 1/3 (5 tasks)'"
|
|
350
|
+
else
|
|
351
|
+
fail_case "wave" "exit=$EXIT"
|
|
352
|
+
fi
|
|
353
|
+
|
|
354
|
+
# 22. task <N> <title>
|
|
355
|
+
TMP=$(mktmp)
|
|
356
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" task 2 "Build login form" 2>&1)
|
|
357
|
+
EXIT=$?
|
|
358
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
359
|
+
if [ "$EXIT" -eq 0 ] \
|
|
360
|
+
&& echo "$CLEAN" | grep -q "Build login form" \
|
|
361
|
+
&& echo "$CLEAN" | grep -q "2\."; then
|
|
362
|
+
pass "task 2 title → '2. Build login form'"
|
|
363
|
+
else
|
|
364
|
+
fail_case "task" "exit=$EXIT"
|
|
365
|
+
fi
|
|
366
|
+
|
|
367
|
+
# 23. done <N> <title> <commit>
|
|
368
|
+
TMP=$(mktmp)
|
|
369
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" done 3 "TaskDone" "abc1234" 2>&1)
|
|
370
|
+
EXIT=$?
|
|
371
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
372
|
+
if [ "$EXIT" -eq 0 ] \
|
|
373
|
+
&& echo "$CLEAN" | grep -q "TaskDone" \
|
|
374
|
+
&& echo "$CLEAN" | grep -q "abc1234" \
|
|
375
|
+
&& echo "$OUT" | grep -q "✓"; then
|
|
376
|
+
pass "done 3 TaskDone abc1234 → check + title + commit"
|
|
377
|
+
else
|
|
378
|
+
fail_case "done" "exit=$EXIT"
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
# 24. next /qualia-build
|
|
382
|
+
TMP=$(mktmp)
|
|
383
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" next /qualia-build 2>&1)
|
|
384
|
+
EXIT=$?
|
|
385
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
386
|
+
if [ "$EXIT" -eq 0 ] \
|
|
387
|
+
&& echo "$CLEAN" | grep -q "Next:" \
|
|
388
|
+
&& echo "$CLEAN" | grep -q "/qualia-build"; then
|
|
389
|
+
pass "next /qualia-build → 'Next: /qualia-build'"
|
|
390
|
+
else
|
|
391
|
+
fail_case "next" "exit=$EXIT"
|
|
392
|
+
fi
|
|
393
|
+
|
|
394
|
+
# 25. end DONE with next command
|
|
395
|
+
TMP=$(mktmp)
|
|
396
|
+
OUT=$(HOME="$TMP" $NODE "$UI_JS" end SHIPPED /qualia-handoff 2>&1)
|
|
397
|
+
EXIT=$?
|
|
398
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
399
|
+
if [ "$EXIT" -eq 0 ] \
|
|
400
|
+
&& echo "$CLEAN" | grep -q "SHIPPED" \
|
|
401
|
+
&& echo "$CLEAN" | grep -q "/qualia-handoff"; then
|
|
402
|
+
pass "end SHIPPED /qualia-handoff → closes with next"
|
|
403
|
+
else
|
|
404
|
+
fail_case "end" "exit=$EXIT"
|
|
405
|
+
fi
|
|
406
|
+
|
|
407
|
+
# 26. unknown command exits 1 with usage line on stderr
|
|
408
|
+
TMP=$(mktmp)
|
|
409
|
+
STDERR=$(HOME="$TMP" $NODE "$UI_JS" frobnicate 2>&1 >/dev/null)
|
|
410
|
+
EXIT=$?
|
|
411
|
+
if [ "$EXIT" -eq 1 ] \
|
|
412
|
+
&& echo "$STDERR" | grep -q "Usage:"; then
|
|
413
|
+
pass "unknown command → exit 1, 'Usage:' on stderr"
|
|
414
|
+
else
|
|
415
|
+
fail_case "unknown command" "exit=$EXIT"
|
|
416
|
+
fi
|
|
417
|
+
|
|
418
|
+
# 27. banner router respects config — shows OWNER + name when config present
|
|
419
|
+
TMP=$(mktmp)
|
|
420
|
+
mkdir -p "$TMP/.claude"
|
|
421
|
+
cat > "$TMP/.claude/.qualia-config.json" <<'EOF'
|
|
422
|
+
{
|
|
423
|
+
"code": "QS-FAWZI-01",
|
|
424
|
+
"installed_by": "Fawzi Goussous",
|
|
425
|
+
"role": "OWNER",
|
|
426
|
+
"version": "2.8.1",
|
|
427
|
+
"installed_at": "2026-04-10"
|
|
428
|
+
}
|
|
429
|
+
EOF
|
|
430
|
+
OUT=$(cd "$TMP" && HOME="$TMP" $NODE "$UI_JS" banner router 2>&1)
|
|
431
|
+
EXIT=$?
|
|
432
|
+
CLEAN=$(echo "$OUT" | strip_ansi)
|
|
433
|
+
if [ "$EXIT" -eq 0 ] \
|
|
434
|
+
&& echo "$CLEAN" | grep -q "OWNER" \
|
|
435
|
+
&& echo "$CLEAN" | grep -q "Fawzi Goussous"; then
|
|
436
|
+
pass "banner router with config → shows OWNER + 'Fawzi Goussous'"
|
|
437
|
+
else
|
|
438
|
+
fail_case "banner with config" "exit=$EXIT"
|
|
439
|
+
fi
|
|
440
|
+
|
|
441
|
+
echo ""
|
|
442
|
+
|
|
443
|
+
# ─── install.js ───────────────────────────────────────────
|
|
444
|
+
echo "install.js:"
|
|
445
|
+
|
|
446
|
+
# 28. Happy path: valid code installs everything
|
|
447
|
+
TMP=$(mktmp)
|
|
448
|
+
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
449
|
+
EXIT=$?
|
|
450
|
+
if [ "$EXIT" -eq 0 ] \
|
|
451
|
+
&& [ -f "$TMP/.claude/skills/qualia/SKILL.md" ] \
|
|
452
|
+
&& [ -f "$TMP/.claude/hooks/session-start.js" ] \
|
|
453
|
+
&& [ -f "$TMP/.claude/bin/state.js" ] \
|
|
454
|
+
&& [ -f "$TMP/.claude/bin/qualia-ui.js" ] \
|
|
455
|
+
&& [ -f "$TMP/.claude/bin/statusline.js" ] \
|
|
456
|
+
&& [ -f "$TMP/.claude/.qualia-config.json" ]; then
|
|
457
|
+
pass "QS-FAWZI-01 → installs skills, hooks, bin/, config"
|
|
458
|
+
else
|
|
459
|
+
fail_case "QS-FAWZI-01 happy path" "exit=$EXIT"
|
|
460
|
+
fi
|
|
461
|
+
|
|
462
|
+
# 29. Config JSON has correct fields after happy path
|
|
463
|
+
if grep -q '"code": "QS-FAWZI-01"' "$TMP/.claude/.qualia-config.json" \
|
|
464
|
+
&& grep -q '"installed_by": "Fawzi Goussous"' "$TMP/.claude/.qualia-config.json" \
|
|
465
|
+
&& grep -q '"role": "OWNER"' "$TMP/.claude/.qualia-config.json"; then
|
|
466
|
+
pass "config JSON has code, installed_by, role=OWNER"
|
|
467
|
+
else
|
|
468
|
+
fail_case "config JSON fields"
|
|
469
|
+
fi
|
|
470
|
+
|
|
471
|
+
# 30. CLAUDE.md role placeholder replaced
|
|
472
|
+
if grep -q "Role: OWNER" "$TMP/.claude/CLAUDE.md" \
|
|
473
|
+
&& ! grep -q "{{ROLE}}" "$TMP/.claude/CLAUDE.md"; then
|
|
474
|
+
pass "CLAUDE.md has Role: OWNER, no {{ROLE}} placeholder"
|
|
475
|
+
else
|
|
476
|
+
fail_case "CLAUDE.md role substitution"
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
# 31. All 8 hooks installed
|
|
480
|
+
HOOK_COUNT=$(ls "$TMP/.claude/hooks/"*.js 2>/dev/null | wc -l)
|
|
481
|
+
if [ "$HOOK_COUNT" -eq 8 ]; then
|
|
482
|
+
pass "8 hooks installed in hooks/"
|
|
483
|
+
else
|
|
484
|
+
fail_case "hook count" "got $HOOK_COUNT"
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
# 32. settings.json written with hooks + statusLine
|
|
488
|
+
if [ -f "$TMP/.claude/settings.json" ] \
|
|
489
|
+
&& grep -q '"SessionStart"' "$TMP/.claude/settings.json" \
|
|
490
|
+
&& grep -q '"PreToolUse"' "$TMP/.claude/settings.json" \
|
|
491
|
+
&& grep -q '"statusLine"' "$TMP/.claude/settings.json"; then
|
|
492
|
+
pass "settings.json has SessionStart, PreToolUse, statusLine"
|
|
493
|
+
else
|
|
494
|
+
fail_case "settings.json contents"
|
|
495
|
+
fi
|
|
496
|
+
|
|
497
|
+
# 33. Lowercase code works (resolveTeamCode normalizes)
|
|
498
|
+
TMP=$(mktmp)
|
|
499
|
+
echo "qs-fawzi-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
500
|
+
EXIT=$?
|
|
501
|
+
if [ "$EXIT" -eq 0 ] \
|
|
502
|
+
&& [ -f "$TMP/.claude/.qualia-config.json" ] \
|
|
503
|
+
&& grep -q '"code": "QS-FAWZI-01"' "$TMP/.claude/.qualia-config.json"; then
|
|
504
|
+
pass "lowercase 'qs-fawzi-01' → canonical 'QS-FAWZI-01'"
|
|
505
|
+
else
|
|
506
|
+
fail_case "lowercase normalization" "exit=$EXIT"
|
|
507
|
+
fi
|
|
508
|
+
|
|
509
|
+
# 34. O/0 typo tolerance — letter O in suffix normalized to digit 0
|
|
510
|
+
TMP=$(mktmp)
|
|
511
|
+
echo "QS-FAWZI-O1" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
512
|
+
EXIT=$?
|
|
513
|
+
if [ "$EXIT" -eq 0 ] \
|
|
514
|
+
&& [ -f "$TMP/.claude/.qualia-config.json" ] \
|
|
515
|
+
&& grep -q '"code": "QS-FAWZI-01"' "$TMP/.claude/.qualia-config.json"; then
|
|
516
|
+
pass "letter 'O' in suffix → normalized to digit '0'"
|
|
517
|
+
else
|
|
518
|
+
fail_case "O/0 fuzzy match" "exit=$EXIT"
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
# 35. MOAYAD real O in name preserved (only suffix is normalized)
|
|
522
|
+
TMP=$(mktmp)
|
|
523
|
+
echo "QS-MOAYAD-03" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
524
|
+
EXIT=$?
|
|
525
|
+
if [ "$EXIT" -eq 0 ] \
|
|
526
|
+
&& [ -f "$TMP/.claude/.qualia-config.json" ] \
|
|
527
|
+
&& grep -q '"code": "QS-MOAYAD-03"' "$TMP/.claude/.qualia-config.json" \
|
|
528
|
+
&& grep -q '"installed_by": "Moayad"' "$TMP/.claude/.qualia-config.json"; then
|
|
529
|
+
pass "QS-MOAYAD-03 → 'O' in name preserved"
|
|
530
|
+
else
|
|
531
|
+
fail_case "Moayad real-O" "exit=$EXIT"
|
|
532
|
+
fi
|
|
533
|
+
|
|
534
|
+
# 36. EMPLOYEE role set correctly
|
|
535
|
+
if grep -q '"role": "EMPLOYEE"' "$TMP/.claude/.qualia-config.json" \
|
|
536
|
+
&& grep -q "Role: EMPLOYEE" "$TMP/.claude/CLAUDE.md"; then
|
|
537
|
+
pass "Moayad role → EMPLOYEE"
|
|
538
|
+
else
|
|
539
|
+
fail_case "Moayad EMPLOYEE role"
|
|
540
|
+
fi
|
|
541
|
+
|
|
542
|
+
# 37. Invalid code exits 1 with helpful message
|
|
543
|
+
TMP=$(mktmp)
|
|
544
|
+
echo "QS-BOGUS-99" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
545
|
+
EXIT=$?
|
|
546
|
+
CLEAN=$(strip_ansi < "$TMP/out.log")
|
|
547
|
+
if [ "$EXIT" -eq 1 ] \
|
|
548
|
+
&& echo "$CLEAN" | grep -q "Invalid code" \
|
|
549
|
+
&& [ ! -f "$TMP/.claude/.qualia-config.json" ]; then
|
|
550
|
+
pass "QS-BOGUS-99 → exit 1, 'Invalid code', no config written"
|
|
551
|
+
else
|
|
552
|
+
fail_case "invalid code" "exit=$EXIT"
|
|
553
|
+
fi
|
|
554
|
+
|
|
555
|
+
# 38. Empty code (newline only) → exit 1, invalid code message
|
|
556
|
+
TMP=$(mktmp)
|
|
557
|
+
printf '\n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
558
|
+
EXIT=$?
|
|
559
|
+
CLEAN=$(strip_ansi < "$TMP/out.log")
|
|
560
|
+
if [ "$EXIT" -eq 1 ] \
|
|
561
|
+
&& echo "$CLEAN" | grep -q "Invalid code" \
|
|
562
|
+
&& [ ! -f "$TMP/.claude/.qualia-config.json" ]; then
|
|
563
|
+
pass "empty code → exit 1, 'Invalid code'"
|
|
564
|
+
else
|
|
565
|
+
fail_case "empty code" "exit=$EXIT"
|
|
566
|
+
fi
|
|
567
|
+
|
|
568
|
+
# 39. Code with surrounding whitespace is accepted
|
|
569
|
+
TMP=$(mktmp)
|
|
570
|
+
printf ' QS-FAWZI-01 \n' | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
571
|
+
EXIT=$?
|
|
572
|
+
if [ "$EXIT" -eq 0 ] \
|
|
573
|
+
&& grep -q '"code": "QS-FAWZI-01"' "$TMP/.claude/.qualia-config.json"; then
|
|
574
|
+
pass "whitespace-padded code → accepted and trimmed"
|
|
575
|
+
else
|
|
576
|
+
fail_case "whitespace trim" "exit=$EXIT"
|
|
577
|
+
fi
|
|
578
|
+
|
|
579
|
+
# 40. Settings.json merge preserves custom top-level keys
|
|
580
|
+
TMP=$(mktmp)
|
|
581
|
+
mkdir -p "$TMP/.claude"
|
|
582
|
+
cat > "$TMP/.claude/settings.json" <<'EOF'
|
|
583
|
+
{
|
|
584
|
+
"customKey": "preserved",
|
|
585
|
+
"env": {
|
|
586
|
+
"MY_CUSTOM_VAR": "hello"
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
EOF
|
|
590
|
+
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
591
|
+
EXIT=$?
|
|
592
|
+
MERGED=$($NODE -e 'const s=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));console.log([s.customKey,s.env&&s.env.MY_CUSTOM_VAR,s.env&&s.env.CLAUDE_CODE_NO_FLICKER,!!s.hooks,!!s.statusLine].join("|"))' "$TMP/.claude/settings.json" 2>/dev/null)
|
|
593
|
+
if [ "$EXIT" -eq 0 ] \
|
|
594
|
+
&& [ "$MERGED" = "preserved|hello|1|true|true" ]; then
|
|
595
|
+
pass "settings.json merge preserves custom keys + adds new hooks/env"
|
|
596
|
+
else
|
|
597
|
+
fail_case "settings.json merge" "got '$MERGED' exit=$EXIT"
|
|
598
|
+
fi
|
|
599
|
+
|
|
600
|
+
# 41. Knowledge files created on first install
|
|
601
|
+
TMP=$(mktmp)
|
|
602
|
+
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
603
|
+
EXIT=$?
|
|
604
|
+
if [ "$EXIT" -eq 0 ] \
|
|
605
|
+
&& [ -f "$TMP/.claude/knowledge/learned-patterns.md" ] \
|
|
606
|
+
&& [ -f "$TMP/.claude/knowledge/common-fixes.md" ] \
|
|
607
|
+
&& [ -f "$TMP/.claude/knowledge/client-prefs.md" ]; then
|
|
608
|
+
pass "knowledge/ files created on first install"
|
|
609
|
+
else
|
|
610
|
+
fail_case "knowledge files created" "exit=$EXIT"
|
|
611
|
+
fi
|
|
612
|
+
|
|
613
|
+
# 42. Idempotent re-install preserves user edits to knowledge files
|
|
614
|
+
printf '\n## CUSTOM LEARNING — DO NOT OVERWRITE\n' >> "$TMP/.claude/knowledge/learned-patterns.md"
|
|
615
|
+
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out2.log" 2>&1
|
|
616
|
+
EXIT=$?
|
|
617
|
+
if [ "$EXIT" -eq 0 ] \
|
|
618
|
+
&& grep -q "CUSTOM LEARNING" "$TMP/.claude/knowledge/learned-patterns.md"; then
|
|
619
|
+
pass "re-install preserves user edits in knowledge files"
|
|
620
|
+
else
|
|
621
|
+
fail_case "knowledge idempotency" "exit=$EXIT"
|
|
622
|
+
fi
|
|
623
|
+
|
|
624
|
+
# 43. ERP API key file created and not overwritten on re-install
|
|
625
|
+
if [ -f "$TMP/.claude/.erp-api-key" ]; then
|
|
626
|
+
echo "custom-erp-key" > "$TMP/.claude/.erp-api-key"
|
|
627
|
+
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out3.log" 2>&1
|
|
628
|
+
if grep -q "custom-erp-key" "$TMP/.claude/.erp-api-key"; then
|
|
629
|
+
pass ".erp-api-key preserved on re-install"
|
|
630
|
+
else
|
|
631
|
+
fail_case ".erp-api-key preservation"
|
|
632
|
+
fi
|
|
633
|
+
else
|
|
634
|
+
fail_case ".erp-api-key missing after install"
|
|
635
|
+
fi
|
|
636
|
+
|
|
637
|
+
# 44. Templates copied to qualia-templates/
|
|
638
|
+
TMP=$(mktmp)
|
|
639
|
+
echo "QS-FAWZI-01" | HOME="$TMP" $NODE "$INSTALL_JS" > "$TMP/out.log" 2>&1
|
|
640
|
+
EXIT=$?
|
|
641
|
+
TMPL_COUNT=$(ls "$TMP/.claude/qualia-templates/" 2>/dev/null | wc -l)
|
|
642
|
+
if [ "$EXIT" -eq 0 ] && [ "$TMPL_COUNT" -gt 0 ]; then
|
|
643
|
+
pass "templates copied to qualia-templates/ ($TMPL_COUNT files)"
|
|
644
|
+
else
|
|
645
|
+
fail_case "templates copied" "exit=$EXIT count=$TMPL_COUNT"
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
# 45. All agents copied
|
|
649
|
+
AGENT_COUNT=$(ls "$TMP/.claude/agents/" 2>/dev/null | wc -l)
|
|
650
|
+
if [ "$AGENT_COUNT" -gt 0 ]; then
|
|
651
|
+
pass "agents copied ($AGENT_COUNT files)"
|
|
652
|
+
else
|
|
653
|
+
fail_case "agents copied" "count=$AGENT_COUNT"
|
|
654
|
+
fi
|
|
655
|
+
|
|
656
|
+
# 46. Rules copied
|
|
657
|
+
RULE_COUNT=$(ls "$TMP/.claude/rules/" 2>/dev/null | wc -l)
|
|
658
|
+
if [ "$RULE_COUNT" -gt 0 ]; then
|
|
659
|
+
pass "rules copied ($RULE_COUNT files)"
|
|
660
|
+
else
|
|
661
|
+
fail_case "rules copied" "count=$RULE_COUNT"
|
|
662
|
+
fi
|
|
663
|
+
|
|
664
|
+
# 47. Config version matches package.json version
|
|
665
|
+
if grep -q "\"version\": \"$PKG_VERSION\"" "$TMP/.claude/.qualia-config.json"; then
|
|
666
|
+
pass "config version matches package.json ($PKG_VERSION)"
|
|
667
|
+
else
|
|
668
|
+
fail_case "config version mismatch"
|
|
669
|
+
fi
|
|
670
|
+
|
|
671
|
+
echo ""
|
|
672
|
+
echo "=== Results: $PASS passed, $FAIL failed ==="
|
|
673
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|