Files
template-app/profiles/convex/backup/test-roundtrip.sh
T

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"