import { EnvironmentId } from "@t3tools/contracts"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it, vi } from "vite-plus/test"; vi.mock("@effect/atom-react", () => ({ useAtomValue: () => null })); vi.mock("../hooks/useTheme", () => ({ useTheme: () => ({ resolvedTheme: "dark" }) })); 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()), usePreparedConnection: () => ({ _tag: "Loading" }), })); vi.mock("../state/entities", () => ({ readThreadShell: () => null, useProjects: () => [], })); vi.mock("../remoteOpen", () => ({ useRemoteOpenResolution: () => ({ state: { mode: "local-exec" }, isResolved: true }), })); vi.mock("../editorPreferences", () => ({ useOpenInPreferredEditor: () => vi.fn(), usePreferredEditor: () => [null, vi.fn()], })); vi.mock("~/lib/openPullRequestLink", () => ({ findProjectForChangeRequest: () => undefined, matchesLinkedPullRequestUrl: () => false, parseChangeRequestUrl: () => null, useOpenChangeRequestLink: () => vi.fn(), })); import ChatMarkdown, { canUseMarkdownFileShellActions, hasMarkdownFilePrimaryAction, orderedListGutterStyle, shouldUseMarkdownFileBrowserPrimaryAction, } from "./ChatMarkdown"; 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 file option chips", () => { it("keeps the fallback button text selectable", () => { const html = renderToStaticMarkup( , ); expect(html).toContain(" { const html = renderToStaticMarkup( , ); 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( , ); 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(); 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( , ); 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( , ); 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( , ); 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( , ); expect(html).not.toContain("
");
    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(
      ,
    );

    expect(html).toContain("index.ts · project/src");
    expect(html).toContain("index.ts · project/test");
  });

  it("preserves rejected citations created by over-indented list recovery", () => {
    const malformedHtml = renderToStaticMarkup(
      ,
    );
    const nestedLinkHtml = renderToStaticMarkup(
      ,
    );
    const nestedLinkText = nestedLinkHtml.replace(/<[^>]+>/g, "");

    expect(malformedHtml).toContain(
      "
  • Bad :codex-file-citation{purpose="output"}
  • ", ); expect(nestedLinkText).toContain( "Bad :codex-file-citation{path="/tmp/project/report.xlsx"}", ); }); }); const ARTIFACT_TEMPLATE_DIRECTIVE = '::artifact-template{skill_name="artifact-template-hello-world" skill_directory="/Users/test/.codex/skills/artifact-template-hello-world" display_name="Hello World" artifact_kind="document"}'; describe("ChatMarkdown artifact-template cards", () => { it.each([true, false])("renders the Codex result card with parseRawHtml=%s", (parseRawHtml) => { const html = renderToStaticMarkup( undefined} />, ); expect(html).not.toContain("::artifact-template"); expect(html).toContain("chat-markdown-artifact-template"); expect(html).toContain('data-artifact-kind="document"'); expect(html).toContain('data-markdown-copy="Hello World (Document template)\n\n"'); expect(html).toContain('data-skill-name="artifact-template-hello-world"'); expect(html).toContain("Hello World"); expect(html).toContain("Document template"); expect(html).toContain("Use template"); expect(html).not.toContain("

    { const html = renderToStaticMarkup( , ); expect(html).toContain("chat-markdown-artifact-template"); expect(html).not.toContain("Use template"); }); it("leaves malformed and unfinished artifact-template directives literal", () => { const malformed = '::artifact-template{skill_name="artifact-template-hello-world" display_name="Hello World" artifact_kind="document"}'; const unfinished = ARTIFACT_TEMPLATE_DIRECTIVE.slice(0, -1); for (const text of [malformed, unfinished]) { const html = renderToStaticMarkup(); expect(html).toContain("::artifact-template"); expect(html).not.toContain("chat-markdown-artifact-template"); } }); it("leaves escaped and similarly named artifact-template directives literal", () => { for (const text of [ `\\${ARTIFACT_TEMPLATE_DIRECTIVE}`, ARTIFACT_TEMPLATE_DIRECTIVE.replace("::artifact-template", "::artifact-template-extra"), ]) { const html = renderToStaticMarkup(); expect(html).toContain("::artifact-template"); expect(html).not.toContain("chat-markdown-artifact-template"); } }); it("preserves artifact-template examples inside code", () => { const html = renderToStaticMarkup( , ); expect(html.match(/::artifact-template/g)).toHaveLength(2); expect(html).not.toContain("chat-markdown-artifact-template"); }); }); describe("shouldUseMarkdownFileBrowserPrimaryAction", () => { it("uses the browser when it is the only available primary action", () => { expect( shouldUseMarkdownFileBrowserPrimaryAction({ iconPath: "/tmp/report.html", canOpenInEditor: false, canOpenInBrowser: true, canOpenInPanel: false, }), ).toBe(true); }); it("preserves the normal editor and panel defaults for HTML files", () => { expect( shouldUseMarkdownFileBrowserPrimaryAction({ iconPath: "/tmp/report.html", canOpenInEditor: true, canOpenInBrowser: true, canOpenInPanel: false, }), ).toBe(false); expect( shouldUseMarkdownFileBrowserPrimaryAction({ iconPath: "/tmp/report.html", canOpenInEditor: false, canOpenInBrowser: true, canOpenInPanel: true, }), ).toBe(false); }); it("continues to open PDF files in the browser by default", () => { expect( shouldUseMarkdownFileBrowserPrimaryAction({ iconPath: "/tmp/report.pdf", canOpenInEditor: true, canOpenInBrowser: true, canOpenInPanel: true, }), ).toBe(true); }); }); describe("orderedListGutterStyle", () => { it("leaves the default gutter alone for single-digit lists", () => { expect(orderedListGutterStyle(9, undefined)).toBeUndefined(); }); it("leaves the default gutter alone for two-digit lists", () => { expect(orderedListGutterStyle(99, undefined)).toBeUndefined(); }); it("leaves the default gutter alone for a two-digit list that starts above 1", () => { // start=50 + 49 items => last marker is "98", still two digits. expect(orderedListGutterStyle(49, 50)).toBeUndefined(); }); it("widens the gutter once the last marker reaches three digits", () => { // item 100 is the bug from #6512: a 100-item list starting at 1. expect(orderedListGutterStyle(100, undefined)).toEqual({ "--list-gutter": "4ch" }); }); it("accounts for a non-default start attribute", () => { // start=95 + 9 items => last marker is "103", three digits. expect(orderedListGutterStyle(9, 95)).toEqual({ "--list-gutter": "4ch" }); expect(orderedListGutterStyle(5, "999995")).toEqual({ "--list-gutter": "7ch" }); }); it("scales further for four-digit markers", () => { expect(orderedListGutterStyle(1000, undefined)).toEqual({ "--list-gutter": "5ch" }); }); it("uses the widest marker and includes a negative start's minus sign", () => { expect(orderedListGutterStyle(1001, -1000)).toEqual({ "--list-gutter": "6ch" }); expect(orderedListGutterStyle(3, -15)).toEqual({ "--list-gutter": "4ch" }); expect(orderedListGutterStyle(3, -5)).toBeUndefined(); }); it("treats a missing/zero item count as a single item", () => { expect(orderedListGutterStyle(0, undefined)).toBeUndefined(); expect(orderedListGutterStyle(0, 100)).toEqual({ "--list-gutter": "4ch" }); }); }); describe("ChatMarkdown Windows file links", () => { const environmentId = EnvironmentId.make("env-windows"); it.each([true, false])("preserves drive paths with parseRawHtml=%s", (parseRawHtml) => { const html = renderToStaticMarkup( , ); expect(html).toContain('href="C:/Users/shawn/project/src/main.ts"'); expect(html).toContain("chat-markdown-file-link"); }); it.each([true, false])("normalizes backslashes with parseRawHtml=%s", (parseRawHtml) => { const html = renderToStaticMarkup( , ); expect(html).toContain('href="C:/Users/shawn/project/src/main.ts"'); expect(html).toContain("chat-markdown-file-link"); }); it.each([true, false])( "distinguishes same-named backslash paths with parseRawHtml=%s", (parseRawHtml) => { const html = renderToStaticMarkup( , ); expect(html).toContain("index.ts · project/src"); expect(html).toContain("index.ts · project/test"); }, ); it.each([true, false])( "does not disambiguate the same file in links and inline code with parseRawHtml=%s", (parseRawHtml) => { const path = String.raw`C:\Users\shawn\project\src\main.ts`; const html = renderToStaticMarkup( , ); expect(html.match(/chat-markdown-file-link/g)).toHaveLength(2); expect(html).not.toContain("main.ts ·"); }, ); it.each([true, false])("preserves reference links with parseRawHtml=%s", (parseRawHtml) => { const html = renderToStaticMarkup( , ); expect(html).toContain('href="C:/Users/shawn/project/src/main.ts"'); expect(html).toContain("chat-markdown-file-link"); }); it.each([true, false])("still rejects unsafe schemes with parseRawHtml=%s", (parseRawHtml) => { const html = renderToStaticMarkup( , ); expect(html).not.toContain("javascript:"); expect(html).not.toContain("d:alert"); expect(html).not.toContain("chat-markdown-file-link"); }); });