import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { test } from 'node:test'; import { cardsFromElements, dotForType, groupOf, hasDiagramBody, isMainFile, isRegistryFile, mergeRegistries, parseRegistry, readModelMeta, resolveRefs, sortedGroups, summarizeDiagram, } from '../src/model/format'; import { parse } from 'yaml'; const model = path.resolve(__dirname, '..', '..', 'test', 'fixtures', 'model'); const read = (rel: string) => fs.readFileSync(path.join(model, rel), 'utf8'); function registryOfModel() { const files = ['registry/people.archify-registry', 'registry/systems.archify-registry', 'registry/containers.archify-registry']; return mergeRegistries(files.map((file) => ({ file, ...parseRegistry(read(file), file) }))); } test('file kinds are told apart by name', () => { assert.ok(isMainFile('/m/online-shop/main.archify')); assert.ok(!isMainFile('/m/online-shop/0100-context/main-flow.archify')); assert.ok(isRegistryFile('registry/people.archify-registry')); assert.ok(!isRegistryFile('people.archify')); }); test('main.archify: model block and optional diagram body', () => { const doc = parse(read('main.archify')); assert.deepEqual(readModelMeta(doc), { id: 'online-shop', name: 'Online Shop', description: 'The web shop, its API and workers, and the stores behind them. Context, containers and components, C4 style.\n', }); assert.ok(hasDiagramBody(doc)); assert.equal(readModelMeta(parse(read('0100-context/online-shop.archify'))), undefined); assert.ok(!hasDiagramBody({ schema_version: 1, model: { id: 'x' } })); }); test('registry: elements with line numbers, duplicates reported once', () => { const people = parseRegistry(read('registry/people.archify-registry'), 'people'); assert.deepEqual(people.problems, []); assert.equal(people.elements.length, 2); const customer = people.elements[0]; assert.equal(customer.id, 'customer'); assert.equal(customer.type, 'external'); assert.equal(customer.line, 2); const merged = mergeRegistries([ { file: 'a', ...parseRegistry('elements:\n api: { type: backend, label: API }\n', 'a') }, { file: 'b', ...parseRegistry('elements:\n api: { type: backend, label: API again }\n db: { type: database, label: DB }\n', 'b') }, ]); assert.equal(merged.elements.size, 2); assert.equal(merged.problems.length, 1); assert.match(merged.problems[0].message, /"api" is defined twice/); assert.equal(merged.problems[0].file, 'b'); }); test('registry: broken files are reported, not thrown', () => { const bad = parseRegistry('elements:\n api: {\n', 'bad'); assert.equal(bad.elements.length, 0); assert.equal(bad.problems[0].severity, 'error'); const noMap = parseRegistry('- just\n- a list\n', 'list'); assert.match(noMap.problems[0].message, /mapping/); }); test('resolve: refs become full components, links and usage are collected', () => { const registry = registryOfModel(); const doc = parse(read('0200-containers/online-shop.archify')); const r = resolveRefs(doc, { registry, fileName: 'containers.archify' }); assert.deepEqual(r.problems, []); const api = (r.diagram.components as Record[]).find((c) => c.id === 'api')!; assert.deepEqual(api, { id: 'api', type: 'backend', label: 'API', sublabel: 'Node.js', brand: 'node-js', pos: [460, 160], size: [130, 60] }); assert.equal(r.links.get('api'), '0300-components/api.archify'); assert.ok(!r.links.has('db')); assert.deepEqual([...r.used].sort(), ['api', 'cache', 'customer', 'db', 'queue', 'web', 'worker']); assert.equal(r.refs.get(2)?.id, 'api'); assert.ok(!('model' in r.diagram)); // Cards from the registry descriptions, in drawing order, one per element. const cards = r.diagram.cards as { dot: string; title: string; items: string[] }[]; assert.equal(cards.length, 7); assert.deepEqual(cards[0], { dot: 'slate', title: 'Customer', items: ['Buys products; signs in with email or a social account.'] }); assert.equal(cards.find((c) => c.title === 'API')?.dot, 'emerald'); }); test('resolve: own cards win, no cards without refs or descriptions', () => { const registry = registryOfModel(); const own = resolveRefs({ components: [{ ref: 'api', pos: [0, 0] }], cards: [{ dot: 'rose', title: 'Mine', items: ['kept'] }] }, { registry, fileName: 'x.archify' }); assert.deepEqual(own.diagram.cards, [{ dot: 'rose', title: 'Mine', items: ['kept'] }]); const inline = resolveRefs({ components: [{ id: 'a', type: 'backend', label: 'A', pos: [0, 0] }] }, { registry, fileName: 'x.archify' }); assert.ok(!('cards' in inline.diagram)); assert.equal(cardsFromElements([{ id: 'x', type: 'cloud', label: 'X', file: 'r', line: 0 }]).length, 0); assert.equal(dotForType('messagebus'), 'orange'); assert.equal(dotForType(undefined), 'slate'); }); test('resolve: overrides, second id for the same element, inline elements and links', () => { const registry = registryOfModel(); const doc = { components: [ { ref: 'db', label: 'Orders DB', pos: [0, 0] }, { ref: 'db', id: 'db-replica', sublabel: 'replica', pos: [0, 0] }, { id: 'cron', type: 'backend', label: 'Nightly job', diagram: 'jobs.archify', pos: [0, 0] }, { ref: 'api', diagram: 'other.archify', pos: [0, 0] }, ], }; const r = resolveRefs(doc, { registry, fileName: 'x.archify' }); const components = r.diagram.components as Record[]; assert.equal(components[0].label, 'Orders DB'); assert.equal(components[0].brand, 'postgresql'); assert.equal(components[1].id, 'db-replica'); assert.equal(components[1].sublabel, 'replica'); assert.deepEqual(components[2], { id: 'cron', type: 'backend', label: 'Nightly job', pos: [0, 0] }); assert.equal(r.links.get('cron'), 'jobs.archify'); assert.equal(r.links.get('api'), 'other.archify', 'the component link wins over the registry link'); assert.deepEqual(r.problems, []); }); test('resolve: unknown refs, forbidden overrides, outside a model', () => { const registry = registryOfModel(); const r = resolveRefs({ components: [{ ref: 'ghost', pos: [0, 0] }, { ref: 'api', type: 'frontend', brand: 'react', pos: [0, 0] }] }, { registry, fileName: 'x.archify' }); assert.equal(r.problems.length, 3); assert.equal(r.problems[0].pointer, '/components/0/ref'); assert.match(r.problems[0].message, /Unknown element "ghost"/); assert.deepEqual(r.problems.slice(1).map((p) => p.pointer), ['/components/1/type', '/components/1/brand']); const ghost = (r.diagram.components as Record[])[0]; assert.deepEqual(ghost, { type: 'external', label: 'ghost', pos: [0, 0], id: 'ghost' }); const outside = resolveRefs({ components: [{ ref: 'api', pos: [0, 0] }] }, { fileName: '/w/loose.archify' }); assert.match(outside.problems[0].message, /not inside a model/); }); test('summaries and groups: root first, then folders', () => { const summaries = [ summarizeDiagram(read('0300-components/api.archify'), '0300-components/api.archify'), summarizeDiagram(read('0100-context/online-shop.archify'), '0100-context/online-shop.archify'), summarizeDiagram('schema_version: 1\ndiagram_type: workflow\nmeta: { title: Checkout }\n', 'checkout.archify'), summarizeDiagram('oops: [', 'broken.archify'), ]; assert.equal(summaries[0].title, 'API components'); assert.equal(summaries[0].kind, 'architecture'); assert.deepEqual(summaries[0].used, ['web', 'db', 'queue']); assert.ok(summaries[3].parseError); assert.equal(groupOf('a/b/c.archify'), 'a/b'); assert.equal(groupOf('c.archify'), ''); const groups = sortedGroups(summaries); assert.deepEqual(groups.map((g) => g.group), ['', '0100-context', '0300-components']); assert.deepEqual(groups[0].diagrams.map((d) => d.relativePath), ['broken.archify', 'checkout.archify']); });