test(integrations): cover the verdict ladder and the coverage guarantee

The load-bearing one is the panel-coverage test: render the panel with an
envelope carrying every integration key and require each configured one to
surface as a card or as a fallback row. Drop the fallback row and it goes red,
which is the whole point — the previous shape of this bug was a provider on
the wire that no screen rendered, and no card-shaped test could have caught it.

Alongside it: the ladder table with the month-silent fixture that resolves to
stalled, the 24-hour and 7-day boundaries asserted against literals rather than
the constants they check, the per-metric stale flag firing only under a healthy
provider, and a structural guard that the verdict-to-pill mapping is defined in
exactly one module.

The card suites migrate with the semantics: a transient failure now expects the
warning pill everywhere, and the Apple Health suite trades its bespoke badge
states for pill assertions. The e2e mocks gain the verdict shape, the
nightscout entry, and an assertion that the card-less row is on the page.
This commit is contained in:
Marc-André Bombeck committed 2026-07-25 09:41:53 +02:00
1 parent ce74fd52a2
commit 10593a11a8
13 files changed
+1245 -183

No files matched your search

@@ -6,6 +6,9 @@ vi.mock("@/lib/db", () => ({
user: { findUnique: vi.fn(), update: vi.fn() },
// v1.4.43 W6 — audit-ledger breadcrumb for validation-failed paths.
auditLog: { create: vi.fn() },
// Per-metric freshness for the APPLE_HEALTH source.
measurement: { groupBy: vi.fn() },
workout: { groupBy: vi.fn() },
},
toJson: <T>(v: T) => v,
}));
@@ -34,6 +37,7 @@ vi.mock("next/headers", () => ({
import { GET, PATCH } from "../route";
import { prisma } from "@/lib/db";
import { getSession } from "@/lib/auth/session";
import { APPLE_HEALTH_DATA_STALE_AFTER_MS } from "@/lib/integrations/sync-verdict";
const SESSION_OK = {
session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) },
@@ -48,6 +52,8 @@ beforeEach(() => {
} as never);
vi.mocked(prisma.user.update).mockResolvedValue({} as never);
vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never);
vi.mocked(prisma.measurement.groupBy).mockResolvedValue([] as never);
vi.mocked(prisma.workout.groupBy).mockResolvedValue([] as never);
});
const callGet = GET as unknown as (req: NextRequest) => Promise<Response>;
@@ -199,3 +205,104 @@ describe("PATCH /api/integrations/healthkit — 422 multi-issue (v1.4.43 W6)", (
expect(res.status).toBe(422);
});
});
/**
* Apple Health is push-based and has no ledger row, so `lastSyncedAt` was the
* only signal the card had — and its mere presence painted green, whether the
* data arrived this morning or three weeks ago. These pin the seven-day
* threshold and the per-metric read that makes a silently revoked HealthKit
* permission visible inside an otherwise healthy connection.
*/
describe("GET /api/integrations/healthkit — sync health", () => {
const DAY = 24 * 60 * 60 * 1000;
const ago = (ms: number) => new Date(Date.now() - ms);
type Body = {
lastSyncedAt: string | null;
syncHealth: { verdict: string; since: string | null };
metricFreshness: Array<{
type: string;
lastSeenAt: string;
stale: boolean;
}>;
};
async function read(): Promise<Body> {
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
const res = await callGet(makeGetReq());
expect(res.status).toBe(200);
return ((await res.json()) as { data: Body }).data;
}
it("calls a recent delivery fresh", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
healthKitConfigJson: null,
healthKitLastSyncedAt: ago(2 * DAY),
} as never);
expect((await read()).syncHealth.verdict).toBe("fresh");
});
it("calls a pipe silent for over a week stale", async () => {
const lastSyncedAt = ago(APPLE_HEALTH_DATA_STALE_AFTER_MS + DAY);
vi.mocked(prisma.user.findUnique).mockResolvedValue({
healthKitConfigJson: null,
healthKitLastSyncedAt: lastSyncedAt,
} as never);
const body = await read();
expect(body.syncHealth.verdict).toBe("stale");
expect(body.syncHealth.since).toBe(lastSyncedAt.toISOString());
});
it("calls an account that never delivered pending, not disconnected", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
healthKitConfigJson: null,
healthKitLastSyncedAt: null,
} as never);
expect((await read()).syncHealth.verdict).toBe("pending_first_sync");
});
it("flags one quiet metric inside an otherwise healthy connection", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
healthKitConfigJson: null,
healthKitLastSyncedAt: ago(60 * 60 * 1000),
} as never);
vi.mocked(prisma.measurement.groupBy).mockResolvedValue([
{
source: "APPLE_HEALTH",
type: "RESPIRATORY_RATE",
_max: { measuredAt: ago(30 * DAY) },
},
{
source: "APPLE_HEALTH",
type: "PULSE",
_max: { measuredAt: ago(60 * 60 * 1000) },
},
] as never);
const body = await read();
// Only the Apple Health source is read.
const groupByArgs = vi.mocked(prisma.measurement.groupBy).mock
.calls[0]?.[0] as unknown as {
where: { source: { in: string[] } };
};
expect(groupByArgs.where.source.in).toEqual(["APPLE_HEALTH"]);
const byType = Object.fromEntries(
body.metricFreshness.map((entry) => [entry.type, entry.stale]),
);
expect(byType.RESPIRATORY_RATE).toBe(true);
expect(byType.PULSE).toBe(false);
});
it("degrades to no per-metric data rather than failing the config read", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
healthKitConfigJson: null,
healthKitLastSyncedAt: ago(DAY),
} as never);
vi.mocked(prisma.measurement.groupBy).mockRejectedValue(
new Error("groupBy hiccup"),
);
const body = await read();
expect(body.metricFreshness).toEqual([]);
expect(body.syncHealth.verdict).toBe("fresh");
});
});
@@ -13,6 +13,8 @@ vi.mock("@/lib/db", () => ({
fitbitConnection: { findUnique: vi.fn(async () => null) },
googleHealthConnection: { findUnique: vi.fn(async () => null) },
moodEntry: { count: vi.fn(async () => 0) },
measurement: { groupBy: vi.fn(async () => []) },
workout: { groupBy: vi.fn(async () => []) },
},
}));
@@ -31,10 +33,11 @@ const ledger: Record<string, unknown> = {
vi.mock("@/lib/integrations/status", () => ({
getIntegrationStatus: vi.fn(async (_u: string, integration: string) => ({
integration,
...ledger,
...(perIntegrationLedger[integration] ?? ledger),
})),
getPersistentFailureThreshold: () => 5,
}));
const perIntegrationLedger: Record<string, Record<string, unknown>> = {};
vi.mock("@/lib/withings/client", () => ({ hasActivityScope: () => false }));
vi.mock("@/lib/moodlog-secret", () => ({ readMoodLogSecret: () => null }));
@@ -50,6 +53,7 @@ vi.mock("@/lib/oura/credentials", () => ({
import { GET } from "../route";
import { prisma } from "@/lib/db";
import { ATTEMPT_STALE_AFTER_MS } from "@/lib/integrations/sync-verdict";
const userFind = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const whoopFind = prisma.whoopConnection.findUnique as ReturnType<typeof vi.fn>;
@@ -60,6 +64,10 @@ type Entry = {
configured?: boolean;
available?: boolean;
hasOwnCredentials?: boolean;
hasToken?: boolean;
allowPrivateHost?: boolean;
syncHealth?: { verdict: string; since: string | null };
metricFreshness?: Array<{ type: string; lastSeenAt: string; stale: boolean }>;
};
async function fetchEntries(): Promise<Entry[]> {
@@ -71,6 +79,9 @@ async function fetchEntries(): Promise<Entry[]> {
describe("/api/integrations/status — Polar/Oura fold (04-M2)", () => {
beforeEach(() => {
vi.clearAllMocks();
for (const key of Object.keys(perIntegrationLedger)) {
delete perIntegrationLedger[key];
}
polarAvailable.mockResolvedValue(true);
ouraAvailable.mockResolvedValue(false);
});
@@ -139,3 +150,95 @@ describe("/api/integrations/status — WHOOP identity ownership", () => {
expect(whoop?.configured).toBe(true);
});
});
/**
* The envelope is the unit of coverage for liveness. Every entry carries the
* verdict, whether or not a card renders it — a card-level verdict misses a
* card-less provider by construction, which is exactly how a month-dead sync
* stayed invisible.
*/
describe("/api/integrations/status — sync-health verdict on every entry", () => {
beforeEach(() => {
vi.clearAllMocks();
for (const key of Object.keys(perIntegrationLedger)) {
delete perIntegrationLedger[key];
}
polarAvailable.mockResolvedValue(true);
ouraAvailable.mockResolvedValue(true);
});
it("carries a syncHealth verdict on every entry", async () => {
userFind.mockResolvedValue({});
const entries = await fetchEntries();
expect(entries.length).toBeGreaterThan(0);
for (const entry of entries) {
expect(entry.syncHealth, entry.integration).toBeDefined();
expect(typeof entry.syncHealth!.verdict).toBe("string");
expect(Array.isArray(entry.metricFreshness)).toBe(true);
}
});
it("reports a configured, month-silent card-less provider as stalled", async () => {
// The live incident, on the wire: moodLog has no Settings card, so this
// entry is the only thing that can tell anyone the sync has stopped.
const lastAttemptAt = new Date(
Date.now() - 28 * 24 * 60 * 60 * 1000,
).toISOString();
perIntegrationLedger.moodlog = {
state: "error_transient",
lastSuccessAt: new Date(
Date.now() - 55 * 24 * 60 * 60 * 1000,
).toISOString(),
lastAttemptAt,
lastError: null,
};
userFind.mockResolvedValue({
moodLogUrlEncrypted: "enc",
moodLogEnabled: true,
});
const moodlog = (await fetchEntries()).find(
(entry) => entry.integration === "moodlog",
)!;
expect(moodlog.configured).toBe(true);
expect(moodlog.syncHealth).toEqual({
verdict: "stalled",
since: lastAttemptAt,
});
});
it("keeps an integration that attempted within the window off the stalled arm", async () => {
perIntegrationLedger.moodlog = {
state: "error_transient",
lastSuccessAt: null,
lastAttemptAt: new Date(
Date.now() - (ATTEMPT_STALE_AFTER_MS - 60_000),
).toISOString(),
lastError: null,
};
userFind.mockResolvedValue({ moodLogUrlEncrypted: "enc" });
const moodlog = (await fetchEntries()).find(
(entry) => entry.integration === "moodlog",
)!;
expect(moodlog.syncHealth!.verdict).toBe("failing");
});
it("includes a nightscout entry with the fields the card's form reads", async () => {
userFind.mockResolvedValue({
nightscoutUrlEncrypted: "enc",
nightscoutTokenEncrypted: "enc",
nightscoutAllowPrivateHost: true,
});
const nightscout = (await fetchEntries()).find(
(entry) => entry.integration === "nightscout",
);
expect(nightscout).toBeDefined();
expect(nightscout!.connected).toBe(true);
expect(nightscout!.configured).toBe(true);
expect(nightscout!.hasToken).toBe(true);
expect(nightscout!.allowPrivateHost).toBe(true);
expect(nightscout!.syncHealth).toBeDefined();
});
});