-
Notifications
You must be signed in to change notification settings - Fork 487
test(cli-e2e): capability probes + capability-based live gating #5749
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
26e7038
8bcc653
45bbf0d
70389dd
6a0a0d3
1ba3539
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
| import { describe, expect } from "vitest"; | ||
| import { expectFunctionOk } from "./invoke.ts"; | ||
| import { seedFunctions, testLiveRequires } from "./live-context.ts"; | ||
|
|
||
| // Capability probes: one minimal test per runtime-capability combination the cli | ||
| // needs from its target. Each is a smoke check that FORCES its capability, so a | ||
| // failure (or a target-driven skip) is a precise signal of what an environment | ||
| // can or cannot do. Distilled from the richer feature tests so each isolates a | ||
| // single capability. | ||
| // | ||
| // Staging is the oracle: with CLI_E2E_TARGET_ENV=staging (provides all | ||
| // capabilities) every probe runs and must pass — proving the probe is sound, so | ||
| // any supabox skip/red is a genuine gap. On supabox each probe only runs once | ||
| // its capability is declared via CLI_E2E_CAPABILITIES, then must match staging. | ||
| // See `testLiveRequires` + PROVIDED_CAPABILITIES in ../env.ts. | ||
| describe("capability probes (live)", () => { | ||
| // C1 — mgmt-api only (no docker, no internet, no external tool). The | ||
| // provisioned project shows up in `projects list`: a pure control-plane read. | ||
| testLiveRequires([])( | ||
| "[C1] mgmt-api only: projects list includes the project", | ||
| async ({ run, projectRef }) => { | ||
| const res = await run(["projects", "list", "--output", "json"]); | ||
| expect(res.exitCode, res.stderr).toBe(0); | ||
| const refs = (JSON.parse(res.stdout) as Array<{ id?: string; ref?: string }>).map( | ||
| (project) => project.ref ?? project.id, | ||
| ); | ||
| expect(refs).toContain(projectRef); | ||
| }, | ||
| ); | ||
|
|
||
| // C2 — docker control, no runtime internet. `db pull`'s schema diff starts a | ||
| // shadow postgres *server* (DockerStart) and runs the diff engine in a | ||
| // container; both use pre-built images (no 3rd-party network). Push first so | ||
| // local history matches the shared per-run project, then pull. A missing | ||
| // Docker socket makes DockerStart fail — a genuine docker gate. | ||
| testLiveRequires(["docker"])( | ||
| "[C2] docker (offline): db push then db pull round-trips", | ||
| async ({ run, dbUrl, workspace }) => { | ||
| const migrations = join(workspace.path, "supabase", "migrations"); | ||
| mkdirSync(migrations, { recursive: true }); | ||
| // Unique timestamp so this never collides with db-sync.live.e2e.test.ts's | ||
| // migration on the shared per-run project (history compares timestamps). | ||
| writeFileSync( | ||
| join(migrations, "20240202020202_capability_probe_push.sql"), | ||
| "create table if not exists capability_probe (id int);\n", | ||
| ); | ||
|
|
||
| const pushed = await run(["db", "push", "--db-url", dbUrl, "--yes"]); | ||
| expect(pushed.exitCode, pushed.stderr).toBe(0); | ||
|
|
||
| const pulled = await run(["db", "pull", "--db-url", dbUrl, "--yes"]); | ||
| const output = `${pulled.stdout}${pulled.stderr}`; | ||
| // Distinguish a real docker/connection failure from a benign "no changes". | ||
| expect(output, "db pull hit a docker/connection error").not.toMatch( | ||
| /cannot connect to the docker daemon|is the docker daemon running|dial|connection refused|could not connect/i, | ||
| ); | ||
| expect(pulled.exitCode === 0 || /No schema changes found/i.test(output), pulled.stderr).toBe( | ||
| true, | ||
| ); | ||
| }, | ||
| ); | ||
|
|
||
| // C3 — external tool, no docker/internet. `db dump` normally runs `pg_dump` in a | ||
| // supabase/postgres CONTAINER; `SUPABASE_DB_USE_LOCAL_TOOLS=1` switches it to the | ||
| // native pg_dump/psql on PATH, so this exercises the external-tool path (no | ||
| // Docker socket) rather than the container path. Fails if the tool is absent. | ||
| testLiveRequires(["external-tool"])( | ||
| "[C3] external tool: db dump exports the remote schema via native pg_dump", | ||
| async ({ run, dbUrl, workspace }) => { | ||
| const file = join(workspace.path, "dump.sql"); | ||
| const res = await run(["db", "dump", "--db-url", dbUrl, "-f", file], { | ||
| env: { SUPABASE_DB_USE_LOCAL_TOOLS: "1" }, | ||
| }); | ||
| expect(res.exitCode, res.stderr).toBe(0); | ||
| expect(existsSync(file)).toBe(true); | ||
| expect(readFileSync(file, "utf8")).toMatch(/CREATE|PostgreSQL database dump|SCHEMA/i); | ||
| }, | ||
| ); | ||
|
|
||
| // C4 — runtime 3rd-party internet, no docker. Deploy a function that imports | ||
| // from jsr.io with the default (server-side) bundler and invoke it; the bundle | ||
| // path must fetch the import over the network. Fails offline. | ||
| testLiveRequires(["internet"])( | ||
| "[C4] internet: deploy a jsr-importing function and invoke", | ||
| async ({ run, invoke, workspace, projectRef }) => { | ||
| seedFunctions(workspace.path); | ||
| const slug = "deploy-e2e-jsr"; | ||
| const deployed = await run(["functions", "deploy", slug, "--project-ref", projectRef]); | ||
| expect(deployed.exitCode, deployed.stderr).toBe(0); | ||
| expect(deployed.stdout).toContain("Deployed Functions"); | ||
| expectFunctionOk(await invoke(slug), slug); | ||
| }, | ||
| ); | ||
|
|
||
| // C5 — docker AND runtime internet. Bundle an npm-importing function locally in | ||
| // a Docker container (`--use-docker`): needs a Docker socket AND the in-container | ||
| // bundler needs internet to fetch the npm import. A DISTINCT slug from C4 so the | ||
| // invoke proves THIS (--use-docker) deploy produced the running function, not | ||
| // C4's earlier server-bundled one on the shared project. Fails if either is | ||
| // missing. | ||
| testLiveRequires(["docker", "internet"])( | ||
| "[C5] docker + internet: --use-docker deploy of an npm-importing function", | ||
| async ({ run, invoke, workspace, projectRef }) => { | ||
| seedFunctions(workspace.path); | ||
| const slug = "deploy-e2e-npm"; | ||
| const deployed = await run([ | ||
| "functions", | ||
| "deploy", | ||
| slug, | ||
| "--project-ref", | ||
| projectRef, | ||
| "--use-docker", | ||
| ]); | ||
| expect(deployed.exitCode, deployed.stderr).toBe(0); | ||
| expect(deployed.stdout).toContain("Deployed Functions"); | ||
| expectFunctionOk(await invoke(slug), slug); | ||
| }, | ||
| ); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,15 @@ | ||
| import { describe, expect } from "vitest"; | ||
| import { testLive } from "./live-context.ts"; | ||
| import { testLiveRequires } from "./live-context.ts"; | ||
|
|
||
| // gen types introspects the remote schema over the IPv4 session pooler and emits | ||
| // TypeScript types. It pulls the postgres-meta Docker image, so it needs Docker | ||
| // (present in the CI live job alongside the --use-docker bundler cell). | ||
| // TypeScript types by running the postgres-meta Docker image (needs `docker`). | ||
| describe("gen types (live, session pooler)", () => { | ||
| testLive("generates TypeScript types from the remote schema", async ({ run, dbUrl }) => { | ||
| const res = await run(["gen", "types", "--db-url", dbUrl, "--lang", "typescript"]); | ||
| expect(res.exitCode, res.stderr).toBe(0); | ||
| expect(res.stdout).toMatch(/export type (Database|Json)/); | ||
| }); | ||
| testLiveRequires(["docker"])( | ||
| "generates TypeScript types from the remote schema", | ||
| async ({ run, dbUrl }) => { | ||
| const res = await run(["gen", "types", "--db-url", dbUrl, "--lang", "typescript"]); | ||
| expect(res.exitCode, res.stderr).toBe(0); | ||
| expect(res.stdout).toMatch(/export type (Database|Json)/); | ||
| }, | ||
| ); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,15 @@ import { | |
| type CLIResult, | ||
| type TempDir, | ||
| } from "@supabase/cli-test-helpers"; | ||
| import { ACCESS_TOKEN, isLive, PROJECT_HOST, TARGET, TARGET_API_URL } from "../env.ts"; | ||
| import { | ||
| ACCESS_TOKEN, | ||
| type Capability, | ||
| isLive, | ||
| PROJECT_HOST, | ||
| PROVIDED_CAPABILITIES, | ||
| TARGET, | ||
| TARGET_API_URL, | ||
| } from "../env.ts"; | ||
| import { invokeFunction, type InvokeResult } from "./invoke.ts"; | ||
|
|
||
| type ExecOptions = NonNullable<Parameters<typeof exec>[2]>; | ||
|
|
@@ -91,7 +99,12 @@ const base = test.extend<LiveFixtures>({ | |
| }, | ||
|
|
||
| workspace: async ({ task }, use) => { | ||
| const dir = makeTempDir(`cli-e2e-live-${task.name.slice(0, 30)}-`); | ||
| // Sanitize the task name: it becomes part of the temp-dir path, which the cli | ||
| // mounts as a Docker volume for docker-backed commands (functions bundling, | ||
| // db diff shadow). A `:` or space in the path breaks the `src:dst:mode` | ||
| // volume spec ("too many colons"), so collapse anything non-alphanumeric. | ||
| const safeName = task.name.replace(/[^a-zA-Z0-9]+/g, "-").slice(0, 30); | ||
| const dir = makeTempDir(`cli-e2e-live-${safeName}-`); | ||
| // Generate config.toml via `supabase init` so the golden paths run against a | ||
| // freshly-generated config (functions tests add functions via seedFunctions). | ||
| const init = await exec(liveHarness(dir.path), ["init"]); | ||
|
|
@@ -120,3 +133,14 @@ const base = test.extend<LiveFixtures>({ | |
| /** Live test API — skipped unless CLI_E2E_MODE=live, so files are inert on | ||
| * replay/PR runs (and globalSetup provisions nothing). */ | ||
| export const testLive = base.skipIf(!isLive); | ||
|
|
||
| /** Live test API that additionally skips unless the target env provides every | ||
| * required runtime capability (docker / internet / external-tool). Lets one | ||
| * suite run against staging (all capabilities → runs everything, the oracle), | ||
| * supabox (only what it currently supports), and Antithesis (offline subset), | ||
| * each skipping only what it genuinely can't do. Put the requirement in the test | ||
| * name (e.g. "[C5] … (docker+internet)") so a skip reads clearly in the report. */ | ||
| export function testLiveRequires(required: readonly Capability[]): typeof testLive { | ||
| const missing = required.filter((capability) => !PROVIDED_CAPABILITIES.has(capability)); | ||
| return missing.length === 0 ? testLive : testLive.skip; | ||
|
Comment on lines
+143
to
+145
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In a supabox/Antithesis run with missing Docker or internet, this helper only skips tests that opt into Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Partially addressed (8bcc653) — tagged the clearly docker/internet existing tests: 🤖 Addressed by Claude Code |
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.