fix: complete production data migration rehearsal
Staging Backend Quality Gate / validate (push) Failing after 49m36s
Staging Backend Quality Gate / publish (push) Has been skipped

This commit is contained in:
Jesse_Chen
2026-08-09 14:17:01 +08:00
parent d36ca7aac2
commit ffbe505c23
3 changed files with 123 additions and 31 deletions
@@ -7,9 +7,11 @@ import {
SafeProductionMigrationError,
assertActiveAdminUsers,
assertTargetEmpty,
copiedDeferredForeignKeys,
normalizeAuthUser,
normalizeAuthUsers,
parseMode,
prepareColumnValues,
readActiveAdminUserIds,
readConfiguration,
readSchema,
@@ -29,6 +31,8 @@ function table(
generated?: boolean;
identity?: boolean;
identityGeneration?: string | null;
dataType?: string;
udtName?: string;
}>,
primaryKey = ["id"],
foreignKeys: Array<{
@@ -45,6 +49,8 @@ function table(
generated: false,
identity: false,
identityGeneration: null,
dataType: "text",
udtName: "text",
...column,
})),
primaryKey,
@@ -65,6 +71,7 @@ test("configuration requires an explicit Owner and ciphertext decision", () => {
SUPABASE_SOURCE_DATABASE_URL: "postgresql://source.invalid/jyotisha",
PRODUCTION_TARGET_DATABASE_URL: "postgresql://target.invalid/jyotisha",
PRODUCTION_OWNER_USER_ID: "018f4e6d-7a11-7000-8000-000000000001",
PRODUCTION_OWNER_EMAIL: "luna@copse.life",
};
assert.equal(
@@ -161,15 +168,19 @@ test("active administrators become usable identity admins", () => {
);
assert.deepEqual(users.map((user) => user.role), ["admin", "admin"]);
assert.doesNotThrow(() => assertActiveAdminUsers(users, activeAdminUserIds, ownerId));
assert.doesNotThrow(() => assertActiveAdminUsers(users, activeAdminUserIds, ownerId, `${ownerId}@example.com`));
assert.throws(
() => assertActiveAdminUsers(users, new Set([adminId]), ownerId),
() => assertActiveAdminUsers(users, new Set([adminId]), ownerId, `${ownerId}@example.com`),
/Owner is not an active source administrator/,
);
assert.throws(
() => assertActiveAdminUsers([{ ...users[0], banned: true }, users[1]], activeAdminUserIds, ownerId),
() => assertActiveAdminUsers([{ ...users[0], banned: true }, users[1]], activeAdminUserIds, ownerId, `${ownerId}@example.com`),
/Owner is blocked/,
);
assert.throws(
() => assertActiveAdminUsers(users, activeAdminUserIds, ownerId, "wrong@example.com"),
/UUID does not match/,
);
});
test("schema reader preserves PostgreSQL identity metadata", async () => {
@@ -185,6 +196,8 @@ test("schema reader preserves PostgreSQL identity metadata", async () => {
generated: false,
identity: true,
identity_generation: "ALWAYS",
data_type: "bigint",
udt_name: "int8",
}],
};
}
@@ -203,9 +216,42 @@ test("schema reader preserves PostgreSQL identity metadata", async () => {
generated: false,
identity: true,
identityGeneration: "ALWAYS",
dataType: "bigint",
udtName: "int8",
});
});
test("JSON columns are serialized without changing PostgreSQL arrays", () => {
const target = table([
{ name: "payload", dataType: "jsonb", udtName: "jsonb" },
{ name: "tags", dataType: "ARRAY", udtName: "_text" },
]);
const tags = ["a", "b"];
assert.deepEqual(
prepareColumnValues({ payload: [], tags }, ["payload", "tags"], target),
["[]", tags],
);
});
test("target-only nullable foreign keys are not restored from the legacy source", () => {
const target = table(
[{ name: "id" }, { name: "new_session_id", nullable: true }],
["id"],
[{
columns: ["new_session_id"],
refSchema: "public",
refTable: "chat_sessions",
refColumns: ["id"],
}],
);
assert.deepEqual(
copiedDeferredForeignKeys("requests", target, new Set(["chat_sessions"]), ["id"]),
[],
);
});
test("transfer plan rejects source-only public columns", () => {
const source = new Map([
["profiles", table([{ name: "id" }, { name: "legacy_value" }])],