import { EnvironmentId } from "@t3tools/contracts";import { act, type ComponentProps, type ReactNode } from "react";import { renderToStaticMarkup } from "react-dom/server";import { create, type ReactTestRenderer } from "react-test-renderer";import { describe, expect, it, vi } from "vite-plus/test"; import { getSyntaxHighlighterPromise } from "../lib/syntaxHighlighting";import { GitHubIcon } from "./Icons";import { Button } from "./ui/button";import { setMarkdownTaskChecked } from "./files/filePreviewMode"; vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null }));vi.mock("../hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "dark" }) }));vi.mock("../hooks/useSettings", async (importOriginal) => { const actual = await importOriginal<typeof import("../hooks/useSettings")>(); const settings = actual.getClientSettings(); return { ...actual, useClientSettings: (select?: (value: typeof settings) => unknown) => select ? select(settings) : settings, };});vi.mock("./ui/tooltip", async () => { const { cloneElement, isValidElement } = await import("react"); return { Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>, TooltipTrigger({ render, children, }: ComponentProps<typeof import("./ui/tooltip").TooltipTrigger>) { if (!isValidElement(render)) return <>{children}</>; return children === undefined ? render : cloneElement(render, undefined, children); }, TooltipPopup: () => null, };});vi.mock("../state/use-atom-query-runner", () => ({ useAtomQueryRunner: () => vi.fn() }));vi.mock("../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() }));vi.mock("../state/session", async (importOriginal) => ({ ...(await importOriginal<typeof import("../state/session")>()), usePreparedConnection: () => ({ _tag: "Loading" }),}));vi.mock("../state/entities", () => ({ readThreadShell: () => null, useProjects: () => [], useServerConfigs: () => new Map(),}));vi.mock("../remoteOpen", () => ({ useRemoteOpenResolution: () => ({ state: { mode: "local-exec" }, isResolved: true }),}));vi.mock("../editorPreferences", () => ({ useOpenInPreferredEditor: () => vi.fn(), usePreferredEditor: () => [null, vi.fn()],}));vi.mock("~/lib/openPullRequestLink", () => ({ findProjectOnChangeRequestHost: () => undefined, parseChangeRequestUrl: () => null, useOpenChangeRequestLink: () => vi.fn(),})); import ChatMarkdown, { canUseMarkdownFileShellActions, hasMarkdownFilePrimaryAction, shouldUseMarkdownFileBrowserPrimaryAction,} from "./ChatMarkdown"; function codeButton(renderer: ReactTestRenderer, label: string) { const button = renderer.root .findAllByType(Button) .find((instance) => instance.props["aria-label"] === label); if (!button) throw new Error(`Missing code button: ${label}`); return button.props as ComponentProps<typeof Button>;} describe("ChatMarkdown favicon privacy", () => { it("suppresses private link images while preserving public links across updates", async () => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); let renderer: ReactTestRenderer | undefined; const markdown = (url: string) => <ChatMarkdown cwd="/tmp/project" text={`[Link](${url})`} />; try { await act(async () => { renderer = create(markdown("https://example.com")); }); expect(renderer!.root.findAllByType("img").map((image) => image.props.src)).toEqual([ "https://www.google.com/s2/favicons?domain=example.com&sz=32", ]); for (const url of ["http://192.168.1.10:8080", "http://localhost:3000", "http://home.arpa"]) { await act(async () => { renderer!.update(markdown(url)); }); expect(renderer!.root.findAllByType("img")).toHaveLength(0); } await act(async () => { renderer!.update(markdown("https://example.com")); }); expect(renderer!.root.findAllByType("img")).toHaveLength(1); // GitHub links draw the brand mark in currentColor instead of fetching a favicon. await act(async () => { renderer!.update(markdown("https://github.com/pingdotgg/t3code/pull/1")); }); expect(renderer!.root.findAllByType("img")).toHaveLength(0); expect(renderer!.root.findAllByType(GitHubIcon)).toHaveLength(1); } finally { await act(async () => { renderer?.unmount(); }); vi.unstubAllGlobals(); } });}); describe("ChatMarkdown streaming", () => { it("recovers highlighting after a failed fence changes without resetting its controls", async () => { const highlighter = await getSyntaxHighlighterPromise("text"); const codeToHtml = highlighter.codeToHtml.bind(highlighter); let fail = true; vi.spyOn(highlighter, "codeToHtml").mockImplementation((...args) => { if (fail) throw new Error("Temporary highlighter failure"); return codeToHtml(...args); }); vi.spyOn(console, "error").mockImplementation(() => {}); vi.spyOn(console, "warn").mockImplementation(() => {}); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); let renderer: ReactTestRenderer | undefined; try { await act(async () => { renderer = create( <ChatMarkdown cwd="/tmp/project" text={"```text\ninitial\n```"} isStreaming />, ); }); const mounted = renderer!; const codeBlock = mounted.root.findByProps({ "data-language": "text" }); const initialWrap = codeBlock.props["data-wrap"] === "true"; const wrap = codeButton(mounted, initialWrap ? "Disable line wrap" : "Wrap lines"); await act(async () => { wrap.onClick?.({} as Parameters<NonNullable<typeof wrap.onClick>>[0]); }); expect(mounted.root.findAllByProps({ className: "chat-markdown-shiki" })).toHaveLength(0); fail = false; await act(async () => { mounted.update( <ChatMarkdown cwd="/tmp/project" text={"```text\nrecovered\n```"} isStreaming />, ); }); expect(mounted.root.findAllByProps({ className: "chat-markdown-shiki" })).toHaveLength(1); expect(mounted.root.findByProps({ "data-language": "text" })).toBe(codeBlock); expect(codeBlock.props["data-wrap"]).toBe(String(!initialWrap)); } finally { await act(async () => renderer?.unmount()); vi.unstubAllGlobals(); vi.restoreAllMocks(); } }); it("preserves code controls and details without highlighting an unchanged fence again", async () => { const highlighter = await getSyntaxHighlighterPromise("text"); const highlight = vi.spyOn(highlighter, "codeToHtml"); const writeText = vi.fn(async (_text: string) => {}); vi.stubGlobal("navigator", { clipboard: { writeText } }); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); let renderer: ReactTestRenderer | undefined; const text = [ "```text", "First code block", "```", "", "<details><summary>More</summary>", "", "Details content", "", "</details>", "", "Streaming reply", ].join("\n"); try { await act(async () => { renderer = create(<ChatMarkdown cwd="/tmp/project" text={text} isStreaming />); }); const mounted = renderer!; const codeBlock = mounted.root.findByProps({ "data-language": "text" }); const initialWrap = codeBlock.props["data-wrap"] === "true"; const wrap = codeButton(mounted, initialWrap ? "Disable line wrap" : "Wrap lines"); const copy = codeButton(mounted, "Copy code"); await act(async () => { wrap.onClick?.({} as Parameters<NonNullable<typeof wrap.onClick>>[0]); copy.onClick?.({} as Parameters<NonNullable<typeof copy.onClick>>[0]); }); const detailsButton = mounted.root.find( (instance) => instance.type === "button" && instance.props["data-markdown-details-summary"] === "", ); await act(async () => { detailsButton.props.onClick({ nativeEvent: new Event("click") }); }); const details = mounted.root.findByProps({ "data-markdown-details": "" }); expect(details.props["data-markdown-details-open"]).toBe("true"); expect(writeText).toHaveBeenCalledWith("First code block\n"); expect(highlight).toHaveBeenCalledTimes(1); for (let index = 0; index < 10; index += 1) { await act(async () => { mounted.update(<ChatMarkdown cwd="/tmp/project" text={`${text} ${index}`} isStreaming />); }); } expect(highlight).toHaveBeenCalledTimes(1); expect(mounted.root.findByProps({ "data-language": "text" })).toBe(codeBlock); expect(codeBlock.props["data-wrap"]).toBe(String(!initialWrap)); expect(mounted.root.findByProps({ "data-markdown-details": "" })).toBe(details); expect(details.props["data-markdown-details-open"]).toBe("true"); await act(async () => { mounted.update( <ChatMarkdown cwd="/tmp/project" text={text.replace("First code block", "Updated code block")} isStreaming />, ); }); const copyUpdated = codeButton(mounted, "Copied"); await act(async () => { copyUpdated.onClick?.({} as Parameters<NonNullable<typeof copyUpdated.onClick>>[0]); }); expect(writeText).toHaveBeenLastCalledWith("Updated code block\n"); expect(highlight).toHaveBeenCalledTimes(2); } finally { await act(async () => renderer?.unmount()); vi.useRealTimers(); vi.unstubAllGlobals(); vi.restoreAllMocks(); } }); it("edits the current task text and marker after reusing a renderer", async () => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); let renderer: ReactTestRenderer | undefined; let editedText: string | undefined; const message = (text: string) => ( <ChatMarkdown cwd="/tmp/project" text={text} onTaskListChange={({ markerOffset, checked }) => { editedText = setMarkdownTaskChecked(text, markerOffset, checked); renderer!.update(message(editedText)); }} /> ); try { await act(async () => { renderer = create(message("- [ ] First\n- [ ] Second")); }); const mounted = renderer!; const originalInput = mounted.root.findAllByType("input")[1]!; await act(async () => { mounted.update(message("- [ ] A longer first task\n- [ ] Second")); }); const input = mounted.root.findAllByType("input")[1]!; const listItem = mounted.root.findAllByType("li")[1]!; const { onChange } = input.props as ComponentProps<"input">; if (!onChange) throw new Error("Task checkbox has no edit handler"); await act(async () => { onChange({ currentTarget: { checked: true, closest: () => ({ dataset: { taskMarkerOffset: String(listItem.props["data-task-marker-offset"]) }, }), }, } as unknown as Parameters<typeof onChange>[0]); }); expect(input).toBe(originalInput); expect(editedText).toBe("- [ ] A longer first task\n- [x] Second"); expect(mounted.root.findAllByType("input")[1]!.props.checked).toBe(true); } finally { await act(async () => renderer?.unmount()); vi.unstubAllGlobals(); } });}); describe("canUseMarkdownFileShellActions", () => { const environmentId = EnvironmentId.make("environment-1"); it("allows editor and file manager actions for local environments", () => { expect(canUseMarkdownFileShellActions(environmentId, "local-exec", true)).toBe(true); }); it("hides shell actions until the environment mode is resolved", () => { expect(canUseMarkdownFileShellActions(environmentId, "local-exec", false)).toBe(false); }); it("hides editor and file manager actions for remote environments", () => { expect(canUseMarkdownFileShellActions(environmentId, "remote-links", true)).toBe(false); expect(canUseMarkdownFileShellActions(environmentId, "remote-unavailable", true)).toBe(false); }); it("hides shell actions when no environment owns the markdown", () => { expect(canUseMarkdownFileShellActions(null, "local-exec", true)).toBe(false); });}); describe("hasMarkdownFilePrimaryAction", () => { it("keeps the chip interactive when an editor, browser, or panel can open it", () => { expect( hasMarkdownFilePrimaryAction({ canOpenInEditor: true, canOpenInBrowser: false, canOpenInPanel: false, }), ).toBe(true); expect( hasMarkdownFilePrimaryAction({ canOpenInEditor: false, canOpenInBrowser: true, canOpenInPanel: false, }), ).toBe(true); expect( hasMarkdownFilePrimaryAction({ canOpenInEditor: false, canOpenInBrowser: false, canOpenInPanel: true, }), ).toBe(true); }); it("removes the link affordance when no primary action can open the file", () => { expect( hasMarkdownFilePrimaryAction({ canOpenInEditor: false, canOpenInBrowser: false, canOpenInPanel: false, }), ).toBe(false); });}); describe("ChatMarkdown skill chips", () => { it("updates digit-leading skill labels when discovered skills change", async () => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); let renderer: ReactTestRenderer | undefined; const text = "Use $2spec with a $20k budget."; try { await act(async () => { renderer = create(<ChatMarkdown cwd="/tmp/project" text={text} />); }); const mounted = renderer!; const labels = (label: string) => mounted.root.findAllByType("span").filter((node) => node.children.includes(label)); expect(labels("2Spec")).toHaveLength(0); await act(async () => { mounted.update( <ChatMarkdown cwd="/tmp/project" text={text} skills={[ { name: "2spec", displayName: "2Spec" }, { name: "20k", displayName: "MoneySkill" }, ]} />, ); }); expect(labels("2Spec")).toHaveLength(1); expect(labels("MoneySkill")).toHaveLength(0); await act(async () => { mounted.update(<ChatMarkdown cwd="/tmp/project" text={text} skills={[]} />); }); expect(labels("2Spec")).toHaveLength(0); } finally { await act(async () => renderer?.unmount()); vi.unstubAllGlobals(); } });}); describe("ChatMarkdown file option chips", () => { it("keeps the fallback button text selectable", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text="[Source](/tmp/project/src/main.ts)" />, ); expect(html).toContain("<button"); expect(html).toContain('aria-haspopup="menu"'); expect(html).toContain("select-text"); }); it.each([true, false])( "renders Codex file citations as file chips with parseRawHtml=%s", (parseRawHtml) => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={ 'Created :codex-file-citation{path="/tmp/project/outputs/report.xlsx" purpose="output"}.' } lineBreaks={!parseRawHtml} parseRawHtml={parseRawHtml} />, ); expect(html).not.toContain("codex-file-citation"); expect(html).toContain("chat-markdown-file-link"); expect(html).toContain( 'data-markdown-copy="[report.xlsx](/tmp/project/outputs/report.xlsx)"', ); expect(html).toContain("report.xlsx"); }, ); it("leaves an unfinished streaming citation visible until it is complete", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={'Created :codex-file-citation{path="/tmp/project/outputs/report.xlsx"'} isStreaming />, ); expect(html).toContain(":codex-file-citation"); expect(html).not.toContain("chat-markdown-file-link"); }); it("leaves malformed and similarly named file directives literal", () => { for (const text of [ ':codex-file-citation{purpose="output"}', ':codex-file-citation-extra{path="/tmp/project/outputs/report.xlsx"}', ]) { const html = renderToStaticMarkup(<ChatMarkdown cwd="/tmp/project" text={text} />); expect(html).toContain(text.replaceAll('"', """)); expect(html).not.toContain("chat-markdown-file-link"); } }); it("preserves Codex file citation examples inside code", () => { const directive = ':codex-file-citation{path="/tmp/project/outputs/report.xlsx"}'; const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={`Example: \`${directive}\`\n\n\`\`\`text\n${directive}\n\`\`\``} />, ); expect(html.match(/:codex-file-citation/g)).toHaveLength(2); expect(html).not.toContain("chat-markdown-file-link"); }); it("preserves escaped Codex file citations as literal text", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={'Example: \\:codex-file-citation{path="/tmp/project/outputs/report.xlsx"}'} />, ); expect(html).toContain(":codex-file-citation"); expect(html).not.toContain("chat-markdown-file-link"); }); it("does not create a nested link for citations inside link text", () => { const directive = ':codex-file-citation{path="/tmp/project/outputs/report.xlsx"}'; const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={`[See ${directive}](https://example.com)`} />, ); const renderedText = html.replace(/<[^>]+>/g, ""); expect(renderedText).toContain("codex-file-citation"); expect(html).not.toContain("chat-markdown-file-link"); }); it("renders file citations created by over-indented list recovery", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={'- Created :codex-file-citation{path="/tmp/project/outputs/report.xlsx"}'} />, ); expect(html).not.toContain("<pre>"); expect(html).toContain("Created "); expect(html).toContain("chat-markdown-file-link"); expect(html).toContain("report.xlsx"); }); it("disambiguates Codex citations with the same basename", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={ 'Changed :codex-file-citation{path="/tmp/project/src/index.ts"} and :codex-file-citation{path="/tmp/project/test/index.ts"}.'Lines 1–500 of 749. Load more
import { EnvironmentId } from "@t3tools/contracts"; import { act, type ComponentProps, type ReactNode } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { create, type ReactTestRenderer } from "react-test-renderer"; import { describe, expect, it, vi } from "vite-plus/test"; import { getSyntaxHighlighterPromise } from "../lib/syntaxHighlighting"; import { GitHubIcon } from "./Icons"; import { Button } from "./ui/button"; import { setMarkdownTaskChecked } from "./files/filePreviewMode"; vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null })); vi.mock("../hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "dark" }) })); vi.mock("../hooks/useSettings", async (importOriginal) => { const actual = await importOriginal<typeof import("../hooks/useSettings")>(); const settings = actual.getClientSettings(); return { ...actual, useClientSettings: (select?: (value: typeof settings) => unknown) => select ? select(settings) : settings, }; }); vi.mock("./ui/tooltip", async () => { const { cloneElement, isValidElement } = await import("react"); return { Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>, TooltipTrigger({ render, children, }: ComponentProps<typeof import("./ui/tooltip").TooltipTrigger>) { if (!isValidElement(render)) return <>{children}</>; return children === undefined ? render : cloneElement(render, undefined, children); }, TooltipPopup: () => null, }; }); vi.mock("../state/use-atom-query-runner", () => ({ useAtomQueryRunner: () => vi.fn() })); vi.mock("../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() })); vi.mock("../state/session", async (importOriginal) => ({ ...(await importOriginal<typeof import("../state/session")>()), usePreparedConnection: () => ({ _tag: "Loading" }), })); vi.mock("../state/entities", () => ({ readThreadShell: () => null, useProjects: () => [], useServerConfigs: () => new Map(), })); vi.mock("../remoteOpen", () => ({ useRemoteOpenResolution: () => ({ state: { mode: "local-exec" }, isResolved: true }), })); vi.mock("../editorPreferences", () => ({ useOpenInPreferredEditor: () => vi.fn(), usePreferredEditor: () => [null, vi.fn()], })); vi.mock("~/lib/openPullRequestLink", () => ({ findProjectOnChangeRequestHost: () => undefined, parseChangeRequestUrl: () => null, useOpenChangeRequestLink: () => vi.fn(), })); import ChatMarkdown, { canUseMarkdownFileShellActions, hasMarkdownFilePrimaryAction, shouldUseMarkdownFileBrowserPrimaryAction, } from "./ChatMarkdown"; function codeButton(renderer: ReactTestRenderer, label: string) { const button = renderer.root .findAllByType(Button) .find((instance) => instance.props["aria-label"] === label); if (!button) throw new Error(`Missing code button: ${label}`); return button.props as ComponentProps<typeof Button>; } describe("ChatMarkdown favicon privacy", () => { it("suppresses private link images while preserving public links across updates", async () => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); let renderer: ReactTestRenderer | undefined; const markdown = (url: string) => <ChatMarkdown cwd="/tmp/project" text={`[Link](${url})`} />; try { await act(async () => { renderer = create(markdown("https://example.com")); }); expect(renderer!.root.findAllByType("img").map((image) => image.props.src)).toEqual([ "https://www.google.com/s2/favicons?domain=example.com&sz=32", ]); for (const url of ["http://192.168.1.10:8080", "http://localhost:3000", "http://home.arpa"]) { await act(async () => { renderer!.update(markdown(url)); }); expect(renderer!.root.findAllByType("img")).toHaveLength(0); } await act(async () => { renderer!.update(markdown("https://example.com")); }); expect(renderer!.root.findAllByType("img")).toHaveLength(1); // GitHub links draw the brand mark in currentColor instead of fetching a favicon. await act(async () => { renderer!.update(markdown("https://github.com/pingdotgg/t3code/pull/1")); }); expect(renderer!.root.findAllByType("img")).toHaveLength(0); expect(renderer!.root.findAllByType(GitHubIcon)).toHaveLength(1); } finally { await act(async () => { renderer?.unmount(); }); vi.unstubAllGlobals(); } }); }); describe("ChatMarkdown streaming", () => { it("recovers highlighting after a failed fence changes without resetting its controls", async () => { const highlighter = await getSyntaxHighlighterPromise("text"); const codeToHtml = highlighter.codeToHtml.bind(highlighter); let fail = true; vi.spyOn(highlighter, "codeToHtml").mockImplementation((...args) => { if (fail) throw new Error("Temporary highlighter failure"); return codeToHtml(...args); }); vi.spyOn(console, "error").mockImplementation(() => {}); vi.spyOn(console, "warn").mockImplementation(() => {}); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); let renderer: ReactTestRenderer | undefined; try { await act(async () => { renderer = create( <ChatMarkdown cwd="/tmp/project" text={"```text\ninitial\n```"} isStreaming />, ); }); const mounted = renderer!; const codeBlock = mounted.root.findByProps({ "data-language": "text" }); const initialWrap = codeBlock.props["data-wrap"] === "true"; const wrap = codeButton(mounted, initialWrap ? "Disable line wrap" : "Wrap lines"); await act(async () => { wrap.onClick?.({} as Parameters<NonNullable<typeof wrap.onClick>>[0]); }); expect(mounted.root.findAllByProps({ className: "chat-markdown-shiki" })).toHaveLength(0); fail = false; await act(async () => { mounted.update( <ChatMarkdown cwd="/tmp/project" text={"```text\nrecovered\n```"} isStreaming />, ); }); expect(mounted.root.findAllByProps({ className: "chat-markdown-shiki" })).toHaveLength(1); expect(mounted.root.findByProps({ "data-language": "text" })).toBe(codeBlock); expect(codeBlock.props["data-wrap"]).toBe(String(!initialWrap)); } finally { await act(async () => renderer?.unmount()); vi.unstubAllGlobals(); vi.restoreAllMocks(); } }); it("preserves code controls and details without highlighting an unchanged fence again", async () => { const highlighter = await getSyntaxHighlighterPromise("text"); const highlight = vi.spyOn(highlighter, "codeToHtml"); const writeText = vi.fn(async (_text: string) => {}); vi.stubGlobal("navigator", { clipboard: { writeText } }); vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] }); let renderer: ReactTestRenderer | undefined; const text = [ "```text", "First code block", "```", "", "<details><summary>More</summary>", "", "Details content", "", "</details>", "", "Streaming reply", ].join("\n"); try { await act(async () => { renderer = create(<ChatMarkdown cwd="/tmp/project" text={text} isStreaming />); }); const mounted = renderer!; const codeBlock = mounted.root.findByProps({ "data-language": "text" }); const initialWrap = codeBlock.props["data-wrap"] === "true"; const wrap = codeButton(mounted, initialWrap ? "Disable line wrap" : "Wrap lines"); const copy = codeButton(mounted, "Copy code"); await act(async () => { wrap.onClick?.({} as Parameters<NonNullable<typeof wrap.onClick>>[0]); copy.onClick?.({} as Parameters<NonNullable<typeof copy.onClick>>[0]); }); const detailsButton = mounted.root.find( (instance) => instance.type === "button" && instance.props["data-markdown-details-summary"] === "", ); await act(async () => { detailsButton.props.onClick({ nativeEvent: new Event("click") }); }); const details = mounted.root.findByProps({ "data-markdown-details": "" }); expect(details.props["data-markdown-details-open"]).toBe("true"); expect(writeText).toHaveBeenCalledWith("First code block\n"); expect(highlight).toHaveBeenCalledTimes(1); for (let index = 0; index < 10; index += 1) { await act(async () => { mounted.update(<ChatMarkdown cwd="/tmp/project" text={`${text} ${index}`} isStreaming />); }); } expect(highlight).toHaveBeenCalledTimes(1); expect(mounted.root.findByProps({ "data-language": "text" })).toBe(codeBlock); expect(codeBlock.props["data-wrap"]).toBe(String(!initialWrap)); expect(mounted.root.findByProps({ "data-markdown-details": "" })).toBe(details); expect(details.props["data-markdown-details-open"]).toBe("true"); await act(async () => { mounted.update( <ChatMarkdown cwd="/tmp/project" text={text.replace("First code block", "Updated code block")} isStreaming />, ); }); const copyUpdated = codeButton(mounted, "Copied"); await act(async () => { copyUpdated.onClick?.({} as Parameters<NonNullable<typeof copyUpdated.onClick>>[0]); }); expect(writeText).toHaveBeenLastCalledWith("Updated code block\n"); expect(highlight).toHaveBeenCalledTimes(2); } finally { await act(async () => renderer?.unmount()); vi.useRealTimers(); vi.unstubAllGlobals(); vi.restoreAllMocks(); } }); it("edits the current task text and marker after reusing a renderer", async () => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); let renderer: ReactTestRenderer | undefined; let editedText: string | undefined; const message = (text: string) => ( <ChatMarkdown cwd="/tmp/project" text={text} onTaskListChange={({ markerOffset, checked }) => { editedText = setMarkdownTaskChecked(text, markerOffset, checked); renderer!.update(message(editedText)); }} /> ); try { await act(async () => { renderer = create(message("- [ ] First\n- [ ] Second")); }); const mounted = renderer!; const originalInput = mounted.root.findAllByType("input")[1]!; await act(async () => { mounted.update(message("- [ ] A longer first task\n- [ ] Second")); }); const input = mounted.root.findAllByType("input")[1]!; const listItem = mounted.root.findAllByType("li")[1]!; const { onChange } = input.props as ComponentProps<"input">; if (!onChange) throw new Error("Task checkbox has no edit handler"); await act(async () => { onChange({ currentTarget: { checked: true, closest: () => ({ dataset: { taskMarkerOffset: String(listItem.props["data-task-marker-offset"]) }, }), }, } as unknown as Parameters<typeof onChange>[0]); }); expect(input).toBe(originalInput); expect(editedText).toBe("- [ ] A longer first task\n- [x] Second"); expect(mounted.root.findAllByType("input")[1]!.props.checked).toBe(true); } finally { await act(async () => renderer?.unmount()); vi.unstubAllGlobals(); } }); }); describe("canUseMarkdownFileShellActions", () => { const environmentId = EnvironmentId.make("environment-1"); it("allows editor and file manager actions for local environments", () => { expect(canUseMarkdownFileShellActions(environmentId, "local-exec", true)).toBe(true); }); it("hides shell actions until the environment mode is resolved", () => { expect(canUseMarkdownFileShellActions(environmentId, "local-exec", false)).toBe(false); }); it("hides editor and file manager actions for remote environments", () => { expect(canUseMarkdownFileShellActions(environmentId, "remote-links", true)).toBe(false); expect(canUseMarkdownFileShellActions(environmentId, "remote-unavailable", true)).toBe(false); }); it("hides shell actions when no environment owns the markdown", () => { expect(canUseMarkdownFileShellActions(null, "local-exec", true)).toBe(false); }); }); describe("hasMarkdownFilePrimaryAction", () => { it("keeps the chip interactive when an editor, browser, or panel can open it", () => { expect( hasMarkdownFilePrimaryAction({ canOpenInEditor: true, canOpenInBrowser: false, canOpenInPanel: false, }), ).toBe(true); expect( hasMarkdownFilePrimaryAction({ canOpenInEditor: false, canOpenInBrowser: true, canOpenInPanel: false, }), ).toBe(true); expect( hasMarkdownFilePrimaryAction({ canOpenInEditor: false, canOpenInBrowser: false, canOpenInPanel: true, }), ).toBe(true); }); it("removes the link affordance when no primary action can open the file", () => { expect( hasMarkdownFilePrimaryAction({ canOpenInEditor: false, canOpenInBrowser: false, canOpenInPanel: false, }), ).toBe(false); }); }); describe("ChatMarkdown skill chips", () => { it("updates digit-leading skill labels when discovered skills change", async () => { vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); let renderer: ReactTestRenderer | undefined; const text = "Use $2spec with a $20k budget."; try { await act(async () => { renderer = create(<ChatMarkdown cwd="/tmp/project" text={text} />); }); const mounted = renderer!; const labels = (label: string) => mounted.root.findAllByType("span").filter((node) => node.children.includes(label)); expect(labels("2Spec")).toHaveLength(0); await act(async () => { mounted.update( <ChatMarkdown cwd="/tmp/project" text={text} skills={[ { name: "2spec", displayName: "2Spec" }, { name: "20k", displayName: "MoneySkill" }, ]} />, ); }); expect(labels("2Spec")).toHaveLength(1); expect(labels("MoneySkill")).toHaveLength(0); await act(async () => { mounted.update(<ChatMarkdown cwd="/tmp/project" text={text} skills={[]} />); }); expect(labels("2Spec")).toHaveLength(0); } finally { await act(async () => renderer?.unmount()); vi.unstubAllGlobals(); } }); }); describe("ChatMarkdown file option chips", () => { it("keeps the fallback button text selectable", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text="[Source](/tmp/project/src/main.ts)" />, ); expect(html).toContain("<button"); expect(html).toContain('aria-haspopup="menu"'); expect(html).toContain("select-text"); }); it.each([true, false])( "renders Codex file citations as file chips with parseRawHtml=%s", (parseRawHtml) => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={ 'Created :codex-file-citation{path="/tmp/project/outputs/report.xlsx" purpose="output"}.' } lineBreaks={!parseRawHtml} parseRawHtml={parseRawHtml} />, ); expect(html).not.toContain("codex-file-citation"); expect(html).toContain("chat-markdown-file-link"); expect(html).toContain( 'data-markdown-copy="[report.xlsx](/tmp/project/outputs/report.xlsx)"', ); expect(html).toContain("report.xlsx"); }, ); it("leaves an unfinished streaming citation visible until it is complete", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={'Created :codex-file-citation{path="/tmp/project/outputs/report.xlsx"'} isStreaming />, ); expect(html).toContain(":codex-file-citation"); expect(html).not.toContain("chat-markdown-file-link"); }); it("leaves malformed and similarly named file directives literal", () => { for (const text of [ ':codex-file-citation{purpose="output"}', ':codex-file-citation-extra{path="/tmp/project/outputs/report.xlsx"}', ]) { const html = renderToStaticMarkup(<ChatMarkdown cwd="/tmp/project" text={text} />); expect(html).toContain(text.replaceAll('"', """)); expect(html).not.toContain("chat-markdown-file-link"); } }); it("preserves Codex file citation examples inside code", () => { const directive = ':codex-file-citation{path="/tmp/project/outputs/report.xlsx"}'; const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={`Example: \`${directive}\`\n\n\`\`\`text\n${directive}\n\`\`\``} />, ); expect(html.match(/:codex-file-citation/g)).toHaveLength(2); expect(html).not.toContain("chat-markdown-file-link"); }); it("preserves escaped Codex file citations as literal text", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={'Example: \\:codex-file-citation{path="/tmp/project/outputs/report.xlsx"}'} />, ); expect(html).toContain(":codex-file-citation"); expect(html).not.toContain("chat-markdown-file-link"); }); it("does not create a nested link for citations inside link text", () => { const directive = ':codex-file-citation{path="/tmp/project/outputs/report.xlsx"}'; const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={`[See ${directive}](https://example.com)`} />, ); const renderedText = html.replace(/<[^>]+>/g, ""); expect(renderedText).toContain("codex-file-citation"); expect(html).not.toContain("chat-markdown-file-link"); }); it("renders file citations created by over-indented list recovery", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={'- Created :codex-file-citation{path="/tmp/project/outputs/report.xlsx"}'} />, ); expect(html).not.toContain("<pre>"); expect(html).toContain("Created "); expect(html).toContain("chat-markdown-file-link"); expect(html).toContain("report.xlsx"); }); it("disambiguates Codex citations with the same basename", () => { const html = renderToStaticMarkup( <ChatMarkdown cwd="/tmp/project" text={ 'Changed :codex-file-citation{path="/tmp/project/src/index.ts"} and :codex-file-citation{path="/tmp/project/test/index.ts"}.'