Files
archify-vscode-ext/test/examples.test.ts
T
root-at-skic ebf320a023 Architecture decisions: files, diagram references, views and panels
- *.archify-decision files: id (unique, words), title, status, date,
  elements, diagrams, supersedes, and prose; parsed, merged and link-checked
  in the model index; diagnostics on the file
- diagrams point at decisions with decisions: [...] on the diagram, nodes
  and edges; stripped before Archify, unknown ids reported, completion,
  hover and go-to-definition for the ids and for elements/diagrams/
  supersedes in decision files
- Decisions panel on the main.archify page; Decisions rail panel under a
  diagram (hover lights up the node or connection); decisions on element
  cards and in the element dialog; decision view for the file, with a
  lost-model note outside a model
- Archify | New Decision… command and template; editor schemas; the three
  New Model examples carry decisions; docs, changelog, 0.8.0
2026-10-19 23:13:05 +03:00

104 lines
5.5 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 { 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<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);
});