diff --git a/src/onboarding/adapters/postgres/restore-validation.ts b/src/onboarding/adapters/postgres/restore-validation.ts index 32bdd24..0fc81f4 100644 --- a/src/onboarding/adapters/postgres/restore-validation.ts +++ b/src/onboarding/adapters/postgres/restore-validation.ts @@ -184,6 +184,15 @@ const FUNCTION_BODY_SHA256 = { } as const; const REQUIRED_TRIGGERS = [ + { + minimumMigration: 5, + maximumMigration: 7, + triggerName: "external_snapshots_immutable", + tableName: "external_source_snapshots", + functionName: "reject_external_history_mutation", + functionBodySha256: FUNCTION_BODY_SHA256.reject_external_history_mutation, + events: ["DELETE", "UPDATE"], + }, { minimumMigration: 5, triggerName: "external_content_immutable", @@ -444,7 +453,10 @@ export function assessRestoredDatabaseEvidence( uniqueTriggers.size === evidence.triggers.length && evidence.triggers.every(({ enabled }) => enabled === "origin") && REQUIRED_TRIGGERS.filter( - ({ minimumMigration }) => minimumMigration <= latestMigration, + (required) => + required.minimumMigration <= latestMigration && + (!("maximumMigration" in required) || + latestMigration <= required.maximumMigration), ).every((required) => evidence.triggers.some( (trigger) => diff --git a/tests/unit/onboarding/restored-database-validation.test.ts b/tests/unit/onboarding/restored-database-validation.test.ts index 4d3d3a0..77c1e56 100644 --- a/tests/unit/onboarding/restored-database-validation.test.ts +++ b/tests/unit/onboarding/restored-database-validation.test.ts @@ -454,6 +454,60 @@ describe("production restored-database validation", () => { }); }); + it("requires the original snapshot immutability trigger through migration 007", () => { + const accountId = randomUUID(); + const checksums = Array.from({ length: 7 }, (_, index) => + (index + 1).toString(16).padStart(64, "0"), + ); + const migration007Triggers = REQUIRED_TRIGGERS.map((entry) => + entry.triggerName === "external_snapshots_immutable" + ? trigger( + "external_snapshots_immutable", + "external_source_snapshots", + "reject_external_history_mutation", + ["DELETE", "UPDATE"], + ) + : entry.triggerName === "external_classification_transition_valid" + ? { + ...entry, + functionBodySha256: + "7e29bd82153cfd0976b925d2dd6a18879f3c9e16a4c79faf1586fdddb9aad718", + } + : entry, + ); + const valid = evidence({ + accountId, + checksums, + triggers: migration007Triggers, + }); + + expect( + assessRestoredDatabaseEvidence(valid, { + expectedMigrations: valid.migrations, + installationAccountId: accountId, + expectedActiveApiKeys: 2, + expectedDatabase: "postgres", + expectedState: databaseStateExpectation(valid), + }), + ).toMatchObject({ latestMigration: "007", ready: true }); + + const missing = { + ...valid, + triggers: valid.triggers.filter( + ({ triggerName }) => triggerName !== "external_snapshots_immutable", + ), + }; + expect(() => + assessRestoredDatabaseEvidence(missing, { + expectedMigrations: missing.migrations, + installationAccountId: accountId, + expectedActiveApiKeys: 2, + expectedDatabase: "postgres", + expectedState: databaseStateExpectation(missing), + }), + ).toThrow(/restore validation/i); + }); + it("rejects the superseded classification-trigger function body after migration 010", () => { const accountId = randomUUID(); const checksums = Array.from({ length: 10 }, (_, index) =>