Merge branch 'fix/v1437-jobs' into release/v1.43.7

This commit is contained in:
Marc-André Bombeck committed 2026-10-20 05:58:02 +02:00
commit ea933b91cc
59 files changed
+2340 -855

No files matched your search

@@ -358,6 +358,86 @@ describe("GET /api/integrations/healthkit — sync health", () => {
expect(byType.PULSE).toBe(false);
});
// #1247 — weight entered here goes to Apple Health and never arrives back,
// so a person who keeps their weight here watched the type go quiet.
describe("a type the app writes back to the phone", () => {
function mockWriteBack(enteredHereAt: Date | null): void {
vi.mocked(listLiveMeasurementTypes).mockResolvedValue([
"WEIGHT",
"PULSE",
]);
vi.mocked(prisma.$queryRaw).mockImplementation((async (
strings: TemplateStringsArray,
) => {
const sql = strings.join("?");
if (sql.includes("medication_intake_events")) {
return [{ last_seen: null }];
}
if (sql.includes("<> 'APPLE_HEALTH'")) {
return [{ type: "WEIGHT", last_seen: enteredHereAt }];
}
return [
{ type: "WEIGHT", last_seen: ago(30 * DAY) },
{ type: "PULSE", last_seen: ago(30 * DAY) },
];
}) as never);
}
function byType(body: Body) {
return Object.fromEntries(
body.metricFreshness.map((entry) => [entry.type, entry]),
) as Record<
string,
Body["metricFreshness"][number] & {
lastEnteredHereAt?: string | null;
}
>;
}
it("is not quiet while readings are entered here", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
healthKitConfigJson: null,
healthKitLastSyncedAt: ago(60 * 60 * 1000),
} as never);
const enteredHereAt = ago(2 * DAY);
mockWriteBack(enteredHereAt);
const entries = byType(await read());
expect(entries.WEIGHT.stale).toBe(false);
expect(entries.WEIGHT.lastEnteredHereAt).toBe(
enteredHereAt.toISOString(),
);
// A read-only type keeps its own verdict and carries no such field.
expect(entries.PULSE.stale).toBe(true);
expect(entries.PULSE).not.toHaveProperty("lastEnteredHereAt");
});
it("is quiet when nothing arrived and nothing was entered here", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
healthKitConfigJson: null,
healthKitLastSyncedAt: ago(60 * 60 * 1000),
} as never);
mockWriteBack(null);
const entries = byType(await read());
expect(entries.WEIGHT.stale).toBe(true);
expect(entries.WEIGHT.lastEnteredHereAt).toBeNull();
});
it("counts only what the entry still writes back", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
healthKitConfigJson: {
entries: [
{ id: "bodyMass", kind: "bodyMass", direction: "readOnly" },
],
},
healthKitLastSyncedAt: ago(60 * 60 * 1000),
} as never);
mockWriteBack(ago(2 * DAY));
const entries = byType(await read());
expect(entries.WEIGHT.stale).toBe(true);
expect(entries.WEIGHT).not.toHaveProperty("lastEnteredHereAt");
});
});
// #586 — the reporter's question is whether the phone delivers on its own or
// only while the app is open. `lastSyncedAt` reads identically either way, so
// it cannot answer it; these two fields are what can.
+29 -9
View File
@@ -23,11 +23,13 @@ import { auditLog } from "@/lib/auth/audit";
import { getAppleHealthSyncProgress } from "@/lib/integrations/apple-health-progress";
import {
getHealthKitTypeArrivals,
healthKitWriteBackTypes,
newestAppleHealthDataAt,
withHealthKitArrivals,
} from "@/lib/integrations/healthkit-type-sync";
import {
getAppleHealthDoseFreshness,
getEnteredHereAt,
getSourceFreshness,
} from "@/lib/integrations/metric-freshness";
import { loadOneConnectionHealth } from "@/lib/integrations/connection-health-load";
@@ -183,14 +185,27 @@ export const GET = apiHandler(async () => {
// v1.42.4 — Apple Health doses join the list as `MEDICATION_DOSE`, so the
// phone can read when doses last arrived from the same entries as every
// other type. Fail-soft on its own: a failed dose read drops that one entry.
const [measurementSamples, doseSample, arrivals, syncProgress, health] =
await Promise.all([
getSourceFreshness(user.id, "APPLE_HEALTH").catch(() => []),
getAppleHealthDoseFreshness(user.id).catch(() => null),
getHealthKitTypeArrivals(user.id).catch(() => null),
getAppleHealthSyncProgress(user.id).catch(() => null),
loadOneConnectionHealth(user.id, "apple-health").catch(() => null),
]);
// #1247 — the types the app writes back to the phone: what is entered here
// goes to Apple Health and never arrives back, so each of them also carries
// the newest reading entered here, and is quiet only when both are. Fail-
// soft to undefined, which leaves the list as it was before.
const [
measurementSamples,
doseSample,
arrivals,
syncProgress,
health,
enteredHere,
] = await Promise.all([
getSourceFreshness(user.id, "APPLE_HEALTH").catch(() => []),
getAppleHealthDoseFreshness(user.id).catch(() => null),
getHealthKitTypeArrivals(user.id).catch(() => null),
getAppleHealthSyncProgress(user.id).catch(() => null),
loadOneConnectionHealth(user.id, "apple-health").catch(() => null),
getEnteredHereAt(user.id, healthKitWriteBackTypes(entries)).catch(
() => undefined,
),
]);
const samples = doseSample
? [...measurementSamples, doseSample].sort((a, b) =>
a.type.localeCompare(b.type),
@@ -216,7 +231,12 @@ export const GET = apiHandler(async () => {
lastBackgroundSyncAt,
syncHealth,
metricFreshness: withHealthKitArrivals(
classifyMetricFreshness(samples, syncHealth.verdict),
classifyMetricFreshness(
samples,
syncHealth.verdict,
Date.now(),
enteredHere,
),
arrivals,
),
syncProgress,
@@ -210,6 +210,7 @@ beforeEach(() => {
});
afterEach(() => {
vi.useRealTimers();
if (ORIGINAL_SECRET === undefined) delete process.env.TELEGRAM_WEBHOOK_SECRET;
else process.env.TELEGRAM_WEBHOOK_SECRET = ORIGINAL_SECRET;
});
@@ -396,7 +397,11 @@ describe("Telegram webhook — callback dispatch", () => {
expect(ackText).toMatch(/1 hour/i);
});
it("'skip:<medId>' creates an intake event with skipped=true and snoozes through end-of-day", async () => {
it("'skip:<medId>' creates an intake event with skipped=true and snoozes through the person's own end of day", async () => {
// 23:30 UTC is already 01:30 the next day in Berlin: the snooze ends at
// Berlin's next midnight, not at the end of the UTC day half an hour on.
vi.useFakeTimers({ toFake: ["Date"] });
vi.setSystemTime(new Date("2026-06-10T23:30:00.000Z"));
const res = await POST(tgRequest(callbackUpdate("skip:med-1")));
expect(res.status).toBe(200);
@@ -417,8 +422,7 @@ describe("Telegram webhook — callback dispatch", () => {
const updateArgs = vi.mocked(prisma.medication.update).mock.calls[0][0];
const snoozedUntil = (updateArgs.data as { snoozedUntil: Date })
.snoozedUntil;
expect(snoozedUntil.getHours()).toBe(23);
expect(snoozedUntil.getMinutes()).toBe(59);
expect(snoozedUntil.toISOString()).toBe("2026-06-11T21:59:59.999Z");
// Skip row + rest-of-day snooze commit atomically.
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
// v1.16.10 — an explicit skip refunds a previously-consumed stamp