mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(coach-nudge): correct the sleep-debt trigger unit so it can fire
The sleep-debt nudge compared a minutes value against an hours threshold, so the trigger was effectively dead. Align the comparison to the unit the sleep-debt value is produced in and pin it with a firing-case test.
This commit is contained in:
1 parent
3de53f549c
commit
9a5e562001
2 files changed
+38
-13
No files matched your search
@@ -45,6 +45,13 @@ vi.mock("@/lib/ai/coach/bytes-codec", () => ({
|
||||
decryptFromBytes: vi.fn(() => "morning blood pressure"),
|
||||
encryptToBytes: vi.fn(),
|
||||
}));
|
||||
// The sleep-debt branch resolves the user's source priority through the
|
||||
// real `@/lib/db` prisma; stub it so the unit suite stays DB-free. A
|
||||
// null priority is the no-custom-priority default — `reconstructSleepNights`
|
||||
// (pure) then runs normally on the stubbed measurement rows.
|
||||
vi.mock("@/lib/rollups/measurement-read", () => ({
|
||||
loadUserSourcePriority: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
function dose(taken: boolean, skipped = false, autoMissed?: boolean) {
|
||||
return {
|
||||
|
||||
+31
-13
@@ -78,6 +78,9 @@ import type { Locale } from "@/lib/i18n/config";
|
||||
import { defaultLocale, locales } from "@/lib/i18n/config";
|
||||
import { getEvent } from "@/lib/logging/context";
|
||||
import { resolveCanonicalRecovery } from "@/lib/insights/derived/recovery-resolve";
|
||||
import { reconstructSleepNights } from "@/lib/analytics/sleep-night";
|
||||
import { loadUserSourcePriority } from "@/lib/rollups/measurement-read";
|
||||
import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver";
|
||||
// Trend thresholds shared with the dashboard hero's verdict resolver
|
||||
// live in a client-safe leaf module; re-exported below so server-side
|
||||
// imports keep their path.
|
||||
@@ -551,9 +554,19 @@ export async function findTriggerForUser(
|
||||
}
|
||||
}
|
||||
|
||||
// 5) Sleep-deficit series: per-night hours over the last 7 days,
|
||||
// longest record per calendar night (multi-source rows collapse
|
||||
// to the fullest one).
|
||||
// 5) Sleep-deficit series: per-night asleep HOURS over the last 7
|
||||
// days, reconstructed through the canonical sleep engine.
|
||||
//
|
||||
// `SLEEP_DURATION` is stored one row per STAGE per night, in
|
||||
// MINUTES. The trigger compares against
|
||||
// `effectiveRange("SLEEP_DURATION").greenMin`, which is in HOURS
|
||||
// (7). The prior inline model kept each night's MAX single stage
|
||||
// row (a fragment, not the night) AND fed minutes straight into
|
||||
// the hours comparison — so a real ~360-480 minute night never
|
||||
// cleared `< 6.5` and the nudge could never fire. Reusing
|
||||
// `reconstructSleepNights` gives the proper asleep total
|
||||
// (CORE+DEEP+REM, multi-source de-duped, user-tz wake-day keyed);
|
||||
// dividing by 60 converts to the hours the trigger expects.
|
||||
const sleepRows = await prisma.measurement.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
@@ -561,20 +574,25 @@ export async function findTriggerForUser(
|
||||
deletedAt: null,
|
||||
measuredAt: { gte: sevenDaysAgo, lte: now },
|
||||
},
|
||||
select: { value: true, measuredAt: true },
|
||||
select: {
|
||||
value: true,
|
||||
measuredAt: true,
|
||||
sleepStage: true,
|
||||
source: true,
|
||||
deviceType: true,
|
||||
},
|
||||
});
|
||||
if (sleepRows.length > 0) {
|
||||
const byNight = new Map<string, number>();
|
||||
for (const row of sleepRows) {
|
||||
const night = row.measuredAt.toISOString().slice(0, 10);
|
||||
const prev = byNight.get(night);
|
||||
if (prev === undefined || row.value > prev) {
|
||||
byNight.set(night, row.value);
|
||||
}
|
||||
}
|
||||
const sourcePriority = await loadUserSourcePriority(user.id);
|
||||
const nights = reconstructSleepNights(
|
||||
sleepRows,
|
||||
user.timezone ?? DEFAULT_TIMEZONE,
|
||||
sourcePriority,
|
||||
);
|
||||
const nightlyHours = nights.map((n) => n.asleepMinutes / 60);
|
||||
if (
|
||||
evaluateSleepDebtTrigger(
|
||||
[...byNight.values()],
|
||||
nightlyHours,
|
||||
effectiveRange("SLEEP_DURATION")?.greenMin ?? null,
|
||||
)
|
||||
) {
|
||||
|
||||
Reference in new issue
Block a user