From 6d1a5b5c8381cd6a9f9f21f3ac481f11c9312690 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Mon, 9 Nov 2026 13:45:40 +0800 Subject: [PATCH] Run pinned Endoleap headless runtime against real HTTP and PostgreSQL in CI --- .gitea/workflows/check.yml | 15 +++- deploy/check-schema.sh | 15 ++++ ...canvas_headless_client_integration_test.go | 85 +++++++++++++++++++ 3 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 internal/platform/canvas_headless_client_integration_test.go diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index 0423026..6683854 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -69,8 +69,19 @@ jobs: print("Negative control result:", control if control in known else "unclassified") sys.exit("Slack client preflight could not verify the configured credentials") PYSLACK - - name: Verify schema migration in an isolated database - run: bash deploy/check-schema.sh + - name: Fetch pinned headless HTTP integration client + env: + HEADLESS_CLIENT_REVISION: 3c5e67c03d944828d0c2e51ecadb4d2dac1c9765 + run: | + set -eu + mkdir -p .headless-client + curl --fail --silent --show-error --location \ + "https://gitea.entroia.com/Zayden-Jung/Endoleap/archive/$HEADLESS_CLIENT_REVISION.tar.gz" \ + | tar -xz --strip-components=1 -C .headless-client + test -f .headless-client/tests/headless/foundry.integration.mjs + printf 'Pinned Endoleap client: %s\n' "$HEADLESS_CLIENT_REVISION" + - name: Verify schema migration and real headless HTTP client in an isolated database + run: HEADLESS_CLIENT_SOURCE="$PWD/.headless-client" bash deploy/check-schema.sh - name: Smoke test HTTP routes env: PUBLIC_ORIGIN: https://api.example.com diff --git a/deploy/check-schema.sh b/deploy/check-schema.sh index 941baa7..629e098 100644 --- a/deploy/check-schema.sh +++ b/deploy/check-schema.sh @@ -78,3 +78,18 @@ for package in platform auth research; do -e RESEARCH_TEST_DATABASE_URL=postgres://foundry_research_fixture@127.0.0.1:5432/postgres?sslmode=disable \ --entrypoint /opt/integration-tests postgres:17-alpine -test.v done + +# Run the actual pinned Endoleap runtime against this build's authenticated HTTP +# routes and the same disposable database. Only this remote container needs Node. +if [[ -n "${HEADLESS_CLIENT_SOURCE:-}" ]]; then + test -f "$HEADLESS_CLIENT_SOURCE/tests/headless/foundry.integration.mjs" + docker run --rm --network "container:$container" --user 1000:1000 \ + --read-only --cap-drop ALL --security-opt no-new-privileges \ + --tmpfs /tmp:rw,nosuid,nodev,noexec,size=32m,mode=1777 \ + --mount "type=bind,source=$(pwd)/bin/platform-tests,target=/opt/integration-tests,readonly" \ + --mount "type=bind,source=$HEADLESS_CLIENT_SOURCE,target=/opt/headless-client,readonly" \ + -e PLATFORM_TEST_DATABASE_URL=postgres://foundry_fixture@127.0.0.1:5432/postgres?sslmode=disable \ + -e FOUNDRY_HEADLESS_SOURCE=/opt/headless-client \ + --entrypoint /opt/integration-tests node:24-bookworm-slim \ + -test.v -test.run '^TestHeadlessClientHTTPIntegration$' -test.timeout 3m +fi diff --git a/internal/platform/canvas_headless_client_integration_test.go b/internal/platform/canvas_headless_client_integration_test.go new file mode 100644 index 0000000..8e707ea --- /dev/null +++ b/internal/platform/canvas_headless_client_integration_test.go @@ -0,0 +1,85 @@ +package platform + +import ( + "context" + "crypto/rand" + "crypto/sha256" + "database/sql" + "encoding/base64" + "encoding/hex" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" +) + +// Runs the real Endoleap runtime against authenticated Foundry HTTP and PostgreSQL. +// This opt-in is enabled by the existing remote schema runner, never local builds. +func TestHeadlessClientHTTPIntegration(t *testing.T) { + source, dsn := os.Getenv("FOUNDRY_HEADLESS_SOURCE"), os.Getenv("PLATFORM_TEST_DATABASE_URL") + if source == "" { + t.Skip("requires remote headless checkout") + } + if dsn == "" { + t.Fatal("headless integration requires isolated PostgreSQL") + } + db, err := sql.Open("pgx", dsn) + if err != nil { + t.Fatal(err) + } + defer db.Close() + ctx := context.Background() + var entropy [12]byte + _, _ = rand.Read(entropy[:]) + project := "headless-" + hex.EncodeToString(entropy[:]) + defer func() { + for _, table := range []string{"canvas_model_operations", "canvas_model_identity_map", "canvas_model_requests", "canvas_model_projects", "canvas_asset_bindings", "canvas_subgraphs", "workspaces", "project_accounts"} { + _, _ = db.ExecContext(ctx, "DELETE FROM "+table+" WHERE project=$1", project) + } + }() + var account, workspace, root string + if err = db.QueryRowContext(ctx, `INSERT INTO project_accounts(project,id) VALUES($1,gen_random_uuid()) RETURNING id::text`, project).Scan(&account); err != nil { + t.Fatal(err) + } + var tokenBytes [32]byte + _, _ = rand.Read(tokenBytes[:]) + token := base64.RawURLEncoding.EncodeToString(tokenBytes[:]) + hash := sha256.Sum256([]byte(token)) + if _, err = db.ExecContext(ctx, `INSERT INTO project_sessions(token_hash,project,account_id,expires_at) VALUES($1,$2,$3,now()+interval '1 hour')`, base64.RawURLEncoding.EncodeToString(hash[:]), project, account); err != nil { + t.Fatal(err) + } + if err = db.QueryRowContext(ctx, `INSERT INTO workspaces(project,name,owner_id) VALUES($1,'Headless integration',$2) RETURNING id::text`, project, account).Scan(&workspace); err != nil { + t.Fatal(err) + } + if _, err = db.ExecContext(ctx, `INSERT INTO workspace_members(project,workspace_id,account_id,role) VALUES($1,$2,$3,'admin')`, project, workspace, account); err != nil { + t.Fatal(err) + } + legacy := json.RawMessage(`{"schema_version":1,"nodes":[` + modelLegacyFrame + `,{"id":"a","type":"plainText","position":{"x":150,"y":260},"width":240,"height":72,"data":{"kind":"plain-text","title":"a","text":"preserved","frameId":"f"}}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}}`) + if err = db.QueryRowContext(ctx, `INSERT INTO materials(project,workspace_id,kind,title,created_by,canvas_document) VALUES($1,$2,'board','Root',$3,$4) RETURNING id::text`, project, workspace, account, legacy).Scan(&root); err != nil { + t.Fatal(err) + } + mux := http.NewServeMux() + Register(mux, db, map[string]string{project: "https://app.example.com/auth/callback"}, "https://api.example.com", nil) + + server := httptest.NewServer(mux) + defer server.Close() + commandContext, cancel := context.WithTimeout(ctx, 2*time.Minute) + defer cancel() + command := exec.CommandContext(commandContext, "node", filepath.Join(source, "tests/headless/foundry.integration.mjs")) + command.Env = append(os.Environ(), "FOUNDRY_HTTP_URL="+server.URL, "FOUNDRY_TEST_PROJECT="+project, "FOUNDRY_TEST_ACCOUNT="+account, "FOUNDRY_TEST_WORKSPACE="+workspace, "FOUNDRY_TEST_ROOT="+root, "FOUNDRY_TEST_TOKEN="+token) + output, err := command.CombinedOutput() + // The fixture token is ephemeral, but never print it even on client failures. + report := strings.ReplaceAll(string(output), token, "[fixture token redacted]") + if err != nil { + t.Fatalf("headless HTTP client failed: %v\n%s", err, report) + } + if !strings.Contains(report, `"status":"ok"`) { + t.Fatalf("headless HTTP client did not return success: %s", report) + } + t.Log(report) +}