#!/usr/bin/env bash # Pathway smoke — pathwayd Mem0-style versioned trace memory (ADR-004). # All assertions go through gateway :3110. # # Validates: # - All 9 HTTP routes (add, add_idempotent, update, revise, retire, # get, history, search, stats) # - Revise creates a predecessor link; History walks the chain # backward (the audit-trail property pathway memory exists for) # - Retire excludes from Search default; still accessible via Get # - AddIdempotent on existing UID bumps replay_count, doesn't replace # - Negative paths: 404 on unknown UIDs, 404 on missing predecessor, # 400 on invalid content # - Persistence: kill + restart pathwayd → all traces survive # # Usage: ./scripts/pathway_smoke.sh set -euo pipefail cd "$(dirname "$0")/.." export PATH="$PATH:/usr/local/go/bin" echo "[pathway-smoke] building pathwayd + gateway..." go build -o bin/ ./cmd/pathwayd ./cmd/gateway pkill -f "bin/(pathwayd|gateway)" 2>/dev/null || true sleep 0.3 PIDS=() TMP="$(mktemp -d)" PERSIST="$TMP/pathway.jsonl" CFG="$TMP/pathwayd.toml" cleanup() { echo "[pathway-smoke] cleanup" for p in "${PIDS[@]}"; do [ -n "$p" ] && kill "$p" 2>/dev/null || true; done rm -rf "$TMP" } trap cleanup EXIT INT TERM # Custom toml — same defaults as lakehouse.toml but with persist_path # pointing at the temp file so kill+restart actually rehydrates. cat > "$CFG" </dev/null 2>&1; then return 0; fi sleep 0.05 done return 1 } launch_pathwayd() { ./bin/pathwayd -config "$CFG" > /tmp/pathwayd.log 2>&1 & PATHWAYD_PID=$! PIDS+=($PATHWAYD_PID) poll_health 3217 || { echo "pathwayd failed"; tail /tmp/pathwayd.log; return 1; } } launch_gateway() { ./bin/gateway -config "$CFG" > /tmp/gateway.log 2>&1 & PIDS+=($!) poll_health 3110 || { echo "gateway failed"; tail /tmp/gateway.log; return 1; } } echo "[pathway-smoke] launching pathwayd → gateway..." launch_pathwayd launch_gateway FAILED=0 # ── 1. Add ──────────────────────────────────────────────────────── echo "[pathway-smoke] Add → fresh UID + replay_count=1:" RESP="$(curl -sS -X POST http://127.0.0.1:3110/v1/pathway/add \ -H 'Content-Type: application/json' \ -d '{"content":{"approach":"forklift-OSHA-30","outcome":"hired"},"tags":["staffing","fill"]}')" UID_A="$(echo "$RESP" | jq -r '.uid')" RC_A="$(echo "$RESP" | jq -r '.replay_count')" if [ -n "$UID_A" ] && [ "$UID_A" != "null" ] && [ "$RC_A" = "1" ]; then echo " ✓ uid=$UID_A replay_count=1" else echo " ✗ resp: $RESP"; FAILED=1 fi # ── 2. Get ──────────────────────────────────────────────────────── echo "[pathway-smoke] Get → returns same trace:" RESP="$(curl -sS "http://127.0.0.1:3110/v1/pathway/get/$UID_A")" APPROACH="$(echo "$RESP" | jq -r '.content.approach')" if [ "$APPROACH" = "forklift-OSHA-30" ]; then echo " ✓ content.approach round-trips" else echo " ✗ resp: $RESP"; FAILED=1 fi # ── 3. AddIdempotent (replay) ───────────────────────────────────── echo "[pathway-smoke] AddIdempotent same UID → replay_count++:" RESP="$(curl -sS -X POST http://127.0.0.1:3110/v1/pathway/add_idempotent \ -H 'Content-Type: application/json' \ -d "{\"uid\":\"$UID_A\",\"content\":{\"approach\":\"forklift-OSHA-30\",\"outcome\":\"hired\"}}")" RC_REPLAY="$(echo "$RESP" | jq -r '.replay_count')" if [ "$RC_REPLAY" = "2" ]; then echo " ✓ replay_count bumped to 2" else echo " ✗ replay_count=$RC_REPLAY"; FAILED=1 fi # ── 4. Update ───────────────────────────────────────────────────── echo "[pathway-smoke] Update → in-place content replace:" HTTP="$(curl -sS -o "$TMP/upd.json" -w '%{http_code}' -X POST http://127.0.0.1:3110/v1/pathway/update \ -H 'Content-Type: application/json' \ -d "{\"uid\":\"$UID_A\",\"content\":{\"approach\":\"forklift-OSHA-30\",\"outcome\":\"hired\",\"note\":\"cert verified\"}}")" if [ "$HTTP" = "200" ]; then NOTE="$(curl -sS "http://127.0.0.1:3110/v1/pathway/get/$UID_A" | jq -r '.content.note')" if [ "$NOTE" = "cert verified" ]; then echo " ✓ Update applied and persisted" else echo " ✗ note=$NOTE after update"; FAILED=1 fi else echo " ✗ Update HTTP=$HTTP"; FAILED=1 fi # ── 5. Revise → predecessor link ────────────────────────────────── echo "[pathway-smoke] Revise → new UID with predecessor link:" RESP="$(curl -sS -X POST http://127.0.0.1:3110/v1/pathway/revise \ -H 'Content-Type: application/json' \ -d "{\"predecessor_uid\":\"$UID_A\",\"content\":{\"approach\":\"forklift-OSHA-30+CDL\",\"outcome\":\"upgraded\"},\"tags\":[\"staffing\",\"revision\"]}")" UID_B="$(echo "$RESP" | jq -r '.uid')" PRED="$(echo "$RESP" | jq -r '.predecessor_uid')" if [ "$UID_B" != "$UID_A" ] && [ "$PRED" = "$UID_A" ]; then echo " ✓ revision uid=$UID_B predecessor=$UID_A" else echo " ✗ uid=$UID_B pred=$PRED"; FAILED=1 fi # ── 6. History → 2-trace chain ──────────────────────────────────── echo "[pathway-smoke] History → walks chain backward:" RESP="$(curl -sS "http://127.0.0.1:3110/v1/pathway/history/$UID_B")" LEN="$(echo "$RESP" | jq -r '.length')" HEAD="$(echo "$RESP" | jq -r '.chain[0].uid')" TAIL="$(echo "$RESP" | jq -r '.chain[1].uid')" if [ "$LEN" = "2" ] && [ "$HEAD" = "$UID_B" ] && [ "$TAIL" = "$UID_A" ]; then echo " ✓ chain length=2, [0]=$UID_B [1]=$UID_A" else echo " ✗ len=$LEN head=$HEAD tail=$TAIL"; FAILED=1 fi # ── 7. Search by tag ────────────────────────────────────────────── echo "[pathway-smoke] Search tag=staffing → finds both traces:" COUNT="$(curl -sS -X POST http://127.0.0.1:3110/v1/pathway/search \ -H 'Content-Type: application/json' -d '{"tag":"staffing"}' | jq -r '.count')" if [ "$COUNT" = "2" ]; then echo " ✓ tag search count=2" else echo " ✗ count=$COUNT"; FAILED=1 fi # ── 8. Retire → excluded from search default, still in Get ──────── echo "[pathway-smoke] Retire → excluded from Search but Get-able:" HTTP="$(curl -sS -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:3110/v1/pathway/retire \ -H 'Content-Type: application/json' -d "{\"uid\":\"$UID_A\"}")" if [ "$HTTP" != "204" ]; then echo " ✗ retire HTTP=$HTTP"; FAILED=1; fi # Default search excludes retired → only revision (UID_B) remains COUNT_DEFAULT="$(curl -sS -X POST http://127.0.0.1:3110/v1/pathway/search \ -H 'Content-Type: application/json' -d '{"tag":"staffing"}' | jq -r '.count')" # IncludeRetired=true brings UID_A back COUNT_ALL="$(curl -sS -X POST http://127.0.0.1:3110/v1/pathway/search \ -H 'Content-Type: application/json' -d '{"tag":"staffing","include_retired":true}' | jq -r '.count')" # Get on retired UID still returns the trace (audit trail intact) RETIRED_FLAG="$(curl -sS "http://127.0.0.1:3110/v1/pathway/get/$UID_A" | jq -r '.retired')" if [ "$COUNT_DEFAULT" = "1" ] && [ "$COUNT_ALL" = "2" ] && [ "$RETIRED_FLAG" = "true" ]; then echo " ✓ retired excluded from default Search, included with flag, still Get-able" else echo " ✗ default=$COUNT_DEFAULT all=$COUNT_ALL retired=$RETIRED_FLAG"; FAILED=1 fi # ── 9. Stats ────────────────────────────────────────────────────── echo "[pathway-smoke] Stats → total/active/retired counters:" STATS="$(curl -sS http://127.0.0.1:3110/v1/pathway/stats)" T="$(echo "$STATS" | jq -r '.Total')" A="$(echo "$STATS" | jq -r '.Active')" R="$(echo "$STATS" | jq -r '.Retired')" if [ "$T" = "2" ] && [ "$A" = "1" ] && [ "$R" = "1" ]; then echo " ✓ total=2 active=1 retired=1" else echo " ✗ total=$T active=$A retired=$R"; FAILED=1 fi # ── 10. Negative paths ──────────────────────────────────────────── echo "[pathway-smoke] Negative paths → 4xx semantics:" GET_404="$(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1:3110/v1/pathway/get/no-such-uid)" UPD_404="$(curl -sS -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:3110/v1/pathway/update \ -H 'Content-Type: application/json' -d '{"uid":"no-such-uid","content":{}}')" REV_404="$(curl -sS -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:3110/v1/pathway/revise \ -H 'Content-Type: application/json' -d '{"predecessor_uid":"no-such-uid","content":{}}')" RET_404="$(curl -sS -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:3110/v1/pathway/retire \ -H 'Content-Type: application/json' -d '{"uid":"no-such-uid"}')" ADD_400="$(curl -sS -o /dev/null -w '%{http_code}' -X POST http://127.0.0.1:3110/v1/pathway/add \ -H 'Content-Type: application/json' -d '{"content":not-json}')" if [ "$GET_404" = "404" ] && [ "$UPD_404" = "404" ] && [ "$REV_404" = "404" ] && [ "$RET_404" = "404" ] && [ "$ADD_400" = "400" ]; then echo " ✓ get/update/revise/retire on unknown → 404; bad content → 400" else echo " ✗ get=$GET_404 upd=$UPD_404 rev=$REV_404 ret=$RET_404 add=$ADD_400"; FAILED=1 fi # ── 11. Persistence → kill + restart preserves all traces ───────── echo "[pathway-smoke] kill + restart pathwayd → state survives:" kill $PATHWAYD_PID 2>/dev/null || true wait $PATHWAYD_PID 2>/dev/null || true sleep 0.3 launch_pathwayd sleep 0.2 # Both traces should reappear, retired flag preserved, replay_count preserved RESP_A="$(curl -sS "http://127.0.0.1:3110/v1/pathway/get/$UID_A")" RESP_B="$(curl -sS "http://127.0.0.1:3110/v1/pathway/get/$UID_B")" RC_AFTER="$(echo "$RESP_A" | jq -r '.replay_count')" RETIRED_AFTER="$(echo "$RESP_A" | jq -r '.retired')" PRED_AFTER="$(echo "$RESP_B" | jq -r '.predecessor_uid')" if [ "$RC_AFTER" = "2" ] && [ "$RETIRED_AFTER" = "true" ] && [ "$PRED_AFTER" = "$UID_A" ]; then echo " ✓ replay_count, retired flag, predecessor link all preserved" else echo " ✗ replay_count=$RC_AFTER retired=$RETIRED_AFTER pred=$PRED_AFTER"; FAILED=1 fi if [ "$FAILED" -eq 0 ]; then echo "[pathway-smoke] Pathway acceptance gate: PASSED" exit 0 else echo "[pathway-smoke] Pathway acceptance gate: FAILED" exit 1 fi