Prove the ECG ingest, the dedupe and the migration on dirty data

A round trip through the real handlers: post a recording, read it back
through the list and the detail routes, and check what a client actually
receives rather than a fixture rebuilt at each end. Post it twice and
the second call says updated, not inserted, and the table does not grow.
Feed the same strip to the export-archive importer afterwards and it
says skipped; feed it to the importer first and the route says
duplicate. Both directions leave one row.

The migration test drops the index, manufactures the collisions the
resolution step exists for, and runs the migration file through the same
executor the deploy uses. It checks which row survived each tiebreak,
that the salvaged event link followed the keeper, that rows differing
only by user, source or sampling rate were left alone, and that the
constraint rejects a new twin afterwards. Applying the migration to the
empty testcontainer proves none of that.

The Withings ECG tests now hand the transaction the same delegate the
shared writer uses, so they still watch the real write instead of a stub
standing in for it.
This commit is contained in:
Marc-André Bombeck committed 2026-08-01 19:26:47 +02:00
1 parent 0e301b580c
commit 8f90fd3174
4 files changed
+882 -13

No files matched your search

@@ -0,0 +1,163 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/withings/ecg-waveform-codec", () => ({
encryptWaveformToBytes: vi.fn(),
}));
import { encryptWaveformToBytes } from "@/lib/withings/ecg-waveform-codec";
import {
ecgDurationSeconds,
persistEcgRecording,
} from "@/lib/ecg/persist-recording";
function fakeDb() {
const findUnique = vi.fn();
const upsert = vi.fn();
const $transaction = vi.fn(
async (fn: (tx: unknown) => unknown) =>
await fn({ ecgRecording: { findUnique, upsert } }),
);
return { db: { $transaction }, findUnique, upsert, $transaction };
}
function input(overrides: Record<string, unknown> = {}) {
return {
userId: "user-1",
source: "APPLE_HEALTH" as const,
externalRecordingId: "hk-uuid-1",
recordedAt: new Date("2026-07-18T06:14:03.000Z"),
samples: [12, -7, 3],
samplingFrequency: 512,
...overrides,
};
}
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(encryptWaveformToBytes).mockImplementation(
() => new Uint8Array(new ArrayBuffer(4)),
);
});
describe("ecgDurationSeconds", () => {
it("divides the sample count by the sampling rate", () => {
expect(ecgDurationSeconds(15_360, 512)).toBe(30);
});
it("is null when the source reported no sampling rate", () => {
expect(ecgDurationSeconds(15_360, 0)).toBeNull();
});
});
describe("persistEcgRecording", () => {
it("fails closed on a crypto error without ever opening a write transaction", async () => {
vi.mocked(encryptWaveformToBytes).mockImplementation(() => {
throw new Error("no encryption key");
});
const { db, $transaction } = fakeDb();
await expect(persistEcgRecording(input(), db as never)).rejects.toThrow(
"no encryption key",
);
expect($transaction).not.toHaveBeenCalled();
});
it("inserts when neither identity matches an existing row", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValue(null);
upsert.mockResolvedValue({ id: "row-new" });
const result = await persistEcgRecording(input(), db as never);
expect(result).toMatchObject({
outcome: "inserted",
id: "row-new",
sampleCount: 3,
});
expect(upsert).toHaveBeenCalledTimes(1);
});
it("updates in place when the source id is already stored", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValueOnce({ id: "row-existing" });
upsert.mockResolvedValue({ id: "row-existing" });
const result = await persistEcgRecording(input(), db as never);
expect(result.outcome).toBe("updated");
expect(result.id).toBe("row-existing");
// The source-id lookup answered; the recording-identity lookup is not
// needed and must not run.
expect(findUnique).toHaveBeenCalledTimes(1);
});
it("writes nothing when the recording is already stored under another id", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique
.mockResolvedValueOnce(null)
.mockResolvedValueOnce({ id: "row-from-the-archive" });
const result = await persistEcgRecording(input(), db as never);
expect(result.outcome).toBe("duplicate");
expect(result.id).toBe("row-from-the-archive");
expect(upsert).not.toHaveBeenCalled();
});
it("looks the twin up by the recording's own identity, not by the source id", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValue(null);
upsert.mockResolvedValue({ id: "row-new" });
await persistEcgRecording(input(), db as never);
expect(findUnique.mock.calls[1][0].where).toEqual({
userId_source_recordedAt_samplingFrequency: {
userId: "user-1",
source: "APPLE_HEALTH",
recordedAt: new Date("2026-07-18T06:14:03.000Z"),
samplingFrequency: 512,
},
});
});
it("builds the row field by field and narrows the owner to the caller's user", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValue(null);
upsert.mockResolvedValue({ id: "row-new" });
await persistEcgRecording(
input({
lead: "I",
averageHeartRate: 64,
rhythmClassification: "IRREGULAR",
}),
db as never,
);
const created = upsert.mock.calls[0][0].create;
expect(created).toMatchObject({
userId: "user-1",
source: "APPLE_HEALTH",
externalRecordingId: "hk-uuid-1",
samplingFrequency: 512,
sampleCount: 3,
lead: "I",
averageHeartRate: 64,
rhythmClassification: "IRREGULAR",
measurementId: null,
});
expect(created.durationSeconds).toBeCloseTo(3 / 512, 10);
});
it("leaves no plaintext samples behind in the caller's array", async () => {
const { db, findUnique, upsert } = fakeDb();
findUnique.mockResolvedValue(null);
upsert.mockResolvedValue({ id: "row-new" });
const payload = input();
await persistEcgRecording(payload, db as never);
expect(payload.samples).toEqual([0, 0, 0]);
});
});
+32 -13
View File
@@ -8,19 +8,31 @@
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/lib/db", () => ({
prisma: {
measurement: {
upsert: vi.fn(),
// The waveform write goes through the shared writer
// (`@/lib/ecg/persist-recording`), which opens a transaction and looks the
// recording up under both identities before upserting. The mock hands the
// transaction the same `ecgRecording` delegate, so these tests still watch the
// real assembly rather than a stub standing in for it.
vi.mock("@/lib/db", () => {
const ecgRecording = {
upsert: vi.fn(),
findUnique: vi.fn(),
};
return {
prisma: {
measurement: {
upsert: vi.fn(),
},
ecgRecording,
withingsConnection: {
findUnique: vi.fn(),
},
$transaction: vi.fn(
async (fn: (tx: unknown) => unknown) => await fn({ ecgRecording }),
),
},
ecgRecording: {
upsert: vi.fn(),
},
withingsConnection: {
findUnique: vi.fn(),
},
},
}));
};
});
vi.mock("../ecg-waveform-codec", () => ({
encryptWaveformToBytes: vi.fn((samples: number[]) =>
@@ -126,7 +138,14 @@ beforeEach(() => {
vi.mocked(prisma.measurement.upsert).mockResolvedValue({
id: "m-1",
} as never);
vi.mocked(prisma.ecgRecording.upsert).mockResolvedValue({} as never);
vi.mocked(prisma.ecgRecording.findUnique).mockResolvedValue(null as never);
vi.mocked(prisma.ecgRecording.upsert).mockResolvedValue({
id: "ecg-1",
} as never);
vi.mocked(prisma.$transaction).mockImplementation(
(async (fn: (tx: unknown) => unknown) =>
await fn({ ecgRecording: prisma.ecgRecording })) as never,
);
});
afterEach(() => {
@@ -0,0 +1,365 @@
/**
* `POST /api/insights/ecg` end to end against real Postgres.
*
* Everything here goes through the actual route handlers — the ingest, the
* metadata list, and the waveform detail — because the thing worth proving is
* the assembly between them, not that each end works in isolation. What a
* client receives is read out of the real GET responses, never rebuilt from a
* fixture.
*
* The cross-door case is the point of the second unique index: the same
* physical recording reaching the export-archive importer and the live route
* carries two different source ids, and must still be one row.
*/
import { NextRequest } from "next/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { cookieJar, headerJar } from "./mock-next-headers";
import { getPrismaClient, truncateAllTables } from "./setup";
process.env.ENCRYPTION_KEY ??=
"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
vi.mock("next/headers", async () => {
const { cookieJar, headerJar } = await import("./mock-next-headers");
return {
headers: vi.fn(async () => ({
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
})),
cookies: vi.fn(async () => ({
get: (name: string) => {
const value = cookieJar.get(name);
return value ? { name, value } : undefined;
},
set: (name: string, value: string) => {
cookieJar.set(name, value);
},
delete: (name: string) => {
cookieJar.delete(name);
},
})),
};
});
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
import {
GET as getEcgList,
POST as postEcg,
} from "@/app/api/insights/ecg/route";
import { GET as getEcgDetail } from "@/app/api/insights/ecg/[id]/route";
import { importAppleHealthEcg } from "@/lib/apple-health/ecg-import";
const USER_ID = "ecg-ingest-roundtrip";
const OTHER_USER_ID = "ecg-ingest-roundtrip-other";
const RECORDED_AT = "2026-07-18T08:14:03+02:00";
const SAMPLES = [12, -7, 3, 240, -180, 60];
type IngestBody = Record<string, unknown>;
function ingestPayload(overrides: IngestBody = {}): IngestBody {
return {
externalRecordingId: "8E1F3B0C-0000-4000-8000-000000000001",
recordedAt: RECORDED_AT,
samplingFrequency: 512,
samples: [...SAMPLES],
lead: "I",
averageHeartRate: 64,
classification: "NOT_DETECTED",
source: "APPLE_HEALTH",
...overrides,
};
}
async function ingest(body: IngestBody): Promise<{
status: number;
data: {
id: string;
status: string;
recordedAt: string;
sampleCount: number;
durationSeconds: number | null;
};
}> {
const response = await postEcg(
new NextRequest("http://localhost/api/insights/ecg", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}),
);
return {
status: response.status,
data: (await response.json()).data,
};
}
async function readList(): Promise<{
recordings: Array<Record<string, unknown>>;
hasRecordings: boolean;
}> {
// The list handler declares no parameters; the wrapper still receives the
// request, so hand it one the way the route's own unit test does.
const callList = getEcgList as unknown as (
request: NextRequest,
) => Promise<Response>;
const response = await callList(
new NextRequest("http://localhost/api/insights/ecg"),
);
expect(response.status).toBe(200);
return (await response.json()).data;
}
async function readDetail(
id: string,
full = false,
): Promise<{ response: Response; body: Record<string, unknown> }> {
const url = full
? `http://localhost/api/insights/ecg/${id}?full=1`
: `http://localhost/api/insights/ecg/${id}`;
const response = await getEcgDetail(new NextRequest(url), {
params: Promise.resolve({ id }),
});
return { response, body: (await response.json()).data };
}
async function signIn(userId: string): Promise<void> {
const session = await getPrismaClient().session.create({
data: { userId, expiresAt: new Date(Date.now() + 60 * 60 * 1000) },
});
cookieJar.set("healthlog_session", session.id);
}
beforeEach(async () => {
const prisma = getPrismaClient();
await truncateAllTables(prisma);
cookieJar.clear();
headerJar.clear();
await prisma.user.createMany({
data: [
{
id: USER_ID,
username: "ecg-roundtrip",
email: "ecg-roundtrip@example.test",
timezone: "Europe/Berlin",
},
{
id: OTHER_USER_ID,
username: "ecg-roundtrip-other",
email: "ecg-roundtrip-other@example.test",
timezone: "Europe/Berlin",
},
],
});
await signIn(USER_ID);
});
describe("POST /api/insights/ecg — round trip through the real routes", () => {
it("stores a posted recording and serves it back through the list and the detail", async () => {
const posted = await ingest(ingestPayload());
expect(posted.status).toBe(201);
expect(posted.data.status).toBe("inserted");
expect(posted.data.sampleCount).toBe(SAMPLES.length);
expect(posted.data.durationSeconds).toBeCloseTo(SAMPLES.length / 512, 10);
const list = await readList();
expect(list.hasRecordings).toBe(true);
expect(list.recordings).toHaveLength(1);
expect(list.recordings[0]).toMatchObject({
id: posted.data.id,
samplingFrequency: 512,
sampleCount: SAMPLES.length,
averageHeartRate: 64,
lead: "I",
classification: "NOT_DETECTED",
source: "APPLE_HEALTH",
hasWaveform: true,
});
expect(
new Date(list.recordings[0].recordedAt as string).toISOString(),
).toBe(new Date(RECORDED_AT).toISOString());
const detail = await readDetail(posted.data.id, true);
expect(detail.response.status).toBe(200);
expect(detail.body.samples).toEqual(SAMPLES);
expect(detail.body.decimated).toBe(false);
expect(detail.body.classification).toBe("NOT_DETECTED");
});
it("never writes the waveform as plaintext", async () => {
const posted = await ingest(ingestPayload());
const row = await getPrismaClient().ecgRecording.findUniqueOrThrow({
where: { id: posted.data.id },
});
const stored = Buffer.from(row.waveformEncrypted).toString("utf8");
for (const sample of SAMPLES) {
expect(stored).not.toContain(String(sample));
}
expect(stored).not.toContain("[12,-7,3");
});
it("reports a re-post as updated and does not grow the table", async () => {
const first = await ingest(ingestPayload());
const second = await ingest(ingestPayload());
expect(second.status).toBe(200);
expect(second.data.status).toBe("updated");
expect(second.data.id).toBe(first.data.id);
expect(
await getPrismaClient().ecgRecording.count({
where: { userId: USER_ID },
}),
).toBe(1);
expect((await readList()).recordings).toHaveLength(1);
});
it("overwrites the stored recording in place when a re-post revises it", async () => {
const first = await ingest(ingestPayload());
const revised = [1, 2, 3, 4];
const second = await ingest(
ingestPayload({ samples: revised, classification: "IRREGULAR" }),
);
expect(second.data.status).toBe("updated");
expect(second.data.id).toBe(first.data.id);
const detail = await readDetail(first.data.id, true);
expect(detail.body.samples).toEqual(revised);
expect(detail.body.classification).toBe("IRREGULAR");
});
it("keeps one row when the same recording also arrives through the export archive", async () => {
const posted = await ingest(ingestPayload());
// The archive importer identifies the strip by a hash of its content, so
// it arrives under a completely different externalRecordingId.
const outcome = await importAppleHealthEcg({
userId: USER_ID,
ecg: {
recordedAt: new Date(RECORDED_AT),
samplingFrequency: 512,
samples: [...SAMPLES],
lead: "I",
averageHeartRate: 64,
rhythmClassification: "NOT_DETECTED",
},
prisma: getPrismaClient(),
});
expect(outcome).toBe("skipped");
const rows = await getPrismaClient().ecgRecording.findMany({
where: { userId: USER_ID },
});
expect(rows).toHaveLength(1);
expect(rows[0].id).toBe(posted.data.id);
expect((await readList()).recordings).toHaveLength(1);
});
it("reports duplicate when the export archive got there first", async () => {
await importAppleHealthEcg({
userId: USER_ID,
ecg: {
recordedAt: new Date(RECORDED_AT),
samplingFrequency: 512,
samples: [...SAMPLES],
lead: "I",
averageHeartRate: 64,
rhythmClassification: "NOT_DETECTED",
},
prisma: getPrismaClient(),
});
const archived = await getPrismaClient().ecgRecording.findFirstOrThrow({
where: { userId: USER_ID },
});
const posted = await ingest(ingestPayload());
expect(posted.status).toBe(200);
expect(posted.data.status).toBe("duplicate");
expect(posted.data.id).toBe(archived.id);
expect(
await getPrismaClient().ecgRecording.count({
where: { userId: USER_ID },
}),
).toBe(1);
// Nothing was written: the archive's own external id still owns the row.
expect(
(
await getPrismaClient().ecgRecording.findUniqueOrThrow({
where: { id: archived.id },
})
).externalRecordingId,
).toBe(archived.externalRecordingId);
});
it("stores distinct recordings distinctly", async () => {
await ingest(ingestPayload());
const other = await ingest(
ingestPayload({
externalRecordingId: "8E1F3B0C-0000-4000-8000-000000000002",
recordedAt: "2026-07-19T08:14:03+02:00",
}),
);
expect(other.data.status).toBe("inserted");
expect((await readList()).recordings).toHaveLength(2);
});
it("writes to the session's user and cannot reach another account's rows", async () => {
const mine = await ingest(ingestPayload());
cookieJar.clear();
await signIn(OTHER_USER_ID);
// Same recording id, same instant — a different account entirely.
const theirs = await ingest(ingestPayload());
expect(theirs.status).toBe(201);
expect(theirs.data.status).toBe("inserted");
expect(theirs.data.id).not.toBe(mine.data.id);
expect((await readList()).recordings).toHaveLength(1);
const foreign = await readDetail(mine.data.id, true);
expect(foreign.response.status).toBe(404);
const owners = await getPrismaClient().ecgRecording.findMany({
select: { userId: true },
});
expect(owners.map((r) => r.userId).sort()).toEqual(
[USER_ID, OTHER_USER_ID].sort(),
);
});
it("refuses a source a client is not allowed to claim", async () => {
const response = await postEcg(
new NextRequest("http://localhost/api/insights/ecg", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(ingestPayload({ source: "WITHINGS" })),
}),
);
expect(response.status).toBe(422);
expect(await getPrismaClient().ecgRecording.count()).toBe(0);
});
it("refuses a userId in the body rather than honouring it", async () => {
const response = await postEcg(
new NextRequest("http://localhost/api/insights/ecg", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(ingestPayload({ userId: OTHER_USER_ID })),
}),
);
expect(response.status).toBe(422);
expect(await getPrismaClient().ecgRecording.count()).toBe(0);
});
it("refuses an event verdict that cannot occur on an ECG", async () => {
const response = await postEcg(
new NextRequest("http://localhost/api/insights/ecg", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(ingestPayload({ classification: "VERY_LOW" })),
}),
);
expect(response.status).toBe(422);
expect(await getPrismaClient().ecgRecording.count()).toBe(0);
});
});
@@ -0,0 +1,322 @@
/**
* Migration `0291_ecg_recording_identity` against a table that ALREADY holds
* colliding rows.
*
* The testcontainer starts empty, so applying this migration during global
* setup proves nothing about the deployed instances it will actually run on:
* two writers have been filling `ecg_recordings` since v1.19.0, and a unique
* index that meets a pre-existing duplicate aborts the migration on boot. So
* this test drops the index, manufactures the collisions the resolution step
* exists for, and runs the REAL migration file through the Prisma CLI — the
* same executor `db:migrate:deploy` uses — then checks which row survived and
* that the constraint is live afterwards.
*/
import { execFileSync } from "node:child_process";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
const PROJECT_ROOT = resolve(
dirname(fileURLToPath(import.meta.url)),
"..",
"..",
);
const MIGRATION_FILE = join(
PROJECT_ROOT,
"prisma",
"migrations",
"0291_ecg_recording_identity",
"migration.sql",
);
const INDEX_NAME = "ecg_recordings_user_source_recorded_at_freq_key";
const USER_ID = "ecg-identity-migration";
function applyMigration(): void {
// Same executor and same connection `db:migrate:deploy` uses; the URL comes
// from the child environment exactly as it does for the real deploy.
execFileSync(
"pnpm",
["exec", "prisma", "db", "execute", "--file", MIGRATION_FILE],
{
cwd: PROJECT_ROOT,
stdio: "pipe",
env: { ...process.env, DATABASE_URL: process.env.DATABASE_URL },
},
);
}
async function dropIndex(): Promise<void> {
await getPrismaClient().$executeRawUnsafe(
`DROP INDEX IF EXISTS "${INDEX_NAME}"`,
);
}
async function indexExists(): Promise<boolean> {
const rows = await getPrismaClient().$queryRaw<Array<{ indexname: string }>>`
SELECT indexname FROM pg_indexes
WHERE tablename = 'ecg_recordings' AND indexname = ${INDEX_NAME}
`;
return rows.length === 1;
}
/**
* Insert a row directly. `waveform_encrypted` is opaque to the migration, so
* a marker byte string is enough and keeps the fixture honest about what the
* migration does and does not read.
*/
async function insertRecording(input: {
id: string;
externalRecordingId: string;
recordedAt: string;
samplingFrequency: number;
sampleCount: number;
measurementId?: string | null;
updatedAt: string;
}): Promise<void> {
await getPrismaClient().$executeRawUnsafe(
`INSERT INTO ecg_recordings
(id, user_id, source, external_recording_id, recorded_at,
waveform_encrypted, sampling_frequency, sample_count, duration_seconds,
measurement_id, created_at, updated_at)
VALUES ($1, $2, 'APPLE_HEALTH', $3, $4::timestamptz, $5, $6, $7, $8, $9,
$10::timestamptz, $10::timestamptz)`,
input.id,
USER_ID,
input.externalRecordingId,
input.recordedAt,
Buffer.from(`ciphertext-${input.id}`, "utf8"),
input.samplingFrequency,
input.sampleCount,
input.sampleCount / input.samplingFrequency,
input.measurementId ?? null,
input.updatedAt,
);
}
async function createMeasurement(
id: string,
measuredAt: string,
): Promise<string> {
const row = await getPrismaClient().measurement.create({
data: {
id,
userId: USER_ID,
type: "IRREGULAR_RHYTHM_NOTIFICATION",
value: 1,
unit: "event",
measuredAt: new Date(measuredAt),
source: "APPLE_HEALTH",
},
select: { id: true },
});
return row.id;
}
beforeEach(async () => {
const prisma = getPrismaClient();
await truncateAllTables(prisma);
await prisma.user.create({
data: {
id: USER_ID,
username: "ecg-identity-migration",
email: "ecg-identity-migration@example.test",
timezone: "UTC",
},
});
});
afterEach(async () => {
// Whatever this test did to the schema, the next file gets the migrated one.
if (!(await indexExists())) applyMigration();
});
describe("0291_ecg_recording_identity against a table that already has duplicates", () => {
it("resolves the duplicates, keeps the canonical row, and leaves the index enforcing", async () => {
const prisma = getPrismaClient();
await dropIndex();
expect(await indexExists()).toBe(false);
const eventId = await createMeasurement(
"ecg-identity-event-1",
"2026-07-18T06:14:03.000Z",
);
// Partition 1 — the same recording under two source ids. The richer strip
// has no event link; the thin one does, and its link must survive.
await insertRecording({
id: "p1-keeper-more-samples",
externalRecordingId: "apple-health:ecg:content-hash",
recordedAt: "2026-07-18T06:14:03Z",
samplingFrequency: 512,
sampleCount: 15_360,
updatedAt: "2026-07-18T07:00:00Z",
});
await insertRecording({
id: "p1-loser-fewer-samples",
externalRecordingId: "8E1F3B0C-0000-4000-8000-000000000001",
recordedAt: "2026-07-18T06:14:03Z",
samplingFrequency: 512,
sampleCount: 12,
measurementId: eventId,
updatedAt: "2026-07-18T09:00:00Z",
});
// Partition 2 — equal strips, one linked to its event row.
await insertRecording({
id: "p2-unlinked",
externalRecordingId: "p2-a",
recordedAt: "2026-07-19T06:14:03Z",
samplingFrequency: 512,
sampleCount: 512,
updatedAt: "2026-07-19T09:00:00Z",
});
const secondEventId = await createMeasurement(
"ecg-identity-event-2",
"2026-07-19T06:14:03.000Z",
);
await insertRecording({
id: "p2-linked",
externalRecordingId: "p2-b",
recordedAt: "2026-07-19T06:14:03Z",
samplingFrequency: 512,
sampleCount: 512,
measurementId: secondEventId,
updatedAt: "2026-07-19T07:00:00Z",
});
// Partition 3 — indistinguishable but for when they were last written.
await insertRecording({
id: "p3-stale",
externalRecordingId: "p3-a",
recordedAt: "2026-07-20T06:14:03Z",
samplingFrequency: 512,
sampleCount: 512,
updatedAt: "2026-07-20T07:00:00Z",
});
await insertRecording({
id: "p3-fresh",
externalRecordingId: "p3-b",
recordedAt: "2026-07-20T06:14:03Z",
samplingFrequency: 512,
sampleCount: 512,
updatedAt: "2026-07-20T09:00:00Z",
});
// A recording that collides with nothing must be left exactly as it is.
await insertRecording({
id: "p4-untouched",
externalRecordingId: "p4-a",
recordedAt: "2026-07-21T06:14:03Z",
samplingFrequency: 512,
sampleCount: 512,
updatedAt: "2026-07-21T07:00:00Z",
});
expect(await prisma.ecgRecording.count()).toBe(7);
applyMigration();
expect(await indexExists()).toBe(true);
const survivors = await prisma.ecgRecording.findMany({
orderBy: { recordedAt: "asc" },
select: { id: true, measurementId: true, externalRecordingId: true },
});
expect(survivors.map((row) => row.id)).toEqual([
"p1-keeper-more-samples",
"p2-linked",
"p3-fresh",
"p4-untouched",
]);
// The loser's event link was salvaged onto the keeper.
expect(survivors[0].measurementId).toBe(eventId);
expect(survivors[1].measurementId).toBe(secondEventId);
expect(survivors[3].externalRecordingId).toBe("p4-a");
// The event rows themselves are untouched — resolving a duplicate
// recording never deletes a measurement.
expect(await prisma.measurement.count({ where: { userId: USER_ID } })).toBe(
2,
);
// And the constraint is live: the collision cannot come back.
await expect(
insertRecording({
id: "post-migration-twin",
externalRecordingId: "another-id-entirely",
recordedAt: "2026-07-21T06:14:03Z",
samplingFrequency: 512,
sampleCount: 512,
updatedAt: "2026-07-21T10:00:00Z",
}),
).rejects.toThrow();
expect(await prisma.ecgRecording.count()).toBe(4);
});
it("leaves rows alone that only look alike across users, sources or sampling rates", async () => {
const prisma = getPrismaClient();
await dropIndex();
const otherUser = await prisma.user.create({
data: {
username: "ecg-identity-migration-other",
email: "ecg-identity-migration-other@example.test",
},
});
await insertRecording({
id: "same-instant-apple",
externalRecordingId: "a",
recordedAt: "2026-07-18T06:14:03Z",
samplingFrequency: 512,
sampleCount: 512,
updatedAt: "2026-07-18T07:00:00Z",
});
// Same instant, different sampling rate — a different recording.
await insertRecording({
id: "same-instant-other-rate",
externalRecordingId: "b",
recordedAt: "2026-07-18T06:14:03Z",
samplingFrequency: 300,
sampleCount: 300,
updatedAt: "2026-07-18T07:00:00Z",
});
// Same instant and rate, but a different source.
await prisma.$executeRawUnsafe(
`INSERT INTO ecg_recordings
(id, user_id, source, external_recording_id, recorded_at,
waveform_encrypted, sampling_frequency, sample_count, created_at, updated_at)
VALUES ('same-instant-withings', $1, 'WITHINGS', 'c',
'2026-07-18T06:14:03Z'::timestamptz, $2, 512, 512, NOW(), NOW())`,
USER_ID,
Buffer.from("ciphertext-withings", "utf8"),
);
// Same instant and rate, but somebody else's account.
await prisma.$executeRawUnsafe(
`INSERT INTO ecg_recordings
(id, user_id, source, external_recording_id, recorded_at,
waveform_encrypted, sampling_frequency, sample_count, created_at, updated_at)
VALUES ('same-instant-other-user', $1, 'APPLE_HEALTH', 'd',
'2026-07-18T06:14:03Z'::timestamptz, $2, 512, 512, NOW(), NOW())`,
otherUser.id,
Buffer.from("ciphertext-other-user", "utf8"),
);
applyMigration();
expect(await indexExists()).toBe(true);
const ids = (
await prisma.ecgRecording.findMany({ select: { id: true } })
).map((row) => row.id);
expect(ids.sort()).toEqual(
[
"same-instant-apple",
"same-instant-other-rate",
"same-instant-other-user",
"same-instant-withings",
].sort(),
);
});
});