Files
HealthLog/src/lib/sleep/__tests__/reconstruct-timeline.test.ts
T
Marc-André Bombeck 9c63d3ce20 chore(release): v1.28.25 — every integration held to the same standard
Platform-wide hardening: the failure classes found live this week hunted down
across every integration.

Fitbit carried the same tombstone wedge fixed for Google — probe now matches
soft-deleted rows and the update resurrects them; the comment describing a
partial unique index that never existed is corrected. Sleep segment ids are
stable across re-scoring for Withings (session+segment-start), WHOOP/Polar
(one segment per stage, no positional index) and Oura (run-start keyed), each
with a session-scoped sweep that soft-deletes rows a re-score orphaned — so
legacy and affected nights heal as they are re-read.

One platform rule for deleted rows: source-owned readings resurrect on
re-import (Withings, WHOOP, Oura, Polar, Nightscout, mood webhook, CSV
re-imports, Apple stats day-totals); Apple sample-grain deletions stick per
the paired-client contract, and the mood bulk route's tombstone match is now
a true no-op reported as duplicate. Import counts reconcile against actual
createMany counts.

Google lifecycle: a dead token registers on the cycle's hard-fail ledger, so
an expired connection parks instead of stamping success hourly; backfill and
sleep-repair markers stamp only after a clean pass (pg-boss retries are now
real); failed measurement writes hold the watermark; no-op updates are
skipped; mapping.md tells the current truth.

Scale: doctor-report reads only consumed columns and enabled sections;
exports and backups keyset-paginate; achievements tally via SQL buckets;
series charts day-bucket dense kinds beyond 90 days; BP pairing is a linear
two-pointer; distinct-type scans run server-side; the graded-series cold
fallback is bounded; the fitbit defer tracker loops instead of spreading.
2026-07-11 01:54:12 +02:00

