"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); let buildBundle; let validateBundle; let preparedMapDocument; let attachmentUrls; let replaceAttachmentUrls; test.before(async () => { ({ buildBundle, validateBundle, preparedMapDocument, attachmentUrls, replaceAttachmentUrls } = await import("../static/cmap/model/interchange.js")); }); const conceptId = "24d27086-9f8b-4b57-a7fb-47b513277555"; const childId = "53ef1cf3-c936-48f7-889a-ad644fab9321"; const styledItem = { id: 10, conceptId, kind: "concept", x: 127.5, y: 88, width: 312, height: 97, backgroundColor: "#ffe5a8", borderColor: "#a97c00", textColor: "#172033", fontFamily: "Georgia", fontSize: 13.5, fontWeight: "700", synopsisFontSize: 9.5 }; const root = { slug: "architecture", title: "Architecture", document: { schemaVersion: 2, metadata: { explanationPageSlug: "cmap:architecture" }, concepts: [{ id: conceptId, label: "Process", synopsis: "Shared content", aspects: ["TODO"], pageSlug: "process:details", cmapSlug: "child", externalUrl: "https://example.com/process" }], items: [styledItem, { id: 11, kind: "phrase", label: "contains", x: 500, y: 90 }], connectors: [{ id: 1, sourceId: 10, targetId: 11, hasArrow: true }] } }; const child = { slug: "child", title: "Child", document: { schemaVersion: 2, concepts: [{ id: childId, label: "Child concept" }], items: [{ id: 1, conceptId: childId, kind: "concept", x: 20, y: 30 }], connectors: [] } }; const pages = new Map([ ["cmap:architecture", { slug: "cmap:architecture", title: "Explanation", markdown: "# Explanation", tags: ["cmap"] }], ["process:details", { slug: "process:details", title: "Details", markdown: "Full page\n\n![Diagram](/uploads/process:details/1700000-diagram.png)", tags: [] }] ]); const exportedAttachment = { mimeType: "image/png", contentBase64: "aW1hZ2U=" }; const loadAttachment = async (url) => { assert.equal(url, "/uploads/process:details/1700000-diagram.png"); return exportedAttachment; }; test("bundle separates shared content while preserving complete diagram layout", async () => { const bundle = await buildBundle({ rootMap: root, maxDepth: 1, exportedAt: "2026-08-25T12:00:00.000Z", generator: "test", loadConceptMap: async (slug) => ({ child }[slug]), loadWikiPage: async (reference) => pages.get(reference), loadAttachment }); assert.equal(bundle.format, "racket-wiki-cmap-bundle"); assert.deepEqual(bundle.cmaps.map((cmap) => cmap.slug), ["architecture", "child"]); assert.deepEqual(bundle.pages.map((page) => page.reference), ["cmap:architecture", "process:details"]); assert.equal(bundle.concepts.find((concept) => concept.id === conceptId).synopsis, "Shared content"); assert.equal(bundle.concepts.find((concept) => concept.id === conceptId).externalUrl, "https://example.com/process"); const exportedItem = bundle.cmaps[0].document.items[0]; assert.equal(exportedItem.x, 127.5); assert.equal(exportedItem.fontSize, 13.5); assert.equal(exportedItem.backgroundColor, "#ffe5a8"); assert.equal(exportedItem.synopsis, undefined, "shared content is not duplicated on a placement"); assert.deepEqual(bundle.cmaps[0].document.connectors, root.document.connectors); assert.deepEqual(bundle.pages.find((page) => page.reference === "process:details").attachments, [{ url: "/uploads/process:details/1700000-diagram.png", name: "1700000-diagram.png", mimeType: "image/png", contentBase64: "aW1hZ2U=" }]); }); test("prepared import document rejoins concepts and placements without changing layout", async () => { const bundle = await buildBundle({ rootMap: root, maxDepth: 0, loadConceptMap: async () => { throw new Error("not loaded"); }, loadWikiPage: async (reference) => pages.get(reference), loadAttachment }); const documentValue = preparedMapDocument(bundle, bundle.cmaps[0]); assert.equal(documentValue.concepts[0].synopsis, "Shared content"); assert.equal(documentValue.concepts[0].externalUrl, "https://example.com/process"); assert.equal(documentValue.items[0].width, 312); assert.equal(documentValue.items[0].fontFamily, "Georgia"); assert.deepEqual(documentValue.connectors, root.document.connectors); }); test("a derived CMap always carries its source map as a layout dependency", async () => { const derived = { slug: "derived", title: "Derived", document: { schemaVersion: 2, derivedView: { sourceCmapSlug: "architecture", rootItemId: 10 }, concepts: [], items: [], connectors: [] } }; const bundle = await buildBundle({ rootMap: derived, maxDepth: 0, loadConceptMap: async (slug) => { assert.equal(slug, "architecture"); return root; }, loadWikiPage: async (reference) => pages.get(reference), loadAttachment }); assert.deepEqual(bundle.cmaps.map((cmap) => cmap.slug), ["derived", "architecture"]); assert.equal(bundle.rootCmapSlug, "derived"); assert.equal(bundle.cmaps[1].document.items[0].x, 127.5); }); test("temporary ids are accepted only when their concepts are placed", () => { const bundle = { format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "generated", pages: [], missing: { cmaps: [], pages: [] }, concepts: [{ id: "new:generated-concept", label: "Generated concept" }], cmaps: [{ slug: "generated", title: "Generated", document: { concepts: [{ id: "new:generated-concept" }], items: [{ id: 1, conceptId: "new:generated-concept", kind: "concept", x: 10, y: 20 }], connectors: [] }}] }; assert.equal(validateBundle(bundle), bundle); const orphan = structuredClone(bundle); orphan.cmaps[0].document.items = []; assert.throws(() => validateBundle(orphan), /must occur as a diagram placement/); }); test("invalid connector endpoints and duplicate slugs are rejected", () => { const bundle = { format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "map", concepts: [{ id: conceptId, label: "Concept" }], pages: [], cmaps: [ { slug: "map", title: "Map", document: { concepts: [{ id: conceptId }], items: [{ id: 1, conceptId, x: 0, y: 0 }], connectors: [{ sourceId: 1, targetId: 99 }] } }, { slug: "map", title: "Duplicate", document: { concepts: [], items: [], connectors: [] } } ] }; assert.throws(() => validateBundle(bundle), /unknown item[\s\S]*duplicated/); }); test("bundle export omits connectors whose endpoint no longer exists", async () => { const mapWithDanglingConnectors = structuredClone(root); mapWithDanglingConnectors.document.connectors.push( { id: 2, sourceId: 10, targetId: 99 }, { id: 3, sourceId: 98, targetId: 11 } ); const bundle = await buildBundle({ rootMap: mapWithDanglingConnectors, maxDepth: 0, loadConceptMap: async () => { throw new Error("not loaded"); }, loadWikiPage: async (reference) => pages.get(reference), loadAttachment }); assert.deepEqual(bundle.cmaps[0].document.connectors, root.document.connectors); }); test("external concept links only accept complete http or https URLs", () => { const bundle = { format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "map", concepts: [{ id: conceptId, label: "Concept", externalUrl: "javascript:alert(1)" }], pages: [], cmaps: [{ slug: "map", title: "Map", document: { concepts: [{ id: conceptId }], items: [{ id: 1, conceptId, x: 0, y: 0 }], connectors: [] }}] }; assert.throws(() => validateBundle(bundle), /externalUrl: must be a complete http or https URL/); bundle.concepts[0].externalUrl = "https://example.com/path"; assert.equal(validateBundle(bundle), bundle); }); test("attachment URLs are detected once and can be rewritten after upload", () => { const oldUrl = "/uploads/process:details/1700000-diagram.png"; const markdown = `![Diagram](${oldUrl})\n`; assert.deepEqual(attachmentUrls(markdown), [oldUrl]); assert.equal( replaceAttachmentUrls(markdown, new Map([[oldUrl, "/uploads/process:details/new-diagram.png"]])), "![Diagram](/uploads/process:details/new-diagram.png)\n" + "" ); assert.deepEqual( attachmentUrls("![Diagram](/uploads/page/1700000-system overview.png)"), ["/uploads/page/1700000-system overview.png"] ); }); test("invalid embedded attachment content is rejected", () => { const bundle = { format: "racket-wiki-cmap-bundle", formatVersion: 1, rootCmapSlug: "map", concepts: [{ id: conceptId, label: "Concept" }], cmaps: [{ slug: "map", title: "Map", document: { concepts: [{ id: conceptId }], items: [{ id: 1, conceptId, x: 0, y: 0 }], connectors: [] }}], pages: [{ reference: "page", title: "Page", markdown: "", tags: [], attachments: [{ url: "/uploads/page/file.png", name: "file.png", mimeType: "image/png", contentBase64: "not base64!" }] }] }; assert.throws(() => validateBundle(bundle), /contentBase64: must be valid base64/); });