mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
feat(ai): answer the status notes with a null note when AI is unavailable
The status family (the five metric cards, medication compliance, the generic metric status and the biomarker assessment) asked the retired switch and the `insights` module and refused with 403. The note is model text and the card around it is data, so they are mixed reads now: while the `statusText` capability is available the note is served and warmed as before, with `ai` beside it; otherwise the answer is 200 with the note null, `preparing: false`, `hasProvider` as provider presence only and `ai` naming the reason, and neither the cache nor the warm queue is touched. The generic metric status still reports `insufficient` from the data. The mood card keeps its mood module gate. Kept apart so it can be held back while the shipped iPhone app's rendering of a 200 without a note is confirmed. Refs #975
This commit is contained in:
1 parent
9f1d1341f9
commit
1f6a13e2f7
19 files changed
+621
-207
No files matched your search
@@ -367,3 +367,55 @@ describe("the snapshot applies the capability per read", () => {
|
||||
expect(after.data.briefingAi.reason).toBe("operator_disabled");
|
||||
});
|
||||
});
|
||||
|
||||
describe("the status family", () => {
|
||||
async function readStatus(path: string) {
|
||||
const route =
|
||||
path === "weight"
|
||||
? await import("@/app/api/insights/weight-status/route")
|
||||
: await import("@/app/api/insights/metric-status/route");
|
||||
const url =
|
||||
path === "weight"
|
||||
? "http://localhost/api/insights/weight-status"
|
||||
: "http://localhost/api/insights/metric-status?metric=RESTING_HEART_RATE";
|
||||
return json<Record<string, unknown>>(await route.GET(new NextRequest(url)));
|
||||
}
|
||||
|
||||
it("S0 publishes the capability beside the note", async () => {
|
||||
const world = await enterState("S0");
|
||||
// The available path resolves (never calls) the provider chain, so the
|
||||
// key has to decrypt.
|
||||
const { encrypt } = await import("@/lib/crypto");
|
||||
await getPrismaClient().user.update({
|
||||
where: { id: world.recordId },
|
||||
data: { aiAnthropicKeyEncrypted: encrypt("sk-ant-integration") },
|
||||
});
|
||||
const status = await readStatus("weight");
|
||||
expect(status.status).toBe(200);
|
||||
expect(status.data.ai).toMatchObject({ available: true });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["S1", "operator_disabled", true],
|
||||
["S3", "no_provider", false],
|
||||
["S6", "user_disabled", true],
|
||||
] as const)(
|
||||
"%s answers 200 with no note and provider presence in hasProvider",
|
||||
async (state, reason, hasProvider) => {
|
||||
await enterState(state);
|
||||
for (const path of ["weight", "metric"]) {
|
||||
const status = await readStatus(path);
|
||||
expect(status.status, path).toBe(200);
|
||||
expect(status.data, path).toMatchObject({
|
||||
text: null,
|
||||
preparing: false,
|
||||
hasProvider,
|
||||
ai: { available: false, reason },
|
||||
});
|
||||
}
|
||||
const metric = await readStatus("metric");
|
||||
// No readings were seeded: the data half still says so.
|
||||
expect(metric.data.insufficient).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user