mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
test(arrivals): pin the spine's guardrails against heavy fixtures
Backfill immunity is proved by replay, not by a hand-picked case: a ten-year Apple Health export (12772 writes) and a 30-day provider re-sync (58 writes) must both emit zero events, while a fresh sleep night, workout and first weight of the day emit exactly one each. The provider-isolation guard walks the worker's static import graph and fails if any provider client or resolver becomes reachable. It carries two positive controls, so a broken entry path cannot make it pass against an empty graph. The integration test covers what unit mocks cannot: the unique index, the ON CONFLICT DO NOTHING claim, the losing side of ten concurrent claims, the forward-only marker, ciphertext at rest, the delete cascade, and the retention window.
This commit is contained in:
1 parent
5d62236412
commit
ccd3c5e68f
5 files changed
+1055
No files matched your search
@@ -0,0 +1,461 @@
|
||||
/**
|
||||
* The acceptance gate for the arrival spine.
|
||||
*
|
||||
* The spine sits in front of every ingest path in the product, so its one
|
||||
* existential failure mode is a backfill storm: a mass import turning into
|
||||
* thousands of queued jobs and, downstream, provider calls. This file proves
|
||||
* the opposite against REALISTIC replayed volumes rather than a hand-picked
|
||||
* two-row case — a hand-picked case would pass even if the classifier only
|
||||
* happened to be right at the boundary it was written against.
|
||||
*
|
||||
* Two heavy fixtures:
|
||||
*
|
||||
* - a multi-year Apple Health export (daily weights, blood-pressure pairs,
|
||||
* sleep nights and workouts across ten years), replayed as the export
|
||||
* importer replays it;
|
||||
* - a 30-day provider re-sync (the shape a WHOOP / Fitbit / Google catch-up
|
||||
* takes after a token refresh or downtime).
|
||||
*
|
||||
* Both must emit ZERO salient events. Then the same seams, given genuinely
|
||||
* fresh data, must emit EXACTLY ONE each.
|
||||
*
|
||||
* The assertions are on `boss.send` — the actual queue write — not on the
|
||||
* classifier's return value. Testing the classifier alone would prove the
|
||||
* predicate correct while saying nothing about whether the seam consults it.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const sendMock = vi.fn(async () => "job-id");
|
||||
const annotateMock = vi.fn();
|
||||
let moduleMap: Record<string, boolean> = {};
|
||||
let timezone = "Europe/Berlin";
|
||||
|
||||
vi.mock("@/lib/jobs/boss-instance", () => ({
|
||||
getGlobalBoss: () => ({ send: sendMock }),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/logging/context", () => ({
|
||||
annotate: (...args: unknown[]) => annotateMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/modules/gate", () => ({
|
||||
resolveModuleMap: async () => moduleMap,
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/tz/resolver", () => ({
|
||||
resolveUserTimezone: async () => timezone,
|
||||
}));
|
||||
|
||||
const { emitDataArrival } = await import("../emit-shared");
|
||||
|
||||
const USER = "user-fixture";
|
||||
|
||||
/** Wall clock the fixtures are replayed against: a Tuesday afternoon. */
|
||||
const NOW = new Date("2026-07-14T14:30:00.000Z");
|
||||
|
||||
const DAY_MS = 86_400_000;
|
||||
|
||||
beforeEach(() => {
|
||||
sendMock.mockClear();
|
||||
annotateMock.mockClear();
|
||||
moduleMap = { sleep: true, workouts: true, labs: true, insights: true };
|
||||
timezone = "Europe/Berlin";
|
||||
});
|
||||
|
||||
/** Every action name the seam annotated, in order. */
|
||||
function annotatedActions(): string[] {
|
||||
return annotateMock.mock.calls
|
||||
.map((call) => {
|
||||
const arg = call[0] as { action?: { name?: string } } | undefined;
|
||||
return arg?.action?.name;
|
||||
})
|
||||
.filter((n): n is string => typeof n === "string");
|
||||
}
|
||||
|
||||
function skipReasons(): string[] {
|
||||
return annotateMock.mock.calls
|
||||
.map((call) => {
|
||||
const arg = call[0] as { meta?: { reason?: string } } | undefined;
|
||||
return arg?.meta?.reason;
|
||||
})
|
||||
.filter((r): r is string => typeof r === "string");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Fixture builders
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
interface ReplayedWrite {
|
||||
kind: "sleep_night" | "workout" | "weight" | "blood_pressure" | "labs_panel";
|
||||
newestSampleAt: Date;
|
||||
insertedCount: number;
|
||||
refId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A ten-year Apple Health export, in the shape the importer replays it: a
|
||||
* weight most mornings, a blood-pressure pair most evenings, a sleep night
|
||||
* every night, and a workout roughly every other day. Every row is genuinely
|
||||
* INSERTED (a first import creates everything), which is exactly the case that
|
||||
* would storm if the recency test were missing — `insertedCount` alone cannot
|
||||
* save us here.
|
||||
*/
|
||||
function appleExportFixture(): ReplayedWrite[] {
|
||||
const writes: ReplayedWrite[] = [];
|
||||
const years = 10;
|
||||
for (let dayOffset = years * 365; dayOffset >= 2; dayOffset--) {
|
||||
const day = new Date(NOW.getTime() - dayOffset * DAY_MS);
|
||||
const at = (hour: number) =>
|
||||
new Date(
|
||||
Date.UTC(
|
||||
day.getUTCFullYear(),
|
||||
day.getUTCMonth(),
|
||||
day.getUTCDate(),
|
||||
hour,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
writes.push({ kind: "weight", newestSampleAt: at(7), insertedCount: 1 });
|
||||
writes.push({
|
||||
kind: "blood_pressure",
|
||||
newestSampleAt: at(20),
|
||||
insertedCount: 2,
|
||||
});
|
||||
writes.push({
|
||||
kind: "sleep_night",
|
||||
newestSampleAt: at(6),
|
||||
insertedCount: 4,
|
||||
});
|
||||
if (dayOffset % 2 === 0) {
|
||||
writes.push({
|
||||
kind: "workout",
|
||||
newestSampleAt: at(18),
|
||||
insertedCount: 1,
|
||||
refId: `workout-${dayOffset}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return writes;
|
||||
}
|
||||
|
||||
/**
|
||||
* A 30-day provider re-sync. Distinct from the export in two ways that matter:
|
||||
* it is recent enough that a naive "is it in the last month" test would let it
|
||||
* through, and it re-posts nights and workouts the record may already hold.
|
||||
*/
|
||||
function providerResyncFixture(): ReplayedWrite[] {
|
||||
const writes: ReplayedWrite[] = [];
|
||||
for (let dayOffset = 30; dayOffset >= 2; dayOffset--) {
|
||||
const day = new Date(NOW.getTime() - dayOffset * DAY_MS);
|
||||
const at = (hour: number) =>
|
||||
new Date(
|
||||
Date.UTC(
|
||||
day.getUTCFullYear(),
|
||||
day.getUTCMonth(),
|
||||
day.getUTCDate(),
|
||||
hour,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
);
|
||||
writes.push({
|
||||
kind: "sleep_night",
|
||||
newestSampleAt: at(6),
|
||||
insertedCount: 5,
|
||||
});
|
||||
writes.push({
|
||||
kind: "workout",
|
||||
newestSampleAt: at(17),
|
||||
insertedCount: 1,
|
||||
refId: `whoop-${dayOffset}`,
|
||||
});
|
||||
}
|
||||
return writes;
|
||||
}
|
||||
|
||||
async function replay(writes: ReplayedWrite[], source: string): Promise<void> {
|
||||
for (const w of writes) {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: w.kind,
|
||||
newestSampleAt: w.newestSampleAt,
|
||||
insertedCount: w.insertedCount,
|
||||
refId: w.refId,
|
||||
source,
|
||||
now: NOW,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
describe("arrival spine — heavy backfill fixtures emit nothing", () => {
|
||||
it("a ten-year Apple Health export produces ZERO salient events", async () => {
|
||||
const writes = appleExportFixture();
|
||||
// The fixture must actually be heavy, or the assertion below is vacuous.
|
||||
expect(writes.length).toBeGreaterThan(10_000);
|
||||
|
||||
await replay(writes, "apple_export");
|
||||
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(annotatedActions().every((n) => n.endsWith(".skipped"))).toBe(true);
|
||||
expect(new Set(skipReasons())).toEqual(new Set(["backfill"]));
|
||||
});
|
||||
|
||||
it("a 30-day provider re-sync produces ZERO salient events", async () => {
|
||||
const writes = providerResyncFixture();
|
||||
expect(writes.length).toBeGreaterThan(50);
|
||||
|
||||
await replay(writes, "whoop");
|
||||
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(new Set(skipReasons())).toEqual(new Set(["backfill"]));
|
||||
});
|
||||
|
||||
it("a re-sync that INSERTS nothing produces zero events, whatever its dates", async () => {
|
||||
// The `stats:` overwrite path: rows are touched, values change, nothing is
|
||||
// new. Dated TODAY, so recency cannot be what saves it — only the
|
||||
// inserted-count test can.
|
||||
for (let i = 0; i < 500; i++) {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "weight",
|
||||
newestSampleAt: new Date(NOW.getTime() - 60_000),
|
||||
insertedCount: 0,
|
||||
source: "batch",
|
||||
now: NOW,
|
||||
});
|
||||
}
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(new Set(skipReasons())).toEqual(new Set(["noop"]));
|
||||
});
|
||||
|
||||
it("a far-future sample is refused, not enqueued", async () => {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "weight",
|
||||
newestSampleAt: new Date(NOW.getTime() + 3 * DAY_MS),
|
||||
insertedCount: 1,
|
||||
source: "manual",
|
||||
now: NOW,
|
||||
});
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(skipReasons()).toContain("backfill");
|
||||
});
|
||||
|
||||
it("a future sample still inside TODAY is refused by the skew guard alone", async () => {
|
||||
// Isolates the future-dated guard from the recency guard. A device clock
|
||||
// running a few hours fast produces a sample whose LOCAL DAY is today — so
|
||||
// the recency test happily passes it — but which has not happened yet.
|
||||
// Without a separate skew guard this would enqueue, and the day's reaction
|
||||
// would be claimed by a reading the user has not taken.
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "weight",
|
||||
newestSampleAt: new Date(NOW.getTime() + 5 * 60 * 60 * 1000),
|
||||
insertedCount: 1,
|
||||
source: "manual",
|
||||
now: NOW,
|
||||
});
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(skipReasons()).toContain("backfill");
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrival spine — genuinely fresh data emits exactly one event", () => {
|
||||
it("a fresh sleep night emits exactly one", async () => {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "sleep_night",
|
||||
// Last night, ending this morning.
|
||||
newestSampleAt: new Date("2026-07-14T05:40:00.000Z"),
|
||||
insertedCount: 6,
|
||||
source: "whoop",
|
||||
now: NOW,
|
||||
});
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(annotatedActions()).toEqual(["arrival.sleep_night.emitted"]);
|
||||
});
|
||||
|
||||
it("a fresh workout emits exactly one", async () => {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "workout",
|
||||
newestSampleAt: new Date("2026-07-14T11:00:00.000Z"),
|
||||
insertedCount: 1,
|
||||
refId: "workout-fresh",
|
||||
source: "strava",
|
||||
now: NOW,
|
||||
});
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(annotatedActions()).toEqual(["arrival.workout.emitted"]);
|
||||
});
|
||||
|
||||
it("the first weight of the day emits exactly one", async () => {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "weight",
|
||||
newestSampleAt: new Date("2026-07-14T06:15:00.000Z"),
|
||||
insertedCount: 1,
|
||||
source: "withings",
|
||||
now: NOW,
|
||||
});
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(annotatedActions()).toEqual(["arrival.weight.emitted"]);
|
||||
});
|
||||
|
||||
it("one fresh row buried in a heavy backfill still emits exactly one", async () => {
|
||||
// The case that matters most in production: a first-time import of an
|
||||
// export taken today. Everything historical must stay silent, and the one
|
||||
// genuinely current reading must still land.
|
||||
await replay(appleExportFixture(), "apple_export");
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "workout",
|
||||
newestSampleAt: new Date("2026-07-14T09:00:00.000Z"),
|
||||
insertedCount: 1,
|
||||
refId: "workout-today",
|
||||
source: "apple_export",
|
||||
now: NOW,
|
||||
});
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrival spine — singleton keys", () => {
|
||||
it("day-scoped kinds carry the local date and NO time-slot window", async () => {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "weight",
|
||||
newestSampleAt: new Date("2026-07-14T06:15:00.000Z"),
|
||||
insertedCount: 1,
|
||||
source: "withings",
|
||||
now: NOW,
|
||||
});
|
||||
const opts = sendMock.mock.calls[0][2] as Record<string, unknown>;
|
||||
expect(opts.singletonKey).toBe(`arrival:${USER}:weight:2026-07-14`);
|
||||
// A wall-clock window cannot express a local date; asserting its ABSENCE
|
||||
// is what stops someone reintroducing one.
|
||||
expect(opts.singletonSeconds).toBeUndefined();
|
||||
});
|
||||
|
||||
it("workout is keyed per workout, with the device double-post window", async () => {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "workout",
|
||||
newestSampleAt: new Date("2026-07-14T11:00:00.000Z"),
|
||||
insertedCount: 1,
|
||||
refId: "workout-abc",
|
||||
source: "strava",
|
||||
now: NOW,
|
||||
});
|
||||
const opts = sendMock.mock.calls[0][2] as Record<string, unknown>;
|
||||
expect(opts.singletonKey).toBe(`arrival:${USER}:workout:workout-abc`);
|
||||
expect(opts.singletonSeconds).toBe(300);
|
||||
});
|
||||
|
||||
it("two workouts on one day do NOT collapse into one event", async () => {
|
||||
for (const id of ["workout-morning", "workout-evening"]) {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "workout",
|
||||
newestSampleAt: new Date("2026-07-14T11:00:00.000Z"),
|
||||
insertedCount: 1,
|
||||
refId: id,
|
||||
source: "strava",
|
||||
now: NOW,
|
||||
});
|
||||
}
|
||||
const keys = sendMock.mock.calls.map(
|
||||
(c) => (c[2] as Record<string, unknown>).singletonKey,
|
||||
);
|
||||
expect(new Set(keys).size).toBe(2);
|
||||
});
|
||||
|
||||
it("the retry policy is the cheap-worker one, not the LLM one", async () => {
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "weight",
|
||||
newestSampleAt: new Date("2026-07-14T06:15:00.000Z"),
|
||||
insertedCount: 1,
|
||||
source: "withings",
|
||||
now: NOW,
|
||||
});
|
||||
const opts = sendMock.mock.calls[0][2] as Record<string, unknown>;
|
||||
expect(opts.retryLimit).toBe(2);
|
||||
expect(opts.retryBackoff).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrival spine — module gating and fault isolation", () => {
|
||||
it("a disabled module emits nothing", async () => {
|
||||
moduleMap = { ...moduleMap, workouts: false };
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "workout",
|
||||
newestSampleAt: new Date("2026-07-14T11:00:00.000Z"),
|
||||
insertedCount: 1,
|
||||
refId: "workout-x",
|
||||
source: "strava",
|
||||
now: NOW,
|
||||
});
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(skipReasons()).toContain("module_off");
|
||||
});
|
||||
|
||||
it("an enqueue failure never propagates to the caller", async () => {
|
||||
sendMock.mockRejectedValueOnce(new Error("queue is down"));
|
||||
await expect(
|
||||
emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "weight",
|
||||
newestSampleAt: new Date("2026-07-14T06:15:00.000Z"),
|
||||
insertedCount: 1,
|
||||
source: "withings",
|
||||
now: NOW,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrival spine — timezone honesty", () => {
|
||||
it("judges recency in the user's zone, not UTC", async () => {
|
||||
// 2026-07-14T22:30Z is still the 14th in UTC but already the 15th in
|
||||
// Auckland. A sample from 2026-07-13T20:00Z is two local days back there
|
||||
// and must be refused, while UTC arithmetic would call it "yesterday".
|
||||
timezone = "Pacific/Auckland";
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "weight",
|
||||
newestSampleAt: new Date("2026-07-13T00:00:00.000Z"),
|
||||
insertedCount: 1,
|
||||
source: "withings",
|
||||
now: new Date("2026-07-14T22:30:00.000Z"),
|
||||
});
|
||||
expect(sendMock).not.toHaveBeenCalled();
|
||||
expect(skipReasons()).toContain("backfill");
|
||||
});
|
||||
|
||||
it("files the arrival under the user's local day, not the UTC day", async () => {
|
||||
// 23:30 in Berlin on the 14th is 21:30Z — same UTC day. Chosen the other
|
||||
// way round: 00:30 Berlin on the 15th is 22:30Z on the 14th, so the local
|
||||
// date must read 2026-07-15 while UTC still reads the 14th.
|
||||
timezone = "Europe/Berlin";
|
||||
const now = new Date("2026-07-14T22:30:00.000Z");
|
||||
await emitDataArrival({
|
||||
userId: USER,
|
||||
kind: "weight",
|
||||
newestSampleAt: new Date("2026-07-14T22:00:00.000Z"),
|
||||
insertedCount: 1,
|
||||
source: "withings",
|
||||
now,
|
||||
});
|
||||
const payload = sendMock.mock.calls[0][1] as { localDate: string };
|
||||
expect(payload.localDate).toBe("2026-07-15");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
/**
|
||||
* Structural guard: the data-arrival worker cannot reach a provider.
|
||||
*
|
||||
* The spine's central cost claim is that it spends nothing. That claim is only
|
||||
* as good as the worker's module graph: the moment anything reachable from
|
||||
* `data-arrival.ts` imports a provider client, a mass ingest becomes a mass of
|
||||
* provider calls, and no amount of runtime gating in the handler would show up
|
||||
* in review as the thing that broke it.
|
||||
*
|
||||
* So the guard is structural, not behavioural. It BFS-walks the static import
|
||||
* graph from the worker and asserts no provider module is reachable — the shape
|
||||
* of `documents/__tests__/fenced-chat-module-graph.test.ts`.
|
||||
*
|
||||
* Two deliberate design points:
|
||||
*
|
||||
* - The forbidden set is GLOBBED, not hand-listed, for the provider clients.
|
||||
* A hand-written array silently stops covering a `gemini-client.ts` the day
|
||||
* someone adds one; a glob covers it on arrival.
|
||||
* - There is a positive control. A graph walk that silently resolves to
|
||||
* nothing (a moved file, a changed alias, a typo in the entry path) would
|
||||
* pass every negative assertion vacuously. The control asserts the walk
|
||||
* DOES reach the modules the worker genuinely uses, so an empty graph fails
|
||||
* loudly instead of reporting success.
|
||||
*
|
||||
* Known limit, stated because it bounds what this proves: `IMPORT_RE` sees only
|
||||
* static import specifiers. A dynamic `await import(...)` built from a variable
|
||||
* is invisible to it. That is acceptable here — the ban is on a static import,
|
||||
* and a dynamic provider import in a worker would be a far louder thing to
|
||||
* write — but it is not a proof, it is a tripwire.
|
||||
*/
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const SRC = resolve(__dirname, "../../..");
|
||||
|
||||
const WORKER_ENTRY = resolve(SRC, "lib/jobs/data-arrival.ts");
|
||||
const EMIT_ENTRY = resolve(SRC, "lib/arrivals/emit-shared.ts");
|
||||
|
||||
/** Non-client provider machinery. Hand-listed because it has no shared suffix. */
|
||||
const PROVIDER_MACHINERY = [
|
||||
"lib/ai/provider.ts",
|
||||
"lib/ai/provider-runner.ts",
|
||||
"lib/ai/provider-chain.ts",
|
||||
"lib/ai/provider-health-ledger.ts",
|
||||
"lib/ai/openai-wire.ts",
|
||||
"lib/ai/codex-oauth.ts",
|
||||
].map((p) => resolve(SRC, p));
|
||||
|
||||
/** Every `*-client.ts` under `src/lib/ai`, discovered rather than enumerated. */
|
||||
function providerClients(): string[] {
|
||||
const dir = resolve(SRC, "lib/ai");
|
||||
return readdirSync(dir)
|
||||
.filter((f) => f.endsWith("-client.ts"))
|
||||
.map((f) => resolve(dir, f));
|
||||
}
|
||||
|
||||
function resolveSpecifier(spec: string, fromFile: string): string | null {
|
||||
let candidate: string;
|
||||
if (spec.startsWith("@/")) {
|
||||
candidate = resolve(SRC, spec.slice(2));
|
||||
} else if (spec.startsWith(".")) {
|
||||
candidate = resolve(dirname(fromFile), spec);
|
||||
} else {
|
||||
return null; // bare package import — outside our graph
|
||||
}
|
||||
for (const ext of [".ts", ".tsx", "/index.ts", "/index.tsx"]) {
|
||||
const p = `${candidate}${ext}`;
|
||||
if (existsSync(p)) return p;
|
||||
}
|
||||
return existsSync(candidate) ? candidate : null;
|
||||
}
|
||||
|
||||
const IMPORT_RE = /(?:from|import)\s+["']([^"']+)["']/g;
|
||||
|
||||
/** BFS the static import graph from `entries`, returning every reachable file. */
|
||||
function reachableFrom(entries: string[]): Set<string> {
|
||||
const seen = new Set<string>();
|
||||
const queue = [...entries];
|
||||
while (queue.length > 0) {
|
||||
const file = queue.pop()!;
|
||||
if (seen.has(file)) continue;
|
||||
seen.add(file);
|
||||
let source: string;
|
||||
try {
|
||||
source = readFileSync(file, "utf8");
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const match of source.matchAll(IMPORT_RE)) {
|
||||
const resolved = resolveSpecifier(match[1], file);
|
||||
if (resolved && !seen.has(resolved)) queue.push(resolved);
|
||||
}
|
||||
}
|
||||
return seen;
|
||||
}
|
||||
|
||||
describe("data-arrival spine — provider isolation", () => {
|
||||
it("the worker's module graph reaches no provider client", () => {
|
||||
const reachable = reachableFrom([WORKER_ENTRY]);
|
||||
const clients = providerClients();
|
||||
expect(clients.length).toBeGreaterThan(0); // the glob itself must not be empty
|
||||
for (const forbidden of clients) {
|
||||
expect(
|
||||
reachable.has(forbidden),
|
||||
`The data-arrival worker must NOT be able to reach ${forbidden}. The spine's cost claim is that it spends nothing; a provider import here voids it.`,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("the worker's module graph reaches no provider resolver or wire module", () => {
|
||||
const reachable = reachableFrom([WORKER_ENTRY]);
|
||||
for (const forbidden of PROVIDER_MACHINERY) {
|
||||
if (!existsSync(forbidden)) continue; // tolerate a renamed internal
|
||||
expect(
|
||||
reachable.has(forbidden),
|
||||
`The data-arrival worker must NOT be able to reach ${forbidden}.`,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("the emit seam's module graph reaches no provider client either", () => {
|
||||
// The seams run inside every ingest request. A provider import here would
|
||||
// be worse than in the worker: it would land on the request hot path.
|
||||
const reachable = reachableFrom([EMIT_ENTRY]);
|
||||
for (const forbidden of providerClients()) {
|
||||
expect(
|
||||
reachable.has(forbidden),
|
||||
`The arrival emit seam must NOT be able to reach ${forbidden}.`,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("sanity: the walk DOES reach the worker's own dependencies", () => {
|
||||
// Positive control. Without this, a broken entry path or a changed alias
|
||||
// would make every assertion above pass against an empty graph.
|
||||
const reachable = reachableFrom([WORKER_ENTRY]);
|
||||
expect(reachable.has(resolve(SRC, "lib/arrivals/types.ts"))).toBe(true);
|
||||
expect(reachable.has(resolve(SRC, "lib/arrivals/emit-shared.ts"))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(reachable.has(resolve(SRC, "lib/cache/invalidate.ts"))).toBe(true);
|
||||
});
|
||||
|
||||
it("sanity: the walk CAN see a provider client when one is genuinely imported", () => {
|
||||
// Second positive control, on the detector rather than the graph: point the
|
||||
// walk at a module that really does import a provider and assert it is
|
||||
// found. If this ever goes red, the negative assertions above have stopped
|
||||
// meaning anything.
|
||||
const chainEntry = resolve(SRC, "lib/ai/provider.ts");
|
||||
const reachable = reachableFrom([chainEntry]);
|
||||
const clients = providerClients();
|
||||
expect(clients.some((c) => reachable.has(c))).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* v1.4.37 dead-queue guard for the `data-arrival` queue.
|
||||
*
|
||||
* Four facts have to hold together or the spine is silently inert: the queue
|
||||
* name is provisioned, a handler drains it, the queue carries a policy that
|
||||
* actually indexes `singleton_key`, and the retention job that prunes its rows
|
||||
* is itself registered. Any one of them missing fails open in the worst way —
|
||||
* the emits succeed, the jobs vanish, and nothing anywhere reports a problem.
|
||||
*
|
||||
* Source-text assertions over the registrar modules, matching the shape of the
|
||||
* sibling `*-queue.test.ts` guards.
|
||||
*/
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const statusRegistrar = readFileSync(
|
||||
join(__dirname, "..", "reminder", "register-status.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const maintenanceSource =
|
||||
readFileSync(
|
||||
join(__dirname, "..", "reminder", "register-maintenance.ts"),
|
||||
"utf8",
|
||||
) +
|
||||
readFileSync(
|
||||
join(__dirname, "..", "reminder", "cleanup-handlers.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
describe("data-arrival queue wiring", () => {
|
||||
it("provisions the queue in the allQueues list", () => {
|
||||
const allQueues = statusRegistrar.match(
|
||||
/const allQueues\s*=\s*\[([\s\S]*?)\];/,
|
||||
);
|
||||
expect(allQueues).not.toBeNull();
|
||||
expect(allQueues![1]).toMatch(/\bDATA_ARRIVAL_QUEUE\b/);
|
||||
});
|
||||
|
||||
it("binds a boss.work handler that drains it", () => {
|
||||
expect(statusRegistrar).toMatch(
|
||||
/boss\.work[\s\S]{0,200}DATA_ARRIVAL_QUEUE[\s\S]{0,200}handleDataArrival/,
|
||||
);
|
||||
});
|
||||
|
||||
it("carries the exclusive policy its singleton keys depend on", () => {
|
||||
// Under pg-boss's default `standard` policy NO index covers
|
||||
// `singleton_key`, so the day-scoped keys would coalesce nothing at all and
|
||||
// a chatty ingest would queue one job per batch. This assertion is anchored
|
||||
// INSIDE the extracted policy table so an unrelated mention of the queue
|
||||
// name elsewhere in the file cannot satisfy it.
|
||||
const table = statusRegistrar.match(
|
||||
/const queuePolicies: QueuePolicyTable\s*=\s*\{([\s\S]*?)\n\};/,
|
||||
);
|
||||
expect(table).not.toBeNull();
|
||||
expect(table![1]).toMatch(
|
||||
/\[DATA_ARRIVAL_QUEUE\][\s\S]{0,200}policy:\s*"exclusive"/,
|
||||
);
|
||||
});
|
||||
|
||||
it("has no cron schedule — it is send-only, driven by ingest", () => {
|
||||
expect(statusRegistrar).not.toMatch(/\[DATA_ARRIVAL_QUEUE,\s*\w+_CRON\]/);
|
||||
});
|
||||
|
||||
it("registers the retention job that prunes the markers", () => {
|
||||
const allQueues = maintenanceSource.match(
|
||||
/const allQueues\s*=\s*\[([\s\S]*?)\];/,
|
||||
);
|
||||
expect(allQueues).not.toBeNull();
|
||||
expect(allQueues![1]).toMatch(/\bARRIVAL_REACTION_CLEANUP_QUEUE\b/);
|
||||
expect(maintenanceSource).toMatch(
|
||||
/\[ARRIVAL_REACTION_CLEANUP_QUEUE,\s*ARRIVAL_REACTION_CLEANUP_CRON\]/,
|
||||
);
|
||||
expect(maintenanceSource).toMatch(
|
||||
/boss\.work[\s\S]{0,200}ARRIVAL_REACTION_CLEANUP_QUEUE[\s\S]{0,200}handleArrivalReactionCleanup/,
|
||||
);
|
||||
});
|
||||
|
||||
it("the retention handler actually deletes arrival reactions by age", () => {
|
||||
expect(maintenanceSource).toMatch(
|
||||
/handleArrivalReactionCleanup[\s\S]{0,600}arrivalReaction\.deleteMany/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* The `data-arrival` worker's contract.
|
||||
*
|
||||
* Two properties carry real risk and are pinned here:
|
||||
*
|
||||
* - The marker is claimed BEFORE any fan-out. The S4 morning-refresh trigger
|
||||
* stamps its debounce marker only after a non-failed run, so a persistently
|
||||
* failing downstream let every subsequent sleep batch re-enqueue — an
|
||||
* unbounded chain. Claiming up front bounds a downstream failure to the
|
||||
* retries of one job.
|
||||
* - A refusal returns `skipped`; it does not throw. pg-boss retries a failed
|
||||
* job, and retrying against a ceiling that will not move until the local
|
||||
* day rolls over is a loop.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/logging/context", () => ({ annotate: vi.fn() }));
|
||||
vi.mock("@/lib/logging/background", () => ({
|
||||
withBackgroundEvent: async (
|
||||
_name: string,
|
||||
fn: (evt: { addMeta: () => void }) => Promise<void>,
|
||||
) => fn({ addMeta: () => {} }),
|
||||
}));
|
||||
vi.mock("@/lib/cache/invalidate", () => ({
|
||||
invalidateUserDashboardSnapshot: vi.fn(),
|
||||
}));
|
||||
vi.mock("@/lib/jobs/reminder/shared", () => ({
|
||||
getWorkerPrisma: () => fakePrisma,
|
||||
workerLog: vi.fn(),
|
||||
}));
|
||||
|
||||
const createMany = vi.fn(async () => ({ count: 1 }));
|
||||
const updateMany = vi.fn(async () => ({ count: 1 }));
|
||||
const fakePrisma = { arrivalReaction: { createMany, updateMany } };
|
||||
|
||||
const { runDataArrival } = await import("../data-arrival");
|
||||
|
||||
type Runnable = Parameters<typeof runDataArrival>[1];
|
||||
|
||||
function arrival(overrides: Partial<Runnable> = {}): Runnable {
|
||||
return {
|
||||
userId: "user-1",
|
||||
kind: "weight",
|
||||
salience: "salient",
|
||||
localDate: "2026-07-14",
|
||||
occurredAt: "2026-07-14T06:15:00.000Z",
|
||||
count: 1,
|
||||
source: "withings",
|
||||
...overrides,
|
||||
} as Runnable;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
createMany.mockClear().mockResolvedValue({ count: 1 });
|
||||
updateMany.mockClear().mockResolvedValue({ count: 1 });
|
||||
});
|
||||
|
||||
describe("data-arrival worker", () => {
|
||||
it("claims the day's marker before any fan-out", async () => {
|
||||
const result = await runDataArrival(
|
||||
fakePrisma as never,
|
||||
arrival({ kind: "workout", refId: "w-1" }),
|
||||
);
|
||||
expect(createMany).toHaveBeenCalledTimes(1);
|
||||
expect(createMany.mock.calls[0][0]).toMatchObject({ skipDuplicates: true });
|
||||
expect(result).toMatchObject({ status: "processed", dedup: false });
|
||||
});
|
||||
|
||||
it("a lost claim de-duplicates instead of writing a second line", async () => {
|
||||
createMany.mockResolvedValue({ count: 0 });
|
||||
const result = await runDataArrival(fakePrisma as never, arrival());
|
||||
expect(result).toMatchObject({ status: "processed", dedup: true });
|
||||
if (result.status !== "processed") throw new Error("unreachable");
|
||||
expect(result.actions).not.toContain("line_pending");
|
||||
});
|
||||
|
||||
it("only a fresh claim schedules the day's reaction line", async () => {
|
||||
const first = await runDataArrival(fakePrisma as never, arrival());
|
||||
if (first.status !== "processed") throw new Error("unreachable");
|
||||
expect(first.actions).toContain("line_pending");
|
||||
});
|
||||
|
||||
it("a second workout the same day still fans out, despite the day marker", async () => {
|
||||
// The day-scoped marker can only be claimed once, but two sessions are two
|
||||
// events. Gating the workout fan-out on the claim would silently drop the
|
||||
// second one.
|
||||
createMany.mockResolvedValue({ count: 0 });
|
||||
const result = await runDataArrival(
|
||||
fakePrisma as never,
|
||||
arrival({ kind: "workout", refId: "w-2" }),
|
||||
);
|
||||
if (result.status !== "processed") throw new Error("unreachable");
|
||||
expect(result.actions).toContain("workout_pending_insight");
|
||||
});
|
||||
|
||||
it("moves the marker forward on a later arrival, never backwards", async () => {
|
||||
createMany.mockResolvedValue({ count: 0 });
|
||||
await runDataArrival(fakePrisma as never, arrival());
|
||||
const where = updateMany.mock.calls[0][0] as {
|
||||
where: { occurredAt?: { lt?: Date } };
|
||||
};
|
||||
expect(where.where.occurredAt?.lt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it("refuses an unknown kind as SKIPPED, not FAILED", async () => {
|
||||
const result = await runDataArrival(
|
||||
fakePrisma as never,
|
||||
arrival({ kind: "not_a_kind" as never }),
|
||||
);
|
||||
// A throw here would earn two pointless retries against a payload that can
|
||||
// never succeed.
|
||||
expect(result).toEqual({ status: "skipped", reason: "unknown_kind" });
|
||||
expect(createMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("a genuine transient fault DOES propagate, so the retry policy applies", async () => {
|
||||
createMany.mockRejectedValue(new Error("connection terminated"));
|
||||
await expect(
|
||||
runDataArrival(fakePrisma as never, arrival()),
|
||||
).rejects.toThrow("connection terminated");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* The `ArrivalReaction` claim, against real Postgres.
|
||||
*
|
||||
* Unit mocks cannot prove any of this: the once-per-kind-per-day contract is a
|
||||
* unique INDEX, the claim is an ON CONFLICT DO NOTHING whose semantics live in
|
||||
* the database, and the losing side of a concurrent claim is a race only a real
|
||||
* connection pool can produce. A mocked `createMany` returning `{count: 1}`
|
||||
* would happily report a successful claim for every caller.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { getPrismaClient, truncateAllTables } from "./setup";
|
||||
|
||||
beforeEach(async () => {
|
||||
await truncateAllTables(getPrismaClient());
|
||||
});
|
||||
|
||||
async function createUser(username: string) {
|
||||
return getPrismaClient().user.create({
|
||||
data: { username, email: `${username}@example.test`, role: "USER" },
|
||||
});
|
||||
}
|
||||
|
||||
const LOCAL_DATE = "2026-07-14";
|
||||
|
||||
function marker(userId: string, kind: string, occurredAt: Date) {
|
||||
return {
|
||||
userId,
|
||||
kind,
|
||||
localDate: LOCAL_DATE,
|
||||
occurredAt,
|
||||
refId: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("ArrivalReaction — the day claim", () => {
|
||||
it("admits the first claim and refuses the second for the same kind and day", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await createUser("arrival-claim");
|
||||
|
||||
const first = await prisma.arrivalReaction.createMany({
|
||||
data: [marker(user.id, "weight", new Date("2026-07-14T06:00:00Z"))],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
const second = await prisma.arrivalReaction.createMany({
|
||||
data: [marker(user.id, "weight", new Date("2026-07-14T18:00:00Z"))],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
expect(first.count).toBe(1);
|
||||
// The second weigh-in of the day does not get its own line. This IS the
|
||||
// throttle — there is no code path to a second generation.
|
||||
expect(second.count).toBe(0);
|
||||
expect(await prisma.arrivalReaction.count()).toBe(1);
|
||||
});
|
||||
|
||||
it("keeps kinds independent within one day", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await createUser("arrival-kinds");
|
||||
const at = new Date("2026-07-14T06:00:00Z");
|
||||
|
||||
for (const kind of ["weight", "sleep_night", "blood_pressure"]) {
|
||||
const res = await prisma.arrivalReaction.createMany({
|
||||
data: [marker(user.id, kind, at)],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
expect(res.count).toBe(1);
|
||||
}
|
||||
expect(await prisma.arrivalReaction.count()).toBe(3);
|
||||
});
|
||||
|
||||
it("keeps users independent — one user's claim never blocks another's", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const a = await createUser("arrival-tenant-a");
|
||||
const b = await createUser("arrival-tenant-b");
|
||||
const at = new Date("2026-07-14T06:00:00Z");
|
||||
|
||||
const first = await prisma.arrivalReaction.createMany({
|
||||
data: [marker(a.id, "weight", at)],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
const second = await prisma.arrivalReaction.createMany({
|
||||
data: [marker(b.id, "weight", at)],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
expect(first.count).toBe(1);
|
||||
expect(second.count).toBe(1);
|
||||
});
|
||||
|
||||
it("exactly one of many concurrent claims wins", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await createUser("arrival-race");
|
||||
const at = new Date("2026-07-14T06:00:00Z");
|
||||
|
||||
// Ten seams firing at once, the shape a device batch storm takes.
|
||||
const results = await Promise.all(
|
||||
Array.from({ length: 10 }, () =>
|
||||
prisma.arrivalReaction.createMany({
|
||||
data: [marker(user.id, "workout", at)],
|
||||
skipDuplicates: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const winners = results.filter((r) => r.count === 1);
|
||||
expect(winners).toHaveLength(1);
|
||||
expect(await prisma.arrivalReaction.count()).toBe(1);
|
||||
});
|
||||
|
||||
it("the marker moves forward on a later arrival and never backwards", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await createUser("arrival-forward");
|
||||
const early = new Date("2026-07-14T06:00:00Z");
|
||||
const late = new Date("2026-07-14T18:00:00Z");
|
||||
|
||||
await prisma.arrivalReaction.createMany({
|
||||
data: [marker(user.id, "weight", early)],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
const forward = await prisma.arrivalReaction.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
kind: "weight",
|
||||
localDate: LOCAL_DATE,
|
||||
occurredAt: { lt: late },
|
||||
},
|
||||
data: { occurredAt: late },
|
||||
});
|
||||
expect(forward.count).toBe(1);
|
||||
|
||||
const backward = await prisma.arrivalReaction.updateMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
kind: "weight",
|
||||
localDate: LOCAL_DATE,
|
||||
occurredAt: { lt: early },
|
||||
},
|
||||
data: { occurredAt: early },
|
||||
});
|
||||
expect(backward.count).toBe(0);
|
||||
|
||||
const row = await prisma.arrivalReaction.findFirst({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
expect(row?.occurredAt.toISOString()).toBe(late.toISOString());
|
||||
});
|
||||
|
||||
it("stores the line as encrypted bytes, and tolerates its absence", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await createUser("arrival-crypto");
|
||||
const { encryptToBytes, decryptFromBytes } =
|
||||
await import("@/lib/ai/coach/bytes-codec");
|
||||
|
||||
await prisma.arrivalReaction.createMany({
|
||||
data: [marker(user.id, "weight", new Date("2026-07-14T06:00:00Z"))],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
// A provider-less install leaves the column null — the marker is the
|
||||
// feature, the sentence is garnish.
|
||||
const before = await prisma.arrivalReaction.findFirst({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
expect(before?.lineEncrypted).toBeNull();
|
||||
expect(before?.generatedAt).toBeNull();
|
||||
|
||||
const line = "Last night is in — a steady base for the day.";
|
||||
await prisma.arrivalReaction.update({
|
||||
where: { id: before!.id },
|
||||
data: { lineEncrypted: encryptToBytes(line), generatedAt: new Date() },
|
||||
});
|
||||
|
||||
const after = await prisma.arrivalReaction.findFirst({
|
||||
where: { userId: user.id },
|
||||
});
|
||||
expect(after?.lineEncrypted).not.toBeNull();
|
||||
// Ciphertext at rest — the plaintext must not be readable off the column.
|
||||
expect(Buffer.from(after!.lineEncrypted!).toString("utf8")).not.toContain(
|
||||
"steady base",
|
||||
);
|
||||
expect(decryptFromBytes(after!.lineEncrypted!)).toBe(line);
|
||||
});
|
||||
|
||||
it("cascades on user delete, leaving no orphan markers", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await createUser("arrival-cascade");
|
||||
await prisma.arrivalReaction.createMany({
|
||||
data: [marker(user.id, "weight", new Date("2026-07-14T06:00:00Z"))],
|
||||
skipDuplicates: true,
|
||||
});
|
||||
|
||||
await prisma.user.delete({ where: { id: user.id } });
|
||||
expect(await prisma.arrivalReaction.count()).toBe(0);
|
||||
});
|
||||
|
||||
it("the retention window deletes only rows past it", async () => {
|
||||
const prisma = getPrismaClient();
|
||||
const user = await createUser("arrival-retention");
|
||||
const now = new Date();
|
||||
const old = new Date(now.getTime() - 20 * 86_400_000);
|
||||
const recent = new Date(now.getTime() - 3 * 86_400_000);
|
||||
|
||||
await prisma.arrivalReaction.create({
|
||||
data: {
|
||||
...marker(user.id, "weight", now),
|
||||
localDate: "2026-06-24",
|
||||
createdAt: old,
|
||||
},
|
||||
});
|
||||
await prisma.arrivalReaction.create({
|
||||
data: {
|
||||
...marker(user.id, "sleep_night", now),
|
||||
localDate: "2026-07-11",
|
||||
createdAt: recent,
|
||||
},
|
||||
});
|
||||
|
||||
const cutoff = new Date();
|
||||
cutoff.setUTCDate(cutoff.getUTCDate() - 14);
|
||||
const deleted = await prisma.arrivalReaction.deleteMany({
|
||||
where: { createdAt: { lt: cutoff } },
|
||||
});
|
||||
|
||||
expect(deleted.count).toBe(1);
|
||||
const survivor = await prisma.arrivalReaction.findFirst();
|
||||
expect(survivor?.kind).toBe("sleep_night");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user