mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
fix(workouts): one canonical-workout picker with one source order
Three pickers decided which of two overlapping workouts to keep: the read-time picker the list, the detail page and the Coach use, a write-time batch picker with its own 90-second window, and an unwired field-merge picker. They used different source orders (the `steps` ladder, or a separate workout default when no order was saved), so the batch route could keep a row the list then ranked below another. The batch route now runs the read-time picker over the rows it is about to insert, with the ladder from `workoutSourceLadder`, and reports what it hides as duplicates. A session HealthKit hands over twice under two ids is stored once, as the list already showed it. The write-time and the unwired picker are removed.
This commit is contained in:
1 parent
2861075676
commit
e355c6120f
8 files changed
+109
-71
No files matched your search
@@ -356,8 +356,16 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
|
||||
it("preserves same-source workouts and surfaces exact re-posts as duplicates", async () => {
|
||||
const { POST } = await import("@/app/api/workouts/batch/route");
|
||||
|
||||
// Two sessions of one source in the same five-minute slot: different
|
||||
// start and length, so two workouts.
|
||||
const body = {
|
||||
workouts: [baseWorkout("hk-uuid-dup-1"), baseWorkout("hk-uuid-dup-2")],
|
||||
workouts: [
|
||||
baseWorkout("hk-uuid-dup-1"),
|
||||
baseWorkout("hk-uuid-dup-2", {
|
||||
startedAt: "2026-05-14T06:32:00.000Z",
|
||||
endedAt: "2026-05-14T06:50:00.000Z",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
const first = await POST(makeRequest(body));
|
||||
@@ -393,6 +401,31 @@ describe("POST /api/workouts/batch (real Postgres)", () => {
|
||||
expect(stored).toBe(2);
|
||||
});
|
||||
|
||||
it("stores a session re-sent under a new id once, the way the list shows it", async () => {
|
||||
// HealthKit can hand the same session over twice with two external ids.
|
||||
// The list collapses it to one workout; the write keeps one row and
|
||||
// reports the other as a duplicate instead of storing what no read shows.
|
||||
const { POST } = await import("@/app/api/workouts/batch/route");
|
||||
const res = await POST(
|
||||
makeRequest({
|
||||
workouts: [
|
||||
baseWorkout("hk-uuid-resend-1"),
|
||||
baseWorkout("hk-uuid-resend-2"),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as {
|
||||
data: { inserted: number; duplicates: number };
|
||||
};
|
||||
expect(json.data.inserted).toBe(1);
|
||||
expect(json.data.duplicates).toBe(1);
|
||||
const stored = await getPrismaClient().workout.count({
|
||||
where: { userId: TEST_USER_ID },
|
||||
});
|
||||
expect(stored).toBe(1);
|
||||
});
|
||||
|
||||
it("replays a cached response when the same Idempotency-Key is reused", async () => {
|
||||
const { POST } = await import("@/app/api/workouts/batch/route");
|
||||
|
||||
|
||||
@@ -182,9 +182,9 @@ describe("POST /api/workouts/batch — concurrent-write race", () => {
|
||||
const { POST } = await import("@/app/api/workouts/batch/route");
|
||||
|
||||
// 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.
|
||||
// The workout picker collapses the Apple Health + Manual pair (same
|
||||
// activity, same five-minute slot) 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, "MANUAL"),
|
||||
|
||||
Reference in new issue
Block a user