- resolveRefs no longer adds cards to a diagram; it returns node id → description, which the page sets as the tooltip of the matching row in Archify's Elements panel - hand-written cards pass through unchanged; docs and changelog
144 lines
7.1 KiB
TypeScript
144 lines
7.1 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { test } from 'node:test';
|
|
import {
|
|
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<string, unknown>[]).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));
|
|
// Descriptions travel with the node ids, for the Elements panel; nothing is added to the diagram itself.
|
|
assert.ok(!('cards' in r.diagram));
|
|
assert.equal(r.descriptions.get('customer'), 'Buys products; signs in with email or a social account.');
|
|
assert.equal(r.descriptions.size, 7);
|
|
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'] }], 'hand-written cards pass through');
|
|
});
|
|
|
|
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<string, unknown>[];
|
|
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<string, unknown>[])[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']);
|
|
});
|