#!/usr/bin/env bash # SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 David (Corey) Herrera # # serve_session_export_smoke.sh — regression test for "session export/restore # only ever worked on the single-user path". # # The defect: /admin/session/export looked the session up in the model's # pn_prefix_cache. A BATCHED model (QWEN35_BATCH_SLOTS=1 — every model a K80 # deployment runs) keeps its session state in the batch scheduler's warm decode slot, # and nothing ever puts an entry in that model's prefix cache, so the lookup # returned NULL and EVERY export against a batched model answered 404 # session_not_found. /session/restore had the mirror-image defect: it imported # into the prefix cache, so it reported 200 restored:true while writing to a # store no batched generation ever reads. /session/release cleared only the # prefix cache too, leaving a departed session's context bound to its slot. # # Since the host KV mirror was removed (Phase 10b / INV3), export is the ONLY # way a session's state can leave the GPU: a serve restart loses all warm # context and the client never receives the blob the design says it owns. # # Asserts, against a LIVE session on the model under test: # 1. export returns HTTP 200 with a non-empty blob, n_tok > 0, a # weights_version, and the committed token ids. (This alone is the # assertion that would have caught the defect.) # 2. weights_version guard: the same blob with a corrupted weights_version # is refused 409 weights_version_mismatch, and nothing is restored. # 3. release actually drops the session: a second export answers 404. # 4. restore of the exported blob answers 200 restored:true. # 5. ROUND TRIP IS LOSSLESS: re-exporting after the restore yields a blob # byte-identical to the original, with the same n_tok and token ids. # This is the discriminating check — it proves the state left the GPU, # crossed the wire format, and came back into a live slot intact. # 6. The session still generates coherently after the restore (recall of a # planted token that was in the context before the export). # # Usage (server must already be running): # BASE_URL=http://127.0.0.1:8201 MODEL=saber-orch \ # tests/serve_session_export_smoke.sh # # Env: # BASE_URL default http://127.0.0.1:8201 # MODEL default saber-orch # SECRET planted recall token (default ZEPHYR-7-QUARTZ) # FILLER words of real context in front of the planted token (default 200) # MAX_TOKENS default 300 # REQ_TIMEOUT per-request curl timeout, seconds (default 600) set -u BASE_URL="${BASE_URL:-http://127.0.0.1:8201}" MODEL="${MODEL:-saber-orch}" SECRET="${SECRET:-ZEPHYR-7-QUARTZ}" FILLER="${FILLER:-200}" MAX_TOKENS="${MAX_TOKENS:-300}" REQ_TIMEOUT="${REQ_TIMEOUT:-600}" WORK="$(mktemp -d)" SID="expsmoke-$$-$(date +%s)" PASS=0 FAIL=0 cleanup() { curl -s -m 30 -o /dev/null "$BASE_URL/session/release" \ -H 'Content-Type: application/json' \ -d "{\"name\":\"$MODEL\",\"session_id\":\"$SID\"}" || true rm -rf "$WORK" } trap cleanup EXIT ok() { PASS=$((PASS+1)); echo " PASS $*"; } bad() { FAIL=$((FAIL+1)); echo " FAIL $*"; } # jq is not assumed — pull scalars with python3, which the K80 test machine has. jget() { # jget e.g. jget f 'd["n_tok"]' python3 -c ' import json,sys d=json.load(open(sys.argv[1])) v=eval(sys.argv[2]) print("" if v is None else v) ' "$1" "$2" 2>/dev/null } post() { # post -> echoes http code curl -s -m "$REQ_TIMEOUT" -o "$3" -w '%{http_code}' \ "$BASE_URL$1" -H 'Content-Type: application/json' --data-binary "@$2" } echo "== session export/restore round trip: model=$MODEL sid=$SID ==" # ── Turn 1: plant a distinctive token behind real context ────────────────── python3 - "$WORK/turn1.json" "$MODEL" "$SID" "$SECRET" "$FILLER" "$MAX_TOKENS" <<'PY' import json, sys out, model, sid, secret, filler, maxtok = sys.argv[1:7] words = ("The maintenance log records routine checks of the cooling loop, the " "power rails and the interconnect fabric. ").split() body = " ".join(words[i % len(words)] for i in range(int(filler))) msg = (f"{body}\n\nRemember this access code exactly: {secret}. " "Reply with only the word: stored") json.dump({"model": model, "session_id": sid, "max_tokens": int(maxtok), "messages": [{"role": "user", "content": msg}]}, open(out, "w")) PY code=$(post /v1/chat/completions "$WORK/turn1.json" "$WORK/turn1.out") if [ "$code" = "200" ] && [ "$(jget "$WORK/turn1.out" 'd["usage"]["completion_tokens"]')" -gt 0 ] 2>/dev/null; then ok "turn 1 generated (${code}, $(jget "$WORK/turn1.out" 'd["usage"]["prompt_tokens"]') prompt tokens)" else bad "turn 1 did not generate (HTTP $code)"; cat "$WORK/turn1.out"; echo echo "== $PASS passed, $FAIL failed =="; exit 1 fi # ── 1. Export ────────────────────────────────────────────────────────────── cat > "$WORK/exp.json" </dev/null \ && [ -n "$WV" ] && [ "${BLOB_BYTES:-0}" -gt 0 ] 2>/dev/null; then ok "export 200: n_tok=$N_TOK blob_bytes=$BLOB_BYTES weights_version=$WV" else bad "export failed (HTTP $code) — THE REGRESSION"; head -c 300 "$WORK/exp1.out"; echo echo "== $PASS passed, $FAIL failed =="; exit 1 fi if [ "$(jget "$WORK/exp1.out" 'len(d.get("tokens") or [])')" = "$N_TOK" ]; then ok "export returned the $N_TOK committed token ids" else bad "export tokens[] length != n_tok" fi # Build the restore bodies (good + weights_version-corrupted). python3 - "$WORK/exp1.out" "$MODEL" "$SID" "$WORK/res_good.json" "$WORK/res_badwv.json" <<'PY' import base64, json, sys, struct src, model, sid, good, badwv = sys.argv[1:6] d = json.load(open(src)) raw = base64.b64decode(d["blob_b64"]) json.dump({"name": model, "session_id": sid, "blob": d["blob_b64"], "tokens": d["tokens"]}, open(good, "w")) # The blob header is {magic, format_version, weights_version, ...}; flip a bit # in the weights_version field so only the identity tag differs. b = bytearray(raw) wv_off = None tag = struct.pack("/dev/null code=$(post /admin/session/export "$WORK/exp.json" "$WORK/exp_gone.out") if [ "$code" = "404" ]; then ok "release dropped the session (export now 404)" else bad "release did not drop the session (export still HTTP $code)" fi # ── 4. Restore ───────────────────────────────────────────────────────────── code=$(post /session/restore "$WORK/res_good.json" "$WORK/res.out") if [ "$code" = "200" ] && [ "$(jget "$WORK/res.out" 'd.get("n_tok")')" = "$N_TOK" ]; then ok "restore 200: n_tok=$N_TOK" else bad "restore failed (HTTP $code): $(head -c 300 "$WORK/res.out")" echo "== $PASS passed, $FAIL failed =="; exit 1 fi # ── 5. Round trip is lossless ────────────────────────────────────────────── code=$(post /admin/session/export "$WORK/exp.json" "$WORK/exp2.out") if [ "$code" != "200" ]; then bad "re-export after restore failed (HTTP $code)" else if python3 - "$WORK/exp1.out" "$WORK/exp2.out" <<'PY' import json, sys a = json.load(open(sys.argv[1])); b = json.load(open(sys.argv[2])) same = (a["n_tok"] == b["n_tok"] and a["tokens"] == b["tokens"] and a["blob_b64"] == b["blob_b64"]) sys.exit(0 if same else 1) PY then ok "round trip lossless: re-exported blob byte-identical to the original" else bad "round trip LOSSY: re-exported blob differs from the original" python3 - "$WORK/exp1.out" "$WORK/exp2.out" <<'PY' import json, sys a = json.load(open(sys.argv[1])); b = json.load(open(sys.argv[2])) print(f" n_tok {a['n_tok']} -> {b['n_tok']}; " f"blob_bytes {a['blob_bytes']} -> {b['blob_bytes']}; " f"tokens equal: {a['tokens'] == b['tokens']}") PY fi fi # ── 6. The restored session still answers ────────────────────────────────── python3 - "$WORK/turn2.json" "$WORK/turn1.json" "$WORK/turn1.out" "$MAX_TOKENS" <<'PY' import json, sys out, t1, t1o, maxtok = sys.argv[1:5] req = json.load(open(t1)); res = json.load(open(t1o)) msgs = list(req["messages"]) msgs.append({"role": "assistant", "content": res["choices"][0]["message"].get("content") or "stored"}) msgs.append({"role": "user", "content": "What was the access code? Reply with only the code."}) req["messages"] = msgs req["max_tokens"] = int(maxtok) json.dump(req, open(out, "w")) PY code=$(post /v1/chat/completions "$WORK/turn2.json" "$WORK/turn2.out") ANS=$(jget "$WORK/turn2.out" '(d["choices"][0]["message"].get("content") or "") + " " + (d["choices"][0]["message"].get("reasoning_content") or "")') if [ "$code" = "200" ] && printf '%s' "$ANS" | grep -q "$SECRET"; then ok "post-restore turn recalls the planted token ($SECRET)" else bad "post-restore turn did not recall the planted token (HTTP $code): $(printf '%s' "$ANS" | head -c 200)" fi echo "== $PASS passed, $FAIL failed ==" [ "$FAIL" -eq 0 ]