mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
350 lines
11 KiB
TypeScript
350 lines
11 KiB
TypeScript
/**
|
|
* 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("replaces a generated marker and rejects the stale generation owner", async () => {
|
|
const prisma = getPrismaClient();
|
|
const user = await createUser("arrival-replacement");
|
|
const early = new Date("2026-07-14T06:00:00Z");
|
|
const late = new Date("2026-07-14T18:00:00Z");
|
|
const providerInvokedAt = new Date("2026-07-14T06:01:00Z");
|
|
const existing = await prisma.arrivalReaction.create({
|
|
data: {
|
|
...marker(user.id, "weight", early),
|
|
refId: "old-ref",
|
|
lineEncrypted: Buffer.from("old-ciphertext"),
|
|
generatedAt: new Date("2026-07-14T06:02:00Z"),
|
|
generationClaimId: "old-claim",
|
|
generationClaimedAt: providerInvokedAt,
|
|
generationReservedTokens: 220,
|
|
generationBudgetDateKey: "2026-07-14",
|
|
generationProviderInvokedAt: providerInvokedAt,
|
|
},
|
|
});
|
|
await prisma.coachUsage.create({
|
|
data: {
|
|
userId: user.id,
|
|
dateKey: "2026-07-14",
|
|
totalTokens: 220,
|
|
messageCount: 1,
|
|
},
|
|
});
|
|
|
|
const { runDataArrival } = await import("@/lib/jobs/data-arrival");
|
|
const outcome = await runDataArrival(prisma as never, {
|
|
userId: user.id,
|
|
kind: "weight",
|
|
salience: "salient",
|
|
localDate: LOCAL_DATE,
|
|
occurredAt: late.toISOString(),
|
|
count: 1,
|
|
source: "withings",
|
|
refId: "new-ref",
|
|
});
|
|
|
|
expect(outcome).toMatchObject({ status: "processed", dedup: false });
|
|
const replaced = await prisma.arrivalReaction.findUniqueOrThrow({
|
|
where: { id: existing.id },
|
|
});
|
|
expect(replaced).toMatchObject({
|
|
occurredAt: late,
|
|
refId: "new-ref",
|
|
lineEncrypted: null,
|
|
generatedAt: null,
|
|
generationClaimId: null,
|
|
generationClaimedAt: null,
|
|
generationReservedTokens: null,
|
|
generationBudgetDateKey: null,
|
|
generationProviderInvokedAt: null,
|
|
});
|
|
|
|
const staleCommit = await prisma.arrivalReaction.updateMany({
|
|
where: {
|
|
id: existing.id,
|
|
generationClaimId: "old-claim",
|
|
generationProviderInvokedAt: providerInvokedAt,
|
|
},
|
|
data: {
|
|
lineEncrypted: Buffer.from("stale-ciphertext"),
|
|
generatedAt: new Date(),
|
|
},
|
|
});
|
|
expect(staleCommit.count).toBe(0);
|
|
|
|
const usage = await prisma.coachUsage.findUniqueOrThrow({
|
|
where: {
|
|
userId_dateKey: { userId: user.id, dateKey: "2026-07-14" },
|
|
},
|
|
});
|
|
expect(usage).toMatchObject({ totalTokens: 220, messageCount: 1 });
|
|
});
|
|
|
|
it("refunds a superseded pre-provider reservation", async () => {
|
|
const prisma = getPrismaClient();
|
|
const user = await createUser("arrival-reservation-refund");
|
|
const early = new Date("2026-07-14T06:00:00Z");
|
|
const late = new Date("2026-07-14T18:00:00Z");
|
|
await prisma.arrivalReaction.create({
|
|
data: {
|
|
...marker(user.id, "weight", early),
|
|
generationClaimId: "pre-provider-claim",
|
|
generationClaimedAt: new Date("2026-07-14T06:01:00Z"),
|
|
generationReservedTokens: 220,
|
|
generationBudgetDateKey: "2026-07-14",
|
|
},
|
|
});
|
|
await prisma.coachUsage.create({
|
|
data: {
|
|
userId: user.id,
|
|
dateKey: "2026-07-14",
|
|
totalTokens: 500,
|
|
messageCount: 2,
|
|
},
|
|
});
|
|
|
|
const { runDataArrival } = await import("@/lib/jobs/data-arrival");
|
|
await runDataArrival(prisma as never, {
|
|
userId: user.id,
|
|
kind: "weight",
|
|
salience: "salient",
|
|
localDate: LOCAL_DATE,
|
|
occurredAt: late.toISOString(),
|
|
count: 1,
|
|
source: "withings",
|
|
});
|
|
|
|
const usage = await prisma.coachUsage.findUniqueOrThrow({
|
|
where: {
|
|
userId_dateKey: { userId: user.id, dateKey: "2026-07-14" },
|
|
},
|
|
});
|
|
expect(usage).toMatchObject({ totalTokens: 280, messageCount: 1 });
|
|
});
|
|
|
|
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");
|
|
});
|
|
});
|