#!/usr/bin/env node // The skill's own checks. Runs every action against examples/c4-e-shop and // a scaffolded temp model. Prints YAML (success first); exit 1 on any failure. import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { toYaml } from '../services/yaml.mjs'; const scripts = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); const skill = path.resolve(scripts, '..'); const example = path.join(skill, 'examples', 'c4-e-shop'); const cli = path.join(scripts, 'src', 'cli', 'commands.mjs'); const checks = []; function cliRun(args) { const result = spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8', cwd: scripts, maxBuffer: 64 * 1024 * 1024 }); return { status: result.status, stdout: result.stdout, stderr: result.stderr }; } function yamlValue(stdout, key) { const match = new RegExp(`^${key}: (.*)$`, 'm').exec(stdout); return match ? match[1].trim() : undefined; } function check(name, command, expect) { const result = cliRun(command); let ok = true; const notes = []; try { expect(result, notes); } catch (error) { ok = false; notes.push(error.message); } checks.push({ name, command: `node src/cli/commands.mjs ${command.join(' ')}`, ok, notes, stdout: ok ? undefined : result.stdout.slice(0, 4000) }); } const assert = (condition, message) => { if (!condition) throw new Error(message); }; const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-skill-test-')); try { check('help prints usage', ['--help'], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(/model-validate/.test(r.stdout) && /diagram-render/.test(r.stdout), 'actions missing from --help'); }); check('model-validate example: 4 diagrams, 0 problems', ['--action', 'model-validate', '--dir', example], (r, notes) => { assert(r.status === 0, `exit ${r.status}`); assert(yamlValue(r.stdout, ' errors') === '0', 'errors are not 0'); assert(yamlValue(r.stdout, ' warnings') === '0', 'warnings are not 0'); assert(yamlValue(r.stdout, ' diagrams') === '4', 'expected 4 diagrams'); assert(yamlValue(r.stdout, ' diagramsOk') === '4', 'expected 4 diagrams ok'); notes.push('4 diagrams validated with 0 problems'); }); check('model-validate from a file inside the model', ['--action', 'model-validate', '--dir', path.join(example, '0300-components', 'api.archify')], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(yamlValue(r.stdout, 'mode') === 'model', 'root was not found from a file inside'); }); check('model-list example: 3 groups plus root-level main.archify', ['--action', 'model-list', '--dir', example], (r, notes) => { assert(r.status === 0, `exit ${r.status}`); assert(yamlValue(r.stdout, ' groups') === '3', 'expected 3 groups'); assert(/- file: main\.archify/.test(r.stdout), 'main.archify not listed at root level'); assert(/folder: 0100-context/.test(r.stdout) && /folder: 0200-containers/.test(r.stdout) && /folder: 0300-components/.test(r.stdout), 'group folders missing'); assert(/id: api\n(?:.*\n)*?\s+usedIn: 2/.test(r.stdout), 'api should be used in 2 diagrams'); assert(yamlValue(r.stdout, ' elements') === '11', 'expected 11 registry elements'); notes.push('3 groups, 11 elements with usage counts'); }); const resolved = path.join(tmp, 'containers.architecture.json'); check('model-resolve writes Archify JSON', ['--action', 'model-resolve', '--file', path.join(example, '0200-containers', 'online-shop.archify'), '--out', resolved], (r, notes) => { assert(r.status === 0, `exit ${r.status}`); const json = JSON.parse(fs.readFileSync(resolved, 'utf8')); assert(json.model === undefined, 'model block leaked into the resolved JSON'); const api = json.components.find((c) => c.id === 'api'); assert(api && api.type === 'backend' && api.brand === 'node-js' && api.description === undefined && api.diagram === undefined, 'api was not resolved from the registry'); const cards = json.cards ?? []; assert(cards.length === 7 && cards.every((c) => c.dot && c.title && c.items.length === 1), `expected 7 generated cards, got ${JSON.stringify(cards).slice(0, 200)}`); assert(cards.find((c) => c.title === 'API')?.dot === 'emerald', 'API card has the backend colour'); notes.push(`${json.components.length} components, refs expanded, ${cards.length} cards from the registry`); }); check('model-resolve prints the JSON inline without --out', ['--action', 'model-resolve', '--file', path.join(example, 'main.archify')], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(/^resolved:/m.test(r.stdout) && /label: Online Shop/.test(r.stdout), 'resolved block missing'); }); check('diagram-validate inside the model', ['--action', 'diagram-validate', '--file', path.join(example, '0100-context', 'online-shop.archify')], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(yamlValue(r.stdout, 'archifyOk') === 'true', 'Archify did not accept the diagram'); }); const html = path.join(tmp, 'context.html'); check('diagram-render writes HTML', ['--action', 'diagram-render', '--file', path.join(example, '0100-context', 'online-shop.archify'), '--out', html], (r, notes) => { assert(r.status === 0, `exit ${r.status}`); assert(fs.existsSync(html) && fs.statSync(html).size > 10000, 'HTML missing or too small'); assert(/ { assert(r.status === 1, `exit ${r.status}, expected 1`); const text = r.stdout.replace(/\\"/g, '"'); for (const expected of ['defined twice', 'Unknown element "nobody"', '"type" of shared element "db"', 'does not exist under', 'Duplicate id "a"', 'less than 8px apart']) { assert(text.includes(expected), `missing problem: ${expected}`); } assert(/pointer: \/components\/0\/ref/.test(text) && /line: 7/.test(text), 'pointer/line of the unknown ref missing'); assert(/file: 0300-components\/overlap\.archify\n(?:.*\n)*?\s+line: 7\n\s+pointer: \/components\/0\n\s+code: layout\/constraint/.test(text), 'Archify layout problem not mapped to a component line'); notes.push('all expected problems reported with file, line and pointer'); }); const single = path.join(tmp, 'single.archify'); fs.writeFileSync(single, 'schema_version: 1\ndiagram_type: architecture\nmeta:\n title: Single\n output: single.html\ncomponents:\n - { id: a, type: backend, label: A, pos: [40, 60], size: [130, 60] }\n - { id: b, type: database, label: B, pos: [300, 60], size: [130, 60] }\nconnections:\n - { from: a, to: b, label: SQL }\n'); check('diagram outside a model validates with Archify only', ['--action', 'model-validate', '--dir', single], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(yamlValue(r.stdout, 'mode') === 'single-diagram', 'expected single-diagram mode'); }); const outside = path.join(tmp, 'outside.archify'); fs.writeFileSync(outside, 'schema_version: 1\ndiagram_type: architecture\nmeta:\n title: Outside\n output: outside.html\ncomponents:\n - { ref: api, pos: [40, 60], size: [130, 60] }\n'); check('ref outside a model is an error', ['--action', 'diagram-validate', '--file', outside], (r) => { assert(r.status === 1, `exit ${r.status}, expected 1`); assert(/cannot be resolved: no main\.archify above/.test(r.stdout), 'missing "no main.archify" problem'); }); const scaffold = path.join(tmp, 'scaffolded'); check('model-scaffold --c4 creates a model', ['--action', 'model-scaffold', '--dir', scaffold, '--id', 'demo-shop', '--name', 'Demo Shop', '--description', 'A demo: scaffolded model', '--c4'], (r, notes) => { assert(r.status === 0, `exit ${r.status}`); for (const file of ['main.archify', 'registry/people.archify-registry', 'registry/systems.archify-registry', 'registry/containers.archify-registry', '0100-context/demo-shop.archify', '0200-containers/demo-shop.archify', '0300-components/api.archify']) { assert(fs.existsSync(path.join(scaffold, file)), `${file} not created`); } notes.push('7 files created'); }); check('scaffolded model validates', ['--action', 'model-validate', '--dir', scaffold], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(yamlValue(r.stdout, ' errors') === '0' && yamlValue(r.stdout, ' warnings') === '0', 'scaffolded model has problems'); assert(yamlValue(r.stdout, ' diagrams') === '4', 'expected 4 diagrams in the scaffolded model'); }); check('scaffolded model lists 3 groups', ['--action', 'model-list', '--dir', scaffold], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(yamlValue(r.stdout, ' groups') === '3', 'expected 3 groups'); }); check('model-scaffold refuses to overwrite', ['--action', 'model-scaffold', '--dir', scaffold, '--id', 'demo-shop', '--name', 'Demo Shop', '--c4'], (r) => { assert(r.status === 1, `exit ${r.status}, expected 1`); assert(/Refusing to overwrite/.test(r.stdout), 'no refusal message'); assert(fs.readFileSync(path.join(scaffold, 'main.archify'), 'utf8').includes('Demo Shop'), 'main.archify was touched'); }); const plain = path.join(tmp, 'plain'); check('model-scaffold without --c4 creates main.archify only', ['--action', 'model-scaffold', '--dir', plain, '--id', 'plain', '--name', 'Plain'], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(fs.readdirSync(plain).join(',') === 'main.archify', `unexpected files: ${fs.readdirSync(plain).join(',')}`); }); check('plain model validates (model block only)', ['--action', 'model-validate', '--dir', plain], (r) => { assert(r.status === 0, `exit ${r.status}`); }); // The shipped examples: the same three the extension offers under "Archify | New Model…". const examplesDir = path.join(skill, 'examples'); const exampleNames = fs.readdirSync(examplesDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort(); check('examples/ holds exactly the three C4 examples', [], () => { assert(exampleNames.join(',') === 'c4-e-shop,c4-event-based-integration,c4-standard-web-app', `unexpected examples: ${exampleNames.join(',')}`); }); for (const name of exampleNames) { check(`example ${name} validates with 0 problems`, ['--action', 'model-validate', '--dir', path.join(examplesDir, name)], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(yamlValue(r.stdout, ' errors') === '0' && yamlValue(r.stdout, ' warnings') === '0', 'example has problems'); assert(yamlValue(r.stdout, ' diagrams') === '4', 'expected 4 diagrams'); }); } const fromExample = path.join(tmp, 'from-example'); check('model-scaffold --example copies and renames an example', ['--action', 'model-scaffold', '--dir', fromExample, '--example', 'c4-event-based-integration', '--id', 'hub', '--name', 'Integration Hub', '--description', 'Our hub: events between systems'], (r) => { assert(r.status === 0, `exit ${r.status}`); const main = fs.readFileSync(path.join(fromExample, 'main.archify'), 'utf8'); assert(/^ id: hub$/m.test(main) && /^ name: "Integration Hub"$/m.test(main) && /Our hub: events between systems/.test(main) && /Integration Hub — overview/.test(main), `main.archify not renamed:\n${main}`); assert(fs.existsSync(path.join(fromExample, 'registry', 'containers.archify-registry')) && fs.existsSync(path.join(fromExample, '0300-components', 'router.archify')), 'example files missing'); }); check('copied example validates', ['--action', 'model-validate', '--dir', fromExample], (r) => { assert(r.status === 0, `exit ${r.status}`); assert(yamlValue(r.stdout, ' errors') === '0' && yamlValue(r.stdout, ' warnings') === '0', 'copied example has problems'); }); check('model-scaffold --example refuses to overwrite and rejects unknown names', ['--action', 'model-scaffold', '--dir', fromExample, '--example', 'c4-e-shop'], (r) => { assert(r.status === 1 && /Refusing to overwrite/.test(r.stdout), 'no refusal'); const unknown = cliRun(['--action', 'model-scaffold', '--dir', path.join(tmp, 'x'), '--example', 'nope']); assert(unknown.status === 1 && /Unknown example/.test(unknown.stdout), 'unknown example accepted'); }); } finally { fs.rmSync(tmp, { recursive: true, force: true }); } const failed = checks.filter((item) => !item.ok).length; process.stdout.write(toYaml({ success: failed === 0, summary: { checks: checks.length, passed: checks.length - failed, failed }, checks })); process.exit(failed ? 1 : 0);