Files
HealthLog/src/lib/integrations/__tests__/status.test.ts
T

1272 lines
44 KiB
TypeScript

/**
* v1.4.15 Phase B2 — IntegrationStatus helper tests.
*
* The persistence path is exercised by the integration suite under
* `tests/integration/integration-status.test.ts` (real Postgres so the
* upsert + concurrent-update story holds). This file is the unit-level
* suite for the threshold + alerting state machine — Prisma is mocked so
* we control every input and observe the exact sequence of calls.
*/
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("@/lib/db", () => ({
prisma: {
integrationStatus: {
findUnique: vi.fn(),
upsert: vi.fn(),
update: vi.fn(),
},
user: {
findUnique: vi.fn(),
findMany: vi.fn(),
},
},
}));
vi.mock("@/lib/auth/audit", () => ({
auditLog: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/notifications/dispatcher", () => ({
dispatchNotification: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/crypto", () => ({
// Deterministic encrypt/decrypt for assertion stability.
encrypt: (s: string) => `enc(${s})`,
decrypt: (s: string) =>
s.startsWith("enc(") && s.endsWith(")") ? s.slice(4, -1) : s,
}));
import {
recordSyncFailure,
recordSyncSuccess,
markReauthRequired,
markDisconnected,
markReconnected,
isReauthRequired,
getPersistentFailureThreshold,
resumeIntegrationFromPark,
markSyncFailureRecorded,
isSyncFailureRecorded,
} from "../status";
import { prisma } from "@/lib/db";
import { auditLog } from "@/lib/auth/audit";
import { dispatchNotification } from "@/lib/notifications/dispatcher";
beforeEach(() => {
vi.resetAllMocks();
delete process.env.INTEGRATION_FAILURE_ALERT_THRESHOLD;
});
describe("getPersistentFailureThreshold", () => {
it("defaults to 3 when env var is unset", () => {
expect(getPersistentFailureThreshold()).toBe(3);
});
it("respects INTEGRATION_FAILURE_ALERT_THRESHOLD when set to a positive integer", () => {
process.env.INTEGRATION_FAILURE_ALERT_THRESHOLD = "5";
expect(getPersistentFailureThreshold()).toBe(5);
});
it("falls back to 3 when env var is non-numeric", () => {
process.env.INTEGRATION_FAILURE_ALERT_THRESHOLD = "many";
expect(getPersistentFailureThreshold()).toBe(3);
});
it("falls back to 3 when env var is zero or negative", () => {
process.env.INTEGRATION_FAILURE_ALERT_THRESHOLD = "0";
expect(getPersistentFailureThreshold()).toBe(3);
process.env.INTEGRATION_FAILURE_ALERT_THRESHOLD = "-2";
expect(getPersistentFailureThreshold()).toBe(3);
});
});
describe("recordSyncSuccess", () => {
it("upserts the row with state=connected, resets streak, clears alertedAt", async () => {
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await recordSyncSuccess("u1", "withings");
const args = vi.mocked(prisma.integrationStatus.upsert).mock.calls[0][0];
expect(args.where).toEqual({
userId_integration: { userId: "u1", integration: "withings" },
});
expect(args.update).toEqual({
state: "connected",
lastSuccessAt: expect.any(Date),
lastAttemptAt: expect.any(Date),
lastError: null,
// v1.4.43 W14 — success clears every per-kind bucket so the
// next failure starts from zero in its own bucket.
// v1.4.47 W1 — the legacy `consecutiveFailures` column was
// dropped (migration 0077); the bucket reset is the only
// counter write now.
consecutiveFailuresByKind: {
transient: 0,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
failingSinceAt: null,
failingLegs: [],
alertedAt: null,
});
});
});
describe("recordSyncFailure — under threshold", () => {
it("increments the counter and does NOT dispatch an admin alert", async () => {
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings refresh error: 503 - upstream",
});
expect(prisma.integrationStatus.upsert).toHaveBeenCalledOnce();
expect(auditLog).toHaveBeenCalledWith(
"integrations.sync.failed",
expect.objectContaining({
userId: "u1",
details: expect.objectContaining({
integration: "withings",
attemptNumber: 1,
state: "error_transient",
}),
}),
);
expect(dispatchNotification).not.toHaveBeenCalled();
});
});
describe("recordSyncFailure — at threshold", () => {
it("dispatches an admin Telegram alert when failure count reaches default threshold (3)", async () => {
// v1.4.47 W1 — the alert ladder reads Math.max(...buckets) so the
// existing row must seed the bucket at N-1 before the in-memory
// increment produces alertSignal = N.
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 2,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
alertedAt: null,
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: null,
} as never);
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
email: "user@example.com",
} as never);
vi.mocked(prisma.user.findMany).mockResolvedValueOnce([
{ id: "admin-1" },
{ id: "admin-2" },
] as never);
vi.mocked(prisma.integrationStatus.update).mockResolvedValueOnce(
{} as never,
);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings refresh error: 503 - upstream service down",
errorCode: "503",
});
expect(dispatchNotification).toHaveBeenCalledTimes(2);
const firstCallPayload = vi.mocked(dispatchNotification).mock.calls[0][0];
expect(firstCallPayload.eventType).toBe("SYSTEM_ALERT");
expect(firstCallPayload.userId).toBe("admin-1");
expect(firstCallPayload.title).toContain(
"Withings sync failing for user@example.com",
);
expect(firstCallPayload.message).toContain("3 times in a row");
expect(firstCallPayload.message).toContain("503");
expect(firstCallPayload.metadata).toEqual({
integration: "withings",
affectedUserId: "u1",
consecutiveFailures: 3,
errorCode: "503",
});
// alertedAt is stamped to gate re-alerting for 24h.
expect(prisma.integrationStatus.update).toHaveBeenCalledWith(
expect.objectContaining({
where: {
userId_integration: { userId: "u1", integration: "withings" },
},
data: expect.objectContaining({ alertedAt: expect.any(Date) }),
}),
);
});
it("uses the custom threshold from INTEGRATION_FAILURE_ALERT_THRESHOLD", async () => {
process.env.INTEGRATION_FAILURE_ALERT_THRESHOLD = "5";
// 4 < 5 → should not page yet. Seed the existing bucket at 3 so
// the in-memory increment lands at 4.
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 3,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
alertedAt: null,
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: null,
} as never);
await recordSyncFailure({
userId: "u1",
integration: "nightscout",
kind: "transient",
message: "Nightscout sync HTTP 502",
});
expect(dispatchNotification).not.toHaveBeenCalled();
});
});
describe("recordSyncFailure — alert window", () => {
it("does NOT dispatch a second alert within 24h on the same streak", async () => {
const now = Date.now();
// Existing bucket at 3 → in-memory increment lands at 4, above
// the default 3-strike threshold. The 24h alertedAt guard must
// suppress the duplicate page.
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 3,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
alertedAt: new Date(now - 60 * 60 * 1000), // 1h ago
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: new Date(now - 60 * 60 * 1000),
} as never);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings refresh error: 503 - upstream",
});
expect(dispatchNotification).not.toHaveBeenCalled();
});
it("DOES dispatch a fresh alert after 24h have elapsed", async () => {
// Existing bucket at 5 → in-memory increment lands at 6.
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 5,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
alertedAt: new Date(Date.now() - 25 * 60 * 60 * 1000), // 25h ago
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: new Date(Date.now() - 25 * 60 * 60 * 1000),
} as never);
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
email: "u@example.com",
} as never);
vi.mocked(prisma.user.findMany).mockResolvedValueOnce([
{ id: "admin-1" },
] as never);
vi.mocked(prisma.integrationStatus.update).mockResolvedValueOnce(
{} as never,
);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings refresh error: 503 - upstream",
});
expect(dispatchNotification).toHaveBeenCalledOnce();
});
});
describe("recordSyncFailure — reauth_required", () => {
it("marks state=error_reauth and audits with kind=reauth_required", async () => {
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "reauth_required",
message: "Withings refresh error: 100 - invalid_grant",
errorCode: "100",
});
const upsertArgs = vi.mocked(prisma.integrationStatus.upsert).mock
.calls[0][0];
expect(upsertArgs.update).toMatchObject({ state: "error_reauth" });
expect(auditLog).toHaveBeenCalledWith(
"integrations.sync.failed",
expect.objectContaining({
details: expect.objectContaining({
kind: "reauth_required",
errorCode: "100",
}),
}),
);
});
});
describe("recordSyncFailure — persistent (v1.4.42 W6)", () => {
it("marks state=error_transient (next sync still runs) and audits with kind=persistent", async () => {
// Contract-mismatch failures (Withings 293 invalid params) MUST
// surface in the audit log with kind=persistent so operations can
// grep for upstream contract bugs, but they MUST NOT park the
// integration — the next sync should still run because the
// mismatch can be one-sided (e.g. Withings introducing a new
// required field that the client release fixes).
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "persistent",
message: "Withings measure error: 293 - invalid params",
errorCode: "293",
});
const upsertArgs = vi.mocked(prisma.integrationStatus.upsert).mock
.calls[0][0];
expect(upsertArgs.update).toMatchObject({ state: "error_transient" });
expect(auditLog).toHaveBeenCalledWith(
"integrations.sync.failed",
expect.objectContaining({
details: expect.objectContaining({
kind: "persistent",
errorCode: "293",
}),
}),
);
});
it("at threshold, the admin alert message includes the 'persistent error' label", async () => {
// Seed persistent bucket at 2 → in-memory increment lands at 3,
// hitting the default threshold.
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 0,
reauth_required: 0,
persistent: 2,
},
persistentFailureStartedAt: new Date(Date.now() - 60 * 60 * 1000),
alertedAt: null,
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: null,
} as never);
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
email: "user@example.com",
} as never);
vi.mocked(prisma.user.findMany).mockResolvedValueOnce([
{ id: "admin-1" },
] as never);
vi.mocked(prisma.integrationStatus.update).mockResolvedValueOnce(
{} as never,
);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "persistent",
message: "Withings activity error: 293",
errorCode: "293",
});
expect(dispatchNotification).toHaveBeenCalledOnce();
const payload = vi.mocked(dispatchNotification).mock.calls[0][0];
expect(payload.message).toContain("persistent error");
expect(payload.message).toContain(
"Action: investigate the upstream contract",
);
});
});
describe("recordSyncFailure — admin Telegram skipped silently when no admins", () => {
it("logs a wide-event warning but does NOT throw when no admin users exist", async () => {
// Seed bucket at 2 so the in-memory increment hits the threshold
// and the no-admins branch is the one being exercised.
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 2,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
alertedAt: null,
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: null,
} as never);
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
email: "u@example.com",
} as never);
vi.mocked(prisma.user.findMany).mockResolvedValueOnce([] as never);
vi.mocked(prisma.integrationStatus.update).mockResolvedValueOnce(
{} as never,
);
await expect(
recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings refresh error: 503 - upstream",
}),
).resolves.toBeUndefined();
expect(dispatchNotification).not.toHaveBeenCalled();
});
});
describe("isReauthRequired", () => {
it("returns true when state === error_reauth", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "error_reauth",
} as never);
expect(await isReauthRequired("u1", "withings")).toBe(true);
});
it("returns true when state === parked (v1.4.43 W14)", async () => {
// A parked integration must short-circuit the sync entry-point
// the same way a reauth-required one does — neither will succeed
// without user / operator action.
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "parked",
} as never);
expect(await isReauthRequired("u1", "withings")).toBe(true);
});
it("returns false for transient errors and absent rows", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "error_transient",
} as never);
expect(await isReauthRequired("u1", "withings")).toBe(false);
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce(null);
expect(await isReauthRequired("u1", "withings")).toBe(false);
});
});
describe("markReauthRequired / markDisconnected / markReconnected", () => {
it("markReauthRequired: writes encrypted error + audit entry", async () => {
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await markReauthRequired("u1", "withings", "invalid grant");
expect(prisma.integrationStatus.upsert).toHaveBeenCalled();
expect(auditLog).toHaveBeenCalledWith(
"integrations.reauth_required",
expect.objectContaining({ userId: "u1" }),
);
});
it("markDisconnected: state=disconnected, clears errors & alertedAt", async () => {
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await markDisconnected("u1", "nightscout");
const args = vi.mocked(prisma.integrationStatus.upsert).mock.calls[0][0];
expect(args.update).toEqual({
state: "disconnected",
lastError: null,
// v1.4.43 W14 — disconnect must clear the per-kind buckets too.
// v1.4.47 W1 — legacy `consecutiveFailures` column dropped
// (migration 0077); the bucket reset is the only counter write.
consecutiveFailuresByKind: {
transient: 0,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
failingSinceAt: null,
failingLegs: [],
alertedAt: null,
});
});
it("markReconnected: state=connected, clears prior reauth state", async () => {
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await markReconnected("u1", "withings");
const args = vi.mocked(prisma.integrationStatus.upsert).mock.calls[0][0];
expect(args.update).toEqual({
state: "connected",
lastError: null,
// v1.4.43 W14 — reconnect clears the per-kind buckets so the
// next sync starts on a clean slate.
// v1.4.47 W1 — legacy `consecutiveFailures` column dropped
// (migration 0077); the bucket reset is the only counter write.
consecutiveFailuresByKind: {
transient: 0,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
failingSinceAt: null,
failingLegs: [],
alertedAt: null,
});
});
});
// ── v1.4.43 W14 — per-kind buckets + 24h park behaviour ────────────────
//
// The four pins below lock in the B4 + B7 contract:
//
// 1. A persistent failure increments ONLY the persistent bucket;
// an intervening transient failure does NOT reset the persistent
// streak.
// 2. The persistent-streak start timestamp is stamped on the FIRST
// persistent failure of a streak and preserved across subsequent
// persistent failures.
// 3. Once the persistent streak has been running for >24h, the state
// flips to `parked` and an `integrations.parked` audit row is
// written.
// 4. `resumeIntegrationFromPark` clears state back to `connected`,
// zeroes every bucket, and writes an `integrations.resumed` audit
// row (only when the row was actually parked — idempotent for
// non-parked inputs).
describe("recordSyncFailure — v1.4.43 W14 per-kind buckets", () => {
it("increments ONLY the matching bucket on each failure", async () => {
// Existing row: transient bucket already at 2 from earlier hiccups.
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 2,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
alertedAt: null,
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: null,
} as never);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "persistent",
message: "Withings measure error: 293 - invalid params",
errorCode: "293",
});
const upsertArgs = vi.mocked(prisma.integrationStatus.upsert).mock
.calls[0][0];
// transient stays at 2 (unchanged), persistent ticks to 1,
// reauth_required stays at 0.
expect(upsertArgs.update.consecutiveFailuresByKind).toEqual({
transient: 2,
reauth_required: 0,
persistent: 1,
});
// First-ever persistent failure stamps the streak anchor.
expect(upsertArgs.update.persistentFailureStartedAt).toBeInstanceOf(Date);
});
it("preserves persistentFailureStartedAt across multiple persistent failures", async () => {
const streakStart = new Date(Date.now() - 60 * 60 * 1000); // 1h ago
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 0,
reauth_required: 0,
persistent: 5,
},
persistentFailureStartedAt: streakStart,
alertedAt: new Date(Date.now() - 30 * 60 * 1000), // already alerted
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: new Date(Date.now() - 30 * 60 * 1000),
} as never);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "persistent",
message: "Withings measure error: 293",
errorCode: "293",
});
const upsertArgs = vi.mocked(prisma.integrationStatus.upsert).mock
.calls[0][0];
// The anchor is preserved exactly — that's the whole point of
// tracking the streak start instead of recomputing it from
// updated_at.
expect(upsertArgs.update.persistentFailureStartedAt).toBe(streakStart);
expect(upsertArgs.update.state).toBe("error_transient"); // < 24h still
expect(upsertArgs.update.consecutiveFailuresByKind).toEqual({
transient: 0,
reauth_required: 0,
persistent: 6,
});
});
it("a transient failure does NOT reset the persistent bucket nor the streak anchor", async () => {
const streakStart = new Date(Date.now() - 6 * 60 * 60 * 1000); // 6h ago
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 0,
reauth_required: 0,
persistent: 4,
},
persistentFailureStartedAt: streakStart,
alertedAt: null,
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: null,
} as never);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings refresh error: 503",
errorCode: "503",
});
const upsertArgs = vi.mocked(prisma.integrationStatus.upsert).mock
.calls[0][0];
// Persistent bucket UNCHANGED — this is the v1.4.43 W14 fix.
// Under the legacy single-counter behaviour the burst above would
// have shown as "5 consecutive failures, state=error_transient"
// and the persistent streak's age would have been masked.
expect(upsertArgs.update.consecutiveFailuresByKind).toEqual({
transient: 1,
reauth_required: 0,
persistent: 4,
});
expect(upsertArgs.update.persistentFailureStartedAt).toBe(streakStart);
});
it("flips state to `parked` and writes an audit row once persistent streak > 24h", async () => {
const streakStart = new Date(Date.now() - 25 * 60 * 60 * 1000); // 25h ago
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 0,
reauth_required: 0,
persistent: 30,
},
persistentFailureStartedAt: streakStart,
alertedAt: new Date(Date.now() - 23 * 60 * 60 * 1000),
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: new Date(Date.now() - 23 * 60 * 60 * 1000),
} as never);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "persistent",
message: "Withings measure error: 293",
errorCode: "293",
});
const upsertArgs = vi.mocked(prisma.integrationStatus.upsert).mock
.calls[0][0];
expect(upsertArgs.update.state).toBe("parked");
expect(auditLog).toHaveBeenCalledWith(
"integrations.parked",
expect.objectContaining({
userId: "u1",
details: expect.objectContaining({
integration: "withings",
reason: "persistent_24h",
persistentFailureStartedAt: streakStart.toISOString(),
}),
}),
);
});
it("seeds a fresh zero envelope when the row has no bucket payload yet", async () => {
// v1.4.47 W1 — the legacy `consecutiveFailures` column was dropped
// (migration 0077). A row that has never written a bucket payload
// (`consecutiveFailuresByKind: null`) starts from a zero envelope
// on the next failure; the matching bucket ticks to 1 in a single
// write.
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: null,
persistentFailureStartedAt: null,
alertedAt: null,
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
email: "u@example.com",
} as never);
vi.mocked(prisma.user.findMany).mockResolvedValueOnce([] as never);
vi.mocked(prisma.integrationStatus.update).mockResolvedValueOnce(
{} as never,
);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "reauth_required",
message: "Withings refresh error: 100 - invalid_grant",
errorCode: "100",
});
const upsertArgs = vi.mocked(prisma.integrationStatus.upsert).mock
.calls[0][0];
expect(upsertArgs.update.consecutiveFailuresByKind).toEqual({
transient: 0,
reauth_required: 1,
persistent: 0,
});
});
});
// ── v1.4.47 W1 — migration 0077 down-script reversibility ──────────────
//
// Migration 0077 drops `consecutive_failures` and documents a down-script
// that restores the legacy column via
//
// SET "consecutive_failures" = COALESCE(
// GREATEST(
// ("consecutive_failures_by_kind"->>'transient')::int,
// ("consecutive_failures_by_kind"->>'reauth_required')::int,
// ("consecutive_failures_by_kind"->>'persistent')::int
// ),
// 0
// )
//
// The recipe maps a bucket payload to a single integer using the SAME
// `Math.max(...)` reducer the live alert ladder uses, so a rollback
// lands every row at the post-v1.4.43 counter value (= the running
// alert signal). This test locks in that contract at the JS level so
// any future tweak to the SQL recipe has to match the in-memory
// behaviour the writer and the alert ladder share.
describe("v1.4.47 W1 — down-script restores legacy consecutiveFailures via Math.max", () => {
/**
* In-memory mirror of the migration's down-script projection. Returns
* the integer the legacy `consecutive_failures` column would land at
* for a given bucket payload. Kept inline so the test file is the
* single source of truth for the recipe.
*/
function projectLegacyCounter(
buckets: {
transient: number;
reauth_required: number;
persistent: number;
} | null,
): number {
if (!buckets) return 0;
return Math.max(
buckets.transient,
buckets.reauth_required,
buckets.persistent,
);
}
it("zero buckets map to 0 (post-success or fresh row)", () => {
expect(
projectLegacyCounter({ transient: 0, reauth_required: 0, persistent: 0 }),
).toBe(0);
});
it("a NULL bucket payload coerces to 0 (rollback against a never-written row)", () => {
expect(projectLegacyCounter(null)).toBe(0);
});
it("picks the max across kinds — a 28-deep persistent streak rolls back to 28", () => {
// The v1.4.43 W14 production scenario: persistent burst at 28, a
// single transient hiccup mid-streak at 2, no reauth. The legacy
// column tracked the running streak total and would have been at
// 28 (no live transient-driven increment after the persistent
// streak anchored). The down-script restores the same 28.
expect(
projectLegacyCounter({
transient: 2,
reauth_required: 0,
persistent: 28,
}),
).toBe(28);
});
it("matches the live alert ladder's reducer (Math.max over Object.values)", () => {
// The whole point of the recipe: a rollback restores the exact
// integer the live alert ladder reads. If `recordSyncFailure`
// pages on alertSignal = 7 today, the rolled-back column lands
// at 7 too.
const buckets = { transient: 1, reauth_required: 7, persistent: 3 };
const liveSignal = Math.max(...Object.values(buckets));
expect(projectLegacyCounter(buckets)).toBe(liveSignal);
});
});
describe("resumeIntegrationFromPark", () => {
it("clears a parked row back to connected with all buckets zeroed", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "parked",
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
const result = await resumeIntegrationFromPark("u1", "withings");
expect(result.wasParked).toBe(true);
const upsertArgs = vi.mocked(prisma.integrationStatus.upsert).mock
.calls[0][0];
expect(upsertArgs.update).toEqual({
state: "connected",
lastError: null,
// v1.4.47 W1 — legacy `consecutiveFailures` column dropped
// (migration 0077); the bucket reset is the only counter write.
consecutiveFailuresByKind: {
transient: 0,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
failingSinceAt: null,
failingLegs: [],
alertedAt: null,
});
expect(auditLog).toHaveBeenCalledWith(
"integrations.resumed",
expect.objectContaining({
userId: "u1",
details: expect.objectContaining({ integration: "withings" }),
}),
);
});
it("is idempotent — calling against a non-parked row does NOT write an audit row", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "connected",
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
const result = await resumeIntegrationFromPark("u1", "withings");
expect(result.wasParked).toBe(false);
expect(auditLog).not.toHaveBeenCalled();
});
});
describe("sync-failure-recorded marker (§5)", () => {
it("round-trips: a marked error reads back as recorded", () => {
const err = new Error("boom");
expect(isSyncFailureRecorded(err)).toBe(false);
const returned = markSyncFailureRecorded(err);
// Returns the SAME object so `throw markSyncFailureRecorded(err)` works.
expect(returned).toBe(err);
expect(isSyncFailureRecorded(err)).toBe(true);
});
it("the marker is non-enumerable (never serialised into logs / JSON)", () => {
const err = markSyncFailureRecorded(new Error("boom"));
expect(Object.keys(err)).not.toContain("healthlog.syncFailureRecorded");
expect(JSON.stringify({ ...err })).not.toContain("syncFailureRecorded");
});
it("is a no-op on non-objects and never reports them as recorded", () => {
expect(markSyncFailureRecorded("boom")).toBe("boom");
expect(markSyncFailureRecorded(null)).toBe(null);
expect(markSyncFailureRecorded(undefined)).toBe(undefined);
expect(isSyncFailureRecorded("boom")).toBe(false);
expect(isSyncFailureRecorded(null)).toBe(false);
expect(isSyncFailureRecorded(undefined)).toBe(false);
});
it("tolerates a frozen error without throwing (falls back to unmarked)", () => {
const frozen = Object.freeze(new Error("frozen"));
expect(() => markSyncFailureRecorded(frozen)).not.toThrow();
// Frozen can't carry the marker, so the boundary records it once — safe.
expect(isSyncFailureRecorded(frozen)).toBe(false);
});
});
describe("transient-streak escalation", () => {
const DAY_MS = 24 * 60 * 60 * 1000;
/** Seed the row the escalation reads: a streak of `transient` of a given age. */
function seedStreak(opts: { transient: number; ageMs: number }) {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: opts.transient,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
failingSinceAt: new Date(Date.now() - opts.ageMs),
alertedAt: null,
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: new Date(),
} as never);
vi.mocked(prisma.user.findMany).mockResolvedValue([] as never);
}
function upsertUpdate() {
return vi.mocked(prisma.integrationStatus.upsert).mock.calls[0]![0]!
.update as Record<string, unknown>;
}
it("records a transient failure as persistent once the streak outlives the window", async () => {
seedStreak({ transient: 3, ageMs: DAY_MS + 60_000 });
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings measure error: 503 - upstream",
});
// The transient bucket is frozen where the window closed; the persistent
// bucket — the only one the park ladder reads — starts counting.
expect(upsertUpdate().consecutiveFailuresByKind).toEqual({
transient: 3,
reauth_required: 0,
persistent: 1,
});
expect(upsertUpdate().persistentFailureStartedAt).toBeInstanceOf(Date);
expect(auditLog).toHaveBeenCalledWith(
"integrations.sync.failed",
expect.objectContaining({
details: expect.objectContaining({
kind: "persistent",
escalatedFrom: "transient",
}),
}),
);
});
it("leaves a streak inside the window alone", async () => {
seedStreak({ transient: 9, ageMs: DAY_MS - 60_000 });
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings measure error: 503 - upstream",
});
expect(upsertUpdate().consecutiveFailuresByKind).toEqual({
transient: 10,
reauth_required: 0,
persistent: 0,
});
expect(upsertUpdate().persistentFailureStartedAt).toBeNull();
expect(auditLog).toHaveBeenCalledWith(
"integrations.sync.failed",
expect.objectContaining({
details: expect.objectContaining({
kind: "transient",
escalatedFrom: null,
}),
}),
);
});
it("needs the consecutive-failure threshold as well as the clock", async () => {
// A provider polled once a day: old streak, but only two attempts in it.
seedStreak({ transient: 1, ageMs: 5 * DAY_MS });
await recordSyncFailure({
userId: "u1",
integration: "nightscout",
kind: "transient",
message: "Nightscout sync HTTP 502",
});
expect(upsertUpdate().consecutiveFailuresByKind).toEqual({
transient: 2,
reauth_required: 0,
persistent: 0,
});
expect(upsertUpdate().persistentFailureStartedAt).toBeNull();
});
it("dates a fresh streak from this failure and never re-dates a running one", async () => {
const anchor = new Date(Date.now() - 3 * DAY_MS);
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 1,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
failingSinceAt: anchor,
alertedAt: new Date(),
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce({
alertedAt: new Date(),
} as never);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings measure error: 503 - upstream",
});
expect(upsertUpdate().failingSinceAt).toEqual(anchor);
});
});
describe("recordSyncSuccess — leg scoping", () => {
it("leaves a sibling leg's error standing and only records the attempt", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "error_transient",
failingLegs: ["sleep"],
} as never);
vi.mocked(prisma.integrationStatus.update).mockResolvedValueOnce(
{} as never,
);
await recordSyncSuccess("u1", "withings", { leg: "ecg" });
expect(prisma.integrationStatus.upsert).not.toHaveBeenCalled();
const args = vi.mocked(prisma.integrationStatus.update).mock.calls[0]![0]!;
// `lastSuccessAt` deliberately absent: it is what the card's
// "connected · X ago" reads, and the sleep pipe has delivered nothing.
// `lastError` is absent too — sleep still owns the message on the row.
expect(args.data).toEqual({
lastAttemptAt: expect.any(Date),
failingLegs: ["sleep"],
});
});
it("drops only its own leg when several are failing at once", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "error_transient",
failingLegs: ["sleep", "activity", "ecg"],
} as never);
vi.mocked(prisma.integrationStatus.update).mockResolvedValueOnce(
{} as never,
);
await recordSyncSuccess("u1", "withings", { leg: "activity" });
expect(prisma.integrationStatus.upsert).not.toHaveBeenCalled();
const args = vi.mocked(prisma.integrationStatus.update).mock.calls[0]![0]!;
// The single slot could hold one of these three. Two legs are still
// failing, and both are still on the row.
expect(args.data).toMatchObject({ failingLegs: ["sleep", "ecg"] });
// `activity` did not write the message the row holds — `ecg` did, and it
// is still failing — so the message stays.
expect(args.data).not.toHaveProperty("lastError");
});
it("clears the error message when the leg that wrote it recovers first", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "error_transient",
failingLegs: ["sleep", "ecg"],
} as never);
vi.mocked(prisma.integrationStatus.update).mockResolvedValueOnce(
{} as never,
);
// `ecg` is last, so it failed most recently and owns `lastError`.
await recordSyncSuccess("u1", "withings", { leg: "ecg" });
const args = vi.mocked(prisma.integrationStatus.update).mock.calls[0]![0]!;
// Keeping the message would leave the row describing a leg that is now
// healthy while `sleep` is the one actually failing.
expect(args.data).toMatchObject({
failingLegs: ["sleep"],
lastError: null,
});
});
it("clears the row whole when the failing leg is the one that succeeded", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "error_transient",
failingLegs: ["sleep"],
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await recordSyncSuccess("u1", "withings", { leg: "sleep" });
expect(prisma.integrationStatus.update).not.toHaveBeenCalled();
const args = vi.mocked(prisma.integrationStatus.upsert).mock.calls[0]![0]!;
expect(args.update).toMatchObject({
state: "connected",
lastError: null,
failingSinceAt: null,
failingLegs: [],
});
});
it("clears the row whole when the failure was never attributed to a leg", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "error_transient",
failingLegs: [],
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await recordSyncSuccess("u1", "oura", { leg: "vitals" });
expect(prisma.integrationStatus.upsert).toHaveBeenCalledOnce();
});
it("clears the row whole when the caller names no leg (a full pass)", async () => {
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await recordSyncSuccess("u1", "whoop");
// No probe read at all — an unnamed success is entitled to clear.
expect(prisma.integrationStatus.findUnique).not.toHaveBeenCalled();
expect(prisma.integrationStatus.upsert).toHaveBeenCalledOnce();
});
it("does not treat a healthy row's stale leg marker as a held error", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "connected",
failingLegs: ["sleep"],
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await recordSyncSuccess("u1", "withings", { leg: "ecg" });
expect(prisma.integrationStatus.upsert).toHaveBeenCalledOnce();
});
it("drops a non-string member rather than coercing it into a leg name", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "error_transient",
failingLegs: ["sleep", 42, null],
} as never);
vi.mocked(prisma.integrationStatus.update).mockResolvedValueOnce(
{} as never,
);
await recordSyncSuccess("u1", "withings", { leg: "ecg" });
const args = vi.mocked(prisma.integrationStatus.update).mock.calls[0]![0]!;
// Coercing `42` into `"42"` would invent a leg no caller can ever succeed
// for, holding the row in error against a failure nobody recorded.
expect(args.data).toMatchObject({ failingLegs: ["sleep"] });
});
it("reads a malformed leg column as unattributed rather than guessing", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
state: "error_transient",
// Not an array of strings — a legacy shape, or a hand-edited row.
failingLegs: { sleep: true },
} as never);
vi.mocked(prisma.integrationStatus.upsert).mockResolvedValueOnce(
{} as never,
);
await recordSyncSuccess("u1", "withings", { leg: "ecg" });
// Unattributed means any success clears — the pre-existing semantics for
// a failure that named no leg, not an invented leg name.
expect(prisma.integrationStatus.upsert).toHaveBeenCalledOnce();
});
});
describe("recordSyncFailure — leg set", () => {
function writtenUpdate() {
return vi.mocked(prisma.integrationStatus.upsert).mock.calls[0]![0]!
.update as Record<string, unknown>;
}
it("adds a second failing leg instead of overwriting the first", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 1,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
failingSinceAt: new Date("2026-01-01T00:00:00Z"),
failingLegs: ["sleep"],
alertedAt: null,
} as never);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings activity error: 503 - upstream",
leg: "activity",
});
expect(writtenUpdate().failingLegs).toEqual(["sleep", "activity"]);
});
it("moves a re-failing leg to the end so it owns the message it just wrote", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 2,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
failingSinceAt: new Date("2026-01-01T00:00:00Z"),
failingLegs: ["sleep", "activity"],
alertedAt: null,
} as never);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings sleep error: 503 - upstream",
leg: "sleep",
});
// No duplicate, and `sleep` is last because it wrote `lastError`.
expect(writtenUpdate().failingLegs).toEqual(["activity", "sleep"]);
});
it("leaves the set alone when the failure names no leg", async () => {
vi.mocked(prisma.integrationStatus.findUnique).mockResolvedValueOnce({
consecutiveFailuresByKind: {
transient: 1,
reauth_required: 0,
persistent: 0,
},
persistentFailureStartedAt: null,
failingSinceAt: new Date("2026-01-01T00:00:00Z"),
failingLegs: ["sleep"],
alertedAt: null,
} as never);
await recordSyncFailure({
userId: "u1",
integration: "withings",
kind: "transient",
message: "Withings error: 503 - upstream",
});
// An unattributed failure carries nothing to record; erasing `sleep` would
// let the sleep leg's next success clear an error it did not fix.
expect(writtenUpdate().failingLegs).toEqual(["sleep"]);
});
});