mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
A background job opens one wide event for its whole run, and the per-request memo cached the operator switches, module availability, module preferences, provider presence and consent kinds on it. The nightly status passes loop over every candidate inside that one event, so a switch turned off mid-run did not stop the remaining users, and the wire re-check read the same cached answer. memoizePerRequest takes a freshInBackground option; every input that decides whether AI work may run sets it, so a background caller reads them on every call while a request still reads them once.
185 lines
5.5 KiB
TypeScript
185 lines
5.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
vi.mock("@/lib/db", () => ({
|
|
prisma: {
|
|
appSettings: {
|
|
findUnique: vi.fn(),
|
|
},
|
|
},
|
|
}));
|
|
|
|
// v1.4.33 — `mockEvent` lets the dedicated memoisation describe-block
|
|
// below pin a stable WideEventBuilder stub so the per-request cache
|
|
// keys on the same reference across multiple `getAssistantFlags()`
|
|
// calls. The legacy describe-blocks leave `mockEvent` null so every
|
|
// call gets a fresh `{}` and the memo never collapses — preserves the
|
|
// pre-v1.4.33 assertion semantics.
|
|
let mockEvent: object | null = null;
|
|
vi.mock("@/lib/logging/context", () => ({
|
|
getEvent: () => mockEvent ?? { getKind: () => "http", addWarning: vi.fn() },
|
|
}));
|
|
|
|
import { prisma } from "@/lib/db";
|
|
import {
|
|
ASSISTANT_FLAGS_DEFAULT,
|
|
getAssistantFlags,
|
|
loadAssistantSwitches,
|
|
resolveAssistantFlags,
|
|
} from "../index";
|
|
|
|
const FIND = prisma.appSettings.findUnique as ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(() => {
|
|
FIND.mockReset();
|
|
mockEvent = null;
|
|
});
|
|
|
|
describe("resolveAssistantFlags", () => {
|
|
it("returns the input unchanged when master is on", () => {
|
|
const input = {
|
|
enabled: true,
|
|
coach: true,
|
|
briefing: false,
|
|
insightStatus: true,
|
|
documentAi: false,
|
|
};
|
|
expect(resolveAssistantFlags(input)).toEqual(input);
|
|
});
|
|
|
|
it("forces every sub-flag false when master is off", () => {
|
|
const input = {
|
|
enabled: false,
|
|
coach: true,
|
|
briefing: true,
|
|
insightStatus: true,
|
|
documentAi: true,
|
|
};
|
|
expect(resolveAssistantFlags(input)).toEqual({
|
|
enabled: false,
|
|
coach: false,
|
|
briefing: false,
|
|
insightStatus: false,
|
|
documentAi: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("getAssistantFlags", () => {
|
|
it("returns defaults when the row is missing", async () => {
|
|
FIND.mockResolvedValue(null);
|
|
const flags = await getAssistantFlags();
|
|
expect(flags).toEqual(ASSISTANT_FLAGS_DEFAULT);
|
|
});
|
|
|
|
it("reads each column correctly", async () => {
|
|
FIND.mockResolvedValue({
|
|
assistantEnabled: true,
|
|
assistantCoachEnabled: false,
|
|
assistantBriefingEnabled: true,
|
|
assistantInsightStatusEnabled: false,
|
|
assistantDocumentAiEnabled: true,
|
|
});
|
|
const flags = await getAssistantFlags();
|
|
expect(flags).toEqual({
|
|
enabled: true,
|
|
coach: false,
|
|
briefing: true,
|
|
insightStatus: false,
|
|
documentAi: true,
|
|
});
|
|
});
|
|
|
|
it("forces every sub-flag false when master is off", async () => {
|
|
FIND.mockResolvedValue({
|
|
assistantEnabled: false,
|
|
assistantCoachEnabled: true,
|
|
assistantBriefingEnabled: true,
|
|
assistantInsightStatusEnabled: true,
|
|
assistantDocumentAiEnabled: true,
|
|
});
|
|
const flags = await getAssistantFlags();
|
|
expect(flags).toEqual({
|
|
enabled: false,
|
|
coach: false,
|
|
briefing: false,
|
|
insightStatus: false,
|
|
documentAi: false,
|
|
});
|
|
});
|
|
|
|
it("fails closed on a Prisma error: every switch reads off", async () => {
|
|
// A database blip must not switch every AI egress on. Safe now, because
|
|
// no data read depends on a switch.
|
|
FIND.mockRejectedValue(new Error("db down"));
|
|
const flags = await getAssistantFlags();
|
|
expect(flags).toEqual({
|
|
enabled: false,
|
|
coach: false,
|
|
briefing: false,
|
|
insightStatus: false,
|
|
documentAi: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("loadAssistantSwitches", () => {
|
|
it("tells a read failure apart from a set of switches that are off", async () => {
|
|
FIND.mockRejectedValue(new Error("db down"));
|
|
expect(await loadAssistantSwitches()).toBeNull();
|
|
FIND.mockResolvedValue({
|
|
assistantEnabled: false,
|
|
assistantCoachEnabled: true,
|
|
assistantBriefingEnabled: true,
|
|
assistantInsightStatusEnabled: true,
|
|
assistantDocumentAiEnabled: true,
|
|
});
|
|
expect((await loadAssistantSwitches())?.enabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("per-request memoisation", () => {
|
|
// v1.4.33 — When the same request opens the Coach drawer, five gated
|
|
// routes (`/api/insights/chat`, the rail list, each `<metric>-status`
|
|
// call, etc.) all funnel through `getAssistantFlags()` → `AppSettings`
|
|
// read. The memo on the active WideEventBuilder collapses those to
|
|
// one DB hit.
|
|
it("collapses repeated reads inside the same request to one DB call", async () => {
|
|
FIND.mockResolvedValue({
|
|
assistantEnabled: true,
|
|
assistantCoachEnabled: true,
|
|
assistantBriefingEnabled: true,
|
|
assistantInsightStatusEnabled: true,
|
|
assistantDocumentAiEnabled: true,
|
|
});
|
|
// Pin a stable stub object so every `getEvent()` inside this test
|
|
// returns the same reference — the memo keys on identity, so a
|
|
// shared reference == shared cache.
|
|
mockEvent = { getKind: () => "http", addWarning: vi.fn() };
|
|
|
|
const a = await getAssistantFlags();
|
|
const b = await getAssistantFlags();
|
|
const c = await getAssistantFlags();
|
|
|
|
expect(a).toBe(b);
|
|
expect(b).toBe(c);
|
|
expect(FIND).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("issues independent reads when the request context changes", async () => {
|
|
FIND.mockResolvedValue({
|
|
assistantEnabled: true,
|
|
assistantCoachEnabled: true,
|
|
assistantBriefingEnabled: true,
|
|
assistantInsightStatusEnabled: true,
|
|
assistantDocumentAiEnabled: true,
|
|
});
|
|
|
|
mockEvent = { getKind: () => "http", addWarning: vi.fn(), id: "req-1" };
|
|
await getAssistantFlags();
|
|
mockEvent = { getKind: () => "http", addWarning: vi.fn(), id: "req-2" };
|
|
await getAssistantFlags();
|
|
|
|
expect(FIND).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|