Files
HealthLog/tests/integration/briefing-cache-locale.test.ts
T
Marc-André Bombeck 9941db1549 refactor(insights): drop the readable cached-text column
Every write has gone to insights_cached_text_encrypted since 0411, and
the boot-time free-text backfill sealed the rows written before it.
Migration 0430 drops insights_cached_text and the backfill index that
served it. A host that skipped the intervening releases loses only a
cache of model output; the next warm writes a fresh, sealed payload.

The cipher module now reads and writes the sealed column only, the
backfill job loses its insights step, and consent withdrawal, the wipe
plan, the backup plan and the session projection follow the schema.
Backups never carried the column, and restore ignores unknown account
settings; a round-trip test now proves an old backup naming the field
still restores cleanly. The access guard pins exact counts for where
the sealed name may appear. Fixtures, the demo seed and the demo
migration script seed the sealed column instead of plaintext.
2026-10-21 19:24:21 +02:00

128 lines
4.4 KiB
TypeScript

/**
* The comprehensive-briefing cache is one slot per user. When it holds prose
* in a language other than the reader's, the dashboard snapshot serves no
* briefing text at all rather than the wrong language.
*
* This runs the real Prisma path end to end: the cache row is written with
* its locale tag through the client, and the snapshot is read through the
* same `readDashboardSnapshotCached` the dashboard route and the Today
* digest use, with the reader's locale resolved from the cookie the client
* sets when the user switches language.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { cookieJar, headerJar } from "./mock-next-headers";
import { getPrismaClient, truncateAllTables } from "./setup";
import { sealInsightsCachedText } from "@/lib/insights/cached-text-cipher";
vi.mock("next/headers", async () => {
const { cookieJar, headerJar } = await import("./mock-next-headers");
return {
headers: vi.fn(async () => ({
get: (name: string) => headerJar.get(name.toLowerCase()) ?? null,
})),
cookies: vi.fn(async () => ({
get: (name: string) => {
const value = cookieJar.get(name);
return value ? { name, value } : undefined;
},
set: (name: string, value: string) => {
cookieJar.set(name, value);
},
delete: (name: string) => {
cookieJar.delete(name);
},
})),
};
});
vi.mock("@/lib/db-compat", () => ({
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
}));
const BRIEFING = {
greeting: "Good morning",
paragraph: "Your readings this week stayed inside their usual range.",
keyFindings: [],
};
beforeEach(async () => {
await truncateAllTables(getPrismaClient());
cookieJar.clear();
headerJar.clear();
});
afterEach(() => {
vi.restoreAllMocks();
});
async function seedUserWithCachedBriefing(cachedLocale: string | null) {
const prisma = getPrismaClient();
return prisma.user.create({
data: {
username: "briefing-user",
email: "briefing@example.test",
role: "USER",
// A German-language account: the stored preference is what the
// snapshot read resolves the reader's locale from.
locale: "de",
insightsCachedAt: new Date(),
// Generated now: only text written today is served as today's read.
insightsCachedTextEncrypted: sealInsightsCachedText(
JSON.stringify({
dailyBriefing: BRIEFING,
briefingGeneratedAt: new Date().toISOString(),
}),
),
insightsCachedLocale: cachedLocale,
// The briefing is shown only while its AI capability is available, so
// the account has its own provider (presence only) and a receipt.
aiProvider: "ANTHROPIC",
aiModel: "claude-sonnet-4-6",
aiAnthropicKeyEncrypted: "v1:presence-only",
consentReceipts: {
create: { kind: "ai_full", artefact: "test", signedAt: new Date() },
},
},
});
}
describe("dashboard snapshot — briefing cache written in another language", () => {
it("serves NO briefing prose to a 'de' reader from a cache tagged 'en'", async () => {
const user = await seedUserWithCachedBriefing("en");
const { readDashboardSnapshotCached } =
await import("@/lib/dashboard/snapshot-read");
const { body, locale } = await readDashboardSnapshotCached(user);
expect(locale).toBe("de");
expect(body.briefing).toBeNull();
expect(body.briefingStale).toBe(false);
expect(body.briefingUpdatedAt).toBeNull();
// A provider is configured, so a warm pass will refill the slot.
expect(body.briefingState).toBe("preparing");
});
it("serves the briefing when the tag matches the reader", async () => {
const user = await seedUserWithCachedBriefing("de");
const { readDashboardSnapshotCached } =
await import("@/lib/dashboard/snapshot-read");
const { body } = await readDashboardSnapshotCached(user);
expect(body.briefingState).toBe("ready");
expect(body.briefing?.paragraph).toBe(BRIEFING.paragraph);
});
it("serves an untagged row written before the tag existed", async () => {
const user = await seedUserWithCachedBriefing(null);
const { readDashboardSnapshotCached } =
await import("@/lib/dashboard/snapshot-read");
const { body } = await readDashboardSnapshotCached(user);
expect(body.briefingState).toBe("ready");
expect(body.briefing?.paragraph).toBe(BRIEFING.paragraph);
});
});