133 lines
5.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { reconstructContiguousSleepTimeline } from "../reconstruct-timeline";
describe("reconstructContiguousSleepTimeline", () => {
const onset = Date.parse("2026-06-09T21:00:00.000Z");
const end = Date.parse("2026-06-10T05:00:00.000Z");
it("lays stages contiguously from onset, each ending at its own instant", () => {
const rows = reconstructContiguousSleepTimeline({
startMs: onset,
stages: [
{ durationMs: 60 * 60_000, stage: "CORE", fieldTag: "sleep_core" },
{ durationMs: 30 * 60_000, stage: "DEEP", fieldTag: "sleep_deep" },
{ durationMs: 90 * 60_000, stage: "REM", fieldTag: "sleep_rem" },
],
externalIdFor: (tag) => `night:seg:${tag}`,
});
const core = rows.find((r) => r.sleepStage === "CORE")!;
const deep = rows.find((r) => r.sleepStage === "DEEP")!;
const rem = rows.find((r) => r.sleepStage === "REM")!;
expect(core.value).toBe(60);
expect(core.measuredAt.getTime()).toBe(onset + 60 * 60_000);
expect(deep.measuredAt.getTime()).toBe(onset + 90 * 60_000);
expect(rem.measuredAt.getTime()).toBe(onset + 180 * 60_000);
// Distinct instants → ordered hypnogram, not a shared right edge.
expect(new Set(rows.map((r) => r.measuredAt.getTime())).size).toBe(3);
});
it("flags every laid segment reconstructed and keys a stage-tagged externalId", () => {
const rows = reconstructContiguousSleepTimeline({
startMs: onset,
stages: [
{ durationMs: 15 * 60_000, stage: "AWAKE", fieldTag: "sleep_awake" },
{ durationMs: 60 * 60_000, stage: "CORE", fieldTag: "sleep_core" },
],
externalIdFor: (tag) => `night:seg:${tag}`,
});
expect(rows.every((r) => r.reconstructed === true)).toBe(true);
expect(rows[0].externalId).toBe("night:seg:sleep_awake");
expect(rows[1].externalId).toBe("night:seg:sleep_core");
});
it("skips non-positive, null, undefined and non-finite durations", () => {
const rows = reconstructContiguousSleepTimeline({
startMs: onset,
stages: [
{ durationMs: 0, stage: "AWAKE", fieldTag: "sleep_awake" },
{ durationMs: null, stage: "CORE", fieldTag: "sleep_core" },
{ durationMs: undefined, stage: "DEEP", fieldTag: "sleep_deep" },
{ durationMs: Number.NaN, stage: "REM", fieldTag: "sleep_rem" },
{ durationMs: 30 * 60_000, stage: "CORE", fieldTag: "sleep_core" },
],
externalIdFor: (tag) => `night:seg:${tag}`,
});
expect(rows).toHaveLength(1);
// The laid stage keys on its tag alone — skipped stages cannot influence
// the id of any other stage (no positional index to renumber).
expect(rows[0].externalId).toBe("night:seg:sleep_core");
expect(rows[0].measuredAt.getTime()).toBe(onset + 30 * 60_000);
});
it("keeps stable externalIds when a re-score flips a stage 0↔positive", () => {
// First scoring: no awake time. Re-score: WHOOP/Polar now report a
// positive AWAKE block. Under the retired positional index the re-score
// renumbered CORE/DEEP (0→1, 1→2), minting fresh externalIds the upsert
// then INSERTED next to the old rows — the night double-counted. The
// stage-tagged key must be identical across both scorings.
const stages = (awakeMs: number) =>
[
{ durationMs: awakeMs, stage: "AWAKE", fieldTag: "sleep_awake" },
{ durationMs: 60 * 60_000, stage: "CORE", fieldTag: "sleep_core" },
{ durationMs: 30 * 60_000, stage: "DEEP", fieldTag: "sleep_deep" },
] as const;
const externalIdFor = (tag: string) => `night:seg:${tag}`;
const first = reconstructContiguousSleepTimeline({
startMs: onset,
stages: stages(0),
externalIdFor,
});
const rescored = reconstructContiguousSleepTimeline({
startMs: onset,
stages: stages(15 * 60_000),
externalIdFor,
});
const idsOf = (rows: typeof first, stage: string) =>
rows.find((r) => r.sleepStage === stage)?.externalId;
// CORE and DEEP keep their ids — the awake flip cannot renumber them.
expect(idsOf(rescored, "CORE")).toBe(idsOf(first, "CORE"));
expect(idsOf(rescored, "DEEP")).toBe(idsOf(first, "DEEP"));
expect(idsOf(rescored, "AWAKE")).toBe("night:seg:sleep_awake");
});
it("appends a single IN_BED envelope at the given END instant", () => {
const rows = reconstructContiguousSleepTimeline({
startMs: onset,
stages: [
{ durationMs: 60 * 60_000, stage: "CORE", fieldTag: "sleep_core" },
],
inBed: {
durationMs: end - onset,
measuredAt: new Date(end),
fieldTag: "sleep_in_bed",
},
externalIdFor: (tag) => `night:seg:${tag}`,
});
const inBed = rows.find((r) => r.sleepStage === "IN_BED")!;
expect(inBed.measuredAt.getTime()).toBe(end);
expect(inBed.value).toBe(480); // 8 h
// IN_BED is an envelope, not a placed segment — no reconstructed flag / id.
expect(inBed.reconstructed).toBeUndefined();
expect(inBed.externalId).toBeUndefined();
});
it("omits IN_BED when its duration is missing or non-positive", () => {
const rows = reconstructContiguousSleepTimeline({
startMs: onset,
stages: [
{ durationMs: 60 * 60_000, stage: "CORE", fieldTag: "sleep_core" },
],
inBed: {
durationMs: 0,
measuredAt: new Date(end),
fieldTag: "sleep_in_bed",
},
externalIdFor: (tag) => `night:seg:${tag}`,
});
expect(rows.find((r) => r.sleepStage === "IN_BED")).toBeUndefined();
});
});