Six example models next to the three C4 ones: arc42 (Cinema Tickets, IoT Telemetry, Team Wiki) by section, ArchiMate (Insurance Claims, HR Onboarding, Logistics Tracking) by layer, each validated with 0 errors and 0 warnings. resolveRefs and the editor schemas now take ref: in workflow and dataflow nodes, sequence participants and lifecycle states, as the skill already did. Example tests check each framework's folders.
112 lines
6.1 KiB
TypeScript
112 lines
6.1 KiB
TypeScript
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 { EXAMPLE_PREFIX, 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];
|
|
});
|
|
}
|
|
|
|
// The folders each framework's examples must have, besides main.archify, registry/ and decisions/.
|
|
const FRAMEWORK_FOLDERS: Record<string, string[]> = {
|
|
c4: ['0100-context', '0200-containers', '0300-components'],
|
|
arc42: ['0300-context', '0500-building-blocks', '0600-runtime', '0700-deployment'],
|
|
archimate: ['0100-business', '0200-application', '0300-technology'],
|
|
};
|
|
|
|
test('every example folder is listed, every listed example exists, three examples per framework', () => {
|
|
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);
|
|
for (const [framework, label] of [['c4', 'C4'], ['arc42', 'arc42'], ['archimate', 'ArchiMate']]) {
|
|
const listed = MODEL_EXAMPLES.filter((e) => e.folder.startsWith(`${framework}-`));
|
|
assert.equal(listed.length, 3, framework);
|
|
for (const e of listed) assert.ok(e.label.startsWith(`${label} - `), e.label);
|
|
}
|
|
});
|
|
|
|
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(EXAMPLE_PREFIX, ''));
|
|
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 framework's folders, each diagram resolving without problems and valid for Archify.
|
|
const diagrams = files.filter((f) => f.endsWith('.archify')).sort();
|
|
for (const level of FRAMEWORK_FOLDERS[example.folder.split('-')[0]]) {
|
|
assert.ok(diagrams.some((d) => d.startsWith(`${level}/`)), `missing ${level}`);
|
|
}
|
|
const used = new Set<string>();
|
|
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);
|
|
});
|