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"
]);
});
});