Files
HealthLog/e2e/insights-generate.spec.ts
Marc-André Bombeck b7830c3a72 test(e2e): cover Insights and the Coach memory without AI
Adds the AI-optional journeys: the overview with AI switched off by the
operator and with no provider (no AI requests, no refusals, one setup
hint, remembered dismissal), and on an account of its own, withdrawing AI
consent deletes the stored briefing, and a hidden Coach's conversation is
read and deleted from Settings, AI.

Specs that drive AI surfaces against mocked routes now serve an available
capability map through /api/auth/me, since the web no longer assumes AI
is on until told otherwise.
2026-09-24 11:36:30 +02:00

119 lines
4.1 KiB
TypeScript

import { expect, test } from "./setup/test";
import { STORAGE_STATE_PATH } from "./setup/global-setup";
import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
/**
* Insights generation flow — `/settings/ai` exposes a "Regenerate
* insights" button that POSTs `/api/insights/generate` and re-renders
* the AI-generated text in the surrounding cards.
*
* The real endpoint hits an AI provider (OpenAI / Codex) which we can't
* call from CI. We mock it with the schema documented in
* `src/app/api/insights/generate/route.ts`:
*
* { data: { insights: { changed, stable, drivers, nextSteps,
* confidence, limitations }, cached: false }, error: null }
*
* We then confirm the success message lands AND a query invalidation
* fires on the insight-fetching endpoints (we re-mock those to assert
* they're touched after the regenerate call).
*/
test.describe("insights generation flow (mocked)", () => {
// AI surfaces render from the `ai` block of `/api/auth/me`, and the suite
// has no reachable provider; every capability reads available here so the
// mocked AI routes below are what the surfaces talk to.
test.beforeEach(async ({ page }) => {
await serveAiBlock(page, aiBlockAvailable());
});
test.use({ storageState: STORAGE_STATE_PATH });
test("regenerate button updates insight cards", async ({ page }) => {
// Settings endpoint — return "connected" so the regenerate button
// is rendered (the section gates it on hasProvider).
await page.route("**/api/insights/settings", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: {
codexStatus: "connected",
codexConnectedAt: new Date().toISOString(),
hasAdminKey: false,
codexOauthConfigured: true,
privacyMode: "aggregated",
lastInsightAt: new Date().toISOString(),
},
error: null,
}),
}),
);
await page.route("**/api/user/ai-provider", (route) =>
route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: {
provider: null,
model: null,
baseUrl: null,
hasAnthropicKey: false,
anthropicKeyPreview: null,
hasLocalKey: false,
hasOpenaiKey: false,
openaiKeyPreview: null,
},
error: null,
}),
}),
);
let generateCalls = 0;
await page.route("**/api/insights/generate", (route) => {
generateCalls += 1;
return route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: {
insights: {
changed:
"Blood-pressure averages dropped by 4/2 mmHg over the last 30 days.",
stable: "Pulse and weight unchanged within natural variance.",
drivers:
"Likely driven by improved medication compliance (88% vs 72% prior).",
nextSteps:
"Continue current routine; revisit BMI target in 30 days.",
confidence: "mittel",
limitations:
"Sample size for systolic readings remains below 60.",
},
cached: false,
},
error: null,
}),
});
});
await page.goto("/settings/ai", { waitUntil: "domcontentloaded" });
const regenerateBtn = page.getByRole("button", {
name: /regenerate (insights|reports)|insights aktualisieren|neu erstellen|berichte/i,
});
await expect(regenerateBtn).toBeVisible({ timeout: 10_000 });
await regenerateBtn.click();
// The settings page surfaces an inline alert after the mutation
// completes. The exact copy comes from i18n, so match permissively.
await expect(
page.getByText(
/reports? regenerated|insight.*regenerated|berichte.*aktualisiert|generated successfully|erfolgreich/i,
),
).toBeVisible({ timeout: 10_000 });
expect(generateCalls).toBeGreaterThanOrEqual(1);
});
});