mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
"Ask the Coach" on a document used to navigate to /coach?doc=<id> — a full-page jump away from the vault. It now opens the real, doc-scoped conversation in the shared side drawer, and the drawer's maximize control expands it to the full /coach page while preserving scope. - Thread an optional documentId through the launch context: askCoach gains a fourth argument, the provider tracks it alongside prefill/scope and clears it on close, and LayoutCoachMount forwards it to the drawer as initialDocumentId. The open-state precedence moves into a pure resolveLaunchState helper. - The vault detail sheet closes itself and calls askCoach(..., doc.id) instead of routing away, so the drawer owns the surface. - Maximize preserves continuity: the drawer reads the live conversation id via a new imperative getter out of the conversation surface (parallel to the reset registration) and hands off to /coach?c=<id> once a thread exists, falling back to /coach?doc=<id> for a chat maximized before its first turn. The send path is untouched: every document turn still routes through the hardened fenced endpoint keyed on documentId — only where the conversation renders (drawer vs page) changed. The documents-chat e2e now asserts the drawer opens in place, doc-scoped, rather than a URL change.
149 lines
5.3 KiB
TypeScript
149 lines
5.3 KiB
TypeScript
/**
|
|
* Document vault — the "chat about a document" entry point (v1.28.52, R3).
|
|
*
|
|
* The bespoke in-sheet chat drawer is gone (Documents R3). Asking the Coach
|
|
* about a document now opens the REAL Coach conversation, scoped to that
|
|
* document, in the shared SIDE DRAWER: the detail sheet's Coach affordance
|
|
* closes the sheet and opens the coach drawer pre-scoped to the document (its
|
|
* maximize control expands to the full `/coach` page). The conversation runs on
|
|
* the same hardened fenced endpoint — only WHERE it renders changed.
|
|
*
|
|
* This suite pins the documents-side contract: the affordance is offered only
|
|
* when a provider is available, and it OPENS THE DOC-SCOPED DRAWER in place (no
|
|
* full-page nav); with NO provider the AI area collapses to the settings
|
|
* pointer and no Coach entry renders at all. The Coach-side rendering (the
|
|
* "Chatting about" scope banner + the not-indexed hint) is covered by the
|
|
* coach-conversation component test; the fenced-send guarantee (a document turn
|
|
* never reaches the tool route) by the coach-send-target + persistence-scope
|
|
* unit tests.
|
|
*
|
|
* v1.27.31 lesson kept: the sheet's AI area depends on the capability probe +
|
|
* usage endpoints, so both are mocked — otherwise the AI area never renders and
|
|
* the specs time out. The seeded demo user has no AI provider, so the
|
|
* no-provider state is the default.
|
|
*/
|
|
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
|
import {
|
|
ensureVaultFixture,
|
|
ensureVaultAiFixture,
|
|
CONTENT_DOC_ID,
|
|
} from "./setup/vault-fixture";
|
|
|
|
/** Fulfil the envelope shape the client unwraps (`(await res.json()).data`). */
|
|
async function fulfilJson(
|
|
route: Parameters<Parameters<Page["route"]>[1]>[0],
|
|
body: unknown,
|
|
) {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
/** The vault usage DTO the sheet reads to gate the AI affordances. */
|
|
function usageBody() {
|
|
return {
|
|
data: {
|
|
usedBytes: 4096,
|
|
quotaBytes: 1_073_741_824,
|
|
maxFileBytes: 26_214_400,
|
|
acceptedExtensions: [".pdf", ".png", ".jpg", ".jpeg", ".webp"],
|
|
linkedEpisodes: [],
|
|
assistAvailable: true,
|
|
contentIndex: { enabled: true, indexedCount: 1, totalCount: 2 },
|
|
},
|
|
error: null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mock the AI-availability surface (usage + the document-scoped capability
|
|
* probe) so the sheet offers the AI area and the Coach entry.
|
|
*/
|
|
async function mockAiEnabled(page: Page): Promise<void> {
|
|
await page.route("**/api/documents/inbound/usage", (route) =>
|
|
fulfilJson(route, usageBody()),
|
|
);
|
|
await page.route("**/api/documents/inbound/capability", (route) =>
|
|
fulfilJson(route, {
|
|
data: {
|
|
available: true,
|
|
mode: "vision",
|
|
reason: null,
|
|
pdfSupported: true,
|
|
egress: "external",
|
|
},
|
|
error: null,
|
|
}),
|
|
);
|
|
await page.route("**/api/auth/me/documents-auto-ai-read", (route) =>
|
|
fulfilJson(route, { data: { documentsAutoAiRead: false }, error: null }),
|
|
);
|
|
}
|
|
|
|
test.describe("document vault — chat about a document", () => {
|
|
test.use({ storageState: STORAGE_STATE_PATH });
|
|
|
|
test.beforeAll(async () => {
|
|
await ensureVaultFixture();
|
|
await ensureVaultAiFixture();
|
|
});
|
|
|
|
test.beforeEach(() => {
|
|
test.slow();
|
|
});
|
|
|
|
// ── (a) Provider available: the Coach entry opens the doc-scoped drawer ──
|
|
|
|
test("offers the Coach entry and opens the document-scoped Coach drawer in place", async ({
|
|
page,
|
|
}) => {
|
|
await mockAiEnabled(page);
|
|
|
|
await page.goto(`/documents?doc=${CONTENT_DOC_ID}`);
|
|
const sheet = page.locator('[data-slot="responsive-sheet-content"]');
|
|
await expect(sheet).toBeVisible();
|
|
|
|
// The neutral Coach affordance lives in the detail-sheet AI area; tapping it
|
|
// closes the sheet and opens the REAL coach drawer scoped to this document.
|
|
const open = sheet.locator('[data-slot="document-chat-open"]');
|
|
await expect(open).toBeVisible();
|
|
await open.click();
|
|
|
|
// The documents-side contract: the coach drawer opens IN PLACE (no
|
|
// full-page nav) scoped to this document — its doc-scope banner renders.
|
|
const drawer = page.locator('[data-slot="coach-drawer"]');
|
|
await expect(drawer).toBeVisible({ timeout: 10_000 });
|
|
await expect(drawer.locator('[data-slot="coach-doc-scope"]')).toBeVisible({
|
|
timeout: 10_000,
|
|
});
|
|
// Still on /documents — the drawer expands to the page only via maximize.
|
|
expect(new URL(page.url()).pathname).not.toContain("/coach");
|
|
});
|
|
|
|
// ── (b) No provider: the calm settings pointer, no Coach entry at all ──
|
|
|
|
test("with no provider there is no Coach entry, only the settings pointer", async ({
|
|
page,
|
|
}) => {
|
|
// No mocks — the seeded demo user has no AI provider configured, so the AI
|
|
// area collapses to the calm pointer and the Coach entry never renders.
|
|
await page.goto(`/documents?doc=${CONTENT_DOC_ID}`);
|
|
const sheet = page.locator('[data-slot="responsive-sheet-content"]');
|
|
await expect(sheet).toBeVisible();
|
|
|
|
await expect(
|
|
sheet.locator('[data-slot="assist-unavailable"]'),
|
|
).toBeVisible();
|
|
await expect(
|
|
sheet.getByRole("link", { name: "Open AI settings" }),
|
|
).toBeVisible();
|
|
await expect(sheet.locator('[data-slot="document-chat-open"]')).toHaveCount(
|
|
0,
|
|
);
|
|
});
|
|
});
|