mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
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.
120 lines
4.4 KiB
TypeScript
120 lines
4.4 KiB
TypeScript
/**
|
|
* What happens to stored AI text and stored Coach rows, against the real
|
|
* server, on an account of its own (`E2E_AI_OPTIONAL`).
|
|
*
|
|
* J5 — consent withdrawn. The account has a local model (an address nothing
|
|
* listens on: presence is all the capability reads, nothing here calls
|
|
* it), a daily briefing a model wrote earlier, and an AI consent. The
|
|
* briefing is on the overview; the consent is withdrawn in Settings, AI
|
|
* through the real control; the briefing is gone, because the server
|
|
* deleted the notes written under that consent.
|
|
* J4 — Coach hidden. With Hide Coach on, Settings, Coach is out of reach, so
|
|
* Settings, AI lists what the Coach stored; one stored conversation is
|
|
* deleted from there and is gone from the database.
|
|
*
|
|
* Both journeys move the same account's state, so they run serially, in one
|
|
* project (see `playwright.config.ts`).
|
|
*/
|
|
import type { Page } from "@playwright/test";
|
|
|
|
import { AI_OPTIONAL_STORAGE_STATE_PATH } from "./setup/global-setup";
|
|
import {
|
|
coachConversationCount,
|
|
resetAiOptionalAccount,
|
|
seedHiddenCoachWithConversation,
|
|
seedStoredBriefing,
|
|
} from "./setup/ai-optional-fixture";
|
|
import { expect, test } from "./setup/test";
|
|
|
|
test.use({ storageState: AI_OPTIONAL_STORAGE_STATE_PATH });
|
|
test.describe.configure({ mode: "serial" });
|
|
|
|
test.beforeEach(async () => {
|
|
await resetAiOptionalAccount();
|
|
});
|
|
test.afterAll(async () => {
|
|
await resetAiOptionalAccount();
|
|
});
|
|
|
|
async function openOverview(page: Page): Promise<void> {
|
|
await page.goto("/insights");
|
|
await expect(page.locator('[data-slot="insights-hero-strip"]')).toBeVisible({
|
|
timeout: 20_000,
|
|
});
|
|
await page.waitForLoadState("networkidle");
|
|
}
|
|
|
|
test("J5: withdrawing AI consent deletes the notes written under it", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(90_000);
|
|
const marker = `Stored briefing ${Date.now()}`;
|
|
await seedStoredBriefing(marker);
|
|
|
|
// Positive control: the stored briefing is what the overview shows.
|
|
await openOverview(page);
|
|
await expect(page.locator('[data-slot="daily-briefing"]')).toHaveCount(1);
|
|
await expect(page.getByText(marker).first()).toBeVisible();
|
|
|
|
// Withdraw through the real control. The confirm names what goes and what
|
|
// stays before anything happens.
|
|
await page.goto("/settings/ai");
|
|
await page.locator('[data-slot="ai-consent-withdraw"]').click();
|
|
const withdrawn = page.waitForResponse(
|
|
(res) =>
|
|
res.url().includes("/api/consent/ai/latest") &&
|
|
res.request().method() === "DELETE",
|
|
);
|
|
await page.locator('[data-slot="ai-consent-withdraw-confirm"]').click();
|
|
expect((await withdrawn).status()).toBe(200);
|
|
await expect(page.locator('[data-slot="ai-consent-grant"]')).toBeVisible();
|
|
|
|
await openOverview(page);
|
|
await expect(page.getByText(marker)).toHaveCount(0);
|
|
});
|
|
|
|
test("J4: the Coach memory stays readable and deletable with the Coach hidden", async ({
|
|
page,
|
|
}) => {
|
|
test.setTimeout(90_000);
|
|
const title = `Stored conversation ${Date.now()}`;
|
|
await seedHiddenCoachWithConversation(title);
|
|
|
|
// Hide Coach takes the Coach page and its launcher away...
|
|
await page.goto("/insights");
|
|
await expect(page.locator('[data-slot="insights-hero-strip"]')).toBeVisible({
|
|
timeout: 20_000,
|
|
});
|
|
await expect(page.locator('[data-slot="coach-fab"]')).toHaveCount(0);
|
|
|
|
// ...and Settings, AI lists what the Coach stored.
|
|
await page.goto("/settings/ai");
|
|
const card = page.locator(
|
|
'[data-testid="settings-coach-conversations-card"]',
|
|
);
|
|
await expect(card).toBeVisible({ timeout: 20_000 });
|
|
const row = card
|
|
.locator('[data-testid="settings-coach-conversation"]')
|
|
.filter({ hasText: title });
|
|
await expect(row).toHaveCount(1);
|
|
|
|
// Readable: the row opens its transcript in place.
|
|
await row.locator('[data-slot="settings-coach-conversation-open"]').click();
|
|
await expect(
|
|
row.locator('[data-slot="settings-coach-conversation-open"]'),
|
|
).toHaveAttribute("aria-expanded", "true");
|
|
|
|
// Deletable: the row goes at once, and the delete reaches the server once
|
|
// the undo window closes.
|
|
const deleted = page.waitForResponse(
|
|
(res) =>
|
|
/\/api\/insights\/chat\/[^/]+$/.test(new URL(res.url()).pathname) &&
|
|
res.request().method() === "DELETE",
|
|
{ timeout: 30_000 },
|
|
);
|
|
await row.locator('[data-slot="settings-coach-conversation-delete"]').click();
|
|
await expect(row).toHaveCount(0);
|
|
expect((await deleted).ok()).toBe(true);
|
|
expect(await coachConversationCount()).toBe(0);
|
|
});
|