mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
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:
1 parent
267652e3b6
commit
0ff5826921
2 files changed
+35
-12
No files matched your search
@@ -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 () => {
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user