Files
HealthLog/tests/integration/rest-mode-chronic.test.ts
T
Marc-André Bombeck 9aa60fb2ff fix(illness): keep chronic conditions out of rest mode
A chronic ongoing condition has no end date, so counting it as rest mode
kept the account in it for good: Day N counted from an onset years back,
cadence nudges never resumed and every streak gap since the onset was
bridged. Only active episodes that are not CHRONIC_ONGOING (acute,
recurring, flare) now trigger rest mode, and Day N counts from the
earliest of those. The achievements streak freeze applies the same
filter to its own episode query.

Chronic conditions stay context: a coincident vital deviation is still
framed as explained by any active episode, and the Coach block and the
briefing still name them, with restMode false.

Refs #1216
2026-10-14 18:57:01 +02:00

174 lines
5.4 KiB
TypeScript

/**
* #1216 — a chronic ongoing condition alone never puts the account into Rest
* Mode, against real Postgres.
*
* A CHRONIC_ONGOING episode has no end date by design, so counting it would
* put the account into Rest Mode for good: the Today strip read "Rest mode ·
* Day 3948", nudges stayed paused and every streak gap since the onset was
* bridged. Pinned here on the three readers that decide it: the resolver
* (which the score, recovery, nudge and Today surfaces read), the Today
* digest's Day N, and the achievements streak freeze, which runs its own
* episode query.
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { Prisma } from "@/generated/prisma/client";
import { __resetAllCachesForTests } from "@/lib/cache/server-cache";
import { loadDailyDigest } from "@/lib/daily/load-digest";
import { buildAchievementsResult } from "@/lib/gamification/achievements-result";
import { REST_MODE_INACTIVE, resolveRestMode } from "@/lib/illness/rest-mode";
import type { ModuleKey } from "@/lib/modules/registry";
import { getPrismaClient, truncateAllTables } from "./setup";
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
const USER = "rest-mode-chronic";
const DAY = 24 * 60 * 60 * 1000;
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
__resetAllCachesForTests();
await getPrismaClient().user.create({
data: {
id: USER,
username: USER,
email: `${USER}@example.test`,
timezone: "UTC",
createdAt: new Date("2026-03-01T00:00:00.000Z"),
},
});
});
function episode(data: Partial<Prisma.IllnessEpisodeUncheckedCreateInput>) {
return getPrismaClient().illnessEpisode.create({
data: {
userId: USER,
label: "Condition",
type: "CHRONIC",
lifecycle: "CHRONIC_ONGOING",
onsetAt: new Date("2015-08-01T00:00:00.000Z"),
...data,
},
});
}
describe("resolveRestMode on a real record", () => {
const NOW = new Date("2026-10-04T12:00:00.000Z");
it("a chronic condition alone is not Rest Mode", async () => {
await episode({});
expect(await resolveRestMode(USER, NOW, getPrismaClient())).toEqual(
REST_MODE_INACTIVE,
);
});
it("chronic + acute is Rest Mode, anchored on the acute onset", async () => {
await episode({});
const acute = await episode({
label: "Cold",
type: "INFECTION",
lifecycle: "ACUTE",
onsetAt: new Date("2026-10-02T18:00:00.000Z"),
});
const ctx = await resolveRestMode(USER, NOW, getPrismaClient());
expect(ctx.active).toBe(true);
expect(ctx.since).toBe("2026-10-02T18:00:00.000Z");
expect(ctx.episodes.map((e) => e.id)).toEqual([acute.id]);
});
it("a flare threaded under the chronic condition is Rest Mode", async () => {
const parent = await episode({});
await episode({
label: "Flare",
lifecycle: "FLARE",
parentConditionId: parent.id,
onsetAt: new Date("2026-10-03T08:00:00.000Z"),
});
const ctx = await resolveRestMode(USER, NOW, getPrismaClient());
expect(ctx.active).toBe(true);
expect(ctx.since).toBe("2026-10-03T08:00:00.000Z");
});
});
describe("the Today digest's Rest Mode fact", () => {
const NOW = new Date("2026-10-04T06:00:00.000Z");
async function digest() {
const user = await getPrismaClient().user.findUniqueOrThrow({
where: { id: USER },
});
return loadDailyDigest(user, NOW, { locale: "en" });
}
it("shows no Rest Mode for a chronic condition alone", async () => {
await episode({});
const d = await digest();
expect(d.restMode).toBeNull();
expect(d.today.some((f) => f.kind === "rest_mode")).toBe(false);
});
it("counts Day N from the acute onset, not the chronic one", async () => {
await episode({});
await episode({
label: "Cold",
type: "INFECTION",
lifecycle: "ACUTE",
onsetAt: new Date("2026-10-02T18:00:00.000Z"),
});
const d = await digest();
expect(d.restMode).toEqual({ day: 3 });
});
});
describe("the achievements streak freeze", () => {
const MODULES = {} as Record<ModuleKey, boolean>;
/** Logins two days apart: a one-day gap the freeze may or may not bridge. */
async function loginsAroundAGap() {
const today = new Date();
today.setUTCHours(12, 0, 0, 0);
const gapDay = new Date(today.getTime() - 4 * DAY);
await getPrismaClient().auditLog.createMany({
data: [5, 3].map((d) => ({
userId: USER,
action: "auth.login.password",
createdAt: new Date(today.getTime() - d * DAY),
})),
});
return gapDay;
}
async function loginStreak() {
const user = await getPrismaClient().user.findUniqueOrThrow({
where: { id: USER },
});
const result = await buildAchievementsResult(
user as Parameters<typeof buildAchievementsResult>[0],
MODULES,
);
return result.metrics.loginDayStreak;
}
it("does not bridge a gap on a chronic-only day", async () => {
await loginsAroundAGap();
await episode({});
expect(await loginStreak()).toBe(1);
});
it("still bridges a gap covered by an acute episode", async () => {
const gapDay = await loginsAroundAGap();
await episode({});
await episode({
label: "Cold",
type: "INFECTION",
lifecycle: "ACUTE",
onsetAt: new Date(gapDay.getTime() - 2 * 60 * 60 * 1000),
resolvedAt: new Date(gapDay.getTime() + 6 * 60 * 60 * 1000),
});
expect(await loginStreak()).toBe(3);
});
});