test(integration): align workout-batch dedup tests with v1.4.42 cross-source semantics

The 'dedupes a re-posted batch' and 'two batches with one overlapping
externalId' tests carried pre-v1.4.42 expectations. The new
dedupeWorkoutBatch helper treats matching externalIds across sources
as one logical row. Closes M-CHRONIC-RED in v1.4.48 backlog — the
integration suite was red on main since v1.4.45.
This commit is contained in:
Marc-André Bombeck committed 2026-05-23 10:24:12 +02:00
1 parent 267652e3b6
commit 0ff5826921
2 files changed
+35 -12

No files matched your search

+22 -4
View File
@@ -290,9 +290,15 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
expect(stored).toBe(0);
});
it("dedupes a re-posted batch into duplicate status without inserting twice", async () => {
it("collapses same-instant cross-source twins via write-time dedup, then surfaces 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"),
@@ -302,8 +308,15 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
const first = await POST(makeRequest(body));
expect(first.status).toBe(200);
const firstJson = (await first.json()) as { data: { inserted: number } };
expect(firstJson.data.inserted).toBe(2);
const firstJson = (await first.json()) as {
data: {
inserted: number;
duplicates: number;
entries: Array<{ status: string }>;
};
};
expect(firstJson.data.inserted).toBe(1);
expect(firstJson.data.duplicates).toBe(1);
const second = await POST(makeRequest(body));
expect(second.status).toBe(200);
@@ -314,6 +327,9 @@ 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(
@@ -323,7 +339,9 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
const stored = await getPrismaClient().workout.count({
where: { userId: TEST_USER_ID },
});
expect(stored).toBe(2);
// Only the first-post survivor lives in the DB — write-time dedup
// dropped the other twin before any DB write.
expect(stored).toBe(1);
});
it("replays a cached response when the same Idempotency-Key is reused", async () => {
+13 -8
View File
@@ -177,13 +177,17 @@ 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 — entries 0..3 unique-to-A plus one shared key.
// 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
// 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-shared", 10),
];
// Batch B — entries 0..3 unique-to-B plus the SAME shared key.
// Batch B — same 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),
@@ -202,16 +206,17 @@ describe("POST /api/workouts/batch — concurrent-write race", () => {
};
const bJson = (await resB.json()) as typeof aJson;
// Five unique tuples total across both batches; one shared. The
// composite unique index means the shared key lands exactly once,
// either as A's "inserted" or B's "inserted" — never both. The
// sum of the two batches' inserted counts is exactly 5.
expect(aJson.data.inserted + bJson.data.inserted).toBe(5);
// 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
// (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);
const stored = await getPrismaClient().workout.findMany({
where: { userId: TEST_USER_ID },
});
expect(stored).toHaveLength(5);
expect(stored).toHaveLength(3);
const sharedRows = stored.filter(
(r) => r.externalId === "hk-uuid-shared",
);