test(ai): answer both switch readers from one source in flag mocks

Eight files mocked `@/lib/feature-flags` with `getAssistantFlags` alone.
The capability loader reads `loadAssistantSwitches`, so wherever it ran
it threw and every capability resolved to `check_failed`. A shared
factory now points both readers at the function the test drives.

Only the config-change chain reached the loader. Its fixture means AI
switched off by the operator; it now supplies the loader's other inputs
and asserts `operator_disabled` on the digest, so a starved loader turns
it red instead of passing on the failure path.
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 10:52:03 +02:00
1 parent c2005fd8f1
commit 2c4dec08f1
9 files changed
+123 -30

No files matched your search

@@ -178,6 +178,8 @@ const fakeDb = vi.hoisted(() => {
personalRecord: { findMany: none },
arrivalReaction: { findMany: none },
encounter: { findFirst: async () => null },
// The AI capability loader's consent read: no receipts.
consentReceipt: { findMany: none },
};
});
@@ -215,17 +217,27 @@ vi.mock("@/lib/analytics/bp-in-target-fast-path", () => ({
computeBpInTargetFastPath: (...a: unknown[]) =>
computeBpInTargetFastPath(...a),
}));
vi.mock("@/lib/feature-flags", () => ({
getAssistantFlags: (...a: unknown[]) => getAssistantFlags(...a),
}));
vi.mock("@/lib/feature-flags", async () =>
(
await import("@/__tests__/helpers/assistant-switches-mock")
).mockAssistantSwitches(() => getAssistantFlags()),
);
vi.mock("@/lib/ai/provider", () => ({
hasAnyConfiguredProvider: (...a: unknown[]) => hasAnyConfiguredProvider(...a),
// The capability loader's presence probe: no provider on the record.
probeProviderChain: async () => ({
entries: [],
localOcrEnabled: false,
managedBy: null,
}),
}));
vi.mock("@/lib/dashboard/meds-today", () => ({
buildMedsTodayBlock: (...a: unknown[]) => buildMedsTodayBlock(...a),
}));
vi.mock("@/lib/modules/gate", () => ({
resolveModuleMap: (...a: unknown[]) => resolveModuleMap(...a),
// The operator offers every module; the AI capability loader reads it.
getOperatorModuleAvailability: async () => moduleMap(),
}));
vi.mock("@/lib/dashboard/score-rings", () => ({
buildScoreRingsBlock: (...a: unknown[]) => buildScoreRingsBlock(...a),
@@ -448,6 +460,12 @@ describe("a settings change is never narrated as a health event", () => {
expect(digest.score?.deltaReason).toBeNull();
expect(digest.score?.delta).toBe(snapshot.healthScore!.delta);
expect(digest.score!.delta!).toBeLessThanOrEqual(-10);
// The fixture's AI is off because the operator switched it off, and the
// capability loader actually ran to say so. `check_failed` here would mean
// a mock starved the loader and the digest was assembled on a failure
// path nobody meant to test.
expect(digest.ai.coach.reason).toBe("operator_disabled");
expect(digest.ai.reactionLines.reason).toBe("operator_disabled");
});
it("renders no delta chip when the recipe changed inside the window", async () => {
+5 -3
View File
@@ -39,9 +39,11 @@ import {
type HealthProfileAiSection,
} from "@/lib/validations/health-profile-facts";
vi.mock("@/lib/feature-flags", () => ({
getAssistantFlags: vi.fn(async () => ({ coach: true })),
}));
vi.mock("@/lib/feature-flags", async () =>
(
await import("@/__tests__/helpers/assistant-switches-mock")
).mockAssistantSwitches(vi.fn(async () => ({ coach: true }))),
);
// Gate 3 resolves the `coach` capability per user; never the real resolver.
vi.mock("@/lib/ai/capabilities/gate", () => ({
aiCapabilityForJob: vi.fn(),
@@ -40,9 +40,11 @@ vi.mock("@/lib/logging/context", async () => {
annotate: (...a: unknown[]) => annotateSpy(...a),
};
});
vi.mock("@/lib/feature-flags", () => ({
getAssistantFlags: (...a: unknown[]) => getAssistantFlags(...a),
}));
vi.mock("@/lib/feature-flags", async () =>
(
await import("@/__tests__/helpers/assistant-switches-mock")
).mockAssistantSwitches(() => getAssistantFlags()),
);
// Per-user capabilities, keyed by capability; every test starts with both
// halves available and overrides one or both.
type CapabilityState = {
@@ -8,9 +8,11 @@ const getAssistantFlags = vi.fn();
vi.mock("@/lib/rate-limit", () => ({
checkRateLimit: (...a: unknown[]) => checkRateLimit(...a),
}));
vi.mock("@/lib/feature-flags", () => ({
getAssistantFlags: (...a: unknown[]) => getAssistantFlags(...a),
}));
vi.mock("@/lib/feature-flags", async () =>
(
await import("@/__tests__/helpers/assistant-switches-mock")
).mockAssistantSwitches(() => getAssistantFlags()),
);
// Never reach the real generator (which imports the provider chain).
vi.mock("@/lib/insights/narrative/period-narrative-generate", () => ({
generatePeriodNarrative: vi.fn(),
@@ -17,9 +17,11 @@ import fs from "node:fs";
import path from "node:path";
const getAssistantFlags = vi.fn();
vi.mock("@/lib/feature-flags", () => ({
getAssistantFlags: (...a: unknown[]) => getAssistantFlags(...a),
}));
vi.mock("@/lib/feature-flags", async () =>
(
await import("@/__tests__/helpers/assistant-switches-mock")
).mockAssistantSwitches(() => getAssistantFlags()),
);
import { findStatusCronCandidates } from "../status-cron-candidates";
import { PREGENERATE_STALE_MS } from "../insight-pregenerate";