Files
HealthLog/src/lib/integrations/__tests__/watchdog-classify.test.ts
T
Marc-André Bombeck c094f217fc feat(integrations): watch connections for silence and lasting trouble
An hourly and a daily pass flag a continuous source that delivered on
most of the previous 14 days and has gone quiet, and open one operator
incident per connection when a connection stays degraded or needs action
for more than a day. The incident is announced once through the existing
admin alert path, adopted rather than repeated when the sync ledger
already paged, and closed with one recovery note. Scales, cuffs and
workout logs are watched for errors only, never for silence.
2026-10-15 11:07:57 +02:00

321 lines
8.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { ConnectionHealth } from "../connection-health";
import {
NORMAL_DELIVERY_MIN_DAYS,
SOURCE_PROFILES,
classifySilence,
currentIncident,
decideWatchTransition,
type WatchRow,
} from "../watchdog-classify";
import { formatWatchdogMessage } from "../watchdog";
const NOW = new Date("2026-10-15T12:00:00.000Z");
const HOUR = 60 * 60 * 1000;
const ago = (hours: number) => new Date(NOW.getTime() - hours * HOUR);
function health(
state: ConnectionHealth["state"],
reason: ConnectionHealth["reason"],
since: Date | null,
): ConnectionHealth {
return {
state,
reason,
since: since?.toISOString() ?? null,
action: null,
unavailable: [],
lastSuccessAt: null,
lastDataAt: null,
diagnostics: {
ledgerState: null,
failingLegs: [],
failingSince: null,
lastAttemptAt: null,
verdict: "fresh",
recentCalls: [],
},
};
}
const openRow = (overrides: Partial<WatchRow> = {}): WatchRow => ({
silentSince: null,
incidentReason: "unexpected_silence",
incidentOpenedAt: ago(5),
alertedAt: ago(5),
...overrides,
});
describe("classifySilence", () => {
const oura = SOURCE_PROFILES.oura;
const withings = SOURCE_PROFILES.withings;
it("never flags an on-demand source for silence, however long and however regular", () => {
expect(withings.kind).toBe("on_demand");
expect(SOURCE_PROFILES.strava.kind).toBe("on_demand");
expect(
classifySilence({
profile: withings,
lastDataAt: ago(24 * 30),
deliveredDays: 14,
currentSilentSince: null,
now: NOW,
}),
).toEqual({ kind: "clear" });
});
it("is clear while a continuous source delivers inside its window", () => {
expect(
classifySilence({
profile: oura,
lastDataAt: ago(20),
deliveredDays: null,
currentSilentSince: null,
now: NOW,
}),
).toEqual({ kind: "clear" });
});
it("asks for the history before flagging (the cheap hourly pass reads it lazily)", () => {
expect(
classifySilence({
profile: oura,
lastDataAt: ago(30),
deliveredDays: null,
currentSilentSince: null,
now: NOW,
}),
).toEqual({ kind: "needs_history" });
});
it("flags a silent source that delivered on most of the previous 14 days", () => {
expect(
classifySilence({
profile: oura,
lastDataAt: ago(30),
deliveredDays: NORMAL_DELIVERY_MIN_DAYS,
currentSilentSince: null,
now: NOW,
}),
).toEqual({ kind: "flag", silentSince: ago(30) });
});
it("does not flag a source its owner wears only now and then", () => {
expect(
classifySilence({
profile: oura,
lastDataAt: ago(30),
deliveredDays: NORMAL_DELIVERY_MIN_DAYS - 1,
currentSilentSince: null,
now: NOW,
}),
).toEqual({ kind: "not_expected" });
});
it("keeps a standing flag on the hourly pass without re-reading history", () => {
expect(
classifySilence({
profile: oura,
lastDataAt: ago(50),
deliveredDays: null,
currentSilentSince: ago(50),
now: NOW,
}),
).toEqual({ kind: "flag", silentSince: ago(50) });
});
it("drops a standing flag on the daily re-check once the history no longer says it delivers", () => {
expect(
classifySilence({
profile: oura,
lastDataAt: ago(24 * 6),
deliveredDays: 8,
currentSilentSince: ago(24 * 6),
now: NOW,
}),
).toEqual({ kind: "not_expected" });
});
it("gives Apple Health a wider window than a wearable", () => {
const apple = SOURCE_PROFILES["apple-health"];
expect(
classifySilence({
profile: apple,
lastDataAt: ago(30),
deliveredDays: 14,
currentSilentSince: null,
now: NOW,
}),
).toEqual({ kind: "clear" });
});
});
describe("currentIncident", () => {
it("opens nothing for a degraded state younger than a day", () => {
expect(
currentIncident({
health: health("degraded", "provider_errors", ago(5)),
silentSince: null,
ledgerAlerted: false,
now: NOW,
}),
).toBeNull();
});
it("opens an incident once degraded or action_required has lasted more than a day", () => {
expect(
currentIncident({
health: health("action_required", "reauth_required", ago(25)),
silentSince: null,
ledgerAlerted: false,
now: NOW,
}),
).toEqual({ reason: "reauth_required", since: ago(25) });
});
it("follows the ledger's own page at once, so its recovery is reported too", () => {
expect(
currentIncident({
health: health("degraded", "provider_errors", ago(3)),
silentSince: null,
ledgerAlerted: true,
now: NOW,
}),
).toEqual({ reason: "provider_errors", since: ago(3) });
});
it("opens a silence incident from the flag", () => {
expect(
currentIncident({
health: health("ok", "data_arriving", null),
silentSince: ago(30),
ledgerAlerted: false,
now: NOW,
}),
).toEqual({ reason: "unexpected_silence", since: ago(30) });
});
it("has no incident for a limited connection", () => {
expect(
currentIncident({
health: health("limited", "plan_limited", null),
silentSince: null,
ledgerAlerted: false,
now: NOW,
}),
).toBeNull();
});
});
describe("decideWatchTransition", () => {
const incident = { reason: "unexpected_silence" as const, since: ago(30) };
it("opens and announces a new incident", () => {
expect(
decideWatchTransition({ row: null, incident, ledgerAlerted: false }),
).toEqual({ kind: "open", ...incident });
});
it("adopts an incident the ledger already paged about instead of paging again", () => {
expect(
decideWatchTransition({ row: null, incident, ledgerAlerted: true }),
).toEqual({ kind: "adopt", ...incident });
});
it("sends nothing while an announced incident stays open (no repeats)", () => {
expect(
decideWatchTransition({
row: openRow(),
incident,
ledgerAlerted: false,
}),
).toEqual({ kind: "keep", reason: "unexpected_silence", announce: false });
});
it("treats a changed reason as the same incident", () => {
expect(
decideWatchTransition({
row: openRow(),
incident: { reason: "reauth_required", since: ago(2) },
ledgerAlerted: false,
}),
).toEqual({ kind: "keep", reason: "reauth_required", announce: false });
});
it("retries the announcement of an incident no channel accepted", () => {
expect(
decideWatchTransition({
row: openRow({ alertedAt: null }),
incident,
ledgerAlerted: false,
}),
).toEqual({ kind: "keep", reason: "unexpected_silence", announce: true });
});
it("closes with one recovery note when the announced incident ends", () => {
expect(
decideWatchTransition({
row: openRow(),
incident: null,
ledgerAlerted: false,
}),
).toEqual({
kind: "close",
notify: true,
previousReason: "unexpected_silence",
});
});
it("closes quietly when the operator was never told it started", () => {
expect(
decideWatchTransition({
row: openRow({ alertedAt: null }),
incident: null,
ledgerAlerted: false,
}),
).toEqual({
kind: "close",
notify: false,
previousReason: "unexpected_silence",
});
});
it("does nothing when there was and is no incident", () => {
expect(
decideWatchTransition({
row: null,
incident: null,
ledgerAlerted: false,
}),
).toEqual({ kind: "none" });
});
});
describe("formatWatchdogMessage", () => {
it("names the provider, the account and the instant, in plain text", () => {
const m = formatWatchdogMessage({
kind: "incident",
key: "oura",
subjectLabel: "someone@example.test",
reason: "unexpected_silence",
since: ago(30),
});
expect(m.title).toBe("Oura needs attention for someone@example.test");
expect(m.message).toContain("2026-10-14 06:00 UTC");
expect(m.message).not.toMatch(/[—–·→]/);
});
it("says why a recovered incident ended", () => {
const m = formatWatchdogMessage({
kind: "recovery",
key: "apple-health",
subjectLabel: "someone@example.test",
reason: "unexpected_silence",
since: null,
closedBecause: "disconnected",
});
expect(m.title).toBe("Apple Health recovered for someone@example.test");
expect(m.message).toContain("removed");
});
});