release: v1.32.0 — hardening and scalability (#580)

* release: prepare v1.32.0

* test: stabilize water capture re-entry
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-22 04:14:15 +02:00
1 parent b01822be48
commit 62eb431d66
372 files changed
+25178 -8561

No files matched your search

@@ -147,6 +147,125 @@ describe("ArrivalReaction — the day claim", () => {
expect(row?.occurredAt.toISOString()).toBe(late.toISOString());
});
it("replaces a generated marker and rejects the stale generation owner", async () => {
const prisma = getPrismaClient();
const user = await createUser("arrival-replacement");
const early = new Date("2026-07-14T06:00:00Z");
const late = new Date("2026-07-14T18:00:00Z");
const providerInvokedAt = new Date("2026-07-14T06:01:00Z");
const existing = await prisma.arrivalReaction.create({
data: {
...marker(user.id, "weight", early),
refId: "old-ref",
lineEncrypted: Buffer.from("old-ciphertext"),
generatedAt: new Date("2026-07-14T06:02:00Z"),
generationClaimId: "old-claim",
generationClaimedAt: providerInvokedAt,
generationReservedTokens: 220,
generationBudgetDateKey: "2026-07-14",
generationProviderInvokedAt: providerInvokedAt,
},
});
await prisma.coachUsage.create({
data: {
userId: user.id,
dateKey: "2026-07-14",
totalTokens: 220,
messageCount: 1,
},
});
const { runDataArrival } = await import("@/lib/jobs/data-arrival");
const outcome = await runDataArrival(prisma as never, {
userId: user.id,
kind: "weight",
salience: "salient",
localDate: LOCAL_DATE,
occurredAt: late.toISOString(),
count: 1,
source: "withings",
refId: "new-ref",
});
expect(outcome).toMatchObject({ status: "processed", dedup: false });
const replaced = await prisma.arrivalReaction.findUniqueOrThrow({
where: { id: existing.id },
});
expect(replaced).toMatchObject({
occurredAt: late,
refId: "new-ref",
lineEncrypted: null,
generatedAt: null,
generationClaimId: null,
generationClaimedAt: null,
generationReservedTokens: null,
generationBudgetDateKey: null,
generationProviderInvokedAt: null,
});
const staleCommit = await prisma.arrivalReaction.updateMany({
where: {
id: existing.id,
generationClaimId: "old-claim",
generationProviderInvokedAt: providerInvokedAt,
},
data: {
lineEncrypted: Buffer.from("stale-ciphertext"),
generatedAt: new Date(),
},
});
expect(staleCommit.count).toBe(0);
const usage = await prisma.coachUsage.findUniqueOrThrow({
where: {
userId_dateKey: { userId: user.id, dateKey: "2026-07-14" },
},
});
expect(usage).toMatchObject({ totalTokens: 220, messageCount: 1 });
});
it("refunds a superseded pre-provider reservation", async () => {
const prisma = getPrismaClient();
const user = await createUser("arrival-reservation-refund");
const early = new Date("2026-07-14T06:00:00Z");
const late = new Date("2026-07-14T18:00:00Z");
await prisma.arrivalReaction.create({
data: {
...marker(user.id, "weight", early),
generationClaimId: "pre-provider-claim",
generationClaimedAt: new Date("2026-07-14T06:01:00Z"),
generationReservedTokens: 220,
generationBudgetDateKey: "2026-07-14",
},
});
await prisma.coachUsage.create({
data: {
userId: user.id,
dateKey: "2026-07-14",
totalTokens: 500,
messageCount: 2,
},
});
const { runDataArrival } = await import("@/lib/jobs/data-arrival");
await runDataArrival(prisma as never, {
userId: user.id,
kind: "weight",
salience: "salient",
localDate: LOCAL_DATE,
occurredAt: late.toISOString(),
count: 1,
source: "withings",
});
const usage = await prisma.coachUsage.findUniqueOrThrow({
where: {
userId_dateKey: { userId: user.id, dateKey: "2026-07-14" },
},
});
expect(usage).toMatchObject({ totalTokens: 280, messageCount: 1 });
});
it("stores the line as encrypted bytes, and tolerates its absence", async () => {
const prisma = getPrismaClient();
const user = await createUser("arrival-crypto");
@@ -0,0 +1,182 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Prisma } from "@/generated/prisma/client";
import { collectDoctorReportData } from "@/lib/doctor-report-data";
import { getPrismaClient, truncateAllTables } from "./setup";
const RANGE = {
start: new Date("2026-01-01T00:00:00.000Z"),
end: new Date("2026-05-01T00:00:00.000Z"),
days: 120,
};
const MODULES = {
cycle: false,
mood: false,
sleep: false,
glucose: true,
workouts: false,
recovery: false,
labs: false,
illness: false,
achievements: true,
coach: true,
insights: true,
medications: true,
doctorReport: true,
environment: true,
mcp: true,
inboundDocuments: true,
mentalHealth: true,
nutrients: true,
} as const;
const SECTIONS = {
bp: false,
weight: true,
pulse: true,
bmi: false,
mood: false,
compliance: false,
sleep: false,
glucose: true,
cycle: false,
labs: false,
allergies: false,
familyHistory: false,
} as const;
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("doctor report — bounded dense measurement reads", () => {
it("keeps dense transfer and points bounded by local days while sparse rows remain raw", async () => {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: "doctor-report-dense",
email: "doctor-report-dense@example.test",
timezone: "America/New_York",
sourcePriorityJson: {
metricPriority: {
pulse: ["WITHINGS", "MANUAL"],
},
} as Prisma.InputJsonValue,
},
});
const sparseRows = [
{
userId: user.id,
type: "WEIGHT" as const,
value: 80,
unit: "kg",
source: "MANUAL" as const,
measuredAt: new Date("2026-01-02T04:15:00.000Z"),
},
{
userId: user.id,
type: "WEIGHT" as const,
value: 81,
unit: "kg",
source: "MANUAL" as const,
measuredAt: new Date("2026-01-02T06:15:00.000Z"),
},
];
await prisma.measurement.createMany({ data: sparseRows });
const seedDenseBatch = async (
offset: number,
samplesPerLocalDay: number,
) => {
const rows: Prisma.MeasurementCreateManyInput[] = [];
const localDayInstants = [
Date.parse("2026-01-02T04:30:00.000Z"), // Jan 1, 23:30 in New York
Date.parse("2026-01-02T05:30:00.000Z"), // Jan 2, 00:30 in New York
];
for (const [dayIndex, baseMs] of localDayInstants.entries()) {
for (let i = 0; i < samplesPerLocalDay; i += 1) {
const measuredAt = new Date(baseMs + (offset + i) * 1_000);
rows.push(
{
userId: user.id,
type: "PULSE",
value: 60 + dayIndex,
unit: "bpm",
source: "WITHINGS",
measuredAt,
},
{
userId: user.id,
type: "PULSE",
value: 160 + dayIndex,
unit: "bpm",
source: "MANUAL",
measuredAt,
},
{
userId: user.id,
type: "BLOOD_GLUCOSE",
value: 90 + dayIndex,
unit: "mg/dL",
source: "MANUAL",
glucoseContext: "FASTING",
measuredAt,
},
);
}
}
await prisma.measurement.createMany({ data: rows });
};
await seedDenseBatch(0, 4);
const findManySpy = vi.spyOn(prisma.measurement, "findMany");
const initial = await collectDoctorReportData(user.id, RANGE, {
sections: { ...SECTIONS },
moduleMap: { ...MODULES },
});
expect(initial.measurements.PULSE).toHaveLength(2);
expect(initial.measurements.BLOOD_GLUCOSE).toHaveLength(2);
expect(initial.measurements.PULSE.map((point) => point.measuredAt)).toEqual(
["2026-01-01T05:00:00.000Z", "2026-01-02T05:00:00.000Z"],
);
expect(initial.stats.PULSE).toMatchObject({
count: 8,
avg: 60.5,
min: 60,
max: 61,
});
expect(initial.measurements.WEIGHT).toEqual(
sparseRows.map((row) => ({
value: row.value,
measuredAt: row.measuredAt.toISOString(),
})),
);
const rawRows = (await findManySpy.mock.results[0]?.value) as Array<{
type: string;
}>;
expect(rawRows).toHaveLength(sparseRows.length);
expect(rawRows.map((row) => row.type)).toEqual(["WEIGHT", "WEIGHT"]);
await seedDenseBatch(10, 300);
const grown = await collectDoctorReportData(user.id, RANGE, {
sections: { ...SECTIONS },
moduleMap: { ...MODULES },
});
expect(grown.measurements.PULSE).toHaveLength(2);
expect(grown.measurements.BLOOD_GLUCOSE).toHaveLength(2);
expect(grown.stats.PULSE.count).toBe(608);
expect(grown.stats.BLOOD_GLUCOSE.count).toBe(608);
expect(grown.measurements.WEIGHT).toEqual(initial.measurements.WEIGHT);
});
});
+17 -6
View File
@@ -269,10 +269,10 @@ describe("document vault — policy error contract", () => {
});
it("cannot be overshot by concurrent uploads racing the quota gate", async () => {
// Four DISTINCT same-sized files fired in parallel against a quota that
// fits exactly two. Without the per-user advisory lock in the upload
// transaction every request reads the same pre-insert SUM and all four
// land — a burst could overshoot the quota by rate-limit × cap.
// The production admission gate allows two same-user uploads in flight,
// so seed one file, then race two DISTINCT files for the one remaining
// quota slot. Without the per-user advisory lock both contenders read the
// same pre-insert SUM and land, overshooting the quota.
const user = await seedVaultUser("vault-quota-race");
const files = Array.from({ length: 4 }, (_, i) => {
const filler = Buffer.alloc(2048, i + 1);
@@ -285,11 +285,22 @@ describe("document vault — policy error contract", () => {
});
const { post } = await routes();
const results = await Promise.all(
files.map((bytes, i) => post(uploadRequest(bytes, `race-${i}.pdf`))),
const first = await post(uploadRequest(files[0], "race-0.pdf"));
const contenders = await Promise.all(
files
.slice(1, 3)
.map((bytes, i) => post(uploadRequest(bytes, `race-${i + 1}.pdf`))),
);
const last = await post(uploadRequest(files[3], "race-3.pdf"));
const results = [first, ...contenders, last];
const statuses = results.map((r) => r.status).sort();
expect(statuses).toEqual([201, 201, 413, 413]);
const bodies = await Promise.all(results.map((result) => result.json()));
expect(
bodies
.filter((_, index) => results[index].status === 413)
.map((body) => body.meta.reason),
).toEqual(["quotaExceeded", "quotaExceeded"]);
const rows = await getPrismaClient().inboundDocument.findMany({
where: { userId: user.id },
@@ -0,0 +1,301 @@
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
import { Prisma, type PrismaClient } from "@/generated/prisma/client";
import { getPrismaClient, truncateAllTables } from "./setup";
import {
_setMedicationIntakeImportPrismaForTests,
processMedicationIntakeImportJob,
type MedicationImportPayload,
type MedicationImportProgress,
} from "@/lib/jobs/medication-intake-import";
import { invalidateUserMedications } from "@/lib/cache/invalidate";
import { queueMedicationIntakeSync } from "@/lib/notifications/medication-intake-sync";
import { toJson } from "@/lib/db";
vi.mock("@/lib/cache/invalidate", () => ({
invalidateUserMedications: vi.fn(),
}));
vi.mock("@/lib/notifications/medication-intake-sync", () => ({
queueMedicationIntakeSync: vi.fn(),
}));
beforeEach(async () => {
const prisma = getPrismaClient();
await truncateAllTables(prisma);
_setMedicationIntakeImportPrismaForTests(prisma);
vi.clearAllMocks();
});
afterAll(() => {
_setMedicationIntakeImportPrismaForTests(null);
});
describe("medication intake import job — concurrent retry", () => {
it("produces one event and one inventory consumption per unique import key", async () => {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: "med-import-owner",
email: "med-import-owner@example.test",
role: "USER",
timezone: "Europe/Berlin",
},
});
const medication = await prisma.medication.create({
data: {
userId: user.id,
name: "Test medication",
dose: "1 tablet",
unitsPerDose: 1,
},
});
const inventory = await prisma.medicationInventoryItem.create({
data: {
userId: user.id,
medicationId: medication.id,
unitsTotal: 10,
unitsRemaining: 10,
},
});
const payload: MedicationImportPayload = {
entries: [
{
takenAt: "2026-07-20T05:00:00.000Z",
idempotencyKey: `import-${medication.id}-1`,
},
{
takenAt: "2026-07-20T05:00:00.000Z",
idempotencyKey: `import-${medication.id}-1`,
},
{
takenAt: "2026-07-20T17:00:00.000Z",
idempotencyKey: `import-${medication.id}-2`,
},
{
takenAt: "2026-07-21T05:00:00.000Z",
idempotencyKey: `import-${medication.id}-3`,
},
],
};
const progress: MedicationImportProgress = {
processed: 0,
total: payload.entries.length,
imported: 0,
skippedDuplicates: 0,
touchedDays: [],
rollupProcessed: 0,
};
const job = await prisma.medicationIntakeImportJob.create({
data: {
userId: user.id,
medicationId: medication.id,
payload: toJson(payload),
progress: toJson(progress),
},
});
await Promise.all([
processMedicationIntakeImportJob(job.id),
processMedicationIntakeImportJob(job.id),
]);
await processMedicationIntakeImportJob(job.id);
const [events, refreshedInventory, rollups, refreshedJob, audits] =
await Promise.all([
prisma.medicationIntakeEvent.findMany({
where: { userId: user.id, medicationId: medication.id },
orderBy: { idempotencyKey: "asc" },
}),
prisma.medicationInventoryItem.findUniqueOrThrow({
where: { id: inventory.id },
}),
prisma.medicationComplianceRollup.findMany({
where: { userId: user.id, medicationId: medication.id },
orderBy: { day: "asc" },
}),
prisma.medicationIntakeImportJob.findUniqueOrThrow({
where: { id: job.id },
}),
prisma.auditLog.findMany({
where: {
userId: user.id,
action: "medication.intake.import",
},
}),
]);
expect(events).toHaveLength(3);
expect(events.map((event) => event.idempotencyKey)).toEqual([
`import-${medication.id}-1`,
`import-${medication.id}-2`,
`import-${medication.id}-3`,
]);
expect(
events.every(
(event) =>
Array.isArray(event.inventoryConsumption) &&
event.inventoryConsumption.length === 1,
),
).toBe(true);
expect(Number(refreshedInventory.unitsRemaining)).toBe(7);
expect(
rollups.map(({ day, scheduled, taken }) => ({
day,
scheduled,
taken,
})),
).toEqual([
{ day: "2026-07-20", scheduled: 2, taken: 2 },
{ day: "2026-07-21", scheduled: 1, taken: 1 },
]);
expect(refreshedJob.status).toBe("done");
expect(refreshedJob.result).toEqual({
imported: 3,
skippedDuplicates: 1,
skippedInvalid: 0,
});
expect(refreshedJob.progress).toEqual({
processed: 4,
total: 4,
imported: 3,
skippedDuplicates: 1,
touchedDays: ["2026-07-20", "2026-07-21"],
rollupProcessed: 2,
});
expect(audits).toHaveLength(1);
expect(invalidateUserMedications).toHaveBeenCalledTimes(1);
expect(queueMedicationIntakeSync).toHaveBeenCalledTimes(1);
});
it("rolls final effects back with a failed terminal marker and retries once", async () => {
const prisma = getPrismaClient();
const user = await prisma.user.create({
data: {
username: "med-import-finalize",
email: "med-import-finalize@example.test",
role: "USER",
timezone: "Europe/Berlin",
},
});
const medication = await prisma.medication.create({
data: {
userId: user.id,
name: "Finalize medication",
dose: "1 tablet",
unitsPerDose: 1,
},
});
const payload: MedicationImportPayload = {
entries: [
{
takenAt: "2026-07-20T05:00:00.000Z",
idempotencyKey: `import-${medication.id}-final`,
},
],
};
const progress: MedicationImportProgress = {
processed: 0,
total: 1,
imported: 0,
skippedDuplicates: 0,
touchedDays: [],
rollupProcessed: 0,
};
const job = await prisma.medicationIntakeImportJob.create({
data: {
userId: user.id,
medicationId: medication.id,
payload: toJson(payload),
progress: toJson(progress),
},
});
let rejectTerminalMarker = true;
const clientWithTerminalFailure = {
$transaction: (
operation: (tx: Prisma.TransactionClient) => Promise<unknown>,
) =>
prisma.$transaction(async (tx) => {
const jobDelegate = new Proxy(tx.medicationIntakeImportJob, {
get(target, property, receiver) {
if (property !== "update") {
return Reflect.get(target, property, receiver);
}
return async (
args: Prisma.MedicationIntakeImportJobUpdateArgs,
) => {
if (rejectTerminalMarker && args.data.status === "done") {
rejectTerminalMarker = false;
throw new Error("terminal marker failed");
}
return tx.medicationIntakeImportJob.update(args);
};
},
});
const transaction = new Proxy(tx, {
get(target, property, receiver) {
if (property === "medicationIntakeImportJob") {
return jobDelegate;
}
return Reflect.get(target, property, receiver);
},
});
return operation(transaction);
}),
} as unknown as PrismaClient;
_setMedicationIntakeImportPrismaForTests(clientWithTerminalFailure);
await expect(processMedicationIntakeImportJob(job.id)).rejects.toThrow(
"terminal marker failed",
);
const afterFailedFinalization = await Promise.all([
prisma.medicationIntakeEvent.count({
where: { medicationId: medication.id },
}),
prisma.medicationComplianceRollup.count({
where: { medicationId: medication.id },
}),
prisma.auditLog.count({
where: {
userId: user.id,
action: "medication.intake.import",
},
}),
prisma.medicationIntakeImportJob.findUniqueOrThrow({
where: { id: job.id },
}),
]);
expect(afterFailedFinalization[0]).toBe(1);
expect(afterFailedFinalization[1]).toBe(0);
expect(afterFailedFinalization[2]).toBe(0);
expect(afterFailedFinalization[3].status).toBe("running");
_setMedicationIntakeImportPrismaForTests(prisma);
await processMedicationIntakeImportJob(job.id);
await processMedicationIntakeImportJob(job.id);
const [events, rollups, audits, completedJob] = await Promise.all([
prisma.medicationIntakeEvent.findMany({
where: { medicationId: medication.id },
}),
prisma.medicationComplianceRollup.findMany({
where: { medicationId: medication.id },
}),
prisma.auditLog.findMany({
where: {
userId: user.id,
action: "medication.intake.import",
},
}),
prisma.medicationIntakeImportJob.findUniqueOrThrow({
where: { id: job.id },
}),
]);
expect(events).toHaveLength(1);
expect(rollups).toHaveLength(1);
expect(audits).toHaveLength(1);
expect(completedJob.status).toBe("done");
});
});
@@ -27,7 +27,7 @@ vi.mock("@/lib/withings/client", async (importOriginal) => ({
}));
import { encrypt, decrypt } from "@/lib/crypto";
import { getValidToken as getValidWhoopToken } from "@/lib/whoop/sync";
import { getValidToken as getValidWhoopToken } from "@/lib/whoop/sync-core";
import { syncUserRecovery } from "@/lib/whoop/sync-recovery";
import { getValidToken as getValidWithingsToken } from "@/lib/withings/sync";
+1
View File
@@ -56,6 +56,7 @@ export async function truncateAllTables(client: PrismaClient): Promise<void> {
"measurement_rollups",
"measurements",
"medication_intake_events",
"medication_intake_import_jobs",
"medication_schedules",
"medications",
"mood_entries",
@@ -459,4 +459,83 @@ describe("per-user timezone — Pacific/Auckland end-to-end", () => {
expect(recent.some((r) => r.date === aucklandDay)).toBe(true);
expect(recent.some((r) => r.date === berlinDay)).toBe(false);
});
it("buckets long dense measurement series by the user's local day", async () => {
const prisma = getPrismaClient();
const me = await prisma.user.create({
data: {
username: "la-series-user",
email: "la-series@example.test",
role: "USER",
timezone: "America/Los_Angeles",
},
});
const session = await prisma.session.create({
data: { userId: me.id, expiresAt: new Date(Date.now() + 60_000) },
});
cookieJar.set("healthlog_session", session.id);
const base = new Date();
base.setUTCDate(base.getUTCDate() - 2);
base.setUTCHours(0, 30, 0, 0);
const samples = [
{ measuredAt: base, value: 10 },
{
measuredAt: new Date(base.getTime() + 8 * 60 * 60_000),
value: 30,
},
{
measuredAt: new Date(base.getTime() + 24 * 60 * 60_000),
value: 50,
},
{
measuredAt: new Date(base.getTime() + 32 * 60 * 60_000),
value: 70,
},
];
await prisma.measurement.createMany({
data: samples.map((sample) => ({
userId: me.id,
type: "PULSE" as const,
value: sample.value,
unit: "bpm",
measuredAt: sample.measuredAt,
source: "MANUAL" as const,
})),
});
const { GET } = await import("@/app/api/measurements/series/route");
const { NextRequest } = await import("next/server");
const res = await GET(
new NextRequest(
"http://localhost/api/measurements/series?kind=pulse&days=91",
),
);
expect(res.status).toBe(200);
const body = (await res.json()) as {
data: {
points: Array<{ id: string; at: string; value: number }>;
stats: { count: number };
};
};
const { userDayKey } = await import("@/lib/tz/resolver");
const expected = [
{ day: userDayKey(samples[0].measuredAt, me.timezone), value: 10 },
{ day: userDayKey(samples[1].measuredAt, me.timezone), value: 40 },
{ day: userDayKey(samples[3].measuredAt, me.timezone), value: 70 },
];
expect(body.data.points).toHaveLength(3);
expect(
body.data.points.map((point) => ({
day: point.id.replace("day:", ""),
value: point.value,
})),
).toEqual(expected);
expect(
body.data.points.map((point) =>
userDayKey(new Date(point.at), me.timezone),
),
).toEqual(expected.map(({ day }) => day));
expect(body.data.stats.count).toBe(4);
});
});
@@ -0,0 +1,119 @@
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { beforeEach, describe, expect, it } from "vitest";
import { getPrismaClient, truncateAllTables } from "./setup";
const WHOOP_OWNER_MIGRATION_SQL = readFileSync(
resolve(
process.cwd(),
"prisma/migrations/0264_whoop_owner_identity/migration.sql",
),
"utf8",
);
async function createUser(tag: string) {
const prisma = getPrismaClient();
return prisma.user.create({
data: {
username: `whoop-owner-${tag}`,
email: `whoop-owner-${tag}@example.test`,
role: "USER",
},
});
}
describe("WHOOP provider identity ownership — integration", () => {
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
});
it("prevents one WHOOP user id from belonging to two local users", async () => {
const prisma = getPrismaClient();
const firstUser = await createUser("first");
const secondUser = await createUser("second");
const tokenExpiresAt = new Date("2026-08-01T00:00:00.000Z");
await prisma.whoopConnection.create({
data: {
userId: firstUser.id,
whoopUserId: "shared-provider-user",
accessToken: "encrypted-access-1",
refreshToken: "encrypted-refresh-1",
tokenExpiresAt,
},
});
await expect(
prisma.whoopConnection.create({
data: {
userId: secondUser.id,
whoopUserId: "shared-provider-user",
accessToken: "encrypted-access-2",
refreshToken: "encrypted-refresh-2",
tokenExpiresAt,
},
}),
).rejects.toMatchObject({ code: "P2002" });
});
it("keeps the earliest duplicate deterministically when the migration is applied", async () => {
const prisma = getPrismaClient();
const winnerUser = await createUser("migration-winner");
const tiedLoserUser = await createUser("migration-tied-loser");
const laterLoserUser = await createUser("migration-later-loser");
const earliest = new Date("2026-01-01T00:00:00.000Z");
const later = new Date("2026-01-02T00:00:00.000Z");
const tokenExpiresAt = new Date("2026-08-01T00:00:00.000Z");
await prisma.$executeRawUnsafe(
'DROP INDEX "whoop_connections_whoop_user_id_key"',
);
await prisma.whoopConnection.createMany({
data: [
{
id: "a-earliest",
userId: winnerUser.id,
whoopUserId: "duplicate-provider-user",
accessToken: "encrypted-access-1",
refreshToken: "encrypted-refresh-1",
tokenExpiresAt,
createdAt: earliest,
updatedAt: earliest,
},
{
id: "z-earliest",
userId: tiedLoserUser.id,
whoopUserId: "duplicate-provider-user",
accessToken: "encrypted-access-2",
refreshToken: "encrypted-refresh-2",
tokenExpiresAt,
createdAt: earliest,
updatedAt: earliest,
},
{
id: "0-later",
userId: laterLoserUser.id,
whoopUserId: "duplicate-provider-user",
accessToken: "encrypted-access-3",
refreshToken: "encrypted-refresh-3",
tokenExpiresAt,
createdAt: later,
updatedAt: later,
},
],
});
await prisma.$executeRawUnsafe(WHOOP_OWNER_MIGRATION_SQL);
const connections = await prisma.whoopConnection.findMany({
where: { id: { in: ["a-earliest", "z-earliest", "0-later"] } },
orderBy: { id: "asc" },
select: { id: true, whoopUserId: true },
});
expect(connections).toEqual([
{ id: "0-later", whoopUserId: null },
{ id: "a-earliest", whoopUserId: "duplicate-provider-user" },
{ id: "z-earliest", whoopUserId: null },
]);
});
});
+4 -15
View File
@@ -336,15 +336,9 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
expect(stored).toBe(0);
});
it("collapses same-instant cross-source twins via write-time dedup, then surfaces re-posts as duplicates", async () => {
it("preserves same-source workouts and surfaces exact re-posts as duplicates", async () => {
const { POST } = await import("@/app/api/workouts/batch/route");
// Two entries share `sportType`, `startedAt`, and `source`. The
// v1.4.42 write-time cross-source picker (`dedupeWorkoutBatch`)
// groups by `(userId, activityType, startedAt ± 90 s)` and keeps
// ONE canonical row — the other lands as `duplicate` BEFORE
// reaching the DB. This is the Apple Watch + Withings ScanWatch
// case the picker exists to handle.
const body = {
workouts: [baseWorkout("hk-uuid-dup-1"), baseWorkout("hk-uuid-dup-2")],
};
@@ -358,8 +352,8 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
entries: Array<{ status: string }>;
};
};
expect(firstJson.data.inserted).toBe(1);
expect(firstJson.data.duplicates).toBe(1);
expect(firstJson.data.inserted).toBe(2);
expect(firstJson.data.duplicates).toBe(0);
const second = await POST(makeRequest(body));
expect(second.status).toBe(200);
@@ -370,9 +364,6 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
entries: Array<{ status: string }>;
};
};
// The write-time dedup still fires on the second post (one twin
// collapses), and the surviving twin now collides with the row
// persisted by the first post — so both end up as `duplicate`.
expect(secondJson.data.inserted).toBe(0);
expect(secondJson.data.duplicates).toBe(2);
expect(secondJson.data.entries.every((e) => e.status === "duplicate")).toBe(
@@ -382,9 +373,7 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
const stored = await getPrismaClient().workout.count({
where: { userId: TEST_USER_ID },
});
// Only the first-post survivor lives in the DB — write-time dedup
// dropped the other twin before any DB write.
expect(stored).toBe(1);
expect(stored).toBe(2);
});
it("replays a cached response when the same Idempotency-Key is reused", async () => {
+25 -13
View File
@@ -85,12 +85,16 @@ function makeRequest(body: { workouts: WorkoutFixture[] }): NextRequest {
});
}
function workout(externalId: string, minuteOffset: number): WorkoutFixture {
function workout(
externalId: string,
minuteOffset: number,
source = "APPLE_HEALTH",
): WorkoutFixture {
return {
sportType: "running",
startedAt: new Date(2026, 4, 14, 6, minuteOffset).toISOString(),
endedAt: new Date(2026, 4, 14, 7, minuteOffset).toISOString(),
source: "APPLE_HEALTH",
source,
externalId,
};
}
@@ -177,20 +181,20 @@ describe("POST /api/workouts/batch — concurrent-write race", () => {
it("two batches with one overlapping externalId resolve to a single DB row each", async () => {
const { POST } = await import("@/app/api/workouts/batch/route");
// Batch A — two entries one minute apart plus one shared key. The
// v1.4.42 `dedupeWorkoutBatch` picker collapses the minute-1 +
// minute-2 pair (same activity, within the 90 s window) to one
// Batch A — a cross-source pair one minute apart plus one shared key.
// The v1.4.42 `dedupeWorkoutBatch` picker collapses the Apple Health +
// Manual pair (same activity, within the 90 s window) to one
// surviving row, leaving 2 effective rows per batch.
const batchA: WorkoutFixture[] = [
workout("hk-uuid-only-a-0", 1),
workout("hk-uuid-only-a-1", 2),
workout("hk-uuid-only-a-1", 2, "MANUAL"),
workout("hk-uuid-shared", 10),
];
// Batch B — same shape, different externalIds for the unique pair
// but the SAME shared key.
// Batch B — same cross-source shape, different externalIds for the
// unique pair but the SAME shared key.
const batchB: WorkoutFixture[] = [
workout("hk-uuid-only-b-0", 21),
workout("hk-uuid-only-b-1", 22),
workout("hk-uuid-only-b-1", 22, "MANUAL"),
workout("hk-uuid-shared", 10),
];
@@ -202,16 +206,24 @@ describe("POST /api/workouts/batch — concurrent-write race", () => {
expect(resB.status).toBe(200);
const aJson = (await resA.json()) as {
data: { inserted: number; duplicates: number };
data: { processed: number; inserted: number; duplicates: number };
};
const bJson = (await resB.json()) as typeof aJson;
// Three unique tuples land in the DB after write-time dedup + the
// composite unique index: one survivor from A's minute-1/2 pair,
// one survivor from B's minute-21/22 pair, and one shared-key row
// Three unique tuples land in the DB after write-time cross-source
// dedup + the composite unique index: one survivor from A's minute-1/2
// pair, one survivor from B's minute-21/22 pair, and one shared-key row
// (either as A's "inserted" or B's "inserted" — never both). The
// sum of the two batches' inserted counts equals the row count.
expect(aJson.data.inserted + bJson.data.inserted).toBe(3);
// The two canonical drops plus the losing shared-key insert must be
// observable as duplicates rather than being counted as inserts.
expect(aJson.data.duplicates + bJson.data.duplicates).toBe(3);
for (const json of [aJson, bJson]) {
expect(json.data.inserted + json.data.duplicates).toBe(
json.data.processed,
);
}
const stored = await getPrismaClient().workout.findMany({
where: { userId: TEST_USER_ID },