From eba9f15539a42df0633193b007ce9da4b37bd541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 27 Jul 2026 14:19:29 +0200 Subject: [PATCH] test(integrations): prove the leg backfill against rows that carry the old value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A migration container starts empty, so applying 0281 there proves the statements parse and nothing else — the backfill's WHERE matches no rows and a broken expression passes green. Every self-hoster runs it against rows that do carry a leg, and those are the only rows it exists for. Seed the pre-migration shape, run the migration's own UPDATE read out of the committed file, and assert an attributed row comes out still knowing which leg broke, an unattributed one is left alone, and a second run does not nest the array. --- .../integration-failing-legs-backfill.test.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 tests/integration/integration-failing-legs-backfill.test.ts diff --git a/tests/integration/integration-failing-legs-backfill.test.ts b/tests/integration/integration-failing-legs-backfill.test.ts new file mode 100644 index 000000000..d50cb01fc --- /dev/null +++ b/tests/integration/integration-failing-legs-backfill.test.ts @@ -0,0 +1,133 @@ +/** + * Migration 0281's backfill, against rows that actually carry the old value. + * + * A migration test container starts EMPTY, so `pnpm db:migrate:deploy` proves + * only that the statements parse and apply — the backfill's `WHERE failing_leg + * IS NOT NULL` matches nothing and a broken expression would sail through + * green. Every existing self-hoster runs it against rows that DO carry a leg, + * and those are the only rows it exists for: a connection that has been failing + * for weeks must come out of the migration still remembering which leg broke, + * or the deploy silently forgives every live failure on the instance. + * + * The statement is read out of the migration file rather than restated here, so + * this cannot drift into testing a copy that no longer matches what ships. + * + * On the raw-SQL convention: every value-bearing statement below binds through + * positional `$N` placeholders. The one spliced string is the migration's own + * `UPDATE`, read from a committed file in this repository — a file literal, not + * an input, and the exact text the deploy runs. + */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { beforeEach, afterEach, describe, expect, it } from "vitest"; + +import { getPrismaClient, truncateAllTables } from "./setup"; + +const MIGRATION_SQL = readFileSync( + join( + process.cwd(), + "prisma/migrations/0281_integration_failing_legs_and_healthkit_trigger/migration.sql", + ), + "utf8", +); + +/** + * The one `UPDATE` in the migration, lifted verbatim. Anchoring on the + * statement keyword rather than a line number keeps the extraction stable + * against comment edits above it. + */ +function backfillStatement(): string { + const start = MIGRATION_SQL.indexOf('UPDATE "integration_statuses"'); + expect(start).toBeGreaterThan(-1); + const end = MIGRATION_SQL.indexOf(";", start); + expect(end).toBeGreaterThan(start); + return MIGRATION_SQL.slice(start, end + 1); +} + +const USER_ID = "user-failing-legs-backfill"; + +async function seedUser() { + await getPrismaClient().user.create({ + data: { + id: USER_ID, + username: "backfill-subject", + passwordHash: "x", + }, + }); +} + +describe("migration 0281 — failing_leg backfill", () => { + beforeEach(async () => { + const client = getPrismaClient(); + await truncateAllTables(client); + // Re-create the pre-migration column so a row can be seeded in the shape + // the migration will actually meet on a live instance. + await client.$executeRawUnsafe( + 'ALTER TABLE "integration_statuses" ADD COLUMN "failing_leg" TEXT', + ); + await seedUser(); + }); + + afterEach(async () => { + await getPrismaClient().$executeRawUnsafe( + 'ALTER TABLE "integration_statuses" DROP COLUMN IF EXISTS "failing_leg"', + ); + }); + + async function seedRow(integration: string, failingLeg: string | null) { + await getPrismaClient().$executeRawUnsafe( + `INSERT INTO "integration_statuses" + (id, user_id, integration, state, failing_leg, failing_legs, created_at, updated_at) + VALUES ($1, $2, $3, 'error_transient', $4, NULL, NOW(), NOW())`, + `st-${integration}`, + USER_ID, + integration, + failingLeg, + ); + } + + async function readLegs(integration: string) { + const rows = await getPrismaClient().$queryRawUnsafe< + Array<{ failing_legs: unknown }> + >( + 'SELECT failing_legs FROM "integration_statuses" WHERE integration = $1', + integration, + ); + return rows[0]?.failing_legs; + } + + it("carries an attributed leg forward as a one-element set", async () => { + await seedRow("withings", "sleep"); + + await getPrismaClient().$executeRawUnsafe(backfillStatement()); + + // The whole point of the migration: an instance that has been failing on + // its sleep leg for weeks still knows that after the deploy. Losing it + // would let the next healthy leg clear the error on the first cron tick. + expect(await readLegs("withings")).toEqual(["sleep"]); + }); + + it("leaves an unattributed row alone", async () => { + await seedRow("oura", null); + + await getPrismaClient().$executeRawUnsafe(backfillStatement()); + + // NULL and `[]` both read as "failing, but unattributed" — the code's + // pre-existing semantics, under which any success clears. Writing `[]` + // here would be churn that changes nothing. + expect(await readLegs("oura")).toBeNull(); + }); + + it("is safe to run twice", async () => { + await seedRow("polar", "vitals"); + + const statement = backfillStatement(); + await getPrismaClient().$executeRawUnsafe(statement); + await getPrismaClient().$executeRawUnsafe(statement); + + // A re-run must not nest the array into `[["vitals"]]`. Migrations get + // re-applied by hand more often than anyone plans for. + expect(await readLegs("polar")).toEqual(["vitals"]); + }); +});