/** * Tests for mermaid_format.ts — the shared agent Mermaid envelope helpers. */ import { describe, expect, it } from "vitest"; import { formatErDiagram, formatGraph, formatPhaseFlow, sanitizeLabel, sanitizeNodeId, type ClassDef, type MermaidBlock, type PhaseEdge, type PhaseNode, } from "../mermaid_format.js"; describe("sanitizeLabel", () => { it("escapes characters Mermaid treats specially inside labels", () => { expect(sanitizeLabel('Hello "world" [1] | &')).toBe( "Hello "world" [1] | &", ); }); it("collapses newlines to a single space", () => { expect(sanitizeLabel("a\n\nb\r\nc")).toBe("a b c"); }); it("trims leading/trailing whitespace", () => { expect(sanitizeLabel(" x ")).toBe("x"); }); }); describe("sanitizeNodeId", () => { it("replaces unsafe chars with underscores and collapses runs", () => { expect(sanitizeNodeId("a.b-c d/e")).toBe("a_b_c_d_e"); }); it("prefixes leading digits with underscore", () => { expect(sanitizeNodeId("123abc")).toBe("_123abc"); }); it("returns `_` for empty or all-special input", () => { expect(sanitizeNodeId("")).toBe("_"); expect(sanitizeNodeId("---")).toBe("_"); }); }); describe("formatGraph", () => { it("produces a valid graph TB envelope", () => { const block: MermaidBlock = formatGraph( "TB", "Infra Topology", [ { id: "vpc-1", label: "VPC 1" }, { id: "lambda-hello", label: "hello()" }, ], [{ from: "vpc-1", to: "lambda-hello", label: "contains" }], ); expect(block.type).toBe("graph TB"); expect(block.title).toBe("Infra Topology"); expect(block.code).toContain("graph TB"); expect(block.code).toContain("vpc_1"); expect(block.code).toContain("lambda_hello"); expect(block.code).toContain('-->|contains|'); }); it("omits the edge label when not provided", () => { const block = formatGraph( "LR", "Deps", [ { id: "a", label: "A" }, { id: "b", label: "B" }, ], [{ from: "a", to: "b" }], ); expect(block.code).toContain("a --> b"); expect(block.code).not.toMatch(/-->\|/); }); it("dedupes repeated nodes", () => { const block = formatGraph( "TB", "T", [ { id: "x", label: "X" }, { id: "x", label: "X'" }, ], [], ); const xLines = block.code.split("\n").filter((l) => l.includes('x["')); expect(xLines.length).toBe(1); }); }); describe("formatErDiagram", () => { it("emits table blocks and relationships", () => { const block = formatErDiagram( "UserService", [ { name: "users", columns: [ { name: "id", type: "bigint", key: "PK" }, { name: "email", type: "varchar" }, ], }, { name: "orders", columns: [ { name: "id", type: "bigint", key: "PK" }, { name: "user_id", type: "bigint", key: "FK" }, ], }, ], [ { from: "users", to: "orders", cardinality: "||--o{", label: "places", }, ], ); expect(block.type).toBe("erDiagram"); expect(block.code).toContain("erDiagram"); expect(block.code).toContain("users {"); expect(block.code).toContain("bigint id PK"); expect(block.code).toContain("bigint user_id FK"); expect(block.code).toMatch(/users \|\|--o\{ orders :/); }); it("handles tables with no columns and no relationships", () => { const block = formatErDiagram("Empty", [], []); expect(block.code).toBe("erDiagram"); }); }); describe("formatPhaseFlow", () => { it("emits the flowchart TD header and quoted-bracket rectangles", () => { const nodes: PhaseNode[] = [ { id: "p1", label: "Phase 1: Detect" }, { id: "p2", label: "Phase 2: Discover" }, ]; const edges: PhaseEdge[] = [{ from: "p1", to: "p2" }]; const block = formatPhaseFlow("pipeline", nodes, edges); expect(block.type).toBe("flowchart TD"); expect(block.title).toBe("pipeline"); expect(block.code.startsWith("flowchart TD\n")).toBe(true); expect(block.code).toContain('p1["Phase 1: Detect"]'); expect(block.code).toContain('p2["Phase 2: Discover"]'); expect(block.code).toContain("p1 --> p2"); }); it("renders diamond shape for decision nodes", () => { const nodes: PhaseNode[] = [ { id: "gate", label: "over budget?", shape: "diamond" }, { id: "abort", label: "Abort" }, { id: "go", label: "Continue" }, ]; const edges: PhaseEdge[] = [ { from: "gate", to: "abort", label: "yes" }, { from: "gate", to: "go", label: "no" }, ]; const block = formatPhaseFlow("decision", nodes, edges); expect(block.code).toContain('gate{"over budget?"}'); expect(block.code).toContain("gate -- yes --> abort"); expect(block.code).toContain("gate -- no --> go"); }); it("converts real newlines in labels to
", () => { const block = formatPhaseFlow( "multi-line", [{ id: "n", label: "line one\nline two\nline three" }], [], ); expect(block.code).toContain('n["line one
line two
line three"]'); expect(block.code).not.toMatch(/line one\nline two/); }); it("emits classDef blocks and groups class assignments by className", () => { const nodes: PhaseNode[] = [ { id: "a", label: "A", className: "det" }, { id: "b", label: "B", className: "llm" }, { id: "c", label: "C", className: "det" }, { id: "d", label: "D" }, ]; const classDefs: ClassDef[] = [ { name: "det", fill: "#d4edda", stroke: "#155724" }, { name: "llm", fill: "#fff3cd", stroke: "#856404" }, ]; const block = formatPhaseFlow("classes", nodes, [], classDefs); expect(block.code).toContain("classDef det fill:#d4edda,stroke:#155724"); expect(block.code).toContain("classDef llm fill:#fff3cd,stroke:#856404"); // a and c share className "det" — should be on one line, not two. expect(block.code).toContain("class a,c det"); expect(block.code).toContain("class b llm"); // d has no className → no class line for it. expect(block.code).not.toMatch(/class d /); }); it("dedupes repeated node ids", () => { const nodes: PhaseNode[] = [ { id: "p1", label: "First" }, { id: "p1", label: "Duplicate" }, ]; const block = formatPhaseFlow("dedupe", nodes, []); const p1Lines = block.code.split("\n").filter((l) => l.includes('p1["')); expect(p1Lines.length).toBe(1); expect(block.code).toContain('p1["First"]'); }); it("escapes special characters inside labels", () => { const block = formatPhaseFlow( "escape", [{ id: "n", label: 'has "quote" and [bracket] and | pipe' }], [], ); expect(block.code).toContain(""quote""); expect(block.code).toContain("[bracket]"); expect(block.code).toContain("| pipe"); }); it("omits the classDef section entirely when no classDefs are passed", () => { const block = formatPhaseFlow( "no-classes", [{ id: "n", label: "n" }], [], ); expect(block.code).not.toContain("classDef"); expect(block.code).not.toMatch(/^\s*class /m); }); });