pathwayd: HTTP service over internal/pathway · 11/11 smoke gate
Network-callable Mem0-style trace memory at :3217, fronted by gateway
/v1/pathway/*. Closes the ADR-004 wire-up: store substrate landed in
2a6234f, this lands the HTTP surface + [pathwayd] config + acceptance
gate.
Smoke proves the architecturally distinctive properties: Revise →
History walks the predecessor chain backward (audit trail), Retire
excludes from Search default but stays Get-able, AddIdempotent bumps
replay_count without replacing — and all survive kill+restart via
JSONL log replay.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+248
@@ -0,0 +1,248 @@
|
||||
#!/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" <<EOF
|
||||
[gateway]
|
||||
bind = "127.0.0.1:3110"
|
||||
storaged_url = "http://127.0.0.1:3211"
|
||||
catalogd_url = "http://127.0.0.1:3212"
|
||||
ingestd_url = "http://127.0.0.1:3213"
|
||||
queryd_url = "http://127.0.0.1:3214"
|
||||
vectord_url = "http://127.0.0.1:3215"
|
||||
embedd_url = "http://127.0.0.1:3216"
|
||||
pathwayd_url = "http://127.0.0.1:3217"
|
||||
|
||||
[pathwayd]
|
||||
bind = "127.0.0.1:3217"
|
||||
persist_path = "$PERSIST"
|
||||
EOF
|
||||
|
||||
poll_health() {
|
||||
local port="$1" deadline=$(($(date +%s) + 5))
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
if curl -sS --max-time 1 "http://127.0.0.1:$port/health" >/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
|
||||
Reference in New Issue
Block a user