mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
The comprehensive-briefing cache (User.insightsCachedText) is one slot per account with no record of the language it was written in. Every reader served whatever was there, so a German-language account whose nightly warm had written English kept seeing English on the dashboard lead and the Today briefing. The request path only replaced it once the row was a day old and the provider answered, which with a provider at its usage limit was never. Add User.insightsCachedLocale (migration 0335). Both writers store the locale they generated in; the timestamp-only refresh on an unchanged snapshot tags the row too, since the fingerprint covers the generation locale. Every reset that nulls the text nulls the tag. The snapshot briefing lift and GET /api/insights/generate treat a tag that differs from the reader's locale as an empty cache: no prose, no timestamp, the preparing / no-provider state, and the out-of-band warm enqueued in the reader's locale through the existing path. The generator's own short-circuit and the POST short-circuit refuse to call a foreign-language row fresh. A NULL tag (rows written before this change) is served as before until the next write tags it. The Today digest derives from the snapshot and follows. Covered by unit tests on the lift, the route and the generator, and an integration test that writes the row through Prisma and reads the snapshot for a reader in another language.
114 lines
3.8 KiB
TypeScript
114 lines
3.8 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";
|
|
|
|
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(),
|
|
insightsCachedText: JSON.stringify({ dailyBriefing: BRIEFING }),
|
|
insightsCachedLocale: cachedLocale,
|
|
},
|
|
});
|
|
}
|
|
|
|
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();
|
|
// No provider is configured on this account, so the honest empty state
|
|
// is "nothing will fill this"; with one it would be "preparing".
|
|
expect(["preparing", "no-provider"]).toContain(body.briefingState);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|