mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
Tag the briefing cache with its language and refuse a foreign one
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.
This commit is contained in:
1 parent
d5cf82fb6e
commit
ba4e34f09e
21 files changed
+404
-26
No files matched your search
@@ -226,10 +226,31 @@ describe("generateComprehensiveInsight — content-hash gate (v1.16.8)", () => {
|
||||
expect(write).toBeTruthy();
|
||||
const data = (write![0] as { data: Record<string, unknown> }).data;
|
||||
expect(data.insightsSnapshotHash).toBe(FEATURES_HASH);
|
||||
// The cache row records the language it was generated in.
|
||||
expect(data.insightsCachedLocale).toBe("de");
|
||||
// v1.16.8 — the blanket per-status eviction is gone.
|
||||
expect(auditDeleteMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("a fresh cache tagged with another language does not short-circuit an unforced run", async () => {
|
||||
findUnique.mockResolvedValue({
|
||||
insightsPrivacyMode: "aggregated",
|
||||
insightsCachedAt: new Date(Date.now() - 1 * 60 * 60 * 1000),
|
||||
insightsCachedText: JSON.stringify({ dailyBriefing: { p: "english" } }),
|
||||
insightsCachedLocale: "en",
|
||||
insightsExcludeMetrics: [],
|
||||
insightsSnapshotHash: null,
|
||||
});
|
||||
|
||||
const outcome = await generateComprehensiveInsight("u1", {
|
||||
locale: "de",
|
||||
});
|
||||
|
||||
// Past the short-circuit → provider chain resolved → no provider.
|
||||
expect(resolveProviderChain).toHaveBeenCalledTimes(1);
|
||||
expect(outcome).toEqual({ status: "skipped", reason: "no-provider" });
|
||||
});
|
||||
|
||||
it("generates when no fingerprint is stored yet (first run after the gate shipped)", async () => {
|
||||
makeByokChain();
|
||||
findUnique.mockResolvedValue({
|
||||
|
||||
@@ -612,6 +612,7 @@ export async function generateComprehensiveInsight(
|
||||
insightsPrivacyMode: true,
|
||||
insightsCachedAt: true,
|
||||
insightsCachedText: true,
|
||||
insightsCachedLocale: true,
|
||||
insightsExcludeMetrics: true,
|
||||
insightsSnapshotHash: true,
|
||||
insightsBriefingRerollDate: true,
|
||||
@@ -639,10 +640,18 @@ export async function generateComprehensiveInsight(
|
||||
AI_BUDGETS.comprehensive.timeoutMs,
|
||||
);
|
||||
|
||||
// A cache written in another language is not this locale's cache: it
|
||||
// must not satisfy an unforced run, or a reader in the requested language
|
||||
// keeps being told the slot is warm while it holds someone else's prose.
|
||||
// An untagged row predates the tag and counts as matching.
|
||||
const cachedLocaleMatches =
|
||||
dbUser?.insightsCachedLocale == null ||
|
||||
dbUser.insightsCachedLocale === locale;
|
||||
if (
|
||||
!force &&
|
||||
dbUser?.insightsCachedAt &&
|
||||
dbUser.insightsCachedText &&
|
||||
cachedLocaleMatches &&
|
||||
Date.now() - dbUser.insightsCachedAt.getTime() < CACHE_TTL_MS
|
||||
) {
|
||||
return { status: "cached" };
|
||||
@@ -896,6 +905,7 @@ export async function generateComprehensiveInsight(
|
||||
{
|
||||
insightsCachedAt: new Date(),
|
||||
insightsCachedText: rerolled.text,
|
||||
insightsCachedLocale: locale,
|
||||
insightsBriefingRerollDate: todayKey,
|
||||
},
|
||||
);
|
||||
@@ -920,6 +930,10 @@ export async function generateComprehensiveInsight(
|
||||
locale,
|
||||
{
|
||||
insightsCachedAt: new Date(),
|
||||
// The fingerprint covers the generation locale, so a matching hash
|
||||
// means the stored text is in `locale`; tag it (a pre-tag row gets
|
||||
// its label on its first unchanged night).
|
||||
insightsCachedLocale: locale,
|
||||
insightsBriefingRerollDate: todayKey,
|
||||
},
|
||||
);
|
||||
@@ -939,6 +953,7 @@ export async function generateComprehensiveInsight(
|
||||
locale,
|
||||
{
|
||||
insightsCachedAt: new Date(),
|
||||
insightsCachedLocale: locale,
|
||||
},
|
||||
);
|
||||
const skipped = scopeChangedSkip(stamped, locale);
|
||||
@@ -1282,6 +1297,7 @@ export async function generateComprehensiveInsight(
|
||||
{
|
||||
insightsCachedAt: new Date(),
|
||||
insightsCachedText: JSON.stringify(insights),
|
||||
insightsCachedLocale: locale,
|
||||
insightsSnapshotHash: snapshotHash,
|
||||
},
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user