This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
#!/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"
|
||||
Reference in New Issue
Block a user