f9447f8e5f
CI / typecheck · build · test · bundle-size (push) Failing after 1s
CI / clean-room dependency closure smoke (push) Failing after 1s
CI / server-runtime e2e (docker · pg + valkey) (push) Failing after 2s
Deploy Install Scripts / deploy (push) Failing after 2s
Windows / build (push) Has been cancelled
177 lines
6.8 KiB
Bash
Executable File
177 lines
6.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# Phase 10 — server-beta Docker E2E.
|
|
#
|
|
# Brings up Postgres + Valkey + claude-mem-server (HTTP) + claude-mem-worker
|
|
# (BullMQ generation) and verifies:
|
|
# - no legacy `worker-service.cjs` / WorkerService process anywhere
|
|
# - POST /v1/events?wait=true generates an observation through the queue
|
|
# - server restart mid-stream preserves jobs and observations
|
|
# - revoking an API key denies subsequent reads/writes (401/403)
|
|
#
|
|
# All assertions are fatal; the script exits non-zero on any failure.
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
PROJECT_NAME="${COMPOSE_PROJECT_NAME:-claude-mem-server-e2e-$(date +%s)}"
|
|
RUN_ID="${E2E_RUN_ID:-$(date +%s)-$RANDOM}"
|
|
COMPOSE_FILES=(-f docker-compose.yml -f docker-compose.e2e.yml)
|
|
# Test-only credentials. docker-compose.yml requires these to be set; the
|
|
# stack will refuse to start without them. The values here are scoped to the
|
|
# ephemeral E2E project namespace and are torn down by the cleanup trap.
|
|
export POSTGRES_USER="${POSTGRES_USER:-claudemem_e2e}"
|
|
export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-claudemem_e2e}"
|
|
export POSTGRES_DB="${POSTGRES_DB:-claudemem_e2e}"
|
|
COMPOSE=(docker compose -p "$PROJECT_NAME" "${COMPOSE_FILES[@]}")
|
|
SERVER_SCRIPT="/opt/claude-mem/scripts/server-service.cjs"
|
|
# server-service.cjs has its own `server api-key create|list|revoke`
|
|
# subtree backed by Postgres (NOT the SQLite worker-service tree).
|
|
SERVER_HEALTH_URL="http://127.0.0.1:37877/healthz"
|
|
|
|
cd "$ROOT_DIR"
|
|
|
|
cleanup() {
|
|
local exit_code=$?
|
|
if [[ $exit_code -ne 0 ]]; then
|
|
echo "[e2e] failure; recent logs:" >&2
|
|
"${COMPOSE[@]}" logs --no-color --tail=200 \
|
|
claude-mem-server claude-mem-worker valkey postgres >&2 || true
|
|
fi
|
|
"${COMPOSE[@]}" down -v --remove-orphans >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
wait_for_container_readiness() {
|
|
local deadline=$((SECONDS + 180))
|
|
until "${COMPOSE[@]}" exec -T claude-mem-server curl -fsS "$SERVER_HEALTH_URL" >/dev/null 2>&1; do
|
|
if (( SECONDS > deadline )); then
|
|
echo "[e2e] server did not become ready within 180s" >&2
|
|
return 1
|
|
fi
|
|
sleep 2
|
|
done
|
|
}
|
|
|
|
json_field() {
|
|
local field="$1"
|
|
node -e '
|
|
const field = process.argv[1];
|
|
let raw = "";
|
|
process.stdin.on("data", chunk => raw += chunk);
|
|
process.stdin.on("end", () => {
|
|
const value = JSON.parse(raw)[field];
|
|
if (value === undefined || value === null) process.exit(1);
|
|
process.stdout.write(String(value));
|
|
});
|
|
' "$field"
|
|
}
|
|
|
|
create_key() {
|
|
local name="$1"
|
|
local scopes="$2"
|
|
"${COMPOSE[@]}" exec -T claude-mem-server \
|
|
bun "$SERVER_SCRIPT" server api-key create --name "$name" --scope "$scopes"
|
|
}
|
|
|
|
assert_no_worker_process() {
|
|
echo "[e2e] verifying no legacy worker process is running"
|
|
# `docker compose ps` should not list a service named "worker" (legacy).
|
|
# The new generation worker is named `claude-mem-worker`; that's allowed.
|
|
local services
|
|
services="$("${COMPOSE[@]}" ps --services)"
|
|
if echo "$services" | grep -E '(^|\s)worker($|\s)' | grep -v 'claude-mem-worker' >/dev/null; then
|
|
echo "[e2e] FAIL — unexpected legacy worker service in compose stack:" >&2
|
|
echo "$services" >&2
|
|
return 1
|
|
fi
|
|
|
|
# No process inside the server container should be running worker-service.cjs.
|
|
if "${COMPOSE[@]}" exec -T claude-mem-server pgrep -af 'worker-service\.cjs' >/dev/null 2>&1; then
|
|
echo "[e2e] FAIL — worker-service.cjs is running inside claude-mem-server" >&2
|
|
"${COMPOSE[@]}" exec -T claude-mem-server pgrep -af 'worker-service\.cjs' >&2 || true
|
|
return 1
|
|
fi
|
|
if "${COMPOSE[@]}" exec -T claude-mem-worker pgrep -af 'worker-service\.cjs' >/dev/null 2>&1; then
|
|
echo "[e2e] FAIL — worker-service.cjs is running inside claude-mem-worker" >&2
|
|
return 1
|
|
fi
|
|
echo "[e2e] no legacy worker processes detected"
|
|
}
|
|
|
|
assert_local_dev_rejected_in_docker() {
|
|
echo "[e2e] verifying local-dev auth is rejected inside Docker"
|
|
# Run a throwaway server-beta container with CLAUDE_MEM_AUTH_MODE=local-dev.
|
|
# validateServerBetaEnv() should refuse to start and exit non-zero.
|
|
local rc=0
|
|
"${COMPOSE[@]}" run --rm \
|
|
--no-deps \
|
|
-e CLAUDE_MEM_AUTH_MODE=local-dev \
|
|
-e CLAUDE_MEM_ALLOW_LOCAL_DEV_BYPASS=1 \
|
|
-e CLAUDE_MEM_CONTAINER_MODE=server \
|
|
claude-mem-server >/tmp/local-dev-stdout.$$ 2>/tmp/local-dev-stderr.$$ \
|
|
|| rc=$?
|
|
if [[ $rc -eq 0 ]]; then
|
|
echo "[e2e] FAIL — server-beta started with CLAUDE_MEM_AUTH_MODE=local-dev in Docker" >&2
|
|
cat /tmp/local-dev-stderr.$$ >&2 || true
|
|
rm -f /tmp/local-dev-stdout.$$ /tmp/local-dev-stderr.$$
|
|
return 1
|
|
fi
|
|
if ! grep -q 'local-dev is not allowed in Docker' /tmp/local-dev-stderr.$$; then
|
|
echo "[e2e] FAIL — expected local-dev rejection message; saw:" >&2
|
|
cat /tmp/local-dev-stderr.$$ >&2 || true
|
|
rm -f /tmp/local-dev-stdout.$$ /tmp/local-dev-stderr.$$
|
|
return 1
|
|
fi
|
|
rm -f /tmp/local-dev-stdout.$$ /tmp/local-dev-stderr.$$
|
|
echo "[e2e] local-dev auth correctly rejected"
|
|
}
|
|
|
|
echo "[e2e] building plugin bundles"
|
|
npm run build
|
|
|
|
echo "[e2e] starting Docker stack project=$PROJECT_NAME run=$RUN_ID"
|
|
"${COMPOSE[@]}" up --build -d postgres valkey claude-mem-server claude-mem-worker
|
|
wait_for_container_readiness
|
|
assert_no_worker_process
|
|
|
|
echo "[e2e] creating API keys inside server container"
|
|
FULL_KEY_JSON="$(create_key "docker-e2e-full-$RUN_ID" "events:write,sessions:write,observations:read,jobs:read,memories:read,memories:write")"
|
|
READ_ONLY_KEY_JSON="$(create_key "docker-e2e-read-$RUN_ID" "observations:read,jobs:read,memories:read")"
|
|
FULL_KEY="$(printf '%s' "$FULL_KEY_JSON" | json_field key)"
|
|
READ_ONLY_KEY="$(printf '%s' "$READ_ONLY_KEY_JSON" | json_field key)"
|
|
READ_ONLY_KEY_ID="$(printf '%s' "$READ_ONLY_KEY_JSON" | json_field id)"
|
|
FULL_PROJECT_ID="$(printf '%s' "$FULL_KEY_JSON" | json_field projectId)"
|
|
|
|
echo "[e2e] running phase1 functional paths in test container"
|
|
"${COMPOSE[@]}" run --rm \
|
|
-e E2E_PHASE=phase1 \
|
|
-e E2E_RUN_ID="$RUN_ID" \
|
|
-e E2E_API_KEY="$FULL_KEY" \
|
|
-e E2E_READ_ONLY_API_KEY="$READ_ONLY_KEY" \
|
|
-e E2E_PROJECT_ID="$FULL_PROJECT_ID" \
|
|
server-e2e
|
|
|
|
echo "[e2e] revoking read-only key inside server container"
|
|
"${COMPOSE[@]}" exec -T claude-mem-server \
|
|
bun "$SERVER_SCRIPT" server api-key revoke "$READ_ONLY_KEY_ID" >/dev/null
|
|
|
|
echo "[e2e] restarting server container to verify persisted state and queue durability"
|
|
"${COMPOSE[@]}" restart claude-mem-server claude-mem-worker
|
|
wait_for_container_readiness
|
|
assert_no_worker_process
|
|
|
|
echo "[e2e] running phase2 persistence and revoked-key checks in test container"
|
|
"${COMPOSE[@]}" run --rm \
|
|
-e E2E_PHASE=phase2 \
|
|
-e E2E_RUN_ID="$RUN_ID" \
|
|
-e E2E_API_KEY="$FULL_KEY" \
|
|
-e E2E_REVOKED_API_KEY="$READ_ONLY_KEY" \
|
|
-e E2E_PROJECT_ID="$FULL_PROJECT_ID" \
|
|
server-e2e
|
|
|
|
echo "[e2e] verifying anti-pattern guards"
|
|
assert_local_dev_rejected_in_docker
|
|
|
|
echo "[e2e] Docker server beta E2E passed for run=$RUN_ID"
|