@rubytech/create-maxy-code 0.1.19 → 0.1.20
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/package.json
CHANGED
|
@@ -62,6 +62,10 @@ Failure signals to grep in `~/.maxy/logs/server.log` (or `~/.realagent/logs/serv
|
|
|
62
62
|
- `[onboarding-gate] step=null complete=true` (the pre-Task-033 bug)
|
|
63
63
|
- missing `[skill-load] name=onboarding` while `[onboarding-gate]` reports `complete=false`
|
|
64
64
|
|
|
65
|
+
### QA regression replay
|
|
66
|
+
|
|
67
|
+
`platform/scripts/qa/onboarding-fresh-install.sh <brand> [--watch]` replays the success chain end-to-end on a freshly-installed Pi. It exits zero only when every line above lands, and exits non-zero with the missing line named verbatim on any miss — so a future regression in this chain is caught by replay instead of by a customer. Pass `--watch` to keep polling Neo4j while the operator drives steps 1–9 in the UI; each `currentStep` advance asserts the matching `stepNCompletedAt` is persisted before the next step renders. Provide the admin PIN via `MAXY_ADMIN_PIN`.
|
|
68
|
+
|
|
65
69
|
## Service Management
|
|
66
70
|
|
|
67
71
|
{{productName}} runs via systemd and starts automatically on boot. You don't need to start it manually. To check if it's running, ask {{productName}} "Check system status."
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Task 035 — Onboarding fresh-install Pi integration script.
|
|
3
|
+
#
|
|
4
|
+
# Replays the Task 033 success chain against a freshly-installed brand on
|
|
5
|
+
# this Pi (Maxy Code or Real Agent Code). Exit zero ↔ the entire chain is
|
|
6
|
+
# observable end-to-end. Any miss exits non-zero with the failing
|
|
7
|
+
# observability line named verbatim.
|
|
8
|
+
#
|
|
9
|
+
# Success chain (fresh install, currentStep=0):
|
|
10
|
+
# [onboarding-seed] currentStep=0
|
|
11
|
+
# → installer log
|
|
12
|
+
# [install-invariant] onboarding-state-present
|
|
13
|
+
# → installer log
|
|
14
|
+
# [onboarding-gate] step=0 complete=false phase=create
|
|
15
|
+
# → server.log, fires on POST /api/admin/session
|
|
16
|
+
# [onboarding-inject] agent=admin step=0 injected=true
|
|
17
|
+
# → server.log, fires on first POST /api/admin/claude-sessions/:id/input
|
|
18
|
+
# [skill-load] skillName=onboarding result=unique
|
|
19
|
+
# → server.log, fires when admin MCP serves skill-load for the agent
|
|
20
|
+
#
|
|
21
|
+
# CONTRACT
|
|
22
|
+
# Exit 0 — every chain link landed; --watch (if set) walked 9 steps.
|
|
23
|
+
# Exit 1 — at least one chain assertion failed; missing line(s) named
|
|
24
|
+
# verbatim in the final report.
|
|
25
|
+
# Exit 2 — environment fault (cypher-shell unreachable, brand.json
|
|
26
|
+
# missing, refused-as-non-fresh-install, etc.) — never PASS.
|
|
27
|
+
#
|
|
28
|
+
# USAGE
|
|
29
|
+
# $ onboarding-fresh-install.sh <brand> [--watch]
|
|
30
|
+
#
|
|
31
|
+
# <brand> maxy-code | realagent-code (matches brands/<brand>/brand.json).
|
|
32
|
+
# --watch After the fresh-install chain passes, poll Neo4j every 2s.
|
|
33
|
+
# For every observed currentStep advance N→N+1, assert that
|
|
34
|
+
# step(N+1)CompletedAt is non-null at the same moment. Exits 0
|
|
35
|
+
# when currentStep reaches 9 or the operator presses Ctrl-C
|
|
36
|
+
# after at least one advance.
|
|
37
|
+
#
|
|
38
|
+
# ENV
|
|
39
|
+
# MAXY_ADMIN_PIN Admin PIN (required for the session-mint step).
|
|
40
|
+
# May also be passed as the 2nd positional argument.
|
|
41
|
+
# MAXY_NEO4J_PASSWORD
|
|
42
|
+
# Neo4j password (default: read from <installDir>/platform/config/.neo4j-password if present).
|
|
43
|
+
# MAXY_QA_TIMEOUT Seconds to wait for the runtime log lines to appear
|
|
44
|
+
# after the first /input call (default 30).
|
|
45
|
+
# MAXY_QA_HOST Admin host (default 127.0.0.1).
|
|
46
|
+
#
|
|
47
|
+
# OUT-OF-SCOPE (per task 035)
|
|
48
|
+
# * Multi-Pi orchestration — one brand per invocation.
|
|
49
|
+
# * CI pipeline gating.
|
|
50
|
+
# * Driving steps 1–9 by curl. Steps 7 (Cloudflare OAuth), 8 (Anthropic
|
|
51
|
+
# API key paste), and 9 (free-form business profile data) cannot be
|
|
52
|
+
# deterministically scripted on a fresh install; the operator drives
|
|
53
|
+
# each step in the UI under --watch while this script polls Neo4j.
|
|
54
|
+
|
|
55
|
+
set -euo pipefail
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Argv + env
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
if [ $# -lt 1 ]; then
|
|
62
|
+
echo "usage: $0 <brand> [--watch] [pin]" >&2
|
|
63
|
+
echo " <brand> = maxy-code | realagent-code" >&2
|
|
64
|
+
exit 2
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
BRAND=""
|
|
68
|
+
WATCH=0
|
|
69
|
+
PIN_POSITIONAL=""
|
|
70
|
+
for arg in "$@"; do
|
|
71
|
+
case "$arg" in
|
|
72
|
+
--watch) WATCH=1 ;;
|
|
73
|
+
-*) echo "unknown flag: $arg" >&2; exit 2 ;;
|
|
74
|
+
*) if [ -z "$BRAND" ]; then BRAND="$arg"; else PIN_POSITIONAL="$arg"; fi ;;
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
if [ -z "$BRAND" ]; then
|
|
79
|
+
echo "error: brand argument required" >&2
|
|
80
|
+
exit 2
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
PIN="${MAXY_ADMIN_PIN:-$PIN_POSITIONAL}"
|
|
84
|
+
if [ -z "$PIN" ]; then
|
|
85
|
+
echo "error: PIN required — set MAXY_ADMIN_PIN env or pass as second positional argument" >&2
|
|
86
|
+
exit 2
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
HOST="${MAXY_QA_HOST:-127.0.0.1}"
|
|
90
|
+
TIMEOUT="${MAXY_QA_TIMEOUT:-30}"
|
|
91
|
+
|
|
92
|
+
# ---------------------------------------------------------------------------
|
|
93
|
+
# Brand resolution — read live brand.json on the Pi for ports + paths.
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
INSTALL_DIR="$HOME/$BRAND"
|
|
97
|
+
BRAND_JSON="$INSTALL_DIR/platform/config/brand.json"
|
|
98
|
+
LOG_DIR="$HOME/.$BRAND/logs"
|
|
99
|
+
SERVER_LOG="$LOG_DIR/server.log"
|
|
100
|
+
|
|
101
|
+
if [ ! -f "$BRAND_JSON" ]; then
|
|
102
|
+
echo "error: brand.json not found at $BRAND_JSON" >&2
|
|
103
|
+
echo "hint: this script expects a brand installed via @rubytech/create-${BRAND}" >&2
|
|
104
|
+
exit 2
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
# jq is on the deployment-doctrine list; if it's absent, the script must
|
|
108
|
+
# loud-fail rather than fall back to a regex parse of brand.json.
|
|
109
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
110
|
+
echo "error: jq required but not installed" >&2
|
|
111
|
+
exit 2
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
NEO4J_PORT="$(jq -r '.neo4jPort' "$BRAND_JSON")"
|
|
115
|
+
if [ -z "$NEO4J_PORT" ] || [ "$NEO4J_PORT" = "null" ]; then
|
|
116
|
+
echo "error: brand.json at $BRAND_JSON has no neo4jPort field" >&2
|
|
117
|
+
exit 2
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Admin server PORT is set by the installer as a systemd Environment var, not
|
|
121
|
+
# in brand.json (verified packages/create-maxy-code/src/port-resolution.ts).
|
|
122
|
+
# Resolution order: MAXY_QA_PORT env → systemctl --user show <serviceName>
|
|
123
|
+
# → fail loud (never a hard-coded default — silent-fallback masks root cause).
|
|
124
|
+
ADMIN_PORT="${MAXY_QA_PORT:-}"
|
|
125
|
+
if [ -z "$ADMIN_PORT" ]; then
|
|
126
|
+
SERVICE_NAME="$(jq -r '.serviceName' "$BRAND_JSON")"
|
|
127
|
+
if [ -n "$SERVICE_NAME" ] && [ "$SERVICE_NAME" != "null" ] && command -v systemctl >/dev/null 2>&1; then
|
|
128
|
+
ADMIN_PORT="$(systemctl --user show "$SERVICE_NAME" -p Environment 2>/dev/null \
|
|
129
|
+
| tr ' ' '\n' \
|
|
130
|
+
| grep -E '^PORT=' \
|
|
131
|
+
| head -n 1 \
|
|
132
|
+
| cut -d= -f2)"
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
if [ -z "$ADMIN_PORT" ]; then
|
|
136
|
+
echo "error: admin PORT not resolvable — set MAXY_QA_PORT env (e.g. MAXY_QA_PORT=19200)" >&2
|
|
137
|
+
exit 2
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
NEO4J_URI="bolt://localhost:$NEO4J_PORT"
|
|
141
|
+
ADMIN_BASE="http://$HOST:$ADMIN_PORT"
|
|
142
|
+
|
|
143
|
+
NEO4J_PASSWORD="${MAXY_NEO4J_PASSWORD:-}"
|
|
144
|
+
NEO4J_PASSWORD_FILE="$INSTALL_DIR/platform/config/.neo4j-password"
|
|
145
|
+
if [ -z "$NEO4J_PASSWORD" ] && [ -r "$NEO4J_PASSWORD_FILE" ]; then
|
|
146
|
+
NEO4J_PASSWORD="$(cat "$NEO4J_PASSWORD_FILE")"
|
|
147
|
+
fi
|
|
148
|
+
if [ -z "$NEO4J_PASSWORD" ]; then
|
|
149
|
+
echo "error: Neo4j password required — set MAXY_NEO4J_PASSWORD env or place it at $NEO4J_PASSWORD_FILE" >&2
|
|
150
|
+
exit 2
|
|
151
|
+
fi
|
|
152
|
+
|
|
153
|
+
for tool in cypher-shell curl grep tail; do
|
|
154
|
+
if ! command -v "$tool" >/dev/null 2>&1; then
|
|
155
|
+
echo "error: $tool not on PATH" >&2
|
|
156
|
+
exit 2
|
|
157
|
+
fi
|
|
158
|
+
done
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# Cypher helper — exit 2 on unreachable, captures stderr tail for the report.
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
cypher() {
|
|
165
|
+
local query="$1"
|
|
166
|
+
cypher-shell -u neo4j -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" --format plain "$query"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
cypher_or_die() {
|
|
170
|
+
local query="$1"
|
|
171
|
+
local out
|
|
172
|
+
if ! out="$(cypher "$query" 2>&1)"; then
|
|
173
|
+
echo "error: cypher-shell unreachable at $NEO4J_URI" >&2
|
|
174
|
+
echo "$out" | tail -n 5 >&2
|
|
175
|
+
exit 2
|
|
176
|
+
fi
|
|
177
|
+
printf '%s' "$out"
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
# Assertion bookkeeping — every assertion appends one row to the report so
|
|
182
|
+
# the final block names every link verbatim, pass or fail.
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
REPORT=()
|
|
186
|
+
FAIL_COUNT=0
|
|
187
|
+
|
|
188
|
+
record() {
|
|
189
|
+
# record <status> <label> <detail>
|
|
190
|
+
REPORT+=("$1|$2|$3")
|
|
191
|
+
if [ "$1" = "FAIL" ]; then
|
|
192
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
193
|
+
fi
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
print_report() {
|
|
197
|
+
echo ""
|
|
198
|
+
echo "## onboarding fresh-install — final report"
|
|
199
|
+
echo ""
|
|
200
|
+
printf '| %-4s | %-50s | %s\n' "Step" "Assertion" "Detail"
|
|
201
|
+
printf '|------|----------------------------------------------------|-----\n'
|
|
202
|
+
local i=1
|
|
203
|
+
for row in "${REPORT[@]}"; do
|
|
204
|
+
local status="${row%%|*}"
|
|
205
|
+
local rest="${row#*|}"
|
|
206
|
+
local label="${rest%%|*}"
|
|
207
|
+
local detail="${rest#*|}"
|
|
208
|
+
local marker
|
|
209
|
+
case "$status" in
|
|
210
|
+
PASS) marker="PASS" ;;
|
|
211
|
+
FAIL) marker="FAIL" ;;
|
|
212
|
+
*) marker="$status" ;;
|
|
213
|
+
esac
|
|
214
|
+
printf '| %-4s | %-50s | %s\n' "$marker" "$label" "$detail"
|
|
215
|
+
i=$((i + 1))
|
|
216
|
+
done
|
|
217
|
+
echo ""
|
|
218
|
+
echo "brand=$BRAND neo4j=$NEO4J_URI logs=$LOG_DIR"
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# ---------------------------------------------------------------------------
|
|
222
|
+
# Step 1 — Pre-flight: refuse if not a fresh install.
|
|
223
|
+
# ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
echo "[1/6] Pre-flight: assert OnboardingState{currentStep:0}"
|
|
226
|
+
PREFLIGHT_OUT="$(cypher_or_die 'MATCH (o:OnboardingState) RETURN o.accountId AS accountId, o.currentStep AS currentStep, o.createdAt AS createdAt')"
|
|
227
|
+
ROW_COUNT="$(printf '%s' "$PREFLIGHT_OUT" | tail -n +2 | grep -c . || true)"
|
|
228
|
+
|
|
229
|
+
if [ "$ROW_COUNT" -eq 0 ]; then
|
|
230
|
+
echo "error: not a fresh install — no OnboardingState row exists" >&2
|
|
231
|
+
echo "hint: the installer's [install-invariant] onboarding-state-MISSING line would have fired; check ~/.${BRAND}/logs/install-*.log" >&2
|
|
232
|
+
exit 2
|
|
233
|
+
fi
|
|
234
|
+
if [ "$ROW_COUNT" -gt 1 ]; then
|
|
235
|
+
echo "error: not a fresh install — $ROW_COUNT OnboardingState rows exist (expected 1)" >&2
|
|
236
|
+
printf '%s\n' "$PREFLIGHT_OUT" >&2
|
|
237
|
+
exit 2
|
|
238
|
+
fi
|
|
239
|
+
|
|
240
|
+
CURRENT_STEP="$(printf '%s' "$PREFLIGHT_OUT" | tail -n 1 | awk -F', ' '{print $2}')"
|
|
241
|
+
ACCOUNT_ID="$(printf '%s' "$PREFLIGHT_OUT" | tail -n 1 | awk -F', ' '{print $1}' | tr -d '"')"
|
|
242
|
+
CREATED_AT="$(printf '%s' "$PREFLIGHT_OUT" | tail -n 1 | awk -F', ' '{print $3}' | tr -d '"')"
|
|
243
|
+
|
|
244
|
+
if [ "$CURRENT_STEP" != "0" ]; then
|
|
245
|
+
echo "error: not a fresh install — currentStep=$CURRENT_STEP (refusing to drive on top of completed state)" >&2
|
|
246
|
+
echo "expected: currentStep=0" >&2
|
|
247
|
+
echo "actual: $PREFLIGHT_OUT" >&2
|
|
248
|
+
exit 2
|
|
249
|
+
fi
|
|
250
|
+
record PASS "pre-flight: OnboardingState{currentStep:0}" "accountId=${ACCOUNT_ID:0:8} createdAt=$CREATED_AT"
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
# Step 2 — Installer log: [onboarding-seed] currentStep=0 + [install-invariant] onboarding-state-present.
|
|
254
|
+
#
|
|
255
|
+
# Installer logs live in $LOG_DIR/install-*.log (separate from server.log).
|
|
256
|
+
# Both lines fire once per install run; pin to the newest install log so a
|
|
257
|
+
# stale prior install on the same Pi cannot produce a false PASS.
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
echo "[2/6] Installer log: [onboarding-seed] + [install-invariant] onboarding-state-present"
|
|
261
|
+
LATEST_INSTALL_LOG="$(ls -t "$LOG_DIR"/install-*.log 2>/dev/null | head -n 1 || true)"
|
|
262
|
+
if [ -z "$LATEST_INSTALL_LOG" ]; then
|
|
263
|
+
record FAIL "[onboarding-seed] currentStep=0" "no install log under $LOG_DIR"
|
|
264
|
+
record FAIL "[install-invariant] onboarding-state-present" "no install log under $LOG_DIR"
|
|
265
|
+
else
|
|
266
|
+
if grep -qF "[onboarding-seed] accountId=$ACCOUNT_ID currentStep=0" "$LATEST_INSTALL_LOG"; then
|
|
267
|
+
record PASS "[onboarding-seed] currentStep=0" "$(basename "$LATEST_INSTALL_LOG")"
|
|
268
|
+
else
|
|
269
|
+
record FAIL "[onboarding-seed] currentStep=0" "missing from $(basename "$LATEST_INSTALL_LOG")"
|
|
270
|
+
fi
|
|
271
|
+
if grep -qF "[install-invariant] onboarding-state-present accountId=$ACCOUNT_ID" "$LATEST_INSTALL_LOG"; then
|
|
272
|
+
record PASS "[install-invariant] onboarding-state-present" "$(basename "$LATEST_INSTALL_LOG")"
|
|
273
|
+
else
|
|
274
|
+
record FAIL "[install-invariant] onboarding-state-present" "missing from $(basename "$LATEST_INSTALL_LOG")"
|
|
275
|
+
fi
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
# Step 3 — POST /api/admin/session: assert onboardingComplete:false in response,
|
|
280
|
+
# then assert [onboarding-gate] step=0 complete=false in server.log.
|
|
281
|
+
# ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
echo "[3/6] Mint admin session via PIN and assert [onboarding-gate] step=0 complete=false"
|
|
284
|
+
TURN_MARKER_BEFORE="$(wc -l < "$SERVER_LOG" 2>/dev/null | tr -d ' ' || echo 0)"
|
|
285
|
+
TURN_MARKER_BEFORE="${TURN_MARKER_BEFORE:-0}"
|
|
286
|
+
SESSION_RESPONSE="$(curl -sS -X POST "$ADMIN_BASE/api/admin/session" \
|
|
287
|
+
-H 'content-type: application/json' \
|
|
288
|
+
-d "{\"pin\":\"$PIN\"}")" || {
|
|
289
|
+
record FAIL "POST /api/admin/session" "curl failed against $ADMIN_BASE"
|
|
290
|
+
print_report
|
|
291
|
+
exit 1
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if ! printf '%s' "$SESSION_RESPONSE" | jq -e '.onboardingComplete == false' >/dev/null 2>&1; then
|
|
295
|
+
record FAIL "POST /api/admin/session onboardingComplete:false" "response=$SESSION_RESPONSE"
|
|
296
|
+
print_report
|
|
297
|
+
exit 1
|
|
298
|
+
fi
|
|
299
|
+
SESSION_KEY="$(printf '%s' "$SESSION_RESPONSE" | jq -r '.session_key')"
|
|
300
|
+
if [ -z "$SESSION_KEY" ] || [ "$SESSION_KEY" = "null" ]; then
|
|
301
|
+
record FAIL "session_key" "absent in /api/admin/session response"
|
|
302
|
+
print_report
|
|
303
|
+
exit 1
|
|
304
|
+
fi
|
|
305
|
+
record PASS "POST /api/admin/session onboardingComplete:false" "session minted"
|
|
306
|
+
|
|
307
|
+
# Tail server.log from the byte offset captured before the call. grep -F
|
|
308
|
+
# matches the substring literally — the gate phrasing in session.ts:193 is
|
|
309
|
+
# `[onboarding-gate] session=... accountId=... step=0 complete=false phase=create`.
|
|
310
|
+
if tail -n +"$TURN_MARKER_BEFORE" "$SERVER_LOG" 2>/dev/null | grep -qF "[onboarding-gate]" \
|
|
311
|
+
&& tail -n +"$TURN_MARKER_BEFORE" "$SERVER_LOG" 2>/dev/null | grep -F "[onboarding-gate]" | grep -qF "step=0 complete=false"; then
|
|
312
|
+
record PASS "[onboarding-gate] step=0 complete=false" "phase=create"
|
|
313
|
+
else
|
|
314
|
+
record FAIL "[onboarding-gate] step=0 complete=false" "not found in $SERVER_LOG since session-mint"
|
|
315
|
+
fi
|
|
316
|
+
|
|
317
|
+
# ---------------------------------------------------------------------------
|
|
318
|
+
# Step 4 — Spawn an admin claude-session so the next /input is injection-eligible.
|
|
319
|
+
# ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
echo "[4/6] Spawn admin claude-session and capture sessionId"
|
|
322
|
+
SPAWN_RESPONSE="$(curl -sS -X POST "$ADMIN_BASE/api/admin/claude-sessions/?session_key=$SESSION_KEY" \
|
|
323
|
+
-H 'content-type: application/json' \
|
|
324
|
+
-d '{"channel":"browser"}')" || {
|
|
325
|
+
record FAIL "POST /api/admin/claude-sessions/" "curl failed"
|
|
326
|
+
print_report
|
|
327
|
+
exit 1
|
|
328
|
+
}
|
|
329
|
+
SESSION_ID="$(printf '%s' "$SPAWN_RESPONSE" | jq -r '.sessionId // empty')"
|
|
330
|
+
if [ -z "$SESSION_ID" ]; then
|
|
331
|
+
record FAIL "POST /api/admin/claude-sessions/" "no sessionId in response: $SPAWN_RESPONSE"
|
|
332
|
+
print_report
|
|
333
|
+
exit 1
|
|
334
|
+
fi
|
|
335
|
+
record PASS "spawn claude-session" "sessionId=${SESSION_ID:0:8}"
|
|
336
|
+
|
|
337
|
+
# ---------------------------------------------------------------------------
|
|
338
|
+
# Step 5 — First /input call. This triggers buildOnboardingPromptBlock,
|
|
339
|
+
# which logs [onboarding-inject] step=0 injected=true. The agent then runs
|
|
340
|
+
# the prepended skill-load directive, which logs [skill-load] skillName=onboarding.
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
echo "[5/6] Submit first admin input and wait up to ${TIMEOUT}s for [onboarding-inject] and [skill-load]"
|
|
344
|
+
INPUT_MARKER_BEFORE="$(wc -l < "$SERVER_LOG" 2>/dev/null | tr -d ' ' || echo 0)"
|
|
345
|
+
INPUT_MARKER_BEFORE="${INPUT_MARKER_BEFORE:-0}"
|
|
346
|
+
INPUT_RESPONSE="$(curl -sS -X POST "$ADMIN_BASE/api/admin/claude-sessions/$SESSION_ID/input?session_key=$SESSION_KEY" \
|
|
347
|
+
-H 'content-type: application/json' \
|
|
348
|
+
-d '{"text":"hello"}')" || {
|
|
349
|
+
record FAIL "POST /api/admin/claude-sessions/:id/input" "curl failed"
|
|
350
|
+
print_report
|
|
351
|
+
exit 1
|
|
352
|
+
}
|
|
353
|
+
# Status-only check — the manager owns the per-input contract.
|
|
354
|
+
record PASS "POST first /input" "(skill-load arrives async)"
|
|
355
|
+
|
|
356
|
+
# Poll-tail server.log for the two remaining lines.
|
|
357
|
+
deadline=$(( $(date +%s) + TIMEOUT ))
|
|
358
|
+
inject_seen=0
|
|
359
|
+
skill_seen=0
|
|
360
|
+
while [ "$(date +%s)" -lt "$deadline" ]; do
|
|
361
|
+
if [ "$inject_seen" -eq 0 ] \
|
|
362
|
+
&& tail -n +"$INPUT_MARKER_BEFORE" "$SERVER_LOG" 2>/dev/null | grep -F "[onboarding-inject]" | grep -qF "step=0 injected=true"; then
|
|
363
|
+
inject_seen=1
|
|
364
|
+
fi
|
|
365
|
+
if [ "$skill_seen" -eq 0 ] \
|
|
366
|
+
&& tail -n +"$INPUT_MARKER_BEFORE" "$SERVER_LOG" 2>/dev/null | grep -F "[skill-load]" | grep -qF "skillName=onboarding"; then
|
|
367
|
+
skill_seen=1
|
|
368
|
+
fi
|
|
369
|
+
if [ "$inject_seen" -eq 1 ] && [ "$skill_seen" -eq 1 ]; then break; fi
|
|
370
|
+
sleep 1
|
|
371
|
+
done
|
|
372
|
+
|
|
373
|
+
if [ "$inject_seen" -eq 1 ]; then
|
|
374
|
+
record PASS "[onboarding-inject] step=0 injected=true" "server.log"
|
|
375
|
+
else
|
|
376
|
+
record FAIL "[onboarding-inject] step=0 injected=true" "not found within ${TIMEOUT}s"
|
|
377
|
+
fi
|
|
378
|
+
if [ "$skill_seen" -eq 1 ]; then
|
|
379
|
+
record PASS "[skill-load] skillName=onboarding" "server.log"
|
|
380
|
+
else
|
|
381
|
+
record FAIL "[skill-load] skillName=onboarding" "not found within ${TIMEOUT}s"
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
# ---------------------------------------------------------------------------
|
|
385
|
+
# Step 6 — --watch mode: poll Neo4j and assert step-advance + timestamp.
|
|
386
|
+
#
|
|
387
|
+
# The operator drives steps 1–9 in the UI. For every observed currentStep
|
|
388
|
+
# transition N→N+1, assert step(N+1)CompletedAt is non-null at the same
|
|
389
|
+
# moment. The script never advances steps itself — steps 7 (Cloudflare
|
|
390
|
+
# OAuth), 8 (Anthropic API key paste), and 9 (free-form business profile)
|
|
391
|
+
# require operator-side inputs that cannot be forged from a script.
|
|
392
|
+
# ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
if [ "$FAIL_COUNT" -gt 0 ]; then
|
|
395
|
+
print_report
|
|
396
|
+
echo "Fresh-install chain failed before --watch could start." >&2
|
|
397
|
+
exit 1
|
|
398
|
+
fi
|
|
399
|
+
|
|
400
|
+
if [ "$WATCH" -eq 1 ]; then
|
|
401
|
+
echo "[6/6] --watch: polling Neo4j every 2s. Drive steps 1-9 in the UI now. Ctrl-C to abort."
|
|
402
|
+
last_step="$CURRENT_STEP"
|
|
403
|
+
while :; do
|
|
404
|
+
sleep 2
|
|
405
|
+
WATCH_OUT="$(cypher_or_die 'MATCH (o:OnboardingState) RETURN o.currentStep AS s, o.step1CompletedAt AS s1, o.step2CompletedAt AS s2, o.step3CompletedAt AS s3, o.step4CompletedAt AS s4, o.step5CompletedAt AS s5, o.step6CompletedAt AS s6, o.step7CompletedAt AS s7, o.step8CompletedAt AS s8, o.step9CompletedAt AS s9')"
|
|
406
|
+
NOW_STEP="$(printf '%s' "$WATCH_OUT" | tail -n 1 | awk -F', ' '{print $1}')"
|
|
407
|
+
if [ "$NOW_STEP" = "$last_step" ]; then continue; fi
|
|
408
|
+
# Advance observed. Check the timestamp for the new step.
|
|
409
|
+
new_step="$NOW_STEP"
|
|
410
|
+
if [ "$new_step" -le "$last_step" ]; then
|
|
411
|
+
record FAIL "step regression" "last=$last_step now=$new_step"
|
|
412
|
+
print_report
|
|
413
|
+
exit 1
|
|
414
|
+
fi
|
|
415
|
+
ts_field=$(( new_step + 1 )) # awk fields are 1-indexed; currentStep is field 1, stepNCompletedAt is field N+1.
|
|
416
|
+
stamp="$(printf '%s' "$WATCH_OUT" | tail -n 1 | awk -F', ' "{print \$$ts_field}" | tr -d '"')"
|
|
417
|
+
if [ -z "$stamp" ] || [ "$stamp" = "NULL" ] || [ "$stamp" = "null" ]; then
|
|
418
|
+
record FAIL "step$new_step advanced without timestamp" "step${new_step}CompletedAt is NULL"
|
|
419
|
+
print_report
|
|
420
|
+
exit 1
|
|
421
|
+
fi
|
|
422
|
+
record PASS "step $new_step advance" "step${new_step}CompletedAt=$stamp"
|
|
423
|
+
last_step="$new_step"
|
|
424
|
+
if [ "$new_step" -ge 9 ]; then
|
|
425
|
+
echo " currentStep=9 — onboarding complete"
|
|
426
|
+
break
|
|
427
|
+
fi
|
|
428
|
+
done
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
print_report
|
|
432
|
+
if [ "$FAIL_COUNT" -gt 0 ]; then
|
|
433
|
+
exit 1
|
|
434
|
+
fi
|
|
435
|
+
exit 0
|