import "@testing-library/jest-dom/vitest"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { RichTextMentionServiceProvider } from "@tutti-os/ui-rich-text/editor"; import type { RichTextMentionService, RichTextMentionSnapshot } from "@tutti-os/ui-rich-text/service"; import { AgentMessageMarkdown, resetCachedMarkdownImagesForTests, splitStreamingMarkdownBlocks } from "./AgentMessageMarkdown"; import { MANAGED_AGENT_ICON_ROUNDED_URLS, managedAgentRoundedIconUrl } from "./managedAgentIcons"; import { parsedDocumentCacheStatsForTests, resetParsedDocumentCacheForTests } from "./parsedDocumentCache"; import { resolveMarkdownWorkspaceMediaPath } from "./agentMessageMarkdownLinks"; describe("AgentMessageMarkdown", () => { afterEach(() => { vi.useRealTimers(); resetCachedMarkdownImagesForTests(); resetParsedDocumentCacheForTests(); }); it("reuses the parsed document after a settled message remounts", () => { resetParsedDocumentCacheForTests(); const first = render( ); first.unmount(); render( ); expect(parsedDocumentCacheStatsForTests()).toMatchObject({ entries: 1, hits: 1, misses: 1 }); }); it("renders a workspace-reference mention as one chip without a file-count badge", () => { const href = `mention://workspace-reference/topic1?count=3&icon=${encodeURIComponent("https://x.png")}&source=task&workspaceId=ws1`; render( ); const chip = screen.getByRole("link", { name: "我的小项目" }); expect(chip).toHaveAttribute( "data-agent-mention-kind", "workspace-reference" ); expect(chip).toHaveAttribute("data-agent-reference-source", "task"); // 角标数字已移除:chip 只展示标签,不再渲染文件数。 expect(chip).toHaveTextContent("我的小项目"); expect(chip).not.toHaveTextContent("3"); }); it("renders app workspace-reference mentions with app icons", () => { const iconUrl = "data:image/png;base64,canvas"; const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute( "data-agent-mention-kind", "workspace-reference" ); expect(mention).toHaveAttribute("data-agent-reference-source", "app"); expect(mention).toHaveAttribute("data-agent-mention-icon-url", iconUrl); expect( mention?.querySelector('[data-agent-mention-app-icon="true"] img') ).toHaveAttribute("src", iconUrl); expect(mention).toHaveTextContent("AI Canvas"); }); it("renders local file mention links whose paths contain spaces", () => { const { container } = render( ); const mentions = container.querySelectorAll( '[data-agent-file-mention="true"]' ); expect(mentions).toHaveLength(2); expect(mentions[0]).toHaveAttribute("data-agent-mention-kind", "file"); expect(mentions[0]).toHaveTextContent("user"); expect(mentions[1]).toHaveTextContent("auth_api.py"); expect(screen.queryByText(/\]\(\/Users\/Sun\/Documents/)).toBeNull(); }); it("renders a Windows output file mention as a clickable file chip", () => { const onLinkAction = vi.fn(); const { container } = render( )" } onLinkAction={onLinkAction} workspaceLinkContext={{ workspaceRoot: "C:/Users/local user/project", basePath: "C:/Users/local user/project", source: "agent-markdown" }} /> ); const fileChip = screen.getByRole("link", { name: "output.docx" }); expect(fileChip).toHaveAttribute("data-agent-mention-kind", "file"); expect(container).not.toHaveTextContent("C:/Users/local user/.tutti"); fireEvent.click(fileChip); expect(onLinkAction).toHaveBeenCalledWith({ type: "open-workspace-file", path: "/C:/Users/local user/.tutti/apps/output.docx", directoryPath: "/C:/Users/local user/.tutti/apps", workspaceRoot: "/C:/Users/local user/project", source: "agent-markdown" }); }); it("renders markdown links, inline code, and lists", () => { render( ); expect(screen.queryByRole("link", { name: "README.md" })).toBeNull(); expect(screen.getByText("README.md")).toBeInTheDocument(); expect(screen.getByText("src/App.tsx")).toBeInTheDocument(); expect(screen.getByText("重点").tagName).toBe("STRONG"); expect(screen.getByText("第一项")).toBeInTheDocument(); expect(screen.getByText("第二项")).toBeInTheDocument(); expect(screen.getByRole("list")).toBeInTheDocument(); expect(screen.getAllByRole("listitem")).toHaveLength(2); }); it("renders a copy button on fenced code blocks", () => { render(); expect(screen.getByTestId("markdown-code-copy")).toBeInTheDocument(); }); it("copies code block content to clipboard", async () => { const writeText = vi.fn().mockResolvedValue(undefined); Object.assign(navigator, { clipboard: { writeText } }); render( ); screen.getByTestId("markdown-code-copy").click(); await waitFor(() => { expect(writeText).toHaveBeenCalledWith("const greeting = 'hello';"); }); }); it("renders GFM tables as table elements", () => { render( ); expect(screen.getByRole("table")).toBeInTheDocument(); expect( screen.getByRole("columnheader", { name: "模式" }) ).toBeInTheDocument(); expect( screen.getByRole("cell", { name: "统一 API 格式适配不同 LLM 提供商" }) ).toBeInTheDocument(); }); it("keeps links inert for now", () => { render( ); const link = screen.getByRole("link", { name: "本地服务" }); const event = fireEvent.click(link); expect(event).toBe(false); }); it("renders relative markdown links as plain text", () => { const onLinkAction = vi.fn(); render( ); expect(screen.queryByRole("link", { name: "README.md" })).toBeNull(); expect(screen.queryByRole("link", { name: "content/posts" })).toBeNull(); expect(screen.getByText("README.md")).toBeInTheDocument(); expect(screen.getByText("content/posts")).toBeInTheDocument(); expect(onLinkAction).not.toHaveBeenCalled(); }); it("keeps standard markdown link hrefs clickable", () => { const onLinkClick = vi.fn(); render( ); for (const [label, href] of [ ["Email", "mailto:hello@example.com"], ["Phone", "tel:+123456789"], ["Section", "#details"], ["Chat", "xmpp:hello@example.com"] ] as const) { fireEvent.click(screen.getByRole("link", { name: label })); expect(onLinkClick).toHaveBeenLastCalledWith(href); } }); it.each([ ",", "。", ";", ":", "!", "?", "、", "…", ")", "】", "》", "」", "』", "〉", "〕", "]", "}", "”", "’" ])( "keeps the CJK %s sentence boundary outside bare http links", (punctuation) => { const onLinkClick = vi.fn(); const url = "https://github.com/settings/applications"; render( ); const link = screen.getByRole("link", { name: url }); expect(link).toHaveAttribute("data-agent-link-href", url); expect(link).not.toHaveTextContent(`${punctuation}然后继续`); expect(link.parentElement).toHaveTextContent( `请打开 ${url}${punctuation}然后继续` ); fireEvent.click(link); expect(onLinkClick).toHaveBeenCalledWith(url); } ); it.each([",", ".", ";", ":", "!", "?", ")", "]"])( "keeps the ASCII %s sentence boundary outside bare http links", (punctuation) => { const url = "https://github.com/settings/applications"; render( ); const link = screen.getByRole("link", { name: url }); expect(link).toHaveAttribute("data-agent-link-href", url); expect(link).not.toHaveTextContent(`${punctuation} 然后继续`); } ); it("preserves explicit markdown links containing CJK punctuation", () => { const target = "https://example.com/发布,版本"; render(); expect(screen.getByRole("link", { name: "发布记录" })).toHaveAttribute( "data-agent-link-href", new URL(target).toString() ); }); it("preserves percent-encoded CJK punctuation in bare http links", () => { const target = "https://example.com/search?q=%E7%94%B2%EF%BC%8C%E4%B9%99"; render(); expect(screen.getByRole("link", { name: target })).toHaveAttribute( "data-agent-link-href", target ); }); it("preserves explicitly authored angle autolinks", () => { const target = "https://example.com/releases/a,b"; render(`} />); expect(screen.getByRole("link", { name: target })).toHaveAttribute( "data-agent-link-href", target ); }); it("applies CJK sentence boundaries to www literal autolinks", () => { const label = "www.example.com/releases"; render(); expect(screen.getByRole("link", { name: label })).toHaveAttribute( "data-agent-link-href", `http://${label}` ); }); it("applies CJK bare-link boundaries while streaming markdown", () => { const url = "https://github.com/settings/applications"; render( ); expect(screen.getByRole("link", { name: url })).toHaveAttribute( "data-agent-link-href", url ); expect(screen.getByRole("link", { name: url })).not.toHaveTextContent( ",然后继续" ); }); it("keeps JSON fields after a bare URL outside the link", () => { const url = "https://github.com/tutti-os/tutti/pull/1355"; const content = JSON.stringify({ result: "mr_created", prUrl: url, branch: "feat/agent-worktree-isolation", commit: "5f640250dc9565834ff4cec925104c05a02cd230", checks: "focused Go tests/build" }); render(); const link = screen.getByRole("link", { name: url }); expect(link).toHaveAttribute("data-agent-link-href", url); expect(screen.getAllByRole("link")).toHaveLength(1); expect(link.parentElement).toHaveTextContent( `"prUrl":"${url}","branch":"feat/agent-worktree-isolation"` ); }); it("renders unsafe markdown links as plain text", () => { const onLinkClick = vi.fn(); render( ); expect(screen.queryByRole("link", { name: "bad" })).toBeNull(); expect(screen.getByText("bad")).toBeInTheDocument(); expect(onLinkClick).not.toHaveBeenCalled(); }); it("does not nest path links inside markdown links with inline code labels", () => { const onLinkClick = vi.fn(); const { container } = render( ); const link = screen.getByRole("link", { name: "AGENTS.md" }); expect(container.querySelectorAll("a")).toHaveLength(1); expect(link.querySelector("code")).toHaveTextContent("AGENTS.md"); fireEvent.click(link); expect(onLinkClick).toHaveBeenCalledWith( "/Users/ryan/Documents/tutti/proj2/AGENTS.md" ); }); it("resolves workspace link actions when workspace context is provided", () => { const onLinkAction = vi.fn(); render( ); fireEvent.click(screen.getByRole("link", { name: "README.md" })); expect(onLinkAction).toHaveBeenCalledWith({ type: "open-workspace-file", path: "/Users/local/project/docs/README.md", directoryPath: "/Users/local/project/docs", workspaceRoot: "/Users/local/project", source: "agent-markdown" }); }); it("resolves local file links from the session cwd without a project root", () => { const onLinkAction = vi.fn(); render( ); fireEvent.click(screen.getByRole("link", { name: "index.html" })); expect(onLinkAction).toHaveBeenCalledWith({ type: "open-workspace-file", path: "/Users/local/session-1/index.html", directoryPath: "/Users/local/session-1", workspaceRoot: "/Users/local/session-1", source: "agent-markdown" }); }); it("resolves home-relative markdown file links when workspace context is provided", () => { const onLinkAction = vi.fn(); render( ); fireEvent.click(screen.getByRole("link", { name: "notes" })); expect(onLinkAction).toHaveBeenCalledWith({ type: "open-workspace-file", path: "~/docs/notes.md", directoryPath: "~/docs", workspaceRoot: "/Users/local/project", source: "agent-markdown" }); }); it("resolves Windows absolute markdown file links when workspace context is provided", () => { const onLinkAction = vi.fn(); render( ); fireEvent.click(screen.getByRole("link", { name: "README.md" })); expect(onLinkAction).toHaveBeenCalledWith({ type: "open-workspace-file", path: "/C:/Users/local/project/docs/README.md", directoryPath: "/C:/Users/local/project/docs", workspaceRoot: "/C:/Users/local/project", source: "agent-markdown" }); }); it("resolves direct generated image links outside the workspace root", () => { const onLinkAction = vi.fn(); render( ); fireEvent.click( screen.getByRole("link", { name: "/Users/local/.tutti-dev/agent/runs/session-1/codex-home/generated_images/imagegen/ig_123.png" }) ); expect(onLinkAction).toHaveBeenCalledWith({ type: "open-workspace-file", path: "/Users/local/.tutti-dev/agent/runs/session-1/codex-home/generated_images/imagegen/ig_123.png", directoryPath: "/Users/local/.tutti-dev/agent/runs/session-1/codex-home/generated_images/imagegen", workspaceRoot: "/Users/local/project", source: "agent-markdown" }); }); it("renders workspace markdown images from workspace file bytes instead of raw path URLs", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); expect( await screen.findByRole("img", { name: "generated image" }) ).toHaveAttribute("src", "blob:tsh-markdown-image"); expect(readFile).toHaveBeenCalledWith({ path: "/workspace/output/imagegen/dance.png" }); }); it("decodes percent-encoded workspace markdown image paths before reading files", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); expect( await screen.findByRole("img", { name: "generated image" }) ).toHaveAttribute("src", "blob:tsh-markdown-image"); expect(readFile).toHaveBeenCalledWith({ path: "/workspace/可爱小狗.png" }); }); it("renders Windows markdown image paths from workspace file bytes", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-windows-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); expect(readFile).toHaveBeenCalledWith({ path: "X:/screenshots/browser.png" }); expect( await screen.findByRole("img", { name: "generated image" }) ).toHaveAttribute("src", "blob:tsh-windows-markdown-image"); }); it("recognizes Windows backslash media paths", () => { const path = String.raw`C:\Users\local user\project\image.png`; expect(resolveMarkdownWorkspaceMediaPath(path)).toBe(path); expect( resolveMarkdownWorkspaceMediaPath( "C:/Users/local%20user/project/image.png" ) ).toBe("C:/Users/local user/project/image.png"); }); it("preserves dots after Windows path separators in markdown media", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tutti-state-image") }); render( ); await screen.findByRole("img", { name: "generated" }); expect(readFile).toHaveBeenCalledWith({ path: "C:/Users/moche/.tutti-dev/apps/image.png" }); }); it("does not pass unknown one-letter media protocols to the DOM", () => { const { container } = render( ); expect( container.querySelector('img[src="x://example.com/image.png"]') ).toBeNull(); }); it("renders workspace markdown videos from workspace file bytes", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([0, 0, 0, 24]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-video") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); const video = await screen.findByLabelText("generated video"); expect(video.tagName).toBe("VIDEO"); expect(video).toHaveAttribute("src", "blob:tsh-markdown-video"); expect(video).toHaveAttribute("controls"); expect(readFile).toHaveBeenCalledWith({ path: "/workspace/output/generated_videos/dance.mp4" }); }); it("shows a loading placeholder while a workspace markdown image is still being read", async () => { let resolveRead: ((value: { bytes: Uint8Array }) => void) | undefined; const readFile = vi.fn( () => new Promise<{ bytes: Uint8Array }>((resolve) => { resolveRead = resolve; }) ); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); await waitFor(() => expect(screen.getByText("Loading preview...")).toBeTruthy() ); expect(screen.queryByRole("img", { name: "generated image" })).toBeNull(); expect(resolveRead).toBeTruthy(); if (!resolveRead) { throw new Error("expected readFile promise resolver"); } resolveRead({ bytes: new Uint8Array([137, 80, 78, 71]) }); expect( await screen.findByRole("img", { name: "generated image" }) ).toHaveAttribute("src", "blob:tsh-markdown-image"); }); it("falls back to the raw workspace image src when workspace file access is unavailable", () => { const workspace = { ...(window.agentHostApi?.workspace ?? {}) } as Partial< NonNullable["workspace"] >; delete workspace.readFile; window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: workspace as NonNullable< typeof window.agentHostApi >["workspace"] } as unknown as typeof window.agentHostApi; render( ); expect( screen.getByRole("img", { name: "generated image" }) ).toHaveAttribute("src", "/workspace/output/imagegen/dance.png"); expect(screen.queryByText("Loading preview...")).toBeNull(); }); it("falls back to a file URL for local absolute markdown image paths when workspace file access is unavailable", () => { const workspace = { ...(window.agentHostApi?.workspace ?? {}) } as Partial< NonNullable["workspace"] >; delete workspace.readFile; window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: workspace as NonNullable< typeof window.agentHostApi >["workspace"] } as unknown as typeof window.agentHostApi; render( ); expect( screen.getByRole("img", { name: "generated image" }) ).toHaveAttribute( "src", "file:///Users/example/Documents/a/output/imagegen/lamb-storybook.png" ); expect(screen.queryByText("Loading preview...")).toBeNull(); }); it("falls back to a file URL video for local absolute markdown video paths when workspace file access is unavailable", () => { const workspace = { ...(window.agentHostApi?.workspace ?? {}) } as Partial< NonNullable["workspace"] >; delete workspace.readFile; window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: workspace as NonNullable< typeof window.agentHostApi >["workspace"] } as unknown as typeof window.agentHostApi; render( ); const video = screen.getByLabelText("generated video"); expect(video.tagName).toBe("VIDEO"); expect(video).toHaveAttribute( "src", "file:///Users/example/.tutti/agent/runs/session/codex-home/generated_videos/dance.mp4" ); expect(screen.queryByText("Loading preview...")).toBeNull(); }); it("does not render arbitrary local absolute markdown video paths when workspace file access is unavailable", () => { const workspace = { ...(window.agentHostApi?.workspace ?? {}) } as Partial< NonNullable["workspace"] >; delete workspace.readFile; window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: workspace as NonNullable< typeof window.agentHostApi >["workspace"] } as unknown as typeof window.agentHostApi; render( ); expect(screen.queryByLabelText("private video")).toBeNull(); expect( screen.queryByText("Preview is not available for this file.") ).toBeTruthy(); expect(screen.queryByText("Loading preview...")).toBeNull(); }); it("keeps image zoom disabled by default outside AgentGui callers", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); expect( await screen.findByRole("img", { name: "generated image" }) ).toHaveAttribute("src", "blob:tsh-markdown-image"); expect(screen.queryByRole("button", { name: /Zoom image/ })).toBeNull(); }); it("opens a zoom preview when a workspace markdown image is clicked", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); const { rerender } = render( ); const zoomButton = await screen.findByRole("button", { name: /Zoom image/ }); fireEvent.click(zoomButton); await waitFor(() => { expect(screen.getByRole("dialog")).toBeInTheDocument(); }); rerender( ); expect(readFile).toHaveBeenCalledTimes(1); expect(screen.queryByText("Loading preview...")).toBeNull(); expect( screen.getAllByRole("img", { name: "generated image" }) ).toHaveLength(2); }); it("resizes the image inside the zoom preview", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); fireEvent.click(await screen.findByRole("button", { name: /Zoom image/ })); const dialog = await screen.findByRole("dialog"); const modalImage = dialog.querySelector("[data-rmiz-modal-img]"); expect(modalImage).toBeInstanceOf(HTMLElement); expect(screen.getByRole("status")).toHaveTextContent("100%"); fireEvent.click( screen.getByRole("button", { name: /Zoom in image|common\.zoomInImage/ }) ); await waitFor(() => { expect(modalImage).toHaveAttribute("data-tsh-image-zoom", "1.25"); }); expect(modalImage).toHaveStyle({ transform: "scale(1.25)" }); expect((modalImage as HTMLElement).style.transformOrigin).toBe(""); expect(screen.getByRole("status")).toHaveTextContent("125%"); fireEvent.click( screen.getByRole("button", { name: /Zoom out image|common\.zoomOutImage/ }) ); await waitFor(() => { expect(modalImage).toHaveAttribute("data-tsh-image-zoom", "1"); }); expect(screen.getByRole("status")).toHaveTextContent("100%"); fireEvent.click( screen.getByRole("button", { name: /Zoom in image|common\.zoomInImage/ }) ); await waitFor(() => { expect(modalImage).toHaveAttribute("data-tsh-image-zoom", "1.25"); }); fireEvent.click( screen.getByRole("button", { name: /Reset image zoom|common\.resetImageZoom/ }) ); await waitFor(() => { expect(modalImage).toHaveAttribute("data-tsh-image-zoom", "1"); }); expect(screen.getByRole("status")).toHaveTextContent("100%"); fireEvent.click( screen.getByRole("button", { name: /Zoom out image|common\.zoomOutImage/ }) ); await waitFor(() => { expect(modalImage).toHaveAttribute("data-tsh-image-zoom", "0.75"); }); expect(modalImage).toHaveStyle({ transform: "scale(0.75)" }); const windowWheel = vi.fn(); window.addEventListener("wheel", windowWheel); try { fireEvent.wheel(modalImage as HTMLElement, { bubbles: true, cancelable: true, deltaY: -20 }); await waitFor(() => { expect( Number(modalImage?.getAttribute("data-tsh-image-zoom")) ).toBeGreaterThan(0.75); }); expect( Number(modalImage?.getAttribute("data-tsh-image-zoom")) ).toBeLessThan(0.8); expect(modalImage).toHaveStyle({ transition: "none" }); expect(windowWheel).not.toHaveBeenCalled(); fireEvent.wheel(modalImage as HTMLElement, { bubbles: true, cancelable: true, deltaY: 20 }); await waitFor(() => { expect(modalImage).toHaveAttribute("data-tsh-image-zoom", "0.75"); }); expect(windowWheel).not.toHaveBeenCalled(); } finally { window.removeEventListener("wheel", windowWheel); } fireEvent.click(screen.getByRole("button", { name: /^Close$/ })); await waitFor(() => { expect(modalImage).toHaveAttribute("data-tsh-image-zoom", "1"); }); }); it("copies and downloads a workspace markdown image from preview actions", async () => { vi.useFakeTimers({ shouldAdvanceTime: true }); const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); const write = vi.fn().mockResolvedValue(undefined); const fetchImage = vi.fn().mockResolvedValue({ blob: () => Promise.resolve(new Blob(["image"], { type: "image/png" })) }); let downloadedName = ""; const clickDownload = vi .spyOn(HTMLAnchorElement.prototype, "click") .mockImplementation(function (this: HTMLAnchorElement) { downloadedName = this.download; }); const clipboardItems: unknown[] = []; class TestClipboardItem { constructor(items: unknown) { clipboardItems.push(items); } } vi.stubGlobal("ClipboardItem", TestClipboardItem); vi.stubGlobal("fetch", fetchImage); Object.defineProperty(navigator, "clipboard", { configurable: true, value: { write } }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); const image = await screen.findByRole("img", { name: "generated image" }); fireEvent.contextMenu(image, { clientX: 12, clientY: 34 }); const inlineMenu = screen.getByRole("menu"); expect(inlineMenu).toHaveStyle({ left: "12px", top: "34px" }); expect(inlineMenu.parentElement).toBe(document.body); fireEvent.click(screen.getByRole("menuitem", { name: "Copy image" })); await waitFor(() => { expect(write).toHaveBeenCalledTimes(1); }); expect(fetchImage).toHaveBeenCalledWith("blob:tsh-markdown-image"); expect(clipboardItems).toHaveLength(1); fireEvent.click(screen.getByRole("button", { name: /Zoom image/ })); const dialog = await screen.findByRole("dialog"); const toolbarActions = dialog.querySelector( ".tsh-zoom-dialog__toolbar-actions" ); expect(toolbarActions).toBeInstanceOf(HTMLElement); const previewActionButtons = [ screen.getByRole("button", { name: "Copy image" }), screen.getByRole("button", { name: "Download image" }), screen.getByRole("button", { name: /^Close$/ }) ]; expect(Array.from(toolbarActions?.children ?? [])).toEqual( previewActionButtons ); for (const button of previewActionButtons) { expect(button).toHaveAttribute("data-size", "icon"); expect(button).toHaveAttribute("data-variant", "chrome"); } expect(previewActionButtons[2]).not.toHaveAttribute("data-rmiz-btn-unzoom"); fireEvent.click(screen.getByRole("button", { name: "Copy image" })); await waitFor(() => { expect(screen.getByRole("status")).toHaveTextContent("Copied"); }); const modalImage = dialog.querySelector("img"); expect(modalImage).toBeInstanceOf(HTMLElement); fireEvent.contextMenu(modalImage as HTMLElement, { clientX: 18, clientY: 40 }); expect(screen.getByRole("menu").closest(".tsh-zoom-dialog")).toBe(dialog); fireEvent.click(screen.getByRole("menuitem", { name: "Copy image" })); await waitFor(() => { expect(screen.getByRole("status")).toHaveTextContent("Copied"); }); fireEvent.click(screen.getByRole("button", { name: "Download image" })); expect(clickDownload).toHaveBeenCalledTimes(1); expect(downloadedName).toMatch(/^dance-\d{8}-\d{6}-[a-z0-9]{4}\.png$/); expect(screen.getByRole("status")).toHaveTextContent("Image downloaded"); await act(async () => { await vi.advanceTimersByTimeAsync(1600); }); expect( document.querySelector('[data-tsh-image-copy-status="true"]') ).toBeNull(); clickDownload.mockRestore(); }); it("closes the zoom preview when the close button is clicked", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); fireEvent.click(await screen.findByRole("button", { name: /Zoom image/ })); const dialog = await screen.findByRole("dialog"); expect(dialog).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /^Close$/ })); const modalImage = dialog.querySelector("[data-rmiz-modal-img]"); expect(modalImage).toBeInstanceOf(HTMLElement); fireEvent.transitionEnd(modalImage as HTMLElement); await waitFor(() => { expect(screen.queryByRole("dialog")).toBeNull(); }); }); it("closes the zoom preview when its empty content area is clicked", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); fireEvent.click(await screen.findByRole("button", { name: /Zoom image/ })); const dialog = await screen.findByRole("dialog"); const emptyContentArea = dialog.querySelector("[data-rmiz-modal-content]"); expect(emptyContentArea).toBeInstanceOf(HTMLElement); fireEvent.click(emptyContentArea as HTMLElement); expect(dialog).toHaveAttribute("data-closing", "true"); fireEvent.animationEnd(dialog); await waitFor(() => { expect(screen.queryByRole("dialog")).toBeNull(); }); }); it("closes the zoom preview with Escape when focus is outside the dialog", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); fireEvent.click(await screen.findByRole("button", { name: /Zoom image/ })); const dialog = await screen.findByRole("dialog"); const backgroundButton = document.createElement("button"); document.body.append(backgroundButton); backgroundButton.focus(); fireEvent.keyDown(window, { key: "Escape" }); expect(dialog).toHaveAttribute("data-closing", "true"); fireEvent.animationEnd(dialog); await waitFor(() => { expect(screen.queryByRole("dialog")).toBeNull(); }); backgroundButton.remove(); }); it("closes the zoom preview when the zoomed image is clicked", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); fireEvent.click(await screen.findByRole("button", { name: /Zoom image/ })); const dialog = await screen.findByRole("dialog"); const modalImage = dialog.querySelector("[data-rmiz-modal-img]"); expect(modalImage).toBeInstanceOf(HTMLElement); fireEvent.click(modalImage as HTMLElement); fireEvent.transitionEnd(modalImage as HTMLElement); await waitFor(() => { expect(screen.queryByRole("dialog")).toBeNull(); }); }); it("does not add a zoom trigger when the markdown image is already inside a link", async () => { const readFile = vi.fn().mockResolvedValue({ bytes: new Uint8Array([137, 80, 78, 71]) }); const onLinkClick = vi.fn(); window.agentHostApi = { ...(window.agentHostApi ?? {}), workspace: { ...(window.agentHostApi?.workspace ?? {}), readFile } } as typeof window.agentHostApi; Object.defineProperty(URL, "createObjectURL", { configurable: true, value: vi.fn(() => "blob:tsh-markdown-image") }); Object.defineProperty(URL, "revokeObjectURL", { configurable: true, value: vi.fn() }); render( ); const link = await screen.findByRole("link", { name: "generated image" }); expect(screen.queryByRole("button", { name: /Zoom image/ })).toBeNull(); fireEvent.click(link); expect(onLinkClick).toHaveBeenCalledWith( "https://example.com/generated-image" ); }); it("supports inline rendering for title-sized markdown content", () => { render(

); expect( screen.getByRole("link", { name: "wang jomes & Codex hi" }) ).toBeInTheDocument(); expect( screen.queryByText( "[@wang jomes & Codex hi](mention://agent-session/session-1?workspaceId=room-1)" ) ).toBeNull(); }); it("marks mention-only markdown so object tokens do not use mixed-text offset", () => { const { container, rerender } = render( ); expect( container.querySelector('[data-workspace-agent-markdown="true"]') ).toHaveAttribute("data-agent-mention-only", "true"); rerender( ); expect( container.querySelector('[data-workspace-agent-markdown="true"]') ).not.toHaveAttribute("data-agent-mention-only"); }); it("renders session mentions as entity tokens instead of raw mention links", () => { const onLinkClick = vi.fn(); const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute("data-agent-mention-kind", "session"); expect(mention).toHaveClass("tsh-agent-object-token"); expect(mention).toHaveTextContent("2046494774160003072 & Codex 哈喽"); expect(screen.queryByText(/mention:\/\/session/)).toBeNull(); fireEvent.click(mention as HTMLElement); expect(onLinkClick).toHaveBeenCalledWith( "mention://agent-session/session-1?workspaceId=room-1" ); }); it("renders workspace file mentions as file object tokens", () => { const onLinkClick = vi.fn(); const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute("data-agent-mention-kind", "file"); expect(mention).toHaveAttribute("data-agent-file-entry-kind", "file"); expect(mention).toHaveAttribute("data-agent-file-visual-kind", "markdown"); expect(mention).toHaveAttribute( "data-agent-mention-href", "/workspace/demo/README.md" ); expect(mention).toHaveClass("tsh-agent-object-token"); expect(mention).toHaveClass("tsh-agent-object-token--file"); expect( mention?.querySelector(".tsh-agent-object-token__icon") ).toBeInTheDocument(); expect( mention?.querySelector(".tsh-agent-object-token__main") ).toHaveTextContent("README.md"); expect( screen.queryByText("[@README.md](/workspace/demo/README.md)") ).toBeNull(); fireEvent.click(mention as HTMLElement); expect(onLinkClick).toHaveBeenCalledWith("/workspace/demo/README.md"); }); it("renders workspace app mentions with app icons", () => { const iconUrl = "data:image/png;base64,weather"; const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute("data-agent-mention-kind", "workspace-app"); expect(mention).toHaveAttribute("data-agent-mention-icon-url", iconUrl); expect( mention?.querySelector('[data-agent-mention-app-icon="true"]') ).toHaveClass("h-4", "w-4"); const image = mention?.querySelector( '[data-agent-mention-app-icon="true"] img' ); expect(image).toHaveAttribute("src", iconUrl); expect( mention?.querySelector('[data-agent-mention-fallback-icon="true"]') ).not.toBeInTheDocument(); expect( mention?.querySelectorAll( '[data-agent-mention-app-icon="true"] img, [data-agent-mention-app-icon="true"] svg' ) ).toHaveLength(1); fireEvent.error(image!); expect( mention?.querySelector('[data-agent-mention-app-icon="true"] img') ).not.toBeInTheDocument(); expect( mention?.querySelector('[data-agent-mention-fallback-icon="true"]') ).toBeInTheDocument(); expect( mention?.querySelectorAll( '[data-agent-mention-app-icon="true"] img, [data-agent-mention-app-icon="true"] svg' ) ).toHaveLength(1); expect(mention).toHaveTextContent("Weather"); }); it("opens workspace app mentions without file path context", () => { const onLinkAction = vi.fn(); const onLinkClick = vi.fn(); render( ); fireEvent.click(screen.getByText("Weather")); expect(onLinkAction).toHaveBeenCalledWith({ type: "open-workspace-app", workspaceId: "room-1", appId: "weather", source: "agent-markdown" }); expect(onLinkClick).not.toHaveBeenCalled(); }); it("renders agent target mentions with managed agent icons", () => { const iconUrl = MANAGED_AGENT_ICON_ROUNDED_URLS["claude-code"]; const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute("data-agent-mention-kind", "agent-target"); expect(mention).toHaveAttribute("data-agent-mention-icon-url", iconUrl); expect( mention?.querySelector('[data-agent-mention-app-icon="true"] img') ).toHaveAttribute("src", iconUrl); expect(mention).toHaveTextContent("Claude Code"); }); it("renders session mentions with their Agent Target icons", () => { const iconUrl = "data:image/svg+xml;base64,gemini"; const { container } = render( ); const mention = container.querySelector( '[data-agent-mention-kind="session"]' ); expect(mention).toHaveAttribute("data-agent-mention-icon-url", iconUrl); expect( mention?.querySelector('[data-agent-mention-session-icon="true"] img') ).toHaveAttribute("src", iconUrl); expect(mention).toHaveTextContent("Gemini session"); }); it("renders agent target mentions without provider ids as agent tokens", () => { const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute("data-agent-mention-kind", "agent-target"); expect(mention).toHaveAttribute( "data-agent-mention-icon-url", managedAgentRoundedIconUrl(undefined) ); expect( mention?.querySelector(".tsh-agent-object-token__icon") ).not.toBeInTheDocument(); }); it("hydrates shared agent-target mention icons from the presentation catalog", () => { const iconUrl = "data:image/png;base64,shared-codex"; const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute("data-agent-mention-kind", "agent-target"); expect(mention).toHaveAttribute("data-agent-mention-icon-url", iconUrl); expect(mention?.querySelector("img")).toHaveAttribute("src", iconUrl); }); it("keeps exact target-directory icons over stale mention-service presentations", () => { const currentIconUrl = "data:image/png;base64,current-codex"; const staleUnifiedIconUrl = "data:image/png;base64,stale-unified"; const snapshot: RichTextMentionSnapshot = { state: "ready", resolved: { label: "Stale Unified Agent", presentation: { iconUrl: staleUnifiedIconUrl } } }; const mentionService: RichTextMentionService = { dispose: vi.fn(), getProvider: () => undefined, getSnapshot: () => snapshot, invalidate: vi.fn(), listProviders: () => [], listTriggerConfigs: () => [], query: async () => [], resolve: async () => snapshot, subscribe: () => () => {} }; const { container } = render( ); const targetMention = container.querySelector( '[data-agent-mention-kind="agent-target"]' ); const sessionMention = container.querySelector( '[data-agent-mention-kind="session"]' ); const missingTargetMention = container.querySelector( '[data-agent-mention-href*="shared-agent:missing"]' ); expect(targetMention).toHaveTextContent("rv4no's Codex"); expect(targetMention?.querySelector("img")).toHaveAttribute( "src", currentIconUrl ); expect(sessionMention?.querySelector("img")).toHaveAttribute( "src", currentIconUrl ); expect(missingTargetMention).toHaveTextContent("Stale Unified Agent"); expect(missingTargetMention?.querySelector("img")).toHaveAttribute( "src", staleUnifiedIconUrl ); }); it("falls back to a scoped provider icon when the presentation catalog misses", () => { const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute( "data-agent-mention-icon-url", managedAgentRoundedIconUrl("codex") ); }); it("renders workspace app factory mentions as object tokens", () => { const { container, queryByText } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute( "data-agent-mention-kind", "workspace-app-factory" ); expect(mention).toHaveTextContent("Create App"); expect( queryByText("[@Create App](mention://workspace-app-factory/create)") ).toBeNull(); }); it("renders extensionless workspace mentions as folder object tokens", () => { const onLinkClick = vi.fn(); const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute("data-agent-mention-kind", "file"); expect(mention).toHaveAttribute("data-agent-file-entry-kind", "directory"); expect(mention).toHaveAttribute("data-agent-file-visual-kind", "folder"); expect(mention).toHaveAttribute( "data-agent-link-href", "/workspace/demo/Codex" ); fireEvent.click(mention as HTMLElement); expect(onLinkClick).toHaveBeenCalledWith("/workspace/demo/Codex"); }); it("keeps explicit extensionless file mentions as file object tokens", () => { const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute("data-agent-file-entry-kind", "file"); expect(mention).toHaveAttribute("data-agent-file-visual-kind", "binary"); expect(mention).toHaveAttribute( "data-agent-link-href", "/workspace/demo/LICENSE" ); }); it("keeps line-wrapped mention markdown links as entity tokens", () => { const { container } = render( ); const mention = container.querySelector('[data-agent-file-mention="true"]'); expect(mention).toHaveAttribute("data-agent-mention-kind", "session"); expect(mention).toHaveTextContent("长标题会话"); expect(screen.queryByText(/mention:\/\/session/)).toBeNull(); }); it("keeps inline code paths as code instead of inferring links", () => { const onLinkClick = vi.fn(); render( ); expect( screen.queryByRole("link", { name: "/Users/example/demo/abc" }) ).toBeNull(); expect(screen.getByText("/Users/example/demo/abc")).toBeInTheDocument(); expect(onLinkClick).not.toHaveBeenCalled(); }); it("keeps inline code home-relative paths as code", () => { const onLinkAction = vi.fn(); render( ); expect(screen.queryByRole("link", { name: "~/docs/a.md" })).toBeNull(); expect(screen.getByText("~/docs/a.md")).toBeInTheDocument(); expect(onLinkAction).not.toHaveBeenCalled(); }); it("keeps inline code Windows absolute paths as code", () => { const onLinkAction = vi.fn(); render( ); expect( screen.queryByRole("link", { name: "C:\\Users\\local\\project\\docs\\README.md" }) ).toBeNull(); expect( screen.getByText("C:\\Users\\local\\project\\docs\\README.md") ).toBeInTheDocument(); expect(onLinkAction).not.toHaveBeenCalled(); }); it("keeps inline code http urls as code", () => { const onLinkClick = vi.fn(); render( ); expect( screen.queryByRole("link", { name: "http://127.0.0.1:9999" }) ).toBeNull(); expect(screen.getByText("http://127.0.0.1:9999")).toBeInTheDocument(); expect(onLinkClick).not.toHaveBeenCalled(); }); it("prevents default navigation for markdown http links", () => { const onLinkClick = vi.fn(); render( ); const link = screen.getByRole("link", { name: "http://127.0.0.1:9999" }); expect(link).not.toHaveAttribute("href"); expect(link).toHaveAttribute( "data-agent-link-href", "http://127.0.0.1:9999" ); const clickEvent = new MouseEvent("click", { bubbles: true, cancelable: true }); link.dispatchEvent(clickEvent); expect(clickEvent.defaultPrevented).toBe(true); expect(onLinkClick).toHaveBeenCalledWith("http://127.0.0.1:9999"); }); it("keeps bare local absolute paths and slash commands as plain text", () => { const onLinkClick = vi.fn(); render( ); expect( screen.queryByRole("link", { name: "/Users/example/demo/83c66a52-4ff2-436a-a300-e346c9fdd9d2/note.txt" }) ).toBeNull(); expect(screen.queryByRole("link", { name: "/login" })).toBeNull(); expect( screen.getByText( "/Users/example/demo/83c66a52-4ff2-436a-a300-e346c9fdd9d2/note.txt" ) ).toBeInTheDocument(); expect(screen.getByText(/Please run \/login/)).toBeInTheDocument(); expect(onLinkClick).not.toHaveBeenCalled(); }); it("does not auto-link bare relative paths", () => { const onLinkClick = vi.fn(); render( ); expect(screen.queryByRole("link", { name: "abc/123.txt" })).toBeNull(); expect(screen.getByText(/abc\/123\.txt/)).toBeInTheDocument(); expect(onLinkClick).not.toHaveBeenCalled(); }); it("does not link relative file paths inside inline code without workspace context", () => { const onLinkClick = vi.fn(); render( ); expect(screen.queryByRole("link", { name: "a.md" })).toBeNull(); expect(screen.getByText("a.md")).toBeInTheDocument(); expect(screen.queryByRole("link", { name: "xxx" })).toBeNull(); expect(onLinkClick).not.toHaveBeenCalled(); }); it("does not link relative file paths inside inline code when workspace context is provided", () => { const onLinkAction = vi.fn(); render( ); for (const label of [ "empty-files", "xx.html", "xx.md", "content/posts", "lib/site.ts", "README.md" ]) { expect(screen.queryByRole("link", { name: label })).toBeNull(); expect(screen.getByText(label)).toBeInTheDocument(); } expect(onLinkAction).not.toHaveBeenCalled(); }); it("does not treat ordinary inline code as a path", () => { const onLinkClick = vi.fn(); render( ); expect(screen.queryByRole("link", { name: "hello world" })).toBeNull(); expect(screen.getByText("hello world")).toBeInTheDocument(); }); it("does not leak markdown ast node props into the DOM", () => { const { container } = render( ); expect(container.querySelector("[node]")).toBeNull(); }); it("collapses long messages and expands them on demand", () => { render( `第 ${index + 1} 行` ).join("\n")} collapsible expandLabel="展开全部" /> ); const expandButton = screen.getByRole("button", { name: "展开全部" }); const markdown = expandButton.parentElement?.querySelector( '[data-workspace-agent-markdown="true"]' ); expect(markdown).toHaveAttribute("data-collapsed", "true"); fireEvent.click(expandButton); expect(markdown).toHaveAttribute("data-collapsed", "false"); expect(screen.queryByRole("button", { name: "展开全部" })).toBeNull(); }); it("renders long messages as markdown on the first render", () => { const content = `# Long answer\n\n${"x".repeat(4096)}`; render(); expect( screen.getByRole("heading", { level: 1, name: "Long answer" }) ).toBeInTheDocument(); }); }); describe("splitStreamingMarkdownBlocks", () => { it("splits stable markdown blocks without splitting fenced code", () => { expect( splitStreamingMarkdownBlocks( [ "Intro paragraph.", "", "```ts", "const value = 1;", "", "console.log(value);", "```", "", "- Tail item" ].join("\n") ).map((block) => block.content) ).toEqual([ "Intro paragraph.\n", "```ts\nconst value = 1;\n\nconsole.log(value);\n```\n", "- Tail item" ]); }); });