import test from "node:test"; import assert from "node:assert/strict"; import { Concept, ConceptOwnership, ConceptRelation, ConceptRepository } from "../static/js/cmap/model/concept-repository.js"; import { CmapModel, ConceptMap, ConceptMapConcept, ConceptMapConnector, ConceptMapPhrase } from "../static/js/cmap/model/concept-map.js"; import { buildBundle } from "../static/js/cmap/model/interchange.js"; import { CmapReferenceIndex } from "../static/js/cmap/model/cmap-reference-index.js"; const firstId = "11111111-1111-4111-8111-111111111111"; const secondId = "22222222-2222-4222-8222-222222222222"; const thirdId = "33333333-3333-4333-8333-333333333333"; function sampleDocument() { return { schemaVersion: 2, metadata: { namespace: "", tags: ["architecture"], summary: "System overview", explanationPageSlug: "cmap:architecture" }, concepts: [{ id: firstId, label: "Application", synopsis: "Runs the application", aspects: ["runtime"], tags: [{ type: "person", value: "Hans" }], descriptionPageSlug: "cmap:application", pageSlug: "application", cmapSlug: null, externalUrl: "https://example.com/application", imageSource: "" }], items: [{ id: 1, conceptId: firstId, kind: "concept", parentCmapLink: false, groupId: null, childMap: null, parentSubmapId: null, submapDepth: 0, expanded: false, submapInitialized: false, separateMap: false, mapReference: null, hiddenContexts: [], layouts: {}, backgroundColor: "#ffffff", borderColor: "#333333", submapBackgroundColor: null, submapBorderColor: null, textColor: "#111111", fontFamily: "Arial", fontSize: "11pt", fontWeight: "700", fontStyle: "normal", synopsisTextColor: "#444444", synopsisFontFamily: "Arial", synopsisFontSize: "9pt", synopsisFontWeight: "400", synopsisFontStyle: "normal", width: 220, height: 70, autoWidth: false, autoHeight: false, x: 80, y: 90 }, { id: 2, conceptId: null, kind: "phrase", label: "uses", synopsis: "", parentCmapLink: false, groupId: null, childMap: null, parentSubmapId: null, submapDepth: 0, expanded: false, submapInitialized: false, separateMap: false, mapReference: null, hiddenContexts: [], layouts: {}, backgroundColor: "#ffffff", borderColor: "transparent", submapBackgroundColor: null, submapBorderColor: null, textColor: "#111111", fontFamily: "Arial", fontSize: "11pt", fontWeight: "700", fontStyle: "normal", synopsisTextColor: "#444444", synopsisFontFamily: "Arial", synopsisFontSize: "9pt", synopsisFontWeight: "400", synopsisFontStyle: "normal", width: 145, height: 36, autoWidth: true, autoHeight: true, x: 330, y: 100 }], connectors: [{ id: 1, sourceId: 1, targetId: 2, hasArrow: false, lineColor: "#333", lineWidth: 2 }], conceptMaps: [] }; } test("CmapModel round-trips repository content, placements, phrases and connectors", () => { const document = sampleDocument(); const model = CmapModel.fromDocument(document); assert.ok(model.repository.concept(firstId) instanceof Concept); assert.ok(model.conceptMap.item(1) instanceof ConceptMapConcept); assert.ok(model.conceptMap.item(2) instanceof ConceptMapPhrase); assert.ok(model.conceptMap.connector(1) instanceof ConceptMapConnector); assert.deepEqual(model.toDocument(), document); }); test("two placements share one repository concept without sharing appearance", () => { const model = CmapModel.fromDocument(sampleDocument()); model.conceptMap.addItem(new ConceptMapConcept(3, firstId, { x: 600, y: 300, width: 180, height: 60, backgroundColor: "#ffeeaa" })); model.repository.concept(firstId).update({ label: "Web application" }); assert.equal(model.repository.concept(firstId).label, "Web application"); assert.equal(model.conceptMap.item(1).backgroundColor, "#ffffff"); assert.equal(model.conceptMap.item(3).backgroundColor, "#ffeeaa"); assert.equal(model.toDocument().concepts.length, 1); }); test("semantic relations and ownership remain independent of map connectors", () => { const repository = new ConceptRepository([ { id: firstId, label: "Parent" }, { id: secondId, label: "Child" } ]); repository.addRelation(new ConceptRelation("contains", firstId, secondId, { label: "contains" })); repository.addOwnership(new ConceptOwnership(firstId, secondId)); const conceptMap = new ConceptMap(); conceptMap.addItem(new ConceptMapConcept(1, firstId, { x: 0, y: 0 })); conceptMap.addItem(new ConceptMapConcept(2, secondId, { parentSubmapId: 1, x: 50, y: 80 })); conceptMap.addConnector(new ConceptMapConnector(1, 1, 2, { relationId: "contains" })); const document = new CmapModel(repository, conceptMap).toDocument(); assert.equal(document.conceptRelations[0].id, "contains"); assert.equal(document.conceptOwnerships[0].parentConceptId, firstId); assert.equal(document.items[1].parentSubmapId, 1); assert.equal(document.connectors[0].relationId, "contains"); }); test("concept ownership rejects cycles", () => { const repository = new ConceptRepository([ { id: firstId, label: "First" }, { id: secondId, label: "Second" } ]); repository.addOwnership({ parentConceptId: firstId, childConceptId: secondId }); assert.throws(() => repository.addOwnership({ parentConceptId: secondId, childConceptId: firstId }), /cycle/); }); test("an embedded submap becomes an independent map linked from its owner", () => { const document = { schemaVersion: 2, metadata: { tags: [], summary: "Parent", explanationPageSlug: "" }, concepts: [ { id: firstId, label: "Owner", synopsis: "Nested subject", aspects: ["design"] }, { id: secondId, label: "Nested concept" }, { id: thirdId, label: "Outside concept" } ], conceptOwnerships: [ { parentConceptId: firstId, childConceptId: secondId }, { parentConceptId: firstId, childConceptId: thirdId } ], items: [ { id: 1, conceptId: firstId, kind: "submap", x: 100, y: 100, submapDepth: 0 }, { id: 2, conceptId: secondId, kind: "concept", x: 240, y: 260, parentSubmapId: 1, submapDepth: 1 }, { id: 3, kind: "phrase", label: "contains", x: 450, y: 270, parentSubmapId: 1, submapDepth: 1 }, { id: 4, conceptId: thirdId, kind: "concept", x: 20, y: 100, submapDepth: 0 } ], connectors: [ { id: 1, sourceId: 2, targetId: 3 }, { id: 2, sourceId: 4, targetId: 2 }, { id: 3, sourceId: 1, targetId: 2 } ], conceptMaps: [] }; const extraction = CmapModel.fromDocument(document) .extractSubmap(1, "nested-subject"); const parentDocument = extraction.parentModel.toDocument(); const childDocument = extraction.childModel.toDocument(); assert.deepEqual(parentDocument.items.map((item) => item.id), [1, 4]); assert.equal(parentDocument.items[0].kind, "concept"); assert.equal(parentDocument.concepts .find((concept) => concept.id === firstId).cmapSlug, "nested-subject"); assert.deepEqual(parentDocument.connectors.map((connector) => ({ sourceId: connector.sourceId, targetId: connector.targetId })), [{ sourceId: 4, targetId: 1 }]); assert.deepEqual(parentDocument.conceptOwnerships, [ { parentConceptId: firstId, childConceptId: thirdId } ]); assert.deepEqual(childDocument.items.map((item) => item.id), [2, 3]); assert.ok(childDocument.items.every((item) => item.parentSubmapId === null)); assert.deepEqual(childDocument.concepts.map((concept) => concept.id), [secondId]); assert.deepEqual(childDocument.connectors.map((connector) => connector.id), [1]); assert.equal(childDocument.conceptOwnerships, undefined); assert.equal(childDocument.derivedView, undefined); assert.equal(childDocument.metadata.summary, "Nested subject"); const convertedDerivedMap = CmapModel.fromDocument(document).extractSubmap( 1, "nested-subject", { tags: ["retained"], summary: "Edited child metadata", explanationPageSlug: "" } ); assert.deepEqual(convertedDerivedMap.childModel.metadata(), { namespace: "", tags: ["retained"], summary: "Edited child metadata", explanationPageSlug: "" }); }); test("a model document is accepted by the complete JSON bundle export", async () => { const model = CmapModel.fromDocument(sampleDocument()); const bundle = await buildBundle({ rootMap: { slug: "architecture", title: "Architecture", document: model.toDocument() }, maxDepth: 0, exportedAt: "2026-09-01T12:00:00.000Z", loadConceptMap: async () => { throw new Error("not requested"); }, loadWikiPage: async (reference) => ({ slug: reference, title: reference, markdown: "Description", tags: [] }), loadAttachment: async () => { throw new Error("not requested"); } }); assert.equal(bundle.rootCmapSlug, "architecture"); assert.deepEqual(bundle.cmaps[0].document.items, model.toDocument().items); assert.equal(bundle.concepts[0].label, "Application"); }); test("CMap reference index stores lightweight destinations per concept", () => { const index = new CmapReferenceIndex().load([ { conceptId: firstId, label: "Application", cmapSlug: "architecture", count: 2 }, { conceptId: firstId, label: "Application", cmapSlug: "operations", count: 1 } ], [ { slug: "architecture", title: "Architecture" }, { slug: "operations", title: "Operations" } ]); assert.deepEqual(index.mapsFor(firstId), [ { slug: "architecture", title: "Architecture", count: 2 }, { slug: "operations", title: "Operations", count: 1 } ]); assert.equal(index.countFor(firstId), 3); assert.equal(index.conceptIdForName("Application"), firstId); }); test("model finds boundary references from active submap crossings", () => { const model = CmapModel.fromDocument({ concepts: [ { id: firstId, label: "Submap" }, { id: secondId, label: "Inside" }, { id: thirdId, label: "Remote", cmapSlug: "remote-map" } ], items: [ { id: 10, kind: "submap", conceptId: firstId, x: 0, y: 0, width: 800, height: 500 }, { id: 1, conceptId: secondId, parentSubmapId: 10, x: 100, y: 120, width: 200, height: 60 }, { id: 2, conceptId: thirdId, x: 420, y: 120, width: 200, height: 60 } ], connectors: [{ id: 1, sourceId: 1, targetId: 2 }], conceptMaps: [{ id: "remote-map", title: "Remote map" }] }); assert.deepEqual(model.boundaryReferencesFor(10), [{ connector: { id: 1, sourceId: 1, targetId: 2, hasArrow: true, lineColor: "#333", lineWidth: 2 }, inside: { id: 1, x: 100, y: 120, width: 200, height: 60 }, outside: { id: 2, x: 420, y: 120, width: 200, height: 60 }, sourceInside: true, conceptId: thirdId, label: "Remote", linkingPhraseLabel: null, targetMaps: [] }]); }); test("model anchors boundary references on the connected concept beyond a phrase", () => { const model = CmapModel.fromDocument({ concepts: [ { id: firstId, label: "Submap" }, { id: secondId, label: "Inside" }, { id: thirdId, label: "Remote", cmapSlug: "remote-map" } ], items: [ { id: 10, kind: "submap", conceptId: firstId, x: 0, y: 0, width: 800, height: 500 }, { id: 1, conceptId: secondId, parentSubmapId: 10, x: 100, y: 120, width: 200, height: 60 }, { id: 2, kind: "phrase", label: "relates", parentSubmapId: 10, x: 330, y: 120, width: 80, height: 30 }, { id: 3, conceptId: thirdId, x: 480, y: 120, width: 200, height: 60 } ], connectors: [ { id: 1, sourceId: 1, targetId: 2 }, { id: 2, sourceId: 2, targetId: 3 } ], conceptMaps: [{ id: "remote-map", title: "Remote map" }] }); assert.equal(model.boundaryReferencesFor(10)[0].inside.id, 1); });