/* Copyright 2026 Marimo. All rights reserved. */ import { describe, expect, it } from "vitest"; import { cellId } from "@/__tests__/branded"; import type { CellErrorEntry } from "@/core/errors/error-entries"; import type { EnvironmentInfo } from "@/core/network/types"; import { buildBugReportUrl, createPartialEnvironment, enrichEnvironment, formatCodeSection, formatEnvironmentSection, formatErrorsSection, markdownCodeFence, MAX_PREFILL_URL_LENGTH, } from "../issue-details"; const environment: EnvironmentInfo = { marimo: "1.2.3", editable: false, location: "~/.venv/site-packages/marimo", OS: "Darwin", "OS Version": "25.0", Processor: "arm", "Python Version": "3.12.9", Locale: "en_US", Binaries: { Browser: "chrome 140", Node: "v22", uv: "0.11" }, Dependencies: { click: "8.4.2" }, "Optional Dependencies": { pandas: "3.0.0" }, "Experimental Flags": {}, }; describe("enrichEnvironment", () => { it("replaces server Chrome detection with the active browser", () => { const result = enrichEnvironment(environment, "Firefox/140"); expect(result.Binaries.Browser).toBe("Firefox/140"); expect(result.Binaries.Node).toBe("v22"); }); it("does not mutate the input", () => { enrichEnvironment(environment, "Firefox/140"); expect(environment.Binaries.Browser).toBe("chrome 140"); }); }); describe("createPartialEnvironment", () => { it("records the collection error and keeps the active browser", () => { const partial = createPartialEnvironment( "1.2.3", "Firefox/140", "en_US", "Server environment information unavailable", ); expect(partial.marimo).toBe("1.2.3"); expect(partial.Binaries?.Browser).toBe("Firefox/140"); expect(partial.Locale).toBe("en_US"); expect(partial["Environment Collection Error"]).toBe( "Server environment information unavailable", ); }); it("omits empty fields rather than filling placeholders", () => { const partial = createPartialEnvironment( "1.2.3", "Firefox/140", "", "boom", ); expect(partial.Locale).toBeUndefined(); expect(partial.location).toBeUndefined(); expect(partial.OS).toBeUndefined(); expect(partial.Dependencies).toBeUndefined(); }); }); describe("markdownCodeFence", () => { it("uses a fence longer than any backtick run in the content", () => { const block = markdownCodeFence("python", 'text = "```"'); expect(block.startsWith("````python\n")).toBe(true); expect(block.endsWith("\n````")).toBe(true); expect(block).toContain('text = "```"'); }); it("uses a minimum fence of three backticks", () => { const block = markdownCodeFence("json", "{}"); expect(block.startsWith("```json\n")).toBe(true); expect(block.endsWith("\n```")).toBe(true); }); }); describe("formatEnvironmentSection", () => { it("wraps the environment JSON in a collapsible section", () => { const markdown = formatEnvironmentSection(environment); expect(markdown).toContain("Environment"); expect(markdown).toContain('"marimo": "1.2.3"'); }); }); describe("formatErrorsSection", () => { it("wraps errors in a collapsible section without cell source", () => { const errors: CellErrorEntry[] = [ { cellId: cellId("cell-1"), cellName: "Cell 1", cellCode: "password = 'private'", errorData: [], tracebackHtml: 'ValueError: bad value', }, ]; const markdown = formatErrorsSection(errors); expect(markdown).toContain("Errors"); expect(markdown).toContain("```text\n"); expect(markdown).toContain("ValueError: bad value"); expect(markdown).not.toContain("password"); }); }); describe("formatCodeSection", () => { it("wraps the code in a collapsible python section", () => { const markdown = formatCodeSection("import marimo"); expect(markdown).toContain("Code"); expect(markdown).toContain("```python\nimport marimo"); }); }); describe("buildBugReportUrl", () => { const baseUrl = "https://github.com/marimo-team/marimo/issues/new?template=bug_report.yaml"; it("prefills each field keyed by its template id", () => { const { url, omitted } = buildBugReportUrl(baseUrl, { env: "hello world", "bug-description": "boom", }); expect(url).toBe(`${baseUrl}&env=hello%20world&bug-description=boom`); expect(omitted).toEqual([]); }); it("appends fields with a query separator when the base URL has none", () => { const { url } = buildBugReportUrl("https://example.com/new", { env: "x" }); expect(url).toBe("https://example.com/new?env=x"); }); it("skips empty fields", () => { const { url } = buildBugReportUrl(baseUrl, { env: "x", "bug-description": "", }); expect(url).toBe(`${baseUrl}&env=x`); expect(url).not.toContain("bug-description"); }); it("returns the base URL when no fields have content", () => { const { url } = buildBugReportUrl(baseUrl, { env: "" }); expect(url).toBe(baseUrl); }); it("encodes markdown so it survives as a single query param", () => { const details = formatEnvironmentSection(environment); const { url } = buildBugReportUrl(baseUrl, { env: details }); expect(url).toContain("&env="); expect(new URL(url).searchParams.get("env")).toBe(details); }); it("keeps earlier fields and omits only the oversized later one", () => { const huge = "x".repeat(MAX_PREFILL_URL_LENGTH); const { url, omitted } = buildBugReportUrl(baseUrl, { env: "small", "reproduction-code": huge, }); expect(url).toBe(`${baseUrl}&env=small`); expect(omitted).toEqual(["reproduction-code"]); }); });