177 lines
10 KiB
Bash
177 lines
10 KiB
Bash
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
|
# AT3.7 proof: a real local Convex backend -> backup runner -> fake Blob -> empty backend.
|
|
set -euo pipefail
|
|
app="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
repo="$(cd "$app/../.." && pwd)"
|
|
node_modules="$app/node_modules"
|
|
docker_backend_image="${CONVEX_BACKEND_IMAGE:-build.conusai.com/conusai/convex-backend@sha256:1738f1673f8d63161043a7859710d2301b1e9d6271e06afbb7af31594ea3a58f}"
|
|
work="$(mktemp -d)"
|
|
suffix="at37-$$"
|
|
source_name="${suffix}-source"
|
|
target_name="${suffix}-target"
|
|
fake_name="${suffix}-platform"
|
|
better_auth_name="${suffix}-better-auth" # kit:convex-better-auth
|
|
network_name="${suffix}-network"
|
|
source_port=""
|
|
target_port=""
|
|
platform_port=""
|
|
cleanup() {
|
|
local status=$?
|
|
if [[ "$status" -ne 0 ]] && docker inspect "$source_name" >/dev/null 2>&1; then
|
|
echo "AT3.7 local Convex backend log on failure:" >&2
|
|
docker logs --tail 100 "$source_name" >&2 || true
|
|
fi
|
|
docker rm -f "$source_name" "$target_name" "$fake_name" >/dev/null 2>&1 || true
|
|
docker rm -f "$better_auth_name" >/dev/null 2>&1 || true # kit:convex-better-auth
|
|
docker network rm "$network_name" >/dev/null 2>&1 || true
|
|
rm -rf "$work"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
command -v docker >/dev/null || { echo "AT3.7 round-trip needs Docker" >&2; exit 1; }
|
|
# An explicit small fixture subnet avoids exhausting a shared host's default pools.
|
|
subnet="$(node -e 'const b=require("node:crypto").randomBytes(3);process.stdout.write("198."+(18+(b[0]&1))+"."+b[1]+"."+(b[2]&240)+"/28")')"
|
|
docker network create --subnet "$subnet" "$network_name" >/dev/null
|
|
for name in "$source_name" "$target_name"; do
|
|
docker run -d --name "$name" --network "$network_name" --add-host=host.docker.internal:host-gateway -e INSTANCE_NAME="$name" \
|
|
-e INSTANCE_SECRET="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("hex"))')" \
|
|
-e CONVEX_CLOUD_ORIGIN="http://$name:3210" -e CONVEX_SITE_ORIGIN="http://$name:3211" \
|
|
-p "127.0.0.1::3210" "$docker_backend_image" >/dev/null
|
|
assigned_port="$(docker port "$name" 3210/tcp | sed -E 's/.*:([0-9]+)$/\1/')"
|
|
if [[ "$name" == "$source_name" ]]; then source_port="$assigned_port"; else target_port="$assigned_port"; fi
|
|
done
|
|
docker run -d --name "$fake_name" --network "$network_name" -p "127.0.0.1::4010" \
|
|
-v "$repo:$repo:ro" -w "$app" \
|
|
node:22 node profiles/convex/backup/test-platform.mjs 4010 >"$work/platform.log" 2>&1
|
|
platform_port="$(docker port "$fake_name" 4010/tcp | sed -E 's/.*:([0-9]+)$/\1/')"
|
|
platform_host_url="http://127.0.0.1:$platform_port/api"
|
|
platform_action_url="http://$fake_name:4010/api"
|
|
|
|
wait_backend() {
|
|
local port="$1" i
|
|
for i in $(seq 1 60); do
|
|
curl -fsS "http://127.0.0.1:$port/version" >/dev/null 2>&1 && return 0
|
|
sleep 2
|
|
done
|
|
echo "Convex backend on $port did not become ready" >&2
|
|
return 1
|
|
}
|
|
wait_backend "$source_port"
|
|
wait_backend "$target_port"
|
|
source_key="$(docker exec "$source_name" ./generate_admin_key.sh 2>/dev/null)"
|
|
target_key="$(docker exec "$target_name" ./generate_admin_key.sh 2>/dev/null)"
|
|
|
|
# kit:convex-better-auth:begin
|
|
# A Better Auth app: its own Better Auth Convex deployment, a third backend from the same image. The
|
|
# deploy job deploys the Better Auth functions to it and installs its URL, admin key and namespace in
|
|
# the app's backend, whose account actions end a deleted person's Better Auth account there.
|
|
docker run -d --name "$better_auth_name" --network "$network_name" -e INSTANCE_NAME="$better_auth_name" \
|
|
-e INSTANCE_SECRET="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("hex"))')" \
|
|
-e CONVEX_CLOUD_ORIGIN="http://127.0.0.1:3210" -e CONVEX_SITE_ORIGIN="http://127.0.0.1:3211" \
|
|
-p "127.0.0.1::3210" -p "127.0.0.1::3211" "$docker_backend_image" >/dev/null
|
|
better_auth_port="$(docker port "$better_auth_name" 3210/tcp | sed -E 's/.*:([0-9]+)$/\1/')"
|
|
better_auth_site_port="$(docker port "$better_auth_name" 3211/tcp | sed -E 's/.*:([0-9]+)$/\1/')"
|
|
wait_backend "$better_auth_port"
|
|
better_auth_key="$(docker exec "$better_auth_name" ./generate_admin_key.sh 2>/dev/null | tail -n 1)"
|
|
export AUTH_PROVIDER=convex-better-auth AUTH_IDENTITY_NAMESPACE=better-auth:convex-smoke
|
|
export BETTER_AUTH_CONVEX_URL="http://127.0.0.1:$better_auth_port" BETTER_AUTH_CONVEX_ADMIN_KEY="$better_auth_key"
|
|
export BETTER_AUTH_SECRET="$(node -e 'process.stdout.write(require("node:crypto").randomBytes(32).toString("base64url"))')"
|
|
export CONUSAI_PUBLIC_URL=http://127.0.0.1:3000
|
|
# kit:convex-better-auth:end
|
|
|
|
cd "$app"
|
|
# Import the same shaped notes row the app's notes fixture exercises. Import is the supported
|
|
# Convex path for seeded documents; backup/export is independent of the function deployment.
|
|
cat > "$work/notes.jsonl" <<'JSONL'
|
|
{"id":"backup-notes-fixture","orgId":"backup-org","workspaceId":"backup-workspace","projectId":"backup-project","createdBy":"backup-test","title":"AT3.7 notes fixture","body":"restored from the Blob archive","createdAt":"2026-10-21T00:00:00.000Z","updatedAt":"2026-10-21T00:00:00.000Z","version":1}
|
|
JSONL
|
|
CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" \
|
|
CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \
|
|
pnpm exec convex import --table notes_note "$work/notes.jsonl" >/dev/null
|
|
|
|
for i in $(seq 1 30); do
|
|
curl -fsS "http://127.0.0.1:$platform_port/__e2e/health" >/dev/null 2>&1 && break
|
|
sleep 1
|
|
done
|
|
if ! curl -fsS "http://127.0.0.1:$platform_port/__e2e/health" >/dev/null; then
|
|
echo "fake platform did not become ready; log follows" >&2
|
|
cat "$work/platform.log" >&2 || true
|
|
exit 1
|
|
fi
|
|
|
|
# Deploy the generated app (including its Convex schema and component) into the source backend,
|
|
# install only test-scoped platform variables, and exercise real functions over the self-hosted
|
|
# HTTP API. This proves module codegen/deploy and the admin-auth identity boundary, beyond the
|
|
# adapter's convex-test unit suite.
|
|
# The support module's effect actions mint sessions for this channel against the fake platform.
|
|
export CONUSAI_SUPPORT_CHANNEL=convex-smoke-channel # kit:support
|
|
CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" \
|
|
CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \
|
|
CONUSAI_API_URL="$platform_action_url" CONUSAI_API_TOKEN=dev-token CONUSAI_PROJECT_ID=1 \
|
|
CONUSAI_ORG_SLUG=fixture-org \
|
|
node profiles/convex/deploy.mjs
|
|
# kit:convex-better-auth:begin
|
|
# deploy.mjs installed the Better Auth deployment's variables in the app's backend. The deploy ran on
|
|
# this host, through the published port; the backend reaches the Better Auth backend on their shared
|
|
# network instead, so its URL is the one variable set again here (in a deployment both are one URL).
|
|
[[ "$(CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \
|
|
pnpm exec convex env get AUTH_IDENTITY_NAMESPACE)" == "better-auth:convex-smoke" ]] \
|
|
|| { echo "deploy.mjs did not install the Better Auth variables in the app's backend" >&2; exit 1; }
|
|
CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \
|
|
pnpm exec convex env set BETTER_AUTH_CONVEX_URL "http://$better_auth_name:3210" >/dev/null
|
|
export SMOKE_BETTER_AUTH_SITE_URL="http://127.0.0.1:$better_auth_site_port"
|
|
export SMOKE_AUTH_IDENTITY_NAMESPACE="$AUTH_IDENTITY_NAMESPACE" SMOKE_PUBLIC_URL="$CONUSAI_PUBLIC_URL"
|
|
# kit:convex-better-auth:end
|
|
# `convex deploy` generated the app-specific API types for this local deployment. Typecheck every
|
|
# query/mutation/action against those generated contexts before exercising the HTTP functions.
|
|
pnpm exec tsc -p convex/tsconfig.json --noEmit
|
|
CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" \
|
|
CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \
|
|
CONUSAI_API_URL="$platform_host_url" \
|
|
node profiles/convex/backup/test-deployed-app.mjs
|
|
|
|
CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \
|
|
CONUSAI_API_URL="$platform_host_url" CONUSAI_API_TOKEN=dev-token \
|
|
CONUSAI_PROJECT_ID=1 CONVEX_BACKUP_RETENTION=1 CONVEX_CLI="$app/node_modules/.bin/convex" \
|
|
CONVEX_APP_DIR="$app" node profiles/convex/backup/runner.mjs --once
|
|
sleep 1
|
|
CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$source_port" CONVEX_SELF_HOSTED_ADMIN_KEY="$source_key" \
|
|
CONUSAI_API_URL="$platform_host_url" CONUSAI_API_TOKEN=dev-token \
|
|
CONUSAI_PROJECT_ID=1 CONVEX_BACKUP_RETENTION=1 CONVEX_CLI="$app/node_modules/.bin/convex" \
|
|
CONVEX_APP_DIR="$app" node profiles/convex/backup/runner.mjs --once
|
|
|
|
backup_path="$(node --input-type=module - "$platform_port" <<'JS'
|
|
import { createObjectStore } from "@conusai-kit/platform"
|
|
const port = process.argv[2]
|
|
const store = createObjectStore({ CONUSAI_API_URL: `http://127.0.0.1:${port}/api`, CONUSAI_API_TOKEN: "dev-token", CONUSAI_PROJECT_ID: "1" })
|
|
const listed = await store.list({ prefix: "convex-backups/1/" })
|
|
if (listed.blobs.length !== 1) throw new Error(`expected one backup, found ${listed.blobs.length}`)
|
|
console.log(listed.blobs[0].pathname)
|
|
JS
|
|
)"
|
|
curl -fsS -H 'Authorization: Bearer dev-token' \
|
|
"http://127.0.0.1:$platform_port/api/blob/1/$backup_path" -o "$work/backup.zip"
|
|
|
|
CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$target_port" \
|
|
CONVEX_SELF_HOSTED_ADMIN_KEY="$target_key" \
|
|
CONUSAI_API_URL="$platform_action_url" CONUSAI_API_TOKEN=dev-token CONUSAI_PROJECT_ID=1 \
|
|
CONUSAI_ORG_SLUG=fixture-org \
|
|
node profiles/convex/deploy.mjs
|
|
CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$target_port" \
|
|
CONVEX_SELF_HOSTED_ADMIN_KEY="$target_key" \
|
|
pnpm exec convex import "$work/backup.zip" >/dev/null
|
|
CONVEX_SELF_HOSTED_URL="http://127.0.0.1:$target_port" \
|
|
CONVEX_SELF_HOSTED_ADMIN_KEY="$target_key" \
|
|
pnpm exec convex export --include-file-storage --path "$work/restored.zip" >/dev/null
|
|
# Read the complete archive: an early-exiting grep can SIGPIPE unzip under pipefail
|
|
# and report a missing fixture even after finding it.
|
|
python3 - "$work/restored.zip" <<'RESTORED_PROOF'
|
|
import json, sys, zipfile
|
|
with zipfile.ZipFile(sys.argv[1]) as archive:
|
|
names = [name for name in archive.namelist() if name == "notes_note/documents.jsonl" or name.endswith("/notes_note/documents.jsonl")]
|
|
rows = [json.loads(line) for name in names for line in archive.read(name).splitlines() if line.strip()]
|
|
assert any(row.get("id") == "backup-notes-fixture" and row.get("title") == "AT3.7 notes fixture" and row.get("body") == "restored from the Blob archive" for row in rows), "restored archive lacks the exact notes fixture"
|
|
RESTORED_PROOF
|
|
echo "AT3.7 round-trip green: local Convex notes fixture -> fake Blob -> empty local Convex backend"
|