From baa11077baf619efadc39748a3dd1f4d74d1a456 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:31:29 +0530 Subject: [PATCH 1/4] test: add migration up and repair live coverage --- .../migration/repair/repair.live.test.ts | 74 +++++++++++++++++++ .../commands/migration/up/up.live.test.ts | 47 ++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts create mode 100644 apps/cli/src/legacy/commands/migration/up/up.live.test.ts diff --git a/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts b/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts new file mode 100644 index 0000000000..6788af1fea --- /dev/null +++ b/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts @@ -0,0 +1,74 @@ +import { mkdir, unlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { requireLiveSuccess, test, throwWithCleanup } from "../../../../../tests/helpers/live.ts"; + +test("amends the migration history status on the remote database", async ({ + cli, + project, + workspace, +}) => { + const version = `${Date.now()}${Math.floor(Math.random() * 10_000) + .toString() + .padStart(4, "0")}`; + const migrations = join(workspace.path, "supabase", "migrations"); + await mkdir(migrations, { recursive: true }); + const migrationFile = join(migrations, `${version}_e2e_repair.sql`); + await writeFile(migrationFile, `create table if not exists e2e_repair_${version} (id int);\n`); + + let targetError: unknown; + const cleanupErrors: Array = []; + try { + const applied = await cli([ + "migration", + "repair", + version, + "--status", + "applied", + "--db-url", + project.dbUrl, + ]); + expect(applied.exitCode, applied.stderr).toBe(0); + expect(applied.stderr, applied.stdout).toContain("=> applied"); + await unlink(migrationFile); + + const listed = await cli(["migration", "list", "--db-url", project.dbUrl]); + requireLiveSuccess(listed, "migration list proof for migration repair"); + expect(listed.stdout, listed.stderr).toContain(version); + + const reverted = await cli([ + "migration", + "repair", + version, + "--status", + "reverted", + "--db-url", + project.dbUrl, + ]); + expect(reverted.exitCode, reverted.stderr).toBe(0); + expect(reverted.stderr, reverted.stdout).toContain("=> reverted"); + + const relisted = await cli(["migration", "list", "--db-url", project.dbUrl]); + requireLiveSuccess(relisted, "migration list proof after revert"); + expect(relisted.stdout, relisted.stderr).not.toContain(version); + } catch (error) { + targetError = error; + } finally { + try { + const cleanup = await cli([ + "migration", + "repair", + version, + "--status", + "reverted", + "--db-url", + project.dbUrl, + ]); + requireLiveSuccess(cleanup, "migration repair cleanup"); + } catch (error) { + cleanupErrors.push(error); + } + } + throwWithCleanup(targetError, cleanupErrors); +}); diff --git a/apps/cli/src/legacy/commands/migration/up/up.live.test.ts b/apps/cli/src/legacy/commands/migration/up/up.live.test.ts new file mode 100644 index 0000000000..048bbca44f --- /dev/null +++ b/apps/cli/src/legacy/commands/migration/up/up.live.test.ts @@ -0,0 +1,47 @@ +import { mkdir, rm, unlink, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { requireLiveSuccess, test, throwWithCleanup } from "../../../../../tests/helpers/live.ts"; + +test("applies a test-written migration to the remote database", async ({ + cli, + project, + workspace, +}) => { + const version = `${Date.now()}${Math.floor(Math.random() * 10_000) + .toString() + .padStart(4, "0")}`; + const migrations = join(workspace.path, "supabase", "migrations"); + await mkdir(migrations, { recursive: true }); + const migrationFile = join(migrations, `${version}_e2e_up.sql`); + await writeFile(migrationFile, `create table if not exists e2e_up_${version} (id int);\n`); + + let targetError: unknown; + const cleanupErrors: Array = []; + try { + const applied = await cli(["migration", "up", "--db-url", project.dbUrl]); + expect(applied.exitCode, applied.stderr).toBe(0); + expect(applied.stderr, applied.stdout).toContain("Applying migration"); + await unlink(migrationFile); + + const listed = await cli(["migration", "list", "--db-url", project.dbUrl]); + requireLiveSuccess(listed, "migration list proof for migration up"); + expect(listed.stdout, listed.stderr).toContain(version); + } catch (error) { + targetError = error; + } finally { + try { + await rm(migrationFile, { force: true }); + } catch (error) { + cleanupErrors.push(error); + } + try { + const reset = await cli(["db", "reset", "--db-url", project.dbUrl, "--yes"]); + requireLiveSuccess(reset, "db reset cleanup after migration up"); + } catch (error) { + cleanupErrors.push(error); + } + } + throwWithCleanup(targetError, cleanupErrors); +}); From 61980b904a9137bbfd22d73bfd31a3af1fbdf3b2 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:49:21 +0530 Subject: [PATCH 2/4] test: scope migration up cleanup to owned state --- .../commands/migration/up/up.live.test.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/cli/src/legacy/commands/migration/up/up.live.test.ts b/apps/cli/src/legacy/commands/migration/up/up.live.test.ts index 048bbca44f..8e16f464db 100644 --- a/apps/cli/src/legacy/commands/migration/up/up.live.test.ts +++ b/apps/cli/src/legacy/commands/migration/up/up.live.test.ts @@ -37,8 +37,28 @@ test("applies a test-written migration to the remote database", async ({ cleanupErrors.push(error); } try { - const reset = await cli(["db", "reset", "--db-url", project.dbUrl, "--yes"]); - requireLiveSuccess(reset, "db reset cleanup after migration up"); + const dropped = await cli([ + "db", + "query", + `drop table if exists e2e_up_${version}`, + "--db-url", + project.dbUrl, + ]); + requireLiveSuccess(dropped, "db query cleanup after migration up"); + } catch (error) { + cleanupErrors.push(error); + } + try { + const reverted = await cli([ + "migration", + "repair", + version, + "--status", + "reverted", + "--db-url", + project.dbUrl, + ]); + requireLiveSuccess(reverted, "migration repair cleanup after migration up"); } catch (error) { cleanupErrors.push(error); } From 9499bdb509e9e35b1c6ba981f6c4781116c0e428 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:27:11 +0530 Subject: [PATCH 3/4] Nits --- .../migration/repair/repair.live.test.ts | 62 ++++++++++++------- .../commands/migration/up/up.live.test.ts | 49 ++++++++++++--- apps/cli/tests/helpers/live.ts | 28 +++++++++ 3 files changed, 107 insertions(+), 32 deletions(-) diff --git a/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts b/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts index 6788af1fea..f84d62810b 100644 --- a/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts +++ b/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts @@ -2,22 +2,29 @@ import { mkdir, unlink, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; -import { requireLiveSuccess, test, throwWithCleanup } from "../../../../../tests/helpers/live.ts"; +import { + liveMigrationVersion, + queryLiveDb, + requireLiveSuccess, + test, + throwWithCleanup, +} from "../../../../../tests/helpers/live.ts"; test("amends the migration history status on the remote database", async ({ cli, project, workspace, }) => { - const version = `${Date.now()}${Math.floor(Math.random() * 10_000) - .toString() - .padStart(4, "0")}`; + const version = liveMigrationVersion(); const migrations = join(workspace.path, "supabase", "migrations"); await mkdir(migrations, { recursive: true }); const migrationFile = join(migrations, `${version}_e2e_repair.sql`); + // `repair --status applied` records the file's statements in migration + // history without executing them, so this table is never actually created. await writeFile(migrationFile, `create table if not exists e2e_repair_${version} (id int);\n`); let targetError: unknown; + let versionReverted = false; const cleanupErrors: Array = []; try { const applied = await cli([ @@ -33,9 +40,12 @@ test("amends the migration history status on the remote database", async ({ expect(applied.stderr, applied.stdout).toContain("=> applied"); await unlink(migrationFile); - const listed = await cli(["migration", "list", "--db-url", project.dbUrl]); - requireLiveSuccess(listed, "migration list proof for migration repair"); - expect(listed.stdout, listed.stderr).toContain(version); + const recorded = await queryLiveDb( + project.dbUrl, + "select version from supabase_migrations.schema_migrations where version = $1", + [version], + ); + expect(recorded).toHaveLength(1); const reverted = await cli([ "migration", @@ -48,26 +58,32 @@ test("amends the migration history status on the remote database", async ({ ]); expect(reverted.exitCode, reverted.stderr).toBe(0); expect(reverted.stderr, reverted.stdout).toContain("=> reverted"); + versionReverted = true; - const relisted = await cli(["migration", "list", "--db-url", project.dbUrl]); - requireLiveSuccess(relisted, "migration list proof after revert"); - expect(relisted.stdout, relisted.stderr).not.toContain(version); + const remaining = await queryLiveDb( + project.dbUrl, + "select version from supabase_migrations.schema_migrations where version = $1", + [version], + ); + expect(remaining).toHaveLength(0); } catch (error) { targetError = error; } finally { - try { - const cleanup = await cli([ - "migration", - "repair", - version, - "--status", - "reverted", - "--db-url", - project.dbUrl, - ]); - requireLiveSuccess(cleanup, "migration repair cleanup"); - } catch (error) { - cleanupErrors.push(error); + if (!versionReverted) { + try { + const cleanup = await cli([ + "migration", + "repair", + version, + "--status", + "reverted", + "--db-url", + project.dbUrl, + ]); + requireLiveSuccess(cleanup, "migration repair cleanup"); + } catch (error) { + cleanupErrors.push(error); + } } } throwWithCleanup(targetError, cleanupErrors); diff --git a/apps/cli/src/legacy/commands/migration/up/up.live.test.ts b/apps/cli/src/legacy/commands/migration/up/up.live.test.ts index 8e16f464db..312fbf03c6 100644 --- a/apps/cli/src/legacy/commands/migration/up/up.live.test.ts +++ b/apps/cli/src/legacy/commands/migration/up/up.live.test.ts @@ -1,19 +1,43 @@ -import { mkdir, rm, unlink, writeFile } from "node:fs/promises"; +import { mkdir, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; -import { requireLiveSuccess, test, throwWithCleanup } from "../../../../../tests/helpers/live.ts"; +import { + liveMigrationVersion, + queryLiveDb, + requireLiveSuccess, + test, + throwWithCleanup, +} from "../../../../../tests/helpers/live.ts"; test("applies a test-written migration to the remote database", async ({ cli, project, workspace, }) => { - const version = `${Date.now()}${Math.floor(Math.random() * 10_000) - .toString() - .padStart(4, "0")}`; + const version = liveMigrationVersion(); const migrations = join(workspace.path, "supabase", "migrations"); await mkdir(migrations, { recursive: true }); + + // The serial suite shares one remote project, so seed a local stub for every + // version already in remote history — otherwise `migration up` rejects them + // as missing locally. The history table may not exist yet on a fresh project. + let remoteVersions: Array<{ version: string }> = []; + try { + remoteVersions = await queryLiveDb( + project.dbUrl, + "select version from supabase_migrations.schema_migrations order by version", + ); + } catch { + remoteVersions = []; + } + for (const row of remoteVersions) { + await writeFile( + join(migrations, `${row.version}_preexisting_remote.sql`), + "-- stub for a version already in remote history\n", + ); + } + const migrationFile = join(migrations, `${version}_e2e_up.sql`); await writeFile(migrationFile, `create table if not exists e2e_up_${version} (id int);\n`); @@ -23,11 +47,18 @@ test("applies a test-written migration to the remote database", async ({ const applied = await cli(["migration", "up", "--db-url", project.dbUrl]); expect(applied.exitCode, applied.stderr).toBe(0); expect(applied.stderr, applied.stdout).toContain("Applying migration"); - await unlink(migrationFile); - const listed = await cli(["migration", "list", "--db-url", project.dbUrl]); - requireLiveSuccess(listed, "migration list proof for migration up"); - expect(listed.stdout, listed.stderr).toContain(version); + const history = await queryLiveDb( + project.dbUrl, + "select version from supabase_migrations.schema_migrations where version = $1", + [version], + ); + expect(history).toHaveLength(1); + + const created = await queryLiveDb(project.dbUrl, "select to_regclass($1) as table_oid", [ + `public.e2e_up_${version}`, + ]); + expect(created[0]?.["table_oid"], "migration up must execute the migration sql").not.toBeNull(); } catch (error) { targetError = error; } finally { diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts index 9b87dc3465..80a2854c12 100644 --- a/apps/cli/tests/helpers/live.ts +++ b/apps/cli/tests/helpers/live.ts @@ -2,6 +2,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; +import pg from "pg"; import { inject, test as vitestTest } from "vitest"; import { makeTempHome, runSupabase } from "./cli.ts"; @@ -122,6 +123,33 @@ export function requireLiveSuccess( } } +/** Unique migration version for a live test: epoch millis plus four random digits. */ +export function liveMigrationVersion(): string { + return `${Date.now()}${Math.floor(Math.random() * 10_000) + .toString() + .padStart(4, "0")}`; +} + +/** + * Runs one query against the live project over a direct pg connection, so + * live assertions can verify database state without invoking another CLI + * command. + */ +export async function queryLiveDb>( + dbUrl: string, + query: string, + values?: ReadonlyArray, +): Promise { + const client = new pg.Client({ connectionString: dbUrl }); + await client.connect(); + try { + const result = await client.query(query, values === undefined ? undefined : [...values]); + return result.rows as T[]; + } finally { + await client.end(); + } +} + /** Rethrow a target failure without discarding failures from exact cleanup. */ export function throwWithCleanup(primary: unknown, cleanup: ReadonlyArray): void { if (primary !== undefined) { From a8a869c24184bcc14d530f5bcc9ba46fec102142 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:36:07 +0530 Subject: [PATCH 4/4] few more nits --- .../commands/migration/repair/repair.live.test.ts | 3 ++- .../src/legacy/commands/migration/up/up.live.test.ts | 6 +++++- apps/cli/tests/helpers/live.ts | 12 ++++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts b/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts index f84d62810b..a069a882a5 100644 --- a/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts +++ b/apps/cli/src/legacy/commands/migration/repair/repair.live.test.ts @@ -58,7 +58,6 @@ test("amends the migration history status on the remote database", async ({ ]); expect(reverted.exitCode, reverted.stderr).toBe(0); expect(reverted.stderr, reverted.stdout).toContain("=> reverted"); - versionReverted = true; const remaining = await queryLiveDb( project.dbUrl, @@ -66,6 +65,8 @@ test("amends the migration history status on the remote database", async ({ [version], ); expect(remaining).toHaveLength(0); + // Only skip the teardown revert once the row is verifiably gone. + versionReverted = true; } catch (error) { targetError = error; } finally { diff --git a/apps/cli/src/legacy/commands/migration/up/up.live.test.ts b/apps/cli/src/legacy/commands/migration/up/up.live.test.ts index 312fbf03c6..9abb5790a3 100644 --- a/apps/cli/src/legacy/commands/migration/up/up.live.test.ts +++ b/apps/cli/src/legacy/commands/migration/up/up.live.test.ts @@ -28,7 +28,11 @@ test("applies a test-written migration to the remote database", async ({ project.dbUrl, "select version from supabase_migrations.schema_migrations order by version", ); - } catch { + } catch (error) { + // 42P01 (undefined relation) covers the fresh-project case where the + // history table or its schema does not exist yet; anything else is a real + // failure the test must surface. + if ((error as { code?: string }).code !== "42P01") throw error; remoteVersions = []; } for (const row of remoteVersions) { diff --git a/apps/cli/tests/helpers/live.ts b/apps/cli/tests/helpers/live.ts index 80a2854c12..1a284f7246 100644 --- a/apps/cli/tests/helpers/live.ts +++ b/apps/cli/tests/helpers/live.ts @@ -123,9 +123,17 @@ export function requireLiveSuccess( } } -/** Unique migration version for a live test: epoch millis plus four random digits. */ +/** + * Unique migration version for a live test: a sortable `YYYYMMDDHHMMSS` UTC + * stamp plus four random digits, so it always orders after any conventional + * timestamp version already in the shared project's migration history. + */ export function liveMigrationVersion(): string { - return `${Date.now()}${Math.floor(Math.random() * 10_000) + const stamp = new Date() + .toISOString() + .replaceAll(/[-:TZ.]/gu, "") + .slice(0, 14); + return `${stamp}${Math.floor(Math.random() * 10_000) .toString() .padStart(4, "0")}`; }