fix(sleep): round night minute totals to whole numbers

`GET /api/sleep/night` summed second-precision stage rows into asleep /
awake totals and serialised them unrounded (e.g. 433.4999), which also
violated the OpenAPI integer contract. Round the present minute values;
`inBedMinutes` / `awakeMinutes` stay null when the night never saw that
stage.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 10:40:08 +02:00
1 parent 6a4d3fd553
commit b59f8118a9
2 files changed
+25 -3

No files matched your search

@@ -103,6 +103,24 @@ describe("GET /api/sleep/night", () => {
expect(core.end).toBe("2026-06-04T01:00:00.000Z");
});
it("rounds asleep / awake minutes to whole numbers", async () => {
// iOS #18 — `asleepMinutesOf` / awake totals sum raw second-precision
// minute values, so a night sums to e.g. 433.4999; the serializer must
// emit whole minutes. CORE+DEEP = 433.4999 → 433; AWAKE 12.6 → 13.
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.measurement.findMany).mockResolvedValue([
stage("2026-06-04T03:00:00.000Z", "CORE", 300.25),
stage("2026-06-04T05:00:00.000Z", "DEEP", 133.2499),
stage("2026-06-04T05:10:00.000Z", "AWAKE", 12.6),
] as never);
const res = await GET(req("date=2026-06-04"));
const body = await res.json();
expect(body.data.main.asleepMinutes).toBe(433);
expect(body.data.main.awakeMinutes).toBe(13);
expect(Number.isInteger(body.data.main.asleepMinutes)).toBe(true);
expect(Number.isInteger(body.data.main.awakeMinutes)).toBe(true);
});
it("defaults to the most recent scorable night when date omitted", async () => {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
vi.mocked(prisma.measurement.findMany).mockResolvedValue([
+7 -3
View File
@@ -56,9 +56,13 @@ function serializeSession(s: SleepSession) {
source: s.source,
start: s.start.toISOString(),
end: s.end.toISOString(),
asleepMinutes: s.asleepMinutes,
inBedMinutes: s.inBedMinutes,
awakeMinutes: s.awakeMinutes,
// iOS #18 — round to whole minutes; the underlying totals are summed from
// second-precision segments and otherwise serialise as e.g. 433.4999.
// `inBedMinutes` / `awakeMinutes` stay null when the night never saw that
// stage — rounding only the present values.
asleepMinutes: Math.round(s.asleepMinutes),
inBedMinutes: s.inBedMinutes === null ? null : Math.round(s.inBedMinutes),
awakeMinutes: s.awakeMinutes === null ? null : Math.round(s.awakeMinutes),
awakenings: s.awakenings,
stages: s.stages,
segments: s.segments.map((seg) => ({