From 92432140fcb5da91275859cc59b39be00bdd4e56 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 18:10:46 +0000 Subject: [PATCH 1/8] test(cli): add live test category and harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a `live` Vitest project (`*.live.test.ts`) for black-box tests that run the built CLI against a real platform — a supabox stack in CI (supabase/cli-e2e-ci) — exercising real Management API, functions, database, and storage flows. - `tests/helpers/live.ts`: `runSupabaseLive` (retargets via SUPABASE_PROFILE=supabase-local), `describeLive` gate, readiness helpers. - `tests/live-global-setup.ts`: fail fast if the platform is unreachable when the live env is configured; no-op otherwise. - vitest `live` project + nx `test:live` target (auto-derived from the project name), `pnpm test:live` script, knip entry, coverage exclude. - Gated by SUPABASE_ACCESS_TOKEN so the suite is inert in the normal unit/integration/e2e loop. - Seed the canonical example: `orgs list` authenticated read-only smoke. Refs CLI-1834 (parent CLI-1825). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RNpmVr6SxaFQHbJSBYLUCj --- apps/cli/package.json | 4 +- .../commands/orgs/list/list.live.test.ts | 24 +++++++ apps/cli/tests/helpers/live.ts | 68 +++++++++++++++++++ apps/cli/tests/live-global-setup.ts | 34 ++++++++++ apps/cli/vitest.config.ts | 16 +++++ nx.json | 11 +++ 6 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 apps/cli/src/legacy/commands/orgs/list/list.live.test.ts create mode 100644 apps/cli/tests/helpers/live.ts create mode 100644 apps/cli/tests/live-global-setup.ts diff --git a/apps/cli/package.json b/apps/cli/package.json index 0ea70e83f2..2548b1eeb3 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -33,6 +33,7 @@ "dev:legacy": "pnpm exec bun src/legacy/main.ts", "test": "nx run-many -t test:core test:e2e --projects=$npm_package_name", "test:core": "nx run-many -t test:unit test:integration --projects=$npm_package_name --coverage.enabled", + "test:live": "nx run-many -t test:live --projects=$npm_package_name", "test:smoke": "bun run tests/smoke-test.ts", "check:all": "nx run-many -t types:check lint:check fmt:check knip:check --projects=$npm_package_name", "fix:all": "nx run-many -t lint:fix fmt:fix knip:fix --projects=$npm_package_name" @@ -107,7 +108,8 @@ "entry": [ "src/shared/cli/bin.ts", "src/**/*.test.ts", - "src/**/*.e2e.test.ts" + "src/**/*.e2e.test.ts", + "src/**/*.live.test.ts" ], "ignore": [ "scripts/*.ts", diff --git a/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts b/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts new file mode 100644 index 0000000000..367e75d56f --- /dev/null +++ b/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from "vitest"; +import { describeLive, runSupabaseLive } from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 60_000; + +// Harness smoke for the `live` Vitest project: the canonical example of a live +// test. It exercises the full path — built binary → SUPABASE_PROFILE resolution +// → authenticated Management API request against the running platform — with a +// read-only call, so it is safe to run repeatedly and creates no resources. +// +// Gated by `describeLive`: skipped unless SUPABASE_ACCESS_TOKEN is set (the +// cli-e2e-ci runner provides supabox's seeded PAT). Broader lifecycle scenarios +// (projects, functions, branching, db, storage) build on this same harness. +describeLive("supabase orgs list (live)", () => { + test( + "lists organizations for the authenticated token", + { timeout: LIVE_TIMEOUT_MS }, + async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive(["orgs", "list"]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }, + ); +}); diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts new file mode 100644 index 0000000000..928d753aec --- /dev/null +++ b/apps/cli/tests/helpers/live.ts @@ -0,0 +1,68 @@ +import { describe } from "vitest"; +import { runSupabase } from "./cli.ts"; + +/** + * Helpers for the `live` Vitest project (`*.live.test.ts`): black-box CLI + * subprocess tests that run against a *real* Supabase platform — in CI that is + * a local supabox stack (see the `supabase/cli-e2e-ci` harness). + * + * Unlike `*.e2e.test.ts`, which use fake tokens and assert error/golden-path + * surface behavior, live tests exercise real Management API, edge function, + * database, and storage flows end to end. They are gated off by default and + * only run when a live access token is present in the environment, so the + * normal unit/integration/e2e loop never touches a network. + * + * Environment contract (provided by the cli-e2e-ci runner): + * - `SUPABASE_ACCESS_TOKEN` — required; the platform PAT (supabox seeds a + * deterministic `sbp_…` token into its mgmt-api database). + * - `SUPABASE_PROFILE` — selects the API base URL; defaults to `supabase-local` + * (→ `http://localhost:8080`, `project_host: supabase.red`). Note the cli does + * NOT honor `SUPABASE_API_URL` (Go parity) — the profile is the override. + * - `SUPABASE_LIVE_API_URL` — base URL the readiness check probes; defaults to + * `http://localhost:8080`. + * - `NODE_EXTRA_CA_CERTS` — trusts the supabox CA for `*.supabase.red` TLS; + * inherited by the subprocess via the parent environment. + */ + +/** Default profile for the host runner: api_url → localhost:8080, project_host → supabase.red. */ +export const LIVE_DEFAULT_PROFILE = "supabase-local"; + +/** Management API base URL probed by the live readiness check. */ +export function liveApiBaseUrl(): string { + return process.env["SUPABASE_LIVE_API_URL"] ?? "http://localhost:8080"; +} + +/** + * True when the environment carries a platform access token, i.e. the live + * suite is expected to run. Used to gate `describeLive` so live tests are inert + * in the default test loop. + */ +export function isLiveConfigured(): boolean { + return Boolean(process.env["SUPABASE_ACCESS_TOKEN"]); +} + +/** + * `describe` that runs only when the live environment is configured. Use this + * for every live suite so the file is inert (skipped, not failed) outside the + * cli-e2e-ci runner. + */ +export const describeLive = describe.skipIf(!isLiveConfigured()); + +/** + * Spawn the built CLI against the live platform, injecting the profile so the + * Management API base resolves to the stack. Defaults to the `legacy` shell, + * which hosts the platform commands (orgs, projects, branches, functions, …). + */ +export function runSupabaseLive( + args: string[], + options?: Parameters[1], +): ReturnType { + return runSupabase(args, { + entrypoint: "legacy", + ...options, + env: { + SUPABASE_PROFILE: process.env["SUPABASE_PROFILE"] ?? LIVE_DEFAULT_PROFILE, + ...options?.env, + }, + }); +} diff --git a/apps/cli/tests/live-global-setup.ts b/apps/cli/tests/live-global-setup.ts new file mode 100644 index 0000000000..8247616ec9 --- /dev/null +++ b/apps/cli/tests/live-global-setup.ts @@ -0,0 +1,34 @@ +import { isLiveConfigured, liveApiBaseUrl } from "./helpers/live.ts"; + +/** + * Global setup for the `live` Vitest project. When the live environment is not + * configured the suite is skipped (via `describeLive`) and this is a no-op. + * + * When it IS configured (the cli-e2e-ci runner sets `SUPABASE_ACCESS_TOKEN`), + * fail fast with a clear message if the platform is unreachable, so a + * misconfigured stack surfaces as a setup error rather than dozens of opaque + * per-test timeouts. + */ +export async function setup(): Promise { + if (!isLiveConfigured()) { + return; + } + + const healthUrl = `${liveApiBaseUrl()}/v1/health`; + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30_000); + try { + const response = await fetch(healthUrl, { signal: controller.signal }); + if (!response.ok) { + throw new Error(`${healthUrl} responded with ${response.status}`); + } + } catch (error) { + const reason = error instanceof Error ? error.message : String(error); + throw new Error( + `Live platform is not reachable at ${healthUrl}: ${reason}.\n` + + "Ensure the supabox stack is up and the host can reach mgmt-api (see cli-e2e-ci).", + ); + } finally { + clearTimeout(timeout); + } +} diff --git a/apps/cli/vitest.config.ts b/apps/cli/vitest.config.ts index 412bff1fc1..a8c53dd4e5 100644 --- a/apps/cli/vitest.config.ts +++ b/apps/cli/vitest.config.ts @@ -31,6 +31,7 @@ export default defineConfig({ "**/*.unit.test.ts", "**/*.integration.test.ts", "**/*.e2e.test.ts", + "**/*.live.test.ts", "**/*.command.ts", "src/app.ts", "src/bin.ts", @@ -66,6 +67,21 @@ export default defineConfig({ hookTimeout: 120_000, }, }, + { + plugins: [dockerfileTextPlugin()], + test: { + // Live tests run against a real platform (a supabox stack in CI) and + // are gated by `describeLive`, so they are inert unless the live env + // is configured. Never part of the default unit/integration/e2e loop. + name: "live", + include: ["**/*.live.test.ts"], + fileParallelism: false, + maxWorkers: 1, + globalSetup: ["tests/live-global-setup.ts"], + testTimeout: 300_000, + hookTimeout: 300_000, + }, + }, ], }, }); diff --git a/nx.json b/nx.json index 7a0dabb5fe..128d1740b7 100644 --- a/nx.json +++ b/nx.json @@ -75,6 +75,17 @@ "{projectRoot}/dist/**/*" ] }, + "test:live": { + "parallelism": false, + "cache": false, + "dependsOn": [ + "build" + ], + "inputs": [ + "default", + "{projectRoot}/dist/**/*" + ] + }, "dev": { "cache": false } From 8a52b767dd501c4215b4c9db89b516fafff162ae Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Jun 2026 14:14:43 +0200 Subject: [PATCH 2/8] test(cli): fix live test:live recursion and readiness probe Two defects surfaced running the live suite against a real supabox stack: - The `test:live` package script (`nx run-many -t test:live`) shadowed the nx-plugin `test:live` target and recursed into itself forever. Remove it; the nx target (auto-derived from the `live` Vitest project, `dependsOn: build`) is the single source of truth, invoked via nx like test:unit/integration/e2e, which have no package scripts either. - The global-setup readiness probe required a 2xx from `/v1/health`, but supabox's mgmt-api has no public health route (`/v1/health` 404s; an unauthenticated request is rejected by the auth middleware with 401). Make it a pure reachability gate: any HTTP response from `/v1/organizations` proves the API is up and routing; functional/auth coverage stays in the live tests. Validated end-to-end: the `orgs list` live smoke passes against a real supabox control plane. Co-Authored-By: Claude Opus 4.8 --- apps/cli/package.json | 1 - apps/cli/tests/live-global-setup.ts | 15 +++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/cli/package.json b/apps/cli/package.json index 2548b1eeb3..835a2d3dde 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -33,7 +33,6 @@ "dev:legacy": "pnpm exec bun src/legacy/main.ts", "test": "nx run-many -t test:core test:e2e --projects=$npm_package_name", "test:core": "nx run-many -t test:unit test:integration --projects=$npm_package_name --coverage.enabled", - "test:live": "nx run-many -t test:live --projects=$npm_package_name", "test:smoke": "bun run tests/smoke-test.ts", "check:all": "nx run-many -t types:check lint:check fmt:check knip:check --projects=$npm_package_name", "fix:all": "nx run-many -t lint:fix fmt:fix knip:fix --projects=$npm_package_name" diff --git a/apps/cli/tests/live-global-setup.ts b/apps/cli/tests/live-global-setup.ts index 8247616ec9..331f7982a9 100644 --- a/apps/cli/tests/live-global-setup.ts +++ b/apps/cli/tests/live-global-setup.ts @@ -14,18 +14,21 @@ export async function setup(): Promise { return; } - const healthUrl = `${liveApiBaseUrl()}/v1/health`; + // Reachability gate only. Any HTTP response — including 401/404 — proves the + // Management API is up and routing, which is all this probe needs to assert. + // supabox's mgmt-api requires auth on every route and exposes no public health + // endpoint (`/v1/health` 404s; an unauthenticated request is rejected by the + // auth middleware with 401), so we deliberately do NOT require a 2xx here. + // Functional and auth coverage is the live tests' job (e.g. `orgs list`). + const probeUrl = `${liveApiBaseUrl()}/v1/organizations`; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30_000); try { - const response = await fetch(healthUrl, { signal: controller.signal }); - if (!response.ok) { - throw new Error(`${healthUrl} responded with ${response.status}`); - } + await fetch(probeUrl, { signal: controller.signal }); } catch (error) { const reason = error instanceof Error ? error.message : String(error); throw new Error( - `Live platform is not reachable at ${healthUrl}: ${reason}.\n` + + `Live platform is not reachable at ${probeUrl}: ${reason}.\n` + "Ensure the supabox stack is up and the host can reach mgmt-api (see cli-e2e-ci).", ); } finally { From 060f9af496789fff6d742589d3e16a36fbc92882 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Jun 2026 16:20:49 +0200 Subject: [PATCH 3/8] ci: dispatch cli-e2e-ci live run on labeled PRs Adds a sender workflow that, on PRs labeled `run-live-e2e-ci`, fires a repository_dispatch (type: cli-pr) to supabase/cli-e2e-ci carrying this PR's head SHA, so the supabox-backed `test:live` suite runs against the PR's cli. Opt-in by label to keep the expensive full-stack run off every PR; cli-e2e-ci reports a `cli-e2e-ci / live` commit status back onto the head SHA. Distinct from live-e2e.yml (the staging cli-e2e package suite). Part of CLI-1831 (epic CLI-1825). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/dispatch-cli-e2e-ci.yml | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/dispatch-cli-e2e-ci.yml diff --git a/.github/workflows/dispatch-cli-e2e-ci.yml b/.github/workflows/dispatch-cli-e2e-ci.yml new file mode 100644 index 0000000000..718c66259e --- /dev/null +++ b/.github/workflows/dispatch-cli-e2e-ci.yml @@ -0,0 +1,52 @@ +name: Dispatch cli-e2e-ci + +# Asks the supabase/cli-e2e-ci harness to run the cli `test:live` suite against +# a full supabox stack, built from THIS PR's head commit (CLI-1825 / CLI-1831). +# +# This is distinct from `live-e2e.yml`, which runs the cli-e2e package against +# real staging (api.supabase.green). Here the suite runs against a local supabox +# stack stood up inside the private cli-e2e-ci repo; we only fire the trigger and +# pass our head SHA — cli-e2e-ci checks that SHA out into its `cli` submodule. +# +# Opt-in by label to keep the expensive full-stack run off every PR: add the +# `run-live-e2e-ci` label (re-dispatches on each subsequent push while labeled). +# cli-e2e-ci reports a `cli-e2e-ci / live` commit status back onto the head SHA. +# +# Fork PRs cannot dispatch (no access to the App secret); run cli-e2e-ci's own +# workflow_dispatch with `cli_ref` for those. +on: + pull_request: + types: [labeled, synchronize, reopened] + +permissions: + contents: read + +jobs: + dispatch: + if: contains(github.event.pull_request.labels.*.name, 'run-live-e2e-ci') + runs-on: ubuntu-latest + steps: + # App token scoped to cli-e2e-ci with contents:write — the + # repository_dispatch REST endpoint requires write on the target repo. + - name: Create GitHub App token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.GH_APP_CLIENT_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + owner: supabase + repositories: cli-e2e-ci + permission-contents: write + + - name: Dispatch live run to cli-e2e-ci + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + CLI_SHA: ${{ github.event.pull_request.head.sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + echo "Dispatching cli-e2e-ci live run for PR #${PR_NUMBER} @ ${CLI_SHA}" + # Build the nested client_payload with jq — `gh api -f` sends a flat + # body and would not nest `client_payload.*` correctly. + jq -n --arg sha "$CLI_SHA" --argjson pr "$PR_NUMBER" \ + '{event_type: "cli-pr", client_payload: {cli_sha: $sha, pr_number: $pr}}' \ + | gh api -X POST repos/supabase/cli-e2e-ci/dispatches --input - From 26045d9cf1f1255e39ae5f651aa577f52e558489 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Jun 2026 16:56:55 +0200 Subject: [PATCH 4/8] =?UTF-8?q?test(cli):=20expand=20live=20scenarios=20?= =?UTF-8?q?=E2=80=94=20projects=20list=20+=20project-scoped=20gate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLI-1834 follow-up: - `projects list` live scenario (account-level, read-only) plus a JSON-output assertion. Validated green against a real supabox control plane. - `describeLiveProject` / `liveProjectRef` / `requireLiveProjectRef` harness gate for project-scoped suites: skip unless SUPABASE_LIVE_PROJECT_REF is set (a provisioned project), so a control-plane-only stack (e.g. local macOS) skips rather than fails them. - `functions list` and `branches list` project-scoped scenarios behind that gate — the entry points for the edge-functions and branching lifecycle coverage, which need a provisioned project on the full stack. Co-Authored-By: Claude Opus 4.8 --- .../commands/branches/list/list.live.test.ts | 30 ++++++++++++++++ .../commands/functions/list/list.live.test.ts | 31 ++++++++++++++++ .../commands/projects/list/list.live.test.ts | 33 +++++++++++++++++ apps/cli/tests/helpers/live.ts | 35 +++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 apps/cli/src/legacy/commands/branches/list/list.live.test.ts create mode 100644 apps/cli/src/legacy/commands/functions/list/list.live.test.ts create mode 100644 apps/cli/src/legacy/commands/projects/list/list.live.test.ts diff --git a/apps/cli/src/legacy/commands/branches/list/list.live.test.ts b/apps/cli/src/legacy/commands/branches/list/list.live.test.ts new file mode 100644 index 0000000000..65422ad51b --- /dev/null +++ b/apps/cli/src/legacy/commands/branches/list/list.live.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from "vitest"; + +import { + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set — i.e. a project has been provisioned on the stack (the cli-e2e-ci runner +// does this; a control-plane-only stack, like local macOS, skips it). +// +// Entry point for the branching lifecycle tracked in CLI-1834 +// (create / switch / delete) — extend here once a provisioned project is +// available on the full stack. +describeLiveProject("supabase branches list (live)", () => { + test("lists branches for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "branches", + "list", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }); +}); diff --git a/apps/cli/src/legacy/commands/functions/list/list.live.test.ts b/apps/cli/src/legacy/commands/functions/list/list.live.test.ts new file mode 100644 index 0000000000..4d39948126 --- /dev/null +++ b/apps/cli/src/legacy/commands/functions/list/list.live.test.ts @@ -0,0 +1,31 @@ +import { expect, test } from "vitest"; + +import { + describeLiveProject, + requireLiveProjectRef, + runSupabaseLive, +} from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 120_000; + +// Project-scoped read-only scenario. Skipped unless SUPABASE_LIVE_PROJECT_REF is +// set — i.e. a project has been provisioned on the stack (the cli-e2e-ci runner +// does this; a control-plane-only stack, like local macOS, skips it). +// +// This is the entry point for the broader edge-functions coverage tracked in +// CLI-1834 (deploy + invoke over :443 / {ref}.supabase.red), which needs the +// project's gateway reachable from the host — author those here as they become +// runnable on the full stack. +describeLiveProject("supabase functions list (live)", () => { + test("lists edge functions for the project", { timeout: LIVE_TIMEOUT_MS }, async () => { + const ref = requireLiveProjectRef(); + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "functions", + "list", + "--project-ref", + ref, + ]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }); +}); diff --git a/apps/cli/src/legacy/commands/projects/list/list.live.test.ts b/apps/cli/src/legacy/commands/projects/list/list.live.test.ts new file mode 100644 index 0000000000..8c4ca20f33 --- /dev/null +++ b/apps/cli/src/legacy/commands/projects/list/list.live.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from "vitest"; + +import { describeLive, runSupabaseLive } from "../../../../../tests/helpers/live.ts"; + +const LIVE_TIMEOUT_MS = 60_000; + +// Account-level read-only live scenario, alongside `orgs list`. Lists every +// project the authenticated token can access — no project ref required, so it +// runs against just the control plane (no provisioned project instance needed). +// Safe to run repeatedly; creates nothing. +describeLive("supabase projects list (live)", () => { + test("lists projects for the authenticated token", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive(["projects", "list"]); + expect(`${stdout}${stderr}`).not.toContain("Unauthorized"); + expect(exitCode).toBe(0); + }); + + test( + "emits machine-readable JSON with --output-format json", + { timeout: LIVE_TIMEOUT_MS }, + async () => { + const { exitCode, stdout } = await runSupabaseLive([ + "projects", + "list", + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // stdout must be payload-only valid JSON in json mode (no spinner/log noise). + expect(() => JSON.parse(stdout)).not.toThrow(); + }, + ); +}); diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts index 928d753aec..2ab7f09420 100644 --- a/apps/cli/tests/helpers/live.ts +++ b/apps/cli/tests/helpers/live.ts @@ -48,6 +48,41 @@ export function isLiveConfigured(): boolean { */ export const describeLive = describe.skipIf(!isLiveConfigured()); +/** + * Project ref for project-scoped live scenarios (functions, branches, db, + * storage, …). The cli-e2e-ci runner sets this once a project has been + * provisioned on the stack; absent → those suites skip. Returns `undefined` + * when unset so callers can branch; use `requireLiveProjectRef` inside a + * `describeLiveProject` block where presence is already guaranteed. + */ +export function liveProjectRef(): string | undefined { + return process.env["SUPABASE_LIVE_PROJECT_REF"]; +} + +/** + * The live project ref, or a thrown error if unset. Safe to call inside a + * `describeLiveProject` block (the gate guarantees it is present) and gives a + * typed `string` without a non-null assertion. + */ +export function requireLiveProjectRef(): string { + const ref = liveProjectRef(); + if (!ref) { + throw new Error( + "SUPABASE_LIVE_PROJECT_REF must be set for project-scoped live tests " + + "(the cli-e2e-ci runner sets it after provisioning a project).", + ); + } + return ref; +} + +/** + * `describe` for project-scoped live suites: runs only when the live env is + * configured AND a project ref is available. On a control-plane-only stack + * (e.g. local macOS where project instances can't be built) these skip rather + * than fail. See `requireLiveProjectRef`. + */ +export const describeLiveProject = describe.skipIf(!isLiveConfigured() || !liveProjectRef()); + /** * Spawn the built CLI against the live platform, injecting the profile so the * Management API base resolves to the stack. Defaults to the `legacy` shell, From 0cff26dfe7d5383f7fbd1d2643ea77a0458e3432 Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Jun 2026 17:42:33 +0200 Subject: [PATCH 5/8] =?UTF-8?q?test(cli):=20add=20negative-auth=20live=20s?= =?UTF-8?q?cenario=20(invalid=20token=20=E2=86=92=20Unauthorized)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bad token must round-trip to the real Management API, come back 401, and surface as a non-zero exit carrying the upstream "Unauthorized" message — exercising the cli's auth + error mapping against the live stack, not just the golden path. Validated green against a real supabox control plane. Refs CLI-1834. Co-Authored-By: Claude Opus 4.8 --- .../src/legacy/commands/orgs/list/list.live.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts b/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts index 367e75d56f..a7cd8035d1 100644 --- a/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts +++ b/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts @@ -21,4 +21,16 @@ describeLive("supabase orgs list (live)", () => { expect(exitCode).toBe(0); }, ); + + // Negative path: a bad token must round-trip to the real Management API, come + // back 401, and surface as a non-zero exit with the upstream "Unauthorized" + // message — i.e. the cli's auth + error mapping work against the live stack, + // not just the golden path. Overrides only the token (profile stays set). + test("fails with Unauthorized for an invalid token", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive(["orgs", "list"], { + env: { SUPABASE_ACCESS_TOKEN: `sbp_${"0".repeat(40)}` }, + }); + expect(exitCode).not.toBe(0); + expect(`${stdout}${stderr}`).toContain("Unauthorized"); + }); }); From f3ef0579deb4b1ba2b541bdac8e166684800b34a Mon Sep 17 00:00:00 2001 From: avallete Date: Wed, 24 Jun 2026 18:23:26 +0200 Subject: [PATCH 6/8] test(cli): add orgs-list JSON + unknown-project-ref live scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit More live coverage validatable on a control-plane-only stack: - `orgs list --output-format json`: stdout is payload-only parseable JSON (machine-output parity with `projects list`). - `functions list --project-ref `: a well-formed but nonexistent ref round-trips to the live Management API, returns 404, and surfaces as a non-zero exit (not a crash, not "Unauthorized") — exercises the `--project-ref` request path + error mapping without a provisioned project, so it runs under `describeLive` rather than `describeLiveProject`. Validated green against a real supabox control plane (6 passed, 2 skipped). Refs CLI-1834. Co-Authored-By: Claude Opus 4.8 --- .../commands/functions/list/list.live.test.ts | 21 +++++++++++++++++++ .../commands/orgs/list/list.live.test.ts | 16 ++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/apps/cli/src/legacy/commands/functions/list/list.live.test.ts b/apps/cli/src/legacy/commands/functions/list/list.live.test.ts index 4d39948126..2bfa93b86f 100644 --- a/apps/cli/src/legacy/commands/functions/list/list.live.test.ts +++ b/apps/cli/src/legacy/commands/functions/list/list.live.test.ts @@ -1,6 +1,7 @@ import { expect, test } from "vitest"; import { + describeLive, describeLiveProject, requireLiveProjectRef, runSupabaseLive, @@ -29,3 +30,23 @@ describeLiveProject("supabase functions list (live)", () => { expect(exitCode).toBe(0); }); }); + +// Project-scoped error path that needs NO provisioned project: a valid token +// with an unknown `--project-ref` must reach the live Management API, come back +// 404, and surface as a non-zero exit (not a crash, not "Unauthorized"). This +// exercises the `--project-ref` request path + error mapping on a control-plane- +// only stack, so it runs under `describeLive`, not `describeLiveProject`. +describeLive("supabase functions list — unknown project (live)", () => { + test("fails with a 404 for an unknown project ref", { timeout: LIVE_TIMEOUT_MS }, async () => { + const { exitCode, stdout, stderr } = await runSupabaseLive([ + "functions", + "list", + "--project-ref", + "a".repeat(20), // well-formed (20 lowercase chars) but nonexistent ref + ]); + const out = `${stdout}${stderr}`; + expect(exitCode).not.toBe(0); + expect(out).not.toContain("Unauthorized"); + expect(out).toContain("404"); + }); +}); diff --git a/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts b/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts index a7cd8035d1..515e8a855d 100644 --- a/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts +++ b/apps/cli/src/legacy/commands/orgs/list/list.live.test.ts @@ -22,6 +22,22 @@ describeLive("supabase orgs list (live)", () => { }, ); + test( + "emits machine-readable JSON with --output-format json", + { timeout: LIVE_TIMEOUT_MS }, + async () => { + const { exitCode, stdout } = await runSupabaseLive([ + "orgs", + "list", + "--output-format", + "json", + ]); + expect(exitCode).toBe(0); + // stdout must be payload-only valid JSON in json mode (no spinner/log noise). + expect(() => JSON.parse(stdout)).not.toThrow(); + }, + ); + // Negative path: a bad token must round-trip to the real Management API, come // back 401, and surface as a non-zero exit with the upstream "Unauthorized" // message — i.e. the cli's auth + error mapping work against the live stack, From b55d0018a776f4836ce000ad5031fa160820948f Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 25 Jun 2026 18:38:39 +0200 Subject: [PATCH 7/8] chore(cli): address Codex review on the live suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dispatch-cli-e2e-ci.yml: guard the dispatch job on head.repo == base repo so a labeled fork PR (which doesn't receive secrets) skips cleanly instead of red-checking on the App-token step. - runSupabaseLive: default exitTimeoutMs to 240s (LIVE_EXIT_TIMEOUT_MS) so a slow-but-valid supabox call isn't killed by runSupabase's 60s default before the live tests' own (60–120s) timeouts fire; callers can still override. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/dispatch-cli-e2e-ci.yml | 7 ++++++- apps/cli/tests/helpers/live.ts | 9 +++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dispatch-cli-e2e-ci.yml b/.github/workflows/dispatch-cli-e2e-ci.yml index 718c66259e..4dfecad569 100644 --- a/.github/workflows/dispatch-cli-e2e-ci.yml +++ b/.github/workflows/dispatch-cli-e2e-ci.yml @@ -23,7 +23,12 @@ permissions: jobs: dispatch: - if: contains(github.event.pull_request.labels.*.name, 'run-live-e2e-ci') + # Same-repo PRs only: fork PRs don't receive secrets (GH_APP_PRIVATE_KEY), so + # the App-token step would fail and leave a red check. Skip them cleanly — + # fork PRs use cli-e2e-ci's workflow_dispatch with `cli_ref` instead. + if: >- + contains(github.event.pull_request.labels.*.name, 'run-live-e2e-ci') + && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest steps: # App token scoped to cli-e2e-ci with contents:write — the diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts index 2ab7f09420..ad69636235 100644 --- a/apps/cli/tests/helpers/live.ts +++ b/apps/cli/tests/helpers/live.ts @@ -27,6 +27,14 @@ import { runSupabase } from "./cli.ts"; /** Default profile for the host runner: api_url → localhost:8080, project_host → supabase.red. */ export const LIVE_DEFAULT_PROFILE = "supabase-local"; +/** + * Default subprocess exit timeout for live runs. `runSupabase` otherwise caps at + * 60s, which would kill a slow-but-valid supabox call before the live tests' + * own (60–120s+) timeouts fire. Generous, but under the `live` project's 300s + * cap so the per-test timeout stays the real gate. Callers may override. + */ +export const LIVE_EXIT_TIMEOUT_MS = 240_000; + /** Management API base URL probed by the live readiness check. */ export function liveApiBaseUrl(): string { return process.env["SUPABASE_LIVE_API_URL"] ?? "http://localhost:8080"; @@ -95,6 +103,7 @@ export function runSupabaseLive( return runSupabase(args, { entrypoint: "legacy", ...options, + exitTimeoutMs: options?.exitTimeoutMs ?? LIVE_EXIT_TIMEOUT_MS, env: { SUPABASE_PROFILE: process.env["SUPABASE_PROFILE"] ?? LIVE_DEFAULT_PROFILE, ...options?.env, From 2e84e5ec686fad7e7ee5b624120278af6c5a6b58 Mon Sep 17 00:00:00 2001 From: avallete Date: Thu, 25 Jun 2026 18:54:42 +0200 Subject: [PATCH 8/8] chore(cli): keep Vitest APIs out of the live globalSetup Split the env-only live helpers (isLiveConfigured, liveApiBaseUrl, project-ref accessors, profile/timeout constants) into tests/helpers/live-env.ts, which imports no Vitest test APIs. globalSetup now imports from there instead of helpers/live.ts (which pulls in `describe`), avoiding evaluating Vitest test APIs in the globalSetup context. live.ts re-exports the env helpers so *.live.test.ts keep a single import site. Validated against a real supabox stack: 8 passed, 0 skipped. Co-Authored-By: Claude Opus 4.8 --- apps/cli/tests/helpers/live-env.ts | 73 ++++++++++++++++++++++ apps/cli/tests/helpers/live.ts | 94 ++++++++--------------------- apps/cli/tests/live-global-setup.ts | 4 +- 3 files changed, 100 insertions(+), 71 deletions(-) create mode 100644 apps/cli/tests/helpers/live-env.ts diff --git a/apps/cli/tests/helpers/live-env.ts b/apps/cli/tests/helpers/live-env.ts new file mode 100644 index 0000000000..f6705d71e0 --- /dev/null +++ b/apps/cli/tests/helpers/live-env.ts @@ -0,0 +1,73 @@ +/** + * Environment-only helpers for the `live` Vitest project, with **no Vitest test + * APIs imported**. Vitest evaluates `globalSetup` (live-global-setup.ts) in a + * separate context before the test workers, where importing `describe`/`test` + * is not valid — so the global setup imports the env helpers from here, while + * the test-facing pieces (`describeLive`, `runSupabaseLive`, …) live in + * `live.ts` and re-export these. + * + * Environment contract (provided by the cli-e2e-ci runner): + * - `SUPABASE_ACCESS_TOKEN` — required; the platform PAT (supabox seeds a + * deterministic `sbp_…` token into its mgmt-api database). + * - `SUPABASE_PROFILE` — selects the API base URL; defaults to `supabase-local` + * (→ `http://localhost:8080`, `project_host: supabase.red`). Note the cli does + * NOT honor `SUPABASE_API_URL` (Go parity) — the profile is the override. + * - `SUPABASE_LIVE_API_URL` — base URL the readiness check probes; defaults to + * `http://localhost:8080`. + * - `SUPABASE_LIVE_PROJECT_REF` — a provisioned project; gates project-scoped + * suites (functions, branches, db, storage). + * - `NODE_EXTRA_CA_CERTS` — trusts the supabox CA for `*.supabase.red` TLS; + * inherited by the subprocess via the parent environment. + */ + +/** Default profile for the host runner: api_url → localhost:8080, project_host → supabase.red. */ +export const LIVE_DEFAULT_PROFILE = "supabase-local"; + +/** + * Default subprocess exit timeout for live runs. `runSupabase` otherwise caps at + * 60s, which would kill a slow-but-valid supabox call before the live tests' + * own (60–120s+) timeouts fire. Generous, but under the `live` project's 300s + * cap so the per-test timeout stays the real gate. Callers may override. + */ +export const LIVE_EXIT_TIMEOUT_MS = 240_000; + +/** Management API base URL probed by the live readiness check. */ +export function liveApiBaseUrl(): string { + return process.env["SUPABASE_LIVE_API_URL"] ?? "http://localhost:8080"; +} + +/** + * True when the environment carries a platform access token, i.e. the live + * suite is expected to run. Used to gate `describeLive` so live tests are inert + * in the default test loop. + */ +export function isLiveConfigured(): boolean { + return Boolean(process.env["SUPABASE_ACCESS_TOKEN"]); +} + +/** + * Project ref for project-scoped live scenarios (functions, branches, db, + * storage, …). The cli-e2e-ci runner sets this once a project has been + * provisioned on the stack; absent → those suites skip. Returns `undefined` + * when unset so callers can branch; use `requireLiveProjectRef` inside a + * `describeLiveProject` block where presence is already guaranteed. + */ +export function liveProjectRef(): string | undefined { + return process.env["SUPABASE_LIVE_PROJECT_REF"]; +} + +/** + * The live project ref, or a thrown error if unset. Safe to call inside a + * `describeLiveProject` block (the gate guarantees it is present) and gives a + * typed `string` without a non-null assertion. + */ +export function requireLiveProjectRef(): string { + const ref = liveProjectRef(); + if (!ref) { + throw new Error( + "SUPABASE_LIVE_PROJECT_REF must be set for project-scoped live tests " + + "(the cli-e2e-ci runner sets it after provisioning a project).", + ); + } + return ref; +} diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts index ad69636235..78d2558190 100644 --- a/apps/cli/tests/helpers/live.ts +++ b/apps/cli/tests/helpers/live.ts @@ -1,53 +1,34 @@ import { describe } from "vitest"; + import { runSupabase } from "./cli.ts"; +import { + isLiveConfigured, + LIVE_DEFAULT_PROFILE, + LIVE_EXIT_TIMEOUT_MS, + liveProjectRef, +} from "./live-env.ts"; /** - * Helpers for the `live` Vitest project (`*.live.test.ts`): black-box CLI - * subprocess tests that run against a *real* Supabase platform — in CI that is - * a local supabox stack (see the `supabase/cli-e2e-ci` harness). - * - * Unlike `*.e2e.test.ts`, which use fake tokens and assert error/golden-path - * surface behavior, live tests exercise real Management API, edge function, - * database, and storage flows end to end. They are gated off by default and - * only run when a live access token is present in the environment, so the - * normal unit/integration/e2e loop never touches a network. + * Test-facing helpers for the `live` Vitest project (`*.live.test.ts`): + * black-box CLI subprocess tests that run against a *real* Supabase platform — + * in CI a local supabox stack (see the `supabase/cli-e2e-ci` harness). * - * Environment contract (provided by the cli-e2e-ci runner): - * - `SUPABASE_ACCESS_TOKEN` — required; the platform PAT (supabox seeds a - * deterministic `sbp_…` token into its mgmt-api database). - * - `SUPABASE_PROFILE` — selects the API base URL; defaults to `supabase-local` - * (→ `http://localhost:8080`, `project_host: supabase.red`). Note the cli does - * NOT honor `SUPABASE_API_URL` (Go parity) — the profile is the override. - * - `SUPABASE_LIVE_API_URL` — base URL the readiness check probes; defaults to - * `http://localhost:8080`. - * - `NODE_EXTRA_CA_CERTS` — trusts the supabox CA for `*.supabase.red` TLS; - * inherited by the subprocess via the parent environment. - */ - -/** Default profile for the host runner: api_url → localhost:8080, project_host → supabase.red. */ -export const LIVE_DEFAULT_PROFILE = "supabase-local"; - -/** - * Default subprocess exit timeout for live runs. `runSupabase` otherwise caps at - * 60s, which would kill a slow-but-valid supabox call before the live tests' - * own (60–120s+) timeouts fire. Generous, but under the `live` project's 300s - * cap so the per-test timeout stays the real gate. Callers may override. + * This module imports Vitest test APIs (`describe`), so it must NOT be imported + * from `globalSetup` (Vitest evaluates that in a different context). The + * env-only helpers live in `./live-env.ts`; `globalSetup` imports from there. + * They are re-exported below so test files have a single import site. */ -export const LIVE_EXIT_TIMEOUT_MS = 240_000; -/** Management API base URL probed by the live readiness check. */ -export function liveApiBaseUrl(): string { - return process.env["SUPABASE_LIVE_API_URL"] ?? "http://localhost:8080"; -} - -/** - * True when the environment carries a platform access token, i.e. the live - * suite is expected to run. Used to gate `describeLive` so live tests are inert - * in the default test loop. - */ -export function isLiveConfigured(): boolean { - return Boolean(process.env["SUPABASE_ACCESS_TOKEN"]); -} +// Re-export the env-only helpers so `*.live.test.ts` files import everything +// from `helpers/live.ts`. +export { + isLiveConfigured, + LIVE_DEFAULT_PROFILE, + LIVE_EXIT_TIMEOUT_MS, + liveApiBaseUrl, + liveProjectRef, + requireLiveProjectRef, +} from "./live-env.ts"; /** * `describe` that runs only when the live environment is configured. Use this @@ -56,33 +37,6 @@ export function isLiveConfigured(): boolean { */ export const describeLive = describe.skipIf(!isLiveConfigured()); -/** - * Project ref for project-scoped live scenarios (functions, branches, db, - * storage, …). The cli-e2e-ci runner sets this once a project has been - * provisioned on the stack; absent → those suites skip. Returns `undefined` - * when unset so callers can branch; use `requireLiveProjectRef` inside a - * `describeLiveProject` block where presence is already guaranteed. - */ -export function liveProjectRef(): string | undefined { - return process.env["SUPABASE_LIVE_PROJECT_REF"]; -} - -/** - * The live project ref, or a thrown error if unset. Safe to call inside a - * `describeLiveProject` block (the gate guarantees it is present) and gives a - * typed `string` without a non-null assertion. - */ -export function requireLiveProjectRef(): string { - const ref = liveProjectRef(); - if (!ref) { - throw new Error( - "SUPABASE_LIVE_PROJECT_REF must be set for project-scoped live tests " + - "(the cli-e2e-ci runner sets it after provisioning a project).", - ); - } - return ref; -} - /** * `describe` for project-scoped live suites: runs only when the live env is * configured AND a project ref is available. On a control-plane-only stack diff --git a/apps/cli/tests/live-global-setup.ts b/apps/cli/tests/live-global-setup.ts index 331f7982a9..d7584e757a 100644 --- a/apps/cli/tests/live-global-setup.ts +++ b/apps/cli/tests/live-global-setup.ts @@ -1,4 +1,6 @@ -import { isLiveConfigured, liveApiBaseUrl } from "./helpers/live.ts"; +// Import from the Vitest-free env module — globalSetup runs in a context where +// importing Vitest test APIs (which `helpers/live.ts` pulls in) is not valid. +import { isLiveConfigured, liveApiBaseUrl } from "./helpers/live-env.ts"; /** * Global setup for the `live` Vitest project. When the live environment is not