From 6cc3de8248a24e15bf9f233d33c2b5be0490d7bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 29 Jul 2026 14:20:56 +0200 Subject: [PATCH] test(01-12): add failing Apple ECG import contracts - pin bounded path-safe archive and parser behavior - require encrypted idempotent fail-soft persistence and WHOOP regressions --- .../__tests__/apple-health-import-ecg.test.ts | 63 +++ .../__tests__/apple-health-ecg-import.test.ts | 373 ++++++++++++++++++ .../source-priority-whoop-applehealth.test.ts | 50 +++ .../whoop/__tests__/sync-user-whoop.test.ts | 39 ++ .../apple-health-import-ecg.test.ts | 365 +++++++++++++++++ 5 files changed, 890 insertions(+) create mode 100644 src/lib/jobs/__tests__/apple-health-import-ecg.test.ts create mode 100644 src/lib/measurements/__tests__/apple-health-ecg-import.test.ts create mode 100644 tests/integration/apple-health-import-ecg.test.ts diff --git a/src/lib/jobs/__tests__/apple-health-import-ecg.test.ts b/src/lib/jobs/__tests__/apple-health-import-ecg.test.ts new file mode 100644 index 000000000..5c0cac646 --- /dev/null +++ b/src/lib/jobs/__tests__/apple-health-import-ecg.test.ts @@ -0,0 +1,63 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +import { APPLE_HEALTH_IMPORT_PARSER_REVISION } from "../apple-health-import-worker"; + +const workerSource = readFileSync( + join(process.cwd(), "src/lib/jobs/apple-health-import-worker.ts"), + "utf8", +); +const parserSource = readFileSync( + join(process.cwd(), "src/lib/measurements/import-apple-health-export.ts"), + "utf8", +); + +describe("Apple Health import worker — ECG auxiliary branch contract", () => { + it("bumps the parser revision so completed revision-2 archives reprocess", () => { + expect(APPLE_HEALTH_IMPORT_PARSER_REVISION).toBeGreaterThan(2); + }); + + it("runs bounded ECG archive ingestion beside the established XML parser", () => { + expect(workerSource).toMatch(/streamAppleHealthEcgMembers/); + expect(workerSource).toMatch(/parseAppleHealthEcgCsv/); + expect(workerSource).toMatch(/importAppleHealthEcg/); + expect(workerSource).toMatch(/streamParseExportXml/); + }); + + it("catches per-ECG failures without routing the valid XML result to the fatal job catch", () => { + expect(workerSource).toMatch( + /for await[\s\S]*streamAppleHealthEcgMembers[\s\S]*try[\s\S]*parseAppleHealthEcgCsv[\s\S]*catch[\s\S]*ecg\.failed/, + ); + expect(workerSource).toMatch( + /result[\s\S]*ecg:\s*\{[\s\S]*discovered[\s\S]*imported[\s\S]*updated[\s\S]*skipped[\s\S]*failed/, + ); + }); + + it("keeps progress bounded to scalar ECG counts", () => { + expect(parserSource).toMatch( + /ecg:\s*\{[\s\S]*discovered:\s*number[\s\S]*imported:\s*number[\s\S]*updated:\s*number[\s\S]*skipped:\s*number[\s\S]*failed:\s*number/, + ); + expect(parserSource).not.toMatch( + /ecg:\s*\{[^}]*?(?:samples|waveform|filename|sourceName|healthValue):/, + ); + }); + + it("does not log ECG filenames, source labels, samples, or raw parser errors", () => { + const loggingCalls = + workerSource.match(/console\.(?:log|warn|error)\([\s\S]{0,250}\)/g) ?? []; + const ecgLogging = loggingCalls.filter((call) => + /ecg|electrocardiogram/i.test(call), + ); + + expect(ecgLogging.join("\n")).not.toMatch( + /member|filename|sourceName|sample|waveform|\$\{(?:err|error)\}/i, + ); + }); + + it("does not claim to diagnose or reclassify a waveform", () => { + expect(workerSource).not.toMatch( + /(?:diagnose|infer|classify)Ecg|analy[sz]eWaveform|detectA(?:fib|trial)/i, + ); + }); +}); diff --git a/src/lib/measurements/__tests__/apple-health-ecg-import.test.ts b/src/lib/measurements/__tests__/apple-health-ecg-import.test.ts new file mode 100644 index 000000000..3f18c9378 --- /dev/null +++ b/src/lib/measurements/__tests__/apple-health-ecg-import.test.ts @@ -0,0 +1,373 @@ +import { afterEach, describe, expect, it, vi, type MockInstance } from "vitest"; +import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { Readable } from "node:stream"; + +import { strToU8, zipSync } from "fflate"; + +type ArchiveLimits = { + maxMembers: number; + maxEcgMembers: number; + maxMemberBytes: number; + maxTotalEcgBytes: number; + maxCompressionRatio: number; +}; + +type ArchiveMember = { + name: string; + stream: Readable; +}; + +type NormalizedEcg = { + recordedAt: Date; + samplingFrequency: number; + samples: number[]; + lead: string | null; + averageHeartRate: number | null; + rhythmClassification: "IRREGULAR" | "NOT_DETECTED" | "INCONCLUSIVE" | null; +}; + +type ArchiveSubject = { + streamAppleHealthEcgMembers(input: { + archivePath: string; + limits: ArchiveLimits; + }): AsyncIterable; +}; + +type ParserSubject = { + parseAppleHealthEcgCsv(input: { + memberName: string; + stream: Readable; + maxSamples: number; + }): Promise; +}; + +const tempDirs: string[] = []; + +const LIMITS: ArchiveLimits = { + maxMembers: 8, + maxEcgMembers: 2, + maxMemberBytes: 2_048, + maxTotalEcgBytes: 3_000, + maxCompressionRatio: 100, +}; + +function loadArchiveSubject(): Promise { + return vi.importActual("@/lib/apple-health/archive-stream"); +} + +function loadParserSubject(): Promise { + return vi.importActual("@/lib/apple-health/ecg-csv"); +} + +function archivePath(files: Record): string { + const dir = mkdtempSync(join(tmpdir(), "healthlog-ecg-contract-")); + tempDirs.push(dir); + const path = join(dir, "export.zip"); + writeFileSync( + path, + zipSync( + Object.fromEntries( + Object.entries(files).map(([name, text]) => [name, strToU8(text)]), + ), + { level: 6 }, + ), + ); + return path; +} + +function replaceSameLengthName( + archive: Buffer, + from: string, + to: string, +): Buffer { + expect(Buffer.byteLength(from)).toBe(Buffer.byteLength(to)); + const out = Buffer.from(archive); + const needle = Buffer.from(from); + const replacement = Buffer.from(to); + let cursor = 0; + while ((cursor = out.indexOf(needle, cursor)) !== -1) { + replacement.copy(out, cursor); + cursor += replacement.length; + } + return out; +} + +function mutateZipHeaders( + archive: Buffer, + memberName: string, + mutate: (header: Buffer, central: boolean) => void, +): Buffer { + const out = Buffer.from(archive); + for (let cursor = 0; cursor <= out.length - 46; cursor += 1) { + const signature = out.readUInt32LE(cursor); + const central = signature === 0x02014b50; + const local = signature === 0x04034b50; + if (!central && !local) continue; + const nameLength = out.readUInt16LE(cursor + (central ? 28 : 26)); + const nameStart = cursor + (central ? 46 : 30); + if ( + out.subarray(nameStart, nameStart + nameLength).toString("utf8") === + memberName + ) { + mutate(out.subarray(cursor), central); + } + } + return out; +} + +function validCsv(overrides: Partial> = {}): string { + const metadata = { + "Recorded Date": "2026-07-18 08:14:03 +0200", + Classification: "Sinus Rhythm", + "Average Heart Rate": "64 bpm", + "Sample Rate": "512 Hz", + ...overrides, + }; + return [ + "Name,Private Patient Name", + ...Object.entries(metadata).map(([key, value]) => `${key},${value}`), + "Lead,Voltage", + "I,0.001", + "I,-0.002", + "I,0.003", + ].join("\n"); +} + +async function collectMembers(path: string, limits = LIMITS) { + const subject = await loadArchiveSubject(); + const members: ArchiveMember[] = []; + for await (const member of subject.streamAppleHealthEcgMembers({ + archivePath: path, + limits, + })) { + members.push(member); + } + return members; +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } + vi.restoreAllMocks(); +}); + +describe("Apple Health ECG archive stream — path and resource bounds", () => { + it("streams only recognized HKElectrocardiogram CSV members", async () => { + const path = archivePath({ + "apple_health_export/export.xml": "", + "apple_health_export/electrocardiograms/ecg_2026-07-18.csv": validCsv(), + "apple_health_export/clinical-records/private.json": "{}", + }); + + const members = await collectMembers(path); + expect(members.map((member) => member.name)).toEqual([ + "apple_health_export/electrocardiograms/ecg_2026-07-18.csv", + ]); + expect(members[0].stream).toBeInstanceOf(Readable); + expect(members[0]).not.toHaveProperty("path"); + }); + + it.each([ + "../electrocardiograms/ecg.csv", + "apple_health_export/electrocardiograms/../../ecg.csv", + "/apple_health_export/electrocardiograms/ecg.csv", + "C:\\apple_health_export\\electrocardiograms\\ecg.csv", + ])("rejects traversal or absolute member name %s", async (name) => { + await expect( + collectMembers( + archivePath({ + "apple_health_export/export.xml": "", + [name]: validCsv(), + }), + ), + ).rejects.toThrow(/unsafe|path|member/i); + }); + + it("rejects duplicate normalized ECG member names", async () => { + const first = "apple_health_export/electrocardiograms/ecg-recording-a.csv"; + const second = "apple_health_export/electrocardiograms/ecg-recording-b.csv"; + const path = archivePath({ + "apple_health_export/export.xml": "", + [first]: validCsv(), + [second]: validCsv(), + }); + const duplicated = replaceSameLengthName( + Buffer.from(await import("node:fs").then((fs) => fs.readFileSync(path))), + second, + first, + ); + writeFileSync(path, duplicated); + + await expect(collectMembers(path)).rejects.toThrow(/duplicate/i); + }); + + it.each([ + [ + "encrypted", + (header: Buffer, central: boolean) => + header.writeUInt16LE(1, central ? 8 : 6), + ], + [ + "unsupported compression", + (header: Buffer, central: boolean) => + header.writeUInt16LE(99, central ? 10 : 8), + ], + ] as const)("rejects %s ECG entries", async (_label, mutate) => { + const member = "apple_health_export/electrocardiograms/ecg_2026-07-18.csv"; + const path = archivePath({ + "apple_health_export/export.xml": "", + [member]: validCsv(), + }); + const fs = await import("node:fs"); + writeFileSync( + path, + mutateZipHeaders(fs.readFileSync(path), member, mutate), + ); + + await expect(collectMembers(path)).rejects.toThrow( + /encrypted|compression|unsupported/i, + ); + }); + + it("enforces member-count and ECG-count caps before parsing", async () => { + const files: Record = { + "apple_health_export/export.xml": "", + }; + for (let index = 0; index < 4; index += 1) { + files[ + `apple_health_export/electrocardiograms/ecg_2026-07-${18 + index}.csv` + ] = validCsv(); + } + + await expect( + collectMembers(archivePath(files), { + ...LIMITS, + maxMembers: 3, + maxEcgMembers: 2, + }), + ).rejects.toThrow(/member|count|limit/i); + }); + + it("rejects declared compression-ratio bombs", async () => { + const repetitive = `${"0,".repeat(2_000)}\n`; + await expect( + collectMembers( + archivePath({ + "apple_health_export/export.xml": "", + "apple_health_export/electrocardiograms/ecg_bomb.csv": repetitive, + }), + { ...LIMITS, maxMemberBytes: 10_000, maxCompressionRatio: 2 }, + ), + ).rejects.toThrow(/compression|ratio|bomb/i); + }); + + it("enforces actual decompressed member bytes even when metadata lies", async () => { + const member = + "apple_health_export/electrocardiograms/ecg_actual_bytes.csv"; + const path = archivePath({ + "apple_health_export/export.xml": "", + [member]: `${"1,0.001\n".repeat(100)}`, + }); + const fs = await import("node:fs"); + const lied = mutateZipHeaders( + fs.readFileSync(path), + member, + (header, central) => { + if (central) header.writeUInt32LE(1, 24); + }, + ); + writeFileSync(path, lied); + + await expect( + collectMembers(path, { ...LIMITS, maxMemberBytes: 64 }), + ).rejects.toThrow(/byte|size|limit/i); + }); + + it("enforces the running total across otherwise valid ECG members", async () => { + await expect( + collectMembers( + archivePath({ + "apple_health_export/export.xml": "", + "apple_health_export/electrocardiograms/ecg_a.csv": validCsv(), + "apple_health_export/electrocardiograms/ecg_b.csv": validCsv(), + }), + { ...LIMITS, maxTotalEcgBytes: validCsv().length + 5 }, + ), + ).rejects.toThrow(/total|byte|limit/i); + }); +}); + +describe("Apple Health HKElectrocardiogram CSV parser", () => { + async function parse(csv: string, maxSamples = 8) { + const subject = await loadParserSubject(); + return subject.parseAppleHealthEcgCsv({ + memberName: "apple_health_export/electrocardiograms/ecg_2026-07-18.csv", + stream: Readable.from([Buffer.from(csv)]), + maxSamples, + }); + } + + it("normalizes a valid recording without returning patient/source labels", async () => { + const parsed = await parse(validCsv()); + expect(parsed).toMatchObject({ + recordedAt: new Date("2026-07-18T06:14:03.000Z"), + samplingFrequency: 512, + samples: [1, -2, 3], + lead: "I", + averageHeartRate: 64, + rhythmClassification: "NOT_DETECTED", + }); + expect(JSON.stringify(parsed)).not.toMatch(/Private Patient|Apple|WHOOP/i); + }); + + it.each([ + ["Sinus Rhythm", "NOT_DETECTED"], + ["Atrial Fibrillation", "IRREGULAR"], + ["Inconclusive", "INCONCLUSIVE"], + ["Localized or unknown verdict", null], + ])("passes through device classification %s only", async (raw, expected) => { + await expect( + parse(validCsv({ Classification: raw })), + ).resolves.toMatchObject({ rhythmClassification: expected }); + }); + + it.each([ + ["bad timestamp", validCsv({ "Recorded Date": "not-a-date" })], + ["bad frequency", validCsv({ "Sample Rate": "not-a-rate" })], + ["non-finite voltage", validCsv().replace("I,-0.002", "I,Infinity")], + ["missing samples", validCsv().split("Lead,Voltage")[0]], + ])("rejects malformed %s", async (_label, csv) => { + await expect(parse(csv)).rejects.toThrow(/invalid|malformed|sample|date/i); + }); + + it("stops reading once the sample cap is exceeded", async () => { + await expect(parse(validCsv(), 2)).rejects.toThrow(/sample|limit/i); + }); + + it("never logs or returns plaintext health values on failure", async () => { + const subject = await loadParserSubject(); + const spies: MockInstance[] = [ + vi.spyOn(console, "log").mockImplementation(() => {}), + vi.spyOn(console, "warn").mockImplementation(() => {}), + vi.spyOn(console, "error").mockImplementation(() => {}), + ]; + const secret = "patient-secret-waveform-0.123456"; + + let message = ""; + try { + await subject.parseAppleHealthEcgCsv({ + memberName: "apple_health_export/electrocardiograms/ecg_2026-07-18.csv", + stream: Readable.from([Buffer.from(`${validCsv()}\nI,${secret}`)]), + maxSamples: 8, + }); + } catch (error) { + message = error instanceof Error ? error.message : String(error); + } + + expect(message).not.toContain(secret); + expect(spies.every((spy) => spy.mock.calls.length === 0)).toBe(true); + }); +}); diff --git a/src/lib/sources/__tests__/source-priority-whoop-applehealth.test.ts b/src/lib/sources/__tests__/source-priority-whoop-applehealth.test.ts index d01840a97..aecd29f61 100644 --- a/src/lib/sources/__tests__/source-priority-whoop-applehealth.test.ts +++ b/src/lib/sources/__tests__/source-priority-whoop-applehealth.test.ts @@ -10,9 +10,17 @@ * summary), keep a real scale ahead of WHOOP's body-measurement estimate for * weight, and rank WHOOP's device-native Recovery above the COMPUTED proxy. */ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { pickCanonicalSourceRows } from "@/lib/analytics/source-priority"; +import { mapAppleHealthEntry } from "@/lib/measurements/apple-health-mapping"; + +const appleImporterSource = readFileSync( + join(process.cwd(), "src/lib/measurements/import-apple-health-export.ts"), + "utf8", +); function isoDayKey(d: Date): string { return d.toISOString().slice(0, 10); @@ -119,3 +127,45 @@ describe("cross-source priority — WHOOP + Apple Health", () => { expect(out.canonicalRows[0].value).toBe(71); }); }); + +describe("WHOOP-written HealthKit records remain source-agnostic", () => { + it("maps a supported HealthKit type independently of its writer label", () => { + const input = { + hkIdentifier: "HKQuantityTypeIdentifierHeartRate", + value: 64, + unit: "count/min", + startDate: "2026-07-18 08:14:03 +0200", + endDate: "2026-07-18 08:14:04 +0200", + }; + + const whoopWritten = mapAppleHealthEntry(input); + const appleWritten = mapAppleHealthEntry(input); + expect(whoopWritten).toEqual(appleWritten); + expect(whoopWritten).toMatchObject({ + type: "PULSE", + value: 64, + }); + expect(whoopWritten).not.toHaveProperty("sourceName"); + expect(whoopWritten).not.toHaveProperty("writer"); + }); + + it("persists supported export.xml rows as APPLE_HEALTH, never as a raw writer label", () => { + expect(appleImporterSource).toMatch( + /measurement\.createManyAndReturn\([\s\S]*source:\s*"APPLE_HEALTH"/, + ); + expect(appleImporterSource).not.toMatch( + /source:\s*attrs\.(?:sourceName|device)/, + ); + }); + + it("does not treat WHOOP writer metadata as evidence of a WHOOP ECG export", () => { + const ecgMapping = mapAppleHealthEntry({ + hkIdentifier: "HKElectrocardiogram", + value: 1, + unit: "count", + startDate: "2026-07-18 08:14:03 +0200", + endDate: "2026-07-18 08:14:04 +0200", + }); + expect(ecgMapping).toBeNull(); + }); +}); diff --git a/src/lib/whoop/__tests__/sync-user-whoop.test.ts b/src/lib/whoop/__tests__/sync-user-whoop.test.ts index 00110e072..2e4c30e30 100644 --- a/src/lib/whoop/__tests__/sync-user-whoop.test.ts +++ b/src/lib/whoop/__tests__/sync-user-whoop.test.ts @@ -10,6 +10,8 @@ * enabled resource soft-skipped and nothing imported. */ import { beforeEach, describe, expect, it, vi } from "vitest"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; const { recordSyncSuccess, @@ -80,6 +82,11 @@ import { import { syncUserWhoop } from "../sync"; import { WhoopApiError } from "../response-classifier"; +const directWhoopSyncSource = readFileSync( + join(process.cwd(), "src/lib/whoop/sync.ts"), + "utf8", +); + /** A resource sync that 403s its collection and soft-skips, like the real one. */ async function softSkip403(...args: unknown[]): Promise { return handleCollectionFetchError( @@ -225,3 +232,35 @@ describe("syncWhoopResourceWithStatus — per-resource lastSuccessAt stamp", () expect(recordSyncSuccess).not.toHaveBeenCalled(); }); }); + +describe("direct WHOOP API regression beside Apple Health ECG support", () => { + it("keeps every established direct WHOOP resource in the orchestration gate", async () => { + syncUserRecovery.mockResolvedValue(1); + syncUserSleep.mockResolvedValue(1); + syncUserCycle.mockResolvedValue(1); + syncUserWorkout.mockResolvedValue(1); + syncUserBody.mockResolvedValue(1); + + await expect(syncUserWhoop("user-direct-whoop")).resolves.toMatchObject({ + imported: 5, + }); + for (const resource of [ + syncUserRecovery, + syncUserSleep, + syncUserCycle, + syncUserWorkout, + syncUserBody, + ]) { + expect(resource).toHaveBeenCalledWith( + "user-direct-whoop", + expect.any(Object), + ); + } + }); + + it("makes no direct WHOOP ECG support claim", () => { + expect(directWhoopSyncSource).not.toMatch( + /syncUser(?:Ecg|Electrocardiogram)|sync-(?:ecg|electrocardiogram)/i, + ); + }); +}); diff --git a/tests/integration/apple-health-import-ecg.test.ts b/tests/integration/apple-health-import-ecg.test.ts new file mode 100644 index 000000000..2e5ad1c15 --- /dev/null +++ b/tests/integration/apple-health-import-ecg.test.ts @@ -0,0 +1,365 @@ +import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createHash } from "node:crypto"; +import type { Job } from "pg-boss"; +import { strToU8, zipSync } from "fflate"; +import { + afterAll, + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from "vitest"; + +import { decryptWaveformFromBytes } from "@/lib/withings/ecg-waveform-codec"; +import { + APPLE_HEALTH_IMPORT_PARSER_REVISION, + _setWorkerPrismaForTests, + handleAppleHealthImport, + type AppleHealthImportPayload, +} from "@/lib/jobs/apple-health-import-worker"; +import { getPrismaClient, truncateAllTables } from "./setup"; + +vi.mock("@/lib/db-compat", () => ({ + ensureDbCompatibility: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("@/lib/jobs/boss-instance", () => ({ + getGlobalBoss: vi.fn(() => null), +})); + +vi.mock("@/lib/rollups/measurement-rollups", () => ({ + recomputeUserRollups: vi.fn(async () => {}), +})); + +const USER_ID = "user-apple-ecg-contract"; +let jobSequence = 0; + +function supportedWhoopXml(): string { + return ` + + +`; +} + +function ecgCsv( + input: { + recordedAt?: string; + classification?: string; + samples?: number[]; + marker?: string; + } = {}, +): string { + const samples = input.samples ?? [0.001, -0.002, 0.003]; + return [ + `Name,${input.marker ?? "patient-name-must-not-persist"}`, + `Recorded Date,${input.recordedAt ?? "2026-07-18 08:14:03 +0200"}`, + `Classification,${input.classification ?? "Sinus Rhythm"}`, + "Average Heart Rate,64 bpm", + "Sample Rate,512 Hz", + "Lead,Voltage", + ...samples.map((sample) => `I,${sample}`), + ].join("\n"); +} + +function createArchive(ecgs: Array<{ filename: string; csv: string }>): { + path: string; + bytes: Buffer; +} { + const dir = mkdtempSync(join(tmpdir(), "healthlog-ecg-integration-")); + const path = join(dir, "export.zip"); + const files: Record = { + "apple_health_export/export.xml": strToU8(supportedWhoopXml()), + }; + for (const ecg of ecgs) { + files[`apple_health_export/electrocardiograms/${ecg.filename}`] = strToU8( + ecg.csv, + ); + } + const bytes = Buffer.from(zipSync(files, { level: 6 })); + writeFileSync(path, bytes); + return { path, bytes }; +} + +async function runArchive( + ecgs: Array<{ filename: string; csv: string }>, +): Promise<{ + outcome: Awaited>; + mirror: Awaited< + ReturnType["importJob"]["findUnique"]> + >; + uploadPath: string; +}> { + const prisma = getPrismaClient(); + const archive = createArchive(ecgs); + jobSequence += 1; + const jobId = `apple-ecg-job-${jobSequence}`; + const mirror = await prisma.importJob.create({ + data: { + userId: USER_ID, + pgBossJobId: jobId, + status: "queued", + uploadBytes: archive.bytes.length, + uploadSha256: createHash("sha256").update(archive.bytes).digest("hex"), + parserRevision: APPLE_HEALTH_IMPORT_PARSER_REVISION, + }, + }); + const data: AppleHealthImportPayload = { + userId: USER_ID, + uploadPath: archive.path, + uploadBytes: archive.bytes.length, + enqueuedAt: "2026-07-18T06:14:03.000Z", + }; + + const outcome = await handleAppleHealthImport({ + id: jobId, + data, + } as unknown as Job); + + return { + outcome, + mirror: await prisma.importJob.findUnique({ where: { id: mirror.id } }), + uploadPath: archive.path, + }; +} + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); + _setWorkerPrismaForTests(getPrismaClient()); + await getPrismaClient().user.create({ + data: { + id: USER_ID, + username: "apple-ecg-contract", + email: "apple-ecg-contract@example.test", + timezone: "Europe/Berlin", + }, + }); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +afterAll(() => { + _setWorkerPrismaForTests(null); +}); + +describe("Apple Health auxiliary ECG import — real Postgres", () => { + it("persists an encrypted APPLE_HEALTH recording and decrypts through the normal codec", async () => { + const marker = "plaintext-waveform-marker-0.001--0.002"; + const run = await runArchive([ + { filename: "ecg_2026-07-18.csv", csv: ecgCsv({ marker }) }, + ]); + + expect(run.outcome.ok).toBe(true); + const rows = await getPrismaClient().ecgRecording.findMany({ + where: { userId: USER_ID }, + }); + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + source: "APPLE_HEALTH", + samplingFrequency: 512, + sampleCount: 3, + averageHeartRate: 64, + lead: "I", + rhythmClassification: "NOT_DETECTED", + }); + expect(decryptWaveformFromBytes(rows[0].waveformEncrypted)).toEqual([ + 1, -2, 3, + ]); + expect(Buffer.from(rows[0].waveformEncrypted).toString("utf8")).not.toMatch( + /0\.001|-0\.002|plaintext-waveform-marker/, + ); + expect(JSON.stringify(run.mirror)).not.toContain(marker); + expect(existsSync(run.uploadPath)).toBe(false); + }); + + it("deduplicates identical content across filenames and keeps distinct recordings distinct", async () => { + const same = ecgCsv(); + await runArchive([{ filename: "first-name.csv", csv: same }]); + await runArchive([{ filename: "renamed-copy.csv", csv: same }]); + expect( + await getPrismaClient().ecgRecording.count({ + where: { userId: USER_ID }, + }), + ).toBe(1); + + await runArchive([ + { + filename: "different.csv", + csv: ecgCsv({ + recordedAt: "2026-07-18 09:14:03 +0200", + samples: [0.004, -0.005, 0.006], + }), + }, + ]); + const rows = await getPrismaClient().ecgRecording.findMany({ + where: { userId: USER_ID }, + orderBy: { recordedAt: "asc" }, + }); + expect(rows).toHaveLength(2); + expect(new Set(rows.map((row) => row.externalRecordingId)).size).toBe(2); + }); + + it("keeps content identity tenant-scoped", async () => { + const same = ecgCsv(); + await runArchive([{ filename: "same.csv", csv: same }]); + const other = await getPrismaClient().user.create({ + data: { + username: "apple-ecg-other", + email: "apple-ecg-other@example.test", + }, + }); + + const archive = createArchive([{ filename: "same.csv", csv: same }]); + jobSequence += 1; + const jobId = `apple-ecg-job-${jobSequence}`; + await getPrismaClient().importJob.create({ + data: { + userId: other.id, + pgBossJobId: jobId, + status: "queued", + uploadBytes: archive.bytes.length, + parserRevision: APPLE_HEALTH_IMPORT_PARSER_REVISION, + }, + }); + await handleAppleHealthImport({ + id: jobId, + data: { + userId: other.id, + uploadPath: archive.path, + uploadBytes: archive.bytes.length, + enqueuedAt: "2026-07-18T06:14:03.000Z", + }, + } as unknown as Job); + + const rows = await getPrismaClient().ecgRecording.findMany({ + orderBy: { userId: "asc" }, + }); + expect(rows).toHaveLength(2); + expect(rows[0].externalRecordingId).toBe(rows[1].externalRecordingId); + expect(rows[0].userId).not.toBe(rows[1].userId); + }); + + it("keeps valid WHOOP-written export.xml data when an ECG is malformed", async () => { + const secret = "malformed-private-waveform-secret"; + const warning = vi.spyOn(console, "warn").mockImplementation(() => {}); + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + const run = await runArchive([ + { + filename: "malformed.csv", + csv: ecgCsv().replace("I,-0.002", `I,${secret}`), + }, + ]); + + expect(run.outcome.ok).toBe(true); + expect(run.mirror?.status).toBe("done"); + const measurements = await getPrismaClient().measurement.findMany({ + where: { userId: USER_ID }, + }); + expect(measurements).toHaveLength(1); + expect(measurements[0]).toMatchObject({ + source: "APPLE_HEALTH", + type: "PULSE", + value: 64, + }); + expect(JSON.stringify(measurements)).not.toContain("WHOOP"); + expect(await getPrismaClient().ecgRecording.count()).toBe(0); + expect(run.mirror?.result).toMatchObject({ + ecg: { + discovered: 1, + imported: 0, + updated: 0, + skipped: 0, + failed: 1, + }, + }); + expect(JSON.stringify(run.mirror)).not.toContain(secret); + expect( + JSON.stringify([...warning.mock.calls, ...error.mock.calls]), + ).not.toContain(secret); + }); + + it("fails an unsafe auxiliary member softly while valid XML survives", async () => { + const run = await runArchive([ + { filename: "../escaped-ecg.csv", csv: ecgCsv() }, + ]); + + expect(run.outcome.ok).toBe(true); + expect(run.mirror?.status).toBe("done"); + expect( + await getPrismaClient().measurement.count({ where: { userId: USER_ID } }), + ).toBe(1); + expect(await getPrismaClient().ecgRecording.count()).toBe(0); + expect(run.mirror?.result).toMatchObject({ + ecg: { discovered: 1, imported: 0, failed: 1 }, + }); + }); + + it("rolls back a failed ECG write while preserving the valid XML transaction", async () => { + const prisma = getPrismaClient(); + await prisma.$executeRawUnsafe(` + CREATE OR REPLACE FUNCTION reject_apple_health_ecg_contract() + RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + IF NEW.source = 'APPLE_HEALTH' THEN + RAISE EXCEPTION 'synthetic ECG write failure'; + END IF; + RETURN NEW; + END; + $$; + CREATE TRIGGER reject_apple_health_ecg_contract + BEFORE INSERT OR UPDATE ON ecg_recordings + FOR EACH ROW EXECUTE FUNCTION reject_apple_health_ecg_contract(); + `); + + try { + const run = await runArchive([ + { filename: "mid-write.csv", csv: ecgCsv() }, + ]); + expect(run.outcome.ok).toBe(true); + expect( + await prisma.measurement.count({ where: { userId: USER_ID } }), + ).toBe(1); + expect( + await prisma.ecgRecording.count({ where: { userId: USER_ID } }), + ).toBe(0); + expect(run.mirror?.result).toMatchObject({ + ecg: { discovered: 1, failed: 1 }, + }); + expect(JSON.stringify(run.mirror)).not.toContain( + "synthetic ECG write failure", + ); + } finally { + await prisma.$executeRawUnsafe( + `DROP TRIGGER IF EXISTS reject_apple_health_ecg_contract ON ecg_recordings`, + ); + await prisma.$executeRawUnsafe( + `DROP FUNCTION IF EXISTS reject_apple_health_ecg_contract()`, + ); + } + }); + + it("stores unknown device classifications as null without waveform diagnosis", async () => { + await runArchive([ + { + filename: "unknown-classification.csv", + csv: ecgCsv({ classification: "Localized Future Verdict" }), + }, + ]); + const row = await getPrismaClient().ecgRecording.findFirstOrThrow({ + where: { userId: USER_ID }, + }); + expect(row.rhythmClassification).toBeNull(); + }); +});