Hold the checkup overdue flag for the whole day it is due

The MCP due-list answered "is this checkup overdue" by comparing the two
instants, so a checkup booked for nine in the morning read overdue from ten
past. The screens read the same reminder as "today" until midnight. One
reminder, two verdicts, and the model reading the tool result repeated the
one nobody could see.

Same counting as the screens and the briefing payload now, on the account's
own clock rather than the server's. A checkup stays due for the whole of the
date it belongs to, and is overdue from the date after.
This commit is contained in:
Marc-André Bombeck committed 2026-08-01 20:09:37 +02:00
1 parent 49ac6679c8
commit 67d4f87720
2 files changed
+101 -2

No files matched your search

+89
View File
@@ -687,6 +687,95 @@ describe("get_preventive_care", () => {
};
expect(result.present).toBe(false);
});
/**
* Overdue is a question about dates, and the answer has to hold for the
* whole of the date. Comparing the two instants flipped the flag at the
* hour the checkup was booked for, so from ten past nine the tool told the
* model a checkup was overdue while the screen beside it still read
* "today" — one reminder, two verdicts.
*
* Read in the profile zone the mocked account carries (UTC), so the
* instants below say the same thing whatever the host machine is set to.
*/
async function overdueFlagFor(
nextDueAt: string,
now: string,
timezone = "UTC",
) {
vi.setSystemTime(Date.parse(now));
vi.mocked(prisma.user.findUnique).mockResolvedValue({ timezone } as never);
vi.mocked(prisma.measurementReminder.findMany).mockResolvedValue([
{ id: "r-1" },
] as never);
vi.mocked(toMeasurementReminderDto).mockReturnValue({
id: "r-1",
label: "Blood pressure check",
measurementType: "BLOOD_PRESSURE_SYS",
intervalDays: 30,
rrule: null,
anchorDate: null,
endsOn: null,
origin: "VORSORGE",
notifyHour: 9,
location: null,
nextDueAt,
lastSatisfiedAt: null,
enabled: true,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
const result = (await tool("get_preventive_care").run(CTX, {})) as {
checkups: Array<{ overdue: boolean }>;
};
return result.checkups[0]?.overdue;
}
it("holds the overdue flag for the whole of the day a checkup is due", async () => {
vi.useFakeTimers();
try {
// The booked hour has passed by eleven hours, and it is still the day
// the checkup belongs to.
expect(
await overdueFlagFor(
"2026-07-17T09:00:00.000Z",
"2026-07-17T20:00:00.000Z",
),
).toBe(false);
// Forty-five minutes later on the clock, and a date later on the
// calendar. Now it is missed.
expect(
await overdueFlagFor(
"2026-07-17T23:30:00.000Z",
"2026-07-18T00:15:00.000Z",
),
).toBe(true);
} finally {
vi.useRealTimers();
}
});
it("reads the dates off the profile's calendar, not the server's", async () => {
vi.useFakeTimers();
try {
// The same pair of instants as the first case above, which UTC reads as
// one date and answers "not overdue". Twelve hours east it is already
// the next morning, so that same checkup was yesterday's and is missed.
// Only the profile zone can produce the second answer.
expect(
await overdueFlagFor(
"2026-07-17T09:00:00.000Z",
"2026-07-17T20:00:00.000Z",
"Pacific/Auckland",
),
).toBe(true);
} finally {
vi.useRealTimers();
vi.mocked(prisma.user.findUnique).mockResolvedValue({
timezone: "UTC",
} as never);
}
});
});
describe("search — cursor pagination", () => {
+12 -2
View File
@@ -67,6 +67,7 @@ import {
resolveSyncVerdict,
} from "@/lib/integrations/sync-verdict";
import { toMeasurementReminderDto } from "@/lib/measurement-reminders/dto";
import { calendarDaysUntil } from "@/lib/measurement-reminders/due-day";
import { fenceUserText, scrubFenceMarkers } from "@/lib/ai/coach/data-fence";
import type { McpAuthContext } from "./auth";
@@ -1566,7 +1567,16 @@ export const MCP_TOOLS: McpToolDefinition[] = [
return { present: false };
}
const nowMs = Date.now();
// Overdue is a CALENDAR-day verdict, the same one the checkups screen
// renders. Comparing the instants said a checkup booked for nine this
// morning was overdue by ten past, so the tool called something overdue
// on the very day it is due while the screen next to it read "today".
const user = await prisma.user.findUnique({
where: { id: ctx.userId },
select: { timezone: true },
});
const timezone = user?.timezone || DEFAULT_TIMEZONE;
const now = new Date();
const checkups = reminders.map((r) => {
const dto = toMeasurementReminderDto(r);
return {
@@ -1575,7 +1585,7 @@ export const MCP_TOOLS: McpToolDefinition[] = [
nextDueAt: dto.nextDueAt,
overdue:
dto.nextDueAt !== null
? new Date(dto.nextDueAt).getTime() < nowMs
? calendarDaysUntil(new Date(dto.nextDueAt), now, timezone) < 0
: false,
location: dto.location,
lastSatisfiedAt: dto.lastSatisfiedAt,