import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { test } from 'node:test'; import { parse } from 'yaml'; import { validate } from '../src/archify/cli'; import { MODEL_EXAMPLES, mainFromExample } from '../src/language/examples'; import { MAIN_FILE, REGISTRY_EXT, hasDiagramBody, mergeRegistries, parseRegistry, readModelMeta, resolveRefs } from '../src/model/format'; import { DECISION_EXT, mergeDecisions, parseDecision, validateDecisions } from '../src/model/decisions'; import { readDiagramSource } from '../src/sources/detect'; const root = path.resolve(__dirname, '..', '..'); const examples = path.join(root, 'templates', 'models'); const runtime = { cliPath: path.join(root, 'vendor', 'archify', 'bin', 'archify.mjs'), nodePath: process.execPath, runAsNode: false, workDir: fs.mkdtempSync(path.join(os.tmpdir(), 'archify-ext-examples-')), }; function walk(dir: string, rel = ''): string[] { return fs.readdirSync(path.join(dir, rel), { withFileTypes: true }).flatMap((entry) => { const next = rel ? `${rel}/${entry.name}` : entry.name; return entry.isDirectory() ? walk(dir, next) : [next]; }); } test('every example folder is listed, every listed example exists, labels are the three C4 examples', () => { const folders = fs.readdirSync(examples, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort(); assert.deepEqual(MODEL_EXAMPLES.map((e) => e.folder).sort(), folders); assert.deepEqual( MODEL_EXAMPLES.map((e) => e.label), ['C4 - Standard Web App', 'C4 - E-Shop', 'C4 - Event-Based Integration'], ); }); for (const example of MODEL_EXAMPLES) { test(`example ${example.folder} is a complete model that validates`, async () => { const dir = path.join(examples, example.folder); const files = walk(dir); const read = (rel: string) => fs.readFileSync(path.join(dir, rel), 'utf8'); // main.archify: model block with id/name/description and an overview diagram. assert.ok(files.includes(MAIN_FILE)); const main = parse(read(MAIN_FILE)); const meta = readModelMeta(main); assert.equal(meta?.id, example.folder.replace(/^c4-/, '')); assert.ok(meta?.name && meta.description); assert.ok(hasDiagramBody(main)); // Registry: at least people, systems and containers; no problems. const registryFiles = files.filter((f) => f.endsWith(REGISTRY_EXT)).sort(); assert.ok(registryFiles.length >= 3, `registry files: ${registryFiles.join(', ')}`); const registry = mergeRegistries(registryFiles.map((file) => ({ file, ...parseRegistry(read(file), file) }))); assert.deepEqual(registry.problems, []); for (const element of registry.elements.values()) { assert.ok(element.type && element.label && element.description, `${element.id} needs type, label and description`); if (element.diagram) assert.ok(files.includes(element.diagram), `${element.id} links to missing ${element.diagram}`); } // Decisions: at least one, all links resolve. const decisionFiles = files.filter((f) => f.endsWith(DECISION_EXT)); assert.ok(decisionFiles.length >= 1, 'an example carries at least one decision'); const decisions = mergeDecisions(decisionFiles.map((file) => ({ file, ...parseDecision(read(file), file) }))); assert.deepEqual(decisions.problems, []); const diagramPaths = new Set(files.filter((f) => f.endsWith('.archify') && f !== MAIN_FILE)); assert.deepEqual(validateDecisions(decisions, registry, diagramPaths), []); for (const d of decisions.decisions.values()) assert.ok(d.title && d.status && d.context && d.decision && d.consequences, `${d.id} is complete`); // Diagrams: the three C4 levels, each resolving without problems and valid for Archify. const diagrams = files.filter((f) => f.endsWith('.archify')).sort(); for (const level of ['0100-context', '0200-containers', '0300-components']) { assert.ok(diagrams.some((d) => d.startsWith(`${level}/`)), `missing ${level}`); } const used = new Set(); for (const file of diagrams) { const source = readDiagramSource(read(file), file); assert.ok(source, `${file} is not a diagram`); const resolved = resolveRefs(source.diagram, { registry, decisions: new Set(decisions.decisions.keys()), fileName: file }); assert.deepEqual(resolved.problems, [], file); for (const link of resolved.links.values()) assert.ok(files.includes(link), `${file} links to missing ${link}`); for (const id of resolved.used) used.add(id); const result = await validate(runtime, source.type, JSON.stringify(resolved.diagram)); assert.deepEqual(result.diagnostics.filter((d) => d.severity === 'error').map((d) => `${file}: ${d.code}: ${d.message}`), []); assert.ok(result.ok, file); } // Every registry element is used by some diagram. for (const id of registry.elements.keys()) assert.ok(used.has(id), `element ${id} is never used`); }); } test('mainFromExample keeps the example as written, or renames it after the folder', () => { const example = MODEL_EXAMPLES[1]; const text = fs.readFileSync(path.join(examples, example.folder, MAIN_FILE), 'utf8'); assert.equal(mainFromExample(text, example.folder, example), text); const renamed = parse(mainFromExample(text, 'shop two', example)); assert.equal(renamed.model.id, 'shop-two'); assert.equal(renamed.model.name, 'Shop two'); assert.equal(renamed.meta.title, 'Shop two — overview'); assert.equal(renamed.model.description, parse(text).model.description); assert.deepEqual(renamed.components, parse(text).components); });