mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
test(e2e): repoint document-chat spec to the Coach hand-off
The bespoke document-chat drawer was removed with Design A, so the old e2e asserting the drawer overlay no longer applies. The spec now pins the documents-side contract that replaced it: with a provider available the Coach affordance hands off to the document-scoped Coach (/coach?doc=<id>), and with no provider the AI area shows only the settings pointer and no Coach entry. The Coach-side scope banner (the Document badge, "Chatting about", and the not-indexed hint) moves to a coach-conversation component test, replacing the drawer's not-indexed/render coverage; the fenced-send guarantee stays pinned by the coach-send-target unit test.
This commit is contained in:
1 parent
5b24ca0fa1
commit
5440ac9070
2 files changed
+131
-339
No files matched your search
+31
-339
@@ -1,20 +1,24 @@
|
||||
/**
|
||||
* Document vault — the P4 "chat about this document" acceptance suite.
|
||||
* Document vault — the "chat about a document" entry point (v1.28.51, R3).
|
||||
*
|
||||
* Pins the scoped chat that lives in the detail sheet's AI area: the entry is
|
||||
* offered only when a provider is available AND the document is content-indexed;
|
||||
* a non-indexed document shows a calm read-it-first hint (never an error); a
|
||||
* conversation streams the assistant reply and settles it as plain text with the
|
||||
* always-on "not medical advice" safety note; an injection-laced document is
|
||||
* described, never obeyed, and any HTML the model emits renders inert (the XSS
|
||||
* posture); and with NO provider the AI area collapses to the settings pointer
|
||||
* with no chat surface at all.
|
||||
* The bespoke in-sheet chat drawer is gone (Documents R3, Design A). Asking the
|
||||
* Coach about a document now hands off to the REAL Coach, scoped to that
|
||||
* document: the detail sheet's Coach affordance navigates to `/coach?doc=<id>`,
|
||||
* where the conversation runs on the same hardened fenced endpoint but inside
|
||||
* the Coach's own surface and history.
|
||||
*
|
||||
* v1.27.31 lesson: the panel depends on new endpoints (the capability probe, the
|
||||
* chat history GET, the streaming chat POST), so every one is mocked here —
|
||||
* otherwise the panel never renders and the specs time out waiting for elements.
|
||||
* The provider round-trips are mocked, so the SSE reply is deterministic. The
|
||||
* seeded demo user has no AI provider, so the no-provider state is the default.
|
||||
* This suite pins the documents-side contract: the affordance is offered only
|
||||
* when a provider is available, and it navigates to the doc-scoped Coach; 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";
|
||||
|
||||
@@ -22,7 +26,6 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import {
|
||||
ensureVaultFixture,
|
||||
ensureVaultAiFixture,
|
||||
AI_PROBE_DOC_ID,
|
||||
CONTENT_DOC_ID,
|
||||
} from "./setup/vault-fixture";
|
||||
|
||||
@@ -54,20 +57,11 @@ function usageBody() {
|
||||
};
|
||||
}
|
||||
|
||||
/** Serialise SSE frames the way the chat route emits them. */
|
||||
function sse(frames: Array<Record<string, unknown>>): string {
|
||||
return frames.map((f) => `data: ${JSON.stringify(f)}\n\n`).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock the AI-availability surface (usage + the document-scoped capability
|
||||
* probe) so the sheet offers the AI area and the chat entry. `egress` drives the
|
||||
* vendor-blind notice; default external.
|
||||
* probe) so the sheet offers the AI area and the Coach entry.
|
||||
*/
|
||||
async function mockAiEnabled(
|
||||
page: Page,
|
||||
opts: { egress?: "local" | "external" } = {},
|
||||
): Promise<void> {
|
||||
async function mockAiEnabled(page: Page): Promise<void> {
|
||||
await page.route("**/api/documents/inbound/usage", (route) =>
|
||||
fulfilJson(route, usageBody()),
|
||||
);
|
||||
@@ -78,13 +72,11 @@ async function mockAiEnabled(
|
||||
mode: "vision",
|
||||
reason: null,
|
||||
pdfSupported: true,
|
||||
egress: opts.egress ?? "external",
|
||||
egress: "external",
|
||||
},
|
||||
error: null,
|
||||
}),
|
||||
);
|
||||
// The detail sheet reads the auto-AI-read opt-in; default it OFF so the AI
|
||||
// section renders its full set of controls alongside the chat entry.
|
||||
await page.route("**/api/auth/me/documents-auto-ai-read", (route) =>
|
||||
fulfilJson(route, { data: { documentsAutoAiRead: false }, error: null }),
|
||||
);
|
||||
@@ -98,334 +90,39 @@ test.describe("document vault — chat about a document", () => {
|
||||
await ensureVaultAiFixture();
|
||||
});
|
||||
|
||||
// The chat drives a mocked provider round-trip; on a loaded CI runner it races
|
||||
// the default 30s, so the whole group earns the tripled timeout.
|
||||
test.beforeEach(() => {
|
||||
test.slow();
|
||||
});
|
||||
|
||||
// ── (a) Indexed + available: the entry opens an empty, safe chat surface ──
|
||||
// ── (a) Provider available: the Coach entry hands off to the doc-scoped Coach ──
|
||||
|
||||
test("offers the chat entry and opens an empty, safety-noted surface", async ({
|
||||
test("offers the Coach entry and hands off to the document-scoped Coach", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockAiEnabled(page);
|
||||
// The document has no thread yet → the history list is empty.
|
||||
await page.route(
|
||||
`**/api/documents/inbound/${CONTENT_DOC_ID}/chat**`,
|
||||
(route) => {
|
||||
if (route.request().method() !== "GET") return route.fallback();
|
||||
return fulfilJson(route, {
|
||||
data: { conversations: [], nextCursor: null },
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await page.goto(`/documents?doc=${CONTENT_DOC_ID}`);
|
||||
const sheet = page.locator('[data-slot="responsive-sheet-content"]');
|
||||
await expect(sheet).toBeVisible();
|
||||
|
||||
// The neutral Coach icon lives in the detail-sheet header; tapping it
|
||||
// opens the doc-scoped chat drawer (a sibling overlay).
|
||||
// The neutral Coach affordance lives in the detail-sheet AI area; tapping it
|
||||
// navigates to the real Coach seeded with this document.
|
||||
const open = sheet.locator('[data-slot="document-chat-open"]');
|
||||
await expect(open).toBeVisible();
|
||||
await open.click();
|
||||
|
||||
const drawer = page.locator('[data-slot="document-chat-drawer"]');
|
||||
await expect(drawer).toBeVisible();
|
||||
await expect(
|
||||
drawer.locator('[data-slot="document-chat-log"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
drawer.locator('[data-slot="document-chat-empty"]'),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
drawer.locator('[data-slot="document-chat-input"]'),
|
||||
).toBeVisible();
|
||||
// The always-on safety note is present.
|
||||
await expect(
|
||||
drawer.locator('[data-slot="document-chat-safety"]'),
|
||||
).toContainText("not medical advice");
|
||||
// The documents-side contract: the URL hands off to the doc-scoped Coach.
|
||||
await page.waitForURL(new RegExp(`/coach\\?doc=${CONTENT_DOC_ID}`));
|
||||
expect(new URL(page.url()).searchParams.get("doc")).toBe(CONTENT_DOC_ID);
|
||||
});
|
||||
|
||||
// ── (b) A turn streams a grounded reply that settles as plain text ──
|
||||
// ── (b) No provider: the calm settings pointer, no Coach entry at all ──
|
||||
|
||||
test("streams the assistant reply and settles the conversation", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockAiEnabled(page);
|
||||
|
||||
const CONV_ID = "e2edocchatconv0000000001";
|
||||
const QUESTION = "What does the Impression say?";
|
||||
const REPLY = "Per the report's Impression, the findings are unremarkable.";
|
||||
let posted = false;
|
||||
|
||||
await page.route(
|
||||
`**/api/documents/inbound/${CONTENT_DOC_ID}/chat**`,
|
||||
async (route) => {
|
||||
const req = route.request();
|
||||
if (req.method() === "POST") {
|
||||
posted = true;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "text/event-stream; charset=utf-8",
|
||||
body: sse([
|
||||
{ type: "token", token: REPLY },
|
||||
{
|
||||
type: "done",
|
||||
conversationId: CONV_ID,
|
||||
messageId: "e2edocchatmsg0000000001",
|
||||
},
|
||||
]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const url = new URL(req.url());
|
||||
if (url.searchParams.get("conversationId")) {
|
||||
// The persisted thread the post-`done` refetch reads back.
|
||||
await fulfilJson(route, {
|
||||
data: {
|
||||
id: CONV_ID,
|
||||
title: QUESTION,
|
||||
createdAt: "2026-07-07T10:00:00.000Z",
|
||||
updatedAt: "2026-07-07T10:00:02.000Z",
|
||||
messageCount: 2,
|
||||
messages: [
|
||||
{
|
||||
id: "e2edocchatu0000000001",
|
||||
role: "user",
|
||||
content: QUESTION,
|
||||
createdAt: "2026-07-07T10:00:00.000Z",
|
||||
metricSource: null,
|
||||
providerType: null,
|
||||
promptVersion: null,
|
||||
tokensUsed: null,
|
||||
model: null,
|
||||
},
|
||||
{
|
||||
id: "e2edocchatmsg0000000001",
|
||||
role: "assistant",
|
||||
content: REPLY,
|
||||
createdAt: "2026-07-07T10:00:02.000Z",
|
||||
metricSource: null,
|
||||
providerType: "mock",
|
||||
promptVersion: null,
|
||||
tokensUsed: 42,
|
||||
model: "mock-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// The thread list: empty until the turn is posted, then the new thread.
|
||||
await fulfilJson(route, {
|
||||
data: {
|
||||
conversations: posted
|
||||
? [
|
||||
{
|
||||
id: CONV_ID,
|
||||
title: QUESTION,
|
||||
createdAt: "2026-07-07T10:00:00.000Z",
|
||||
updatedAt: "2026-07-07T10:00:02.000Z",
|
||||
messageCount: 2,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
nextCursor: null,
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await page.goto(`/documents?doc=${CONTENT_DOC_ID}`);
|
||||
const sheet = page.locator('[data-slot="responsive-sheet-content"]');
|
||||
await expect(sheet).toBeVisible();
|
||||
|
||||
await sheet.locator('[data-slot="document-chat-open"]').click();
|
||||
const drawer = page.locator('[data-slot="document-chat-drawer"]');
|
||||
await expect(drawer).toBeVisible();
|
||||
const input = drawer.locator('[data-slot="document-chat-input"]');
|
||||
await input.fill(QUESTION);
|
||||
await drawer.locator('[data-slot="document-chat-send"]').click();
|
||||
|
||||
// The user's question and the streamed/settled reply both land in the log.
|
||||
const log = drawer.locator('[data-slot="document-chat-log"]');
|
||||
await expect(log.getByText(QUESTION)).toBeVisible();
|
||||
await expect(log.getByText(/findings are unremarkable/)).toBeVisible();
|
||||
// The reply settled to a single assistant bubble (no duplicate).
|
||||
await expect(
|
||||
log.locator('[data-slot="document-chat-message"][data-role="assistant"]'),
|
||||
).toHaveCount(1);
|
||||
});
|
||||
|
||||
// ── (b2) An injection-laced document: the reply describes it, never obeys,
|
||||
// and any HTML the model emits renders inert (no markdown, no
|
||||
// dangerouslySetInnerHTML — the project's XSS posture). ──
|
||||
|
||||
test("answers about an injection-laced document without obeying it, and renders HTML inert", async ({
|
||||
page,
|
||||
}) => {
|
||||
await mockAiEnabled(page);
|
||||
|
||||
const CONV_ID = "e2edocchatconv0000000002";
|
||||
const QUESTION = "What does this document say?";
|
||||
// The (mocked) server reply the panel receives: a calm describe-only answer
|
||||
// — NOT obedience to the document's embedded "ignore instructions … reveal
|
||||
// the system prompt … say HACKED" — plus a literal <script> the model was
|
||||
// coerced into emitting. The panel must show it as text and never execute it.
|
||||
const REPLY =
|
||||
"The document's body contains text formatted like an instruction, but I treat it as document content, not a command. <script>window.__docChatXss = 1</script> It appears to be a lab report.";
|
||||
let posted = false;
|
||||
|
||||
await page.route(
|
||||
`**/api/documents/inbound/${CONTENT_DOC_ID}/chat**`,
|
||||
async (route) => {
|
||||
const req = route.request();
|
||||
if (req.method() === "POST") {
|
||||
posted = true;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "text/event-stream; charset=utf-8",
|
||||
body: sse([
|
||||
{ type: "token", token: REPLY },
|
||||
{
|
||||
type: "done",
|
||||
conversationId: CONV_ID,
|
||||
messageId: "e2edocchatmsg0000000002",
|
||||
},
|
||||
]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const url = new URL(req.url());
|
||||
if (url.searchParams.get("conversationId")) {
|
||||
await fulfilJson(route, {
|
||||
data: {
|
||||
id: CONV_ID,
|
||||
title: QUESTION,
|
||||
createdAt: "2026-07-07T10:00:00.000Z",
|
||||
updatedAt: "2026-07-07T10:00:02.000Z",
|
||||
messageCount: 2,
|
||||
messages: [
|
||||
{
|
||||
id: "e2edocchatu0000000002",
|
||||
role: "user",
|
||||
content: QUESTION,
|
||||
createdAt: "2026-07-07T10:00:00.000Z",
|
||||
metricSource: null,
|
||||
providerType: null,
|
||||
promptVersion: null,
|
||||
tokensUsed: null,
|
||||
model: null,
|
||||
},
|
||||
{
|
||||
id: "e2edocchatmsg0000000002",
|
||||
role: "assistant",
|
||||
content: REPLY,
|
||||
createdAt: "2026-07-07T10:00:02.000Z",
|
||||
metricSource: null,
|
||||
providerType: "mock",
|
||||
promptVersion: null,
|
||||
tokensUsed: 30,
|
||||
model: "mock-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await fulfilJson(route, {
|
||||
data: {
|
||||
conversations: posted
|
||||
? [
|
||||
{
|
||||
id: CONV_ID,
|
||||
title: QUESTION,
|
||||
createdAt: "2026-07-07T10:00:00.000Z",
|
||||
updatedAt: "2026-07-07T10:00:02.000Z",
|
||||
messageCount: 2,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
nextCursor: null,
|
||||
},
|
||||
error: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await page.goto(`/documents?doc=${CONTENT_DOC_ID}`);
|
||||
const sheet = page.locator('[data-slot="responsive-sheet-content"]');
|
||||
await expect(sheet).toBeVisible();
|
||||
|
||||
await sheet.locator('[data-slot="document-chat-open"]').click();
|
||||
const drawer = page.locator('[data-slot="document-chat-drawer"]');
|
||||
await expect(drawer).toBeVisible();
|
||||
const input = drawer.locator('[data-slot="document-chat-input"]');
|
||||
await input.fill(QUESTION);
|
||||
await drawer.locator('[data-slot="document-chat-send"]').click();
|
||||
|
||||
const log = drawer.locator('[data-slot="document-chat-log"]');
|
||||
// The describe-only answer lands; the panel renders the model's <script>
|
||||
// payload as literal text (proof it is a React text child, not HTML).
|
||||
await expect(log.getByText(/treat it as document content/)).toBeVisible();
|
||||
await expect(log.getByText(/window\.__docChatXss/)).toBeVisible();
|
||||
|
||||
// The injected script never executed — no markdown lib, no innerHTML sink.
|
||||
const executed = await page.evaluate(
|
||||
() =>
|
||||
(window as unknown as { __docChatXss?: number }).__docChatXss ?? null,
|
||||
);
|
||||
expect(executed).toBeNull();
|
||||
// And no real <script> element was injected into the document from the reply.
|
||||
const scriptCount = await page.locator("script#doc-chat-xss").count();
|
||||
expect(scriptCount).toBe(0);
|
||||
});
|
||||
|
||||
// ── (c) Not indexed: a calm read-it-first hint, never an error ──
|
||||
|
||||
test("shows a calm hint when the document is not indexed", async ({
|
||||
page,
|
||||
}) => {
|
||||
// AI_PROBE_DOC_ID carries no content index (hasContentIndex === false).
|
||||
await mockAiEnabled(page);
|
||||
await page.goto(`/documents?doc=${AI_PROBE_DOC_ID}`);
|
||||
const sheet = page.locator('[data-slot="responsive-sheet-content"]');
|
||||
await expect(sheet).toBeVisible();
|
||||
|
||||
// A provider is available, so the Coach entry still shows — it opens the
|
||||
// drawer, whose body carries the calm read-it-first hint (never an error)
|
||||
// and no composer, because there is nothing indexed to ground on yet.
|
||||
const open = sheet.locator('[data-slot="document-chat-open"]');
|
||||
await expect(open).toBeVisible();
|
||||
await open.click();
|
||||
|
||||
const drawer = page.locator('[data-slot="document-chat-drawer"]');
|
||||
await expect(drawer).toBeVisible();
|
||||
await expect(
|
||||
drawer.locator('[data-slot="document-chat-not-indexed"]'),
|
||||
).toBeVisible();
|
||||
// No composer, and no error styling.
|
||||
await expect(
|
||||
drawer.locator('[data-slot="document-chat-input"]'),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
drawer.locator('[data-slot="document-chat"] [role="alert"]'),
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
// ── (d) No provider: the calm settings pointer, no chat surface at all ──
|
||||
|
||||
test("with no provider there is no chat surface, only the settings pointer", async ({
|
||||
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 chat slot never renders.
|
||||
// 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();
|
||||
@@ -436,13 +133,8 @@ test.describe("document vault — chat about a document", () => {
|
||||
await expect(
|
||||
sheet.getByRole("link", { name: "Open AI settings" }),
|
||||
).toBeVisible();
|
||||
// No provider → no Coach entry in the header and no chat drawer mounted.
|
||||
await expect(sheet.locator('[data-slot="document-chat-open"]')).toHaveCount(
|
||||
0,
|
||||
);
|
||||
await expect(
|
||||
page.locator('[data-slot="document-chat-drawer"]'),
|
||||
).toHaveCount(0);
|
||||
await expect(page.locator('[data-slot="document-chat"]')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
// The page surface reads `useRouter` (Conversations routes via the app router).
|
||||
const pushMock = vi.fn();
|
||||
vi.mock("next/navigation", () => ({
|
||||
useRouter: () => ({ push: pushMock }),
|
||||
}));
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { CoachConversation } from "../coach-conversation";
|
||||
|
||||
/**
|
||||
* v1.28.51 (Documents R3, Design A) — the doc-scope banner.
|
||||
*
|
||||
* When the Coach is scoped to a document (the `/coach?doc=<id>` hand-off from
|
||||
* the vault, before any thread exists), the shared `<CoachConversation>` surface
|
||||
* paints a "Document" badge + "Chatting about: <title>" and, when the document
|
||||
* is not yet content-indexed, the calm "read it with AI first" hint. On a normal
|
||||
* health thread the banner is absent. Replaces the deleted drawer's not-indexed /
|
||||
* scope coverage at the component level; the fenced-send guarantee is pinned by
|
||||
* `coach-send-target.test.ts`.
|
||||
*
|
||||
* `pendingDocumentId` seeds from the `initialDocumentId` prop via a `useState`
|
||||
* initializer, so the banner resolves in a single static-markup pass. The
|
||||
* document detail (`/api/documents/inbound/<id>`) is seeded into the cache so the
|
||||
* title + indexed status render without a network call.
|
||||
*/
|
||||
|
||||
const DOC_ID = "e2edocscopedoc0000000001";
|
||||
const TITLE = "Chest X-ray report";
|
||||
|
||||
const NOT_INDEXED_HINT =
|
||||
"Read this document with AI first in the vault so the Coach can answer about it.";
|
||||
|
||||
function makeClient(hasContentIndex: boolean): QueryClient {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
// A fresh doc scope has no thread yet — keep the rail empty so the surface
|
||||
// stays on the new-chat hero and the banner is the thing under test.
|
||||
client.setQueryData(queryKeys.coachConversations(), {
|
||||
conversations: [],
|
||||
nextCursor: null,
|
||||
});
|
||||
client.setQueryData(queryKeys.coachAboutMeQuestions(), { questions: [] });
|
||||
client.setQueryData(queryKeys.inboundDocument(DOC_ID), {
|
||||
id: DOC_ID,
|
||||
title: TITLE,
|
||||
filename: "xray.pdf",
|
||||
hasContentIndex,
|
||||
});
|
||||
return client;
|
||||
}
|
||||
|
||||
function render(node: React.ReactNode, client: QueryClient): string {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<QueryClientProvider client={client}>{node}</QueryClientProvider>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe("<CoachConversation> doc-scope banner (R3)", () => {
|
||||
it("shows the badge + 'Chatting about' title for an indexed document scope", () => {
|
||||
const html = render(
|
||||
<CoachConversation surface="page" initialDocumentId={DOC_ID} />,
|
||||
makeClient(true),
|
||||
);
|
||||
expect(html).toContain('data-slot="coach-doc-scope"');
|
||||
expect(html).toContain("Document");
|
||||
expect(html).toContain(`Chatting about: ${TITLE}`);
|
||||
// Indexed → no "read it first" hint.
|
||||
expect(html).not.toContain(NOT_INDEXED_HINT);
|
||||
});
|
||||
|
||||
it("shows the calm not-indexed hint when the document has no content index", () => {
|
||||
const html = render(
|
||||
<CoachConversation surface="page" initialDocumentId={DOC_ID} />,
|
||||
makeClient(false),
|
||||
);
|
||||
expect(html).toContain('data-slot="coach-doc-scope"');
|
||||
expect(html).toContain(NOT_INDEXED_HINT);
|
||||
});
|
||||
|
||||
it("paints no banner on a normal health thread (no document scope)", () => {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
client.setQueryData(queryKeys.coachConversations(), {
|
||||
conversations: [],
|
||||
nextCursor: null,
|
||||
});
|
||||
client.setQueryData(queryKeys.coachAboutMeQuestions(), { questions: [] });
|
||||
const html = render(<CoachConversation surface="page" />, client);
|
||||
expect(html).not.toContain('data-slot="coach-doc-scope"');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user