import { test } from 'node:test'; import assert from 'node:assert/strict'; import { spawn, spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { extractSvgs, parseXml } from './helpers/xml.mjs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const skillRoot = path.resolve(__dirname, '..'); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-cli-')); const cli = path.join(skillRoot, 'bin/archify.mjs'); function run(args, options = {}) { return spawnSync(process.execPath, [cli, ...args], { cwd: options.cwd || skillRoot, encoding: 'utf8', env: options.env || process.env, }); } function sha256(file) { return createHash('sha256').update(fs.readFileSync(file)).digest('hex'); } function makeFakeOpeners(name, { exitCode = 0 } = {}) { const bin = path.join(tmp, name); const log = path.join(bin, 'open-log.json'); fs.mkdirSync(bin, { recursive: true }); const source = `#!/usr/bin/env node const fs = require('node:fs'); const target = process.argv[process.argv.length - 1]; fs.writeFileSync(process.env.ARCHIFY_TEST_OPEN_LOG, JSON.stringify({ argv: process.argv.slice(2), target, existed: fs.existsSync(target), })); process.exit(${exitCode}); `; for (const command of ['open', 'xdg-open']) { const executable = path.join(bin, command); fs.writeFileSync(executable, source); fs.chmodSync(executable, 0o755); } return { log, env: { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH || ''}`, ARCHIFY_TEST_OPEN_LOG: log, }, }; } function copyInstalledSkill(target) { fs.cpSync(skillRoot, target, { recursive: true, filter(source) { const rel = path.relative(skillRoot, source); return rel !== 'node_modules' && !rel.startsWith(`node_modules${path.sep}`) && rel !== 'test' && !rel.startsWith(`test${path.sep}`) // Another test creates this short-lived directory under skillRoot so // Ajv resolves from the checkout. Never copy a concurrently removed // test fixture into an installed-skill simulation. && !rel.startsWith('.validator-check-'); }, }); } test('cli: help lists commands and diagram types', () => { const result = run(['--help']); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /archify render /); assert.match(result.stdout, /archify compare architecture /); assert.match(result.stdout, /archify deliver /); assert.match(result.stdout, /archify preview /); assert.match(result.stdout, /archify visual-check /); assert.match(result.stdout, /--open/); assert.match(result.stdout, /--repo-root path \(architecture only\)/); assert.match(result.stdout, /archify guide \[scenario or question\]/); assert.match(result.stdout, /archify doctor/); assert.match(result.stdout, /archify demo \[output-directory\]/); assert.match(result.stdout, /architecture, workflow, sequence, dataflow, lifecycle/); }); test('cli: doctor reports a complete installation is ready', () => { const result = run(['doctor']); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /\[ok\] Node\.js v\d+/); assert.match(result.stdout, /\[ok\] Core template/); assert.match(result.stdout, /\[ok\] Example renderer/); assert.match(result.stdout, /\[ok\] Live preview runtime/); assert.match(result.stdout, /\[ok\] Scenario recipe guide/); assert.match(result.stdout, /\[ok\] Progressive authoring references/); assert.match(result.stdout, /\[ok\] Architecture compare runtime and proof fixtures/); assert.match(result.stdout, /\[ok\] Standalone schema validators/); assert.match(result.stdout, /\[ok\] architecture renderer, schema, and example/); assert.match(result.stdout, /\[ok\] lifecycle renderer, schema, and example/); assert.match(result.stdout, /Archify is ready\./); }); test('cli: doctor identifies an incomplete installation', () => { const incompleteRoot = path.join(tmp, 'incomplete-skill'); const incompleteBin = path.join(incompleteRoot, 'bin'); fs.mkdirSync(incompleteBin, { recursive: true }); fs.copyFileSync(cli, path.join(incompleteBin, 'archify.mjs')); const result = spawnSync(process.execPath, [path.join(incompleteBin, 'archify.mjs'), 'doctor'], { cwd: incompleteRoot, encoding: 'utf8', }); assert.equal(result.status, 1); assert.match(result.stdout, /\[missing\] Core template/); assert.match(result.stdout, /\[missing\] Scenario recipe guide/); assert.match(result.stdout, /\[missing\] workflow renderer, schema, and example/); assert.match(result.stderr, /Archify is not ready: \d+ required files? missing\./); }); test('cli: doctor rejects a corrupt standalone validator', () => { const corruptRoot = path.join(tmp, 'corrupt-skill'); copyInstalledSkill(corruptRoot); fs.writeFileSync(path.join(corruptRoot, 'renderers/shared/generated-validators.mjs'), 'export const workflow = ;\n'); const result = spawnSync(process.execPath, [path.join(corruptRoot, 'bin/archify.mjs'), 'doctor'], { cwd: corruptRoot, encoding: 'utf8', }); assert.equal(result.status, 1); assert.match(result.stdout, /\[invalid\] Standalone schema validators/); assert.match(result.stderr, /Archify is not ready: 1 runtime check failed\./); }); test('cli: examples renders from an installed skill', () => { const installedRoot = path.join(tmp, 'installed-skill'); copyInstalledSkill(installedRoot); const result = spawnSync(process.execPath, [path.join(installedRoot, 'bin/archify.mjs'), 'examples'], { cwd: installedRoot, encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); for (const output of [ 'workflow-agent-tool-call-rendered.html', 'sequence-cache-miss-request.html', 'dataflow-product-analytics.html', 'lifecycle-agent-run.html', 'web-app-rendered.html', ]) { assert.equal(fs.existsSync(path.join(installedRoot, 'examples', output)), true, output); } }); test('cli: guide lists all scenario recipes by diagram type', () => { const result = run(['guide']); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Archify scenario recipes \(11\)/); for (const type of ['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']) { assert.match(result.stdout, new RegExp(`\\[${type}\\]`)); } }); test('cli: guide recommends a scenario as structured json', () => { const result = run(['guide', 'Show an API request with Redis cache miss', '--json']); assert.equal(result.status, 0, result.stderr); const parsed = JSON.parse(result.stdout); assert.equal(parsed.ok, true); assert.equal(parsed.lang, 'en'); assert.equal(parsed.confidence, 'high'); assert.equal(parsed.recommendation.id, 'api-request'); assert.equal(parsed.recommendation.type, 'sequence'); }); test('cli: guide detects Chinese and explains the recommendation boundary', () => { const result = run(['guide', '展示 Kafka topic 消费者组和死信队列']); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /推荐: 事件流拓扑 \[dataflow\]/); assert.match(result.stdout, /不要这样用:/); assert.match(result.stdout, /必须包含:/); assert.match(result.stdout, /可直接复制的提示词:/); }); test('cli: guide works from an installed skill without node_modules', () => { const installedRoot = path.join(tmp, 'installed-guide-skill'); copyInstalledSkill(installedRoot); const installedCli = path.join(installedRoot, 'bin/archify.mjs'); const result = spawnSync(process.execPath, [installedCli, 'guide', 'incident-runbook', '--json'], { cwd: installedRoot, encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); assert.equal(JSON.parse(result.stdout).recommendation.id, 'incident-runbook'); }); test('cli: demo creates a ready-to-open diagram in a chosen directory', () => { const outputDirectory = path.join(tmp, 'my-demo'); const output = path.join(outputDirectory, 'archify-demo.html'); const result = run(['demo', outputDirectory]); assert.equal(result.status, 0, result.stderr); assert.equal(fs.existsSync(output), true); assert.match(fs.readFileSync(output, 'utf8'), /Sample Web App Diagram/); assert.match(result.stdout, new RegExp(`Demo ready: ${output.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)); assert.match(result.stdout, /Next: open the HTML in your browser/); assert.match(result.stdout, /archify render architecture/); }); test('cli: demo defaults to the current directory', () => { const workingDirectory = path.join(tmp, 'default-demo'); fs.mkdirSync(workingDirectory); const result = run(['demo'], { cwd: workingDirectory }); assert.equal(result.status, 0, result.stderr); assert.equal(fs.existsSync(path.join(workingDirectory, 'archify-demo.html')), true); }); test('cli: render writes a diagram html file', () => { const out = path.join(tmp, 'workflow.html'); const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json'); const result = run(['render', 'workflow', input, out]); assert.equal(result.status, 0, result.stderr); assert.equal(fs.existsSync(out), true); assert.match(fs.readFileSync(out, 'utf8'), /Agent Tool Call Workflow/); }); test('cli: visual-check returns a skipped receipt with exit 2 when Chrome is unavailable', () => { const out = path.join(tmp, 'visual-check-skipped.html'); fs.writeFileSync(out, 'delivered'); const missingChrome = path.join(tmp, 'missing-chrome'); const result = run(['visual-check', out, '--json'], { env: { ...process.env, ARCHIFY_CHROME: missingChrome }, }); assert.equal(result.status, 2, result.stderr); const receipt = JSON.parse(result.stdout); assert.equal(receipt.status, 'skipped'); assert.equal(receipt.visualReview, 'pending'); assert.equal(receipt.chrome.status, 'unavailable'); assert.equal(fs.existsSync(out.replace(/\.html$/, '.visual-check.json')), true); }); test('cli: deliver atomically writes a checked artifact and structured receipt', () => { const out = path.join(tmp, 'delivered-workflow.html'); const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json'); const result = run(['deliver', 'workflow', input, out, '--quality', 'showcase', '--json']); assert.equal(result.status, 0, result.stderr); assert.equal(fs.existsSync(out), true); assert.match(fs.readFileSync(out, 'utf8'), /Agent Tool Call Workflow/); const receipt = JSON.parse(result.stdout); assert.equal(receipt.schemaVersion, 1); assert.equal(receipt.ok, true); assert.equal(receipt.command, 'deliver'); assert.equal(receipt.type, 'workflow'); assert.equal(receipt.input, input); assert.equal(receipt.output, out); assert.deepEqual(receipt.specification, { sha256: sha256(input), bytes: fs.statSync(input).size, }); assert.match(receipt.artifact.sha256, /^[a-f0-9]{64}$/); assert.equal(receipt.artifact.sha256, sha256(out)); assert.equal(receipt.artifact.bytes, fs.statSync(out).size); assert.deepEqual(receipt.validation, { checksPassed: 9, checkCount: 9, compositionProfile: 'showcase', compositionStatus: 'pass', errors: 0, warnings: 0, }); assert.equal('open' in receipt, false); }); test('cli: deliver --open launches only the committed absolute artifact as one argument', { skip: process.platform === 'win32', }, () => { const fake = makeFakeOpeners('successful-open'); const out = path.join(tmp, `-复杂 path 'quoted'`, 'verified diagram.html'); const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json'); const result = run(['deliver', 'workflow', input, out, '--open', '--json'], { env: fake.env }); assert.equal(result.status, 0, result.stderr); const receipt = JSON.parse(result.stdout); assert.deepEqual(receipt.open, { requested: true, status: 'opened', target: out, method: process.platform === 'darwin' ? 'open' : 'xdg-open', }); const invocation = JSON.parse(fs.readFileSync(fake.log, 'utf8')); assert.equal(invocation.existed, true, 'the opener must run after the atomic commit'); assert.deepEqual(invocation.argv, [out]); assert.equal(invocation.target, out); assert.equal(fs.existsSync(out), true); }); test('cli: opener failure does not invalidate a verified delivery or pollute json stdout', { skip: process.platform === 'win32', }, () => { const fake = makeFakeOpeners('failed-open', { exitCode: 17 }); const out = path.join(tmp, 'open-failure-preserves-delivery.html'); const input = path.join(skillRoot, 'examples/web-app.architecture.json'); const result = run(['deliver', 'architecture', input, out, '--open', '--json'], { env: fake.env }); assert.equal(result.status, 0, result.stderr); const receipt = JSON.parse(result.stdout); assert.equal(receipt.ok, true); assert.equal(receipt.open.status, 'failed'); assert.equal(receipt.open.target, out); assert.match(result.stderr, /Could not open the verified artifact/); assert.match(result.stderr, new RegExp(out.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); assert.equal(fs.existsSync(out), true); assert.equal(receipt.artifact.sha256, sha256(out)); }); test('cli: deliver failure never invokes the optional opener', { skip: process.platform === 'win32', }, () => { const fake = makeFakeOpeners('never-open'); const input = path.join(tmp, 'invalid-open-delivery.json'); fs.writeFileSync(input, '{broken json'); const out = path.join(tmp, 'must-not-open.html'); const result = run(['deliver', 'architecture', input, out, '--open', '--json'], { env: fake.env }); assert.equal(result.status, 1); assert.equal(JSON.parse(result.stdout).stage, 'input'); assert.equal(fs.existsSync(fake.log), false); assert.equal(fs.existsSync(out), false); }); test('cli: a missing optional opener module preserves verified delivery with a fallback receipt', () => { const installedRoot = path.join(tmp, 'missing-open-module-skill'); copyInstalledSkill(installedRoot); const installedCli = path.join(installedRoot, 'bin/archify.mjs'); fs.rmSync(path.join(installedRoot, 'bin/open-artifact.mjs')); const input = path.join(installedRoot, 'examples/agent-tool-call.workflow.json'); const out = path.join(tmp, 'missing-open-module-delivery.html'); const result = spawnSync(process.execPath, [installedCli, 'deliver', 'workflow', input, out, '--open', '--json'], { cwd: installedRoot, encoding: 'utf8', }); assert.equal(result.status, 0, result.stderr); const receipt = JSON.parse(result.stdout); assert.equal(receipt.ok, true); assert.deepEqual(receipt.open, { requested: true, status: 'unsupported', target: out, method: null, }); assert.match(result.stderr, /Open it manually/); assert.equal(receipt.artifact.sha256, sha256(out)); }); test('cli: deliver preserves the renderer default output contract', () => { const workingDirectory = path.join(tmp, 'delivery-default-output'); fs.mkdirSync(workingDirectory, { recursive: true }); const input = path.join(workingDirectory, 'source.architecture.json'); const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/web-app.architecture.json'), 'utf8')); source.meta.output = 'verified-default.html'; fs.writeFileSync(input, JSON.stringify(source)); const result = run(['deliver', 'architecture', input, '--json'], { cwd: workingDirectory }); assert.equal(result.status, 0, result.stderr); const receipt = JSON.parse(result.stdout); assert.equal(receipt.output, path.join(fs.realpathSync(workingDirectory), 'verified-default.html')); assert.equal(fs.existsSync(receipt.output), true); }); test('cli: deliver works from an installed skill without node_modules', () => { const installedRoot = path.join(tmp, 'installed-deliver-skill'); copyInstalledSkill(installedRoot); const installedCli = path.join(installedRoot, 'bin/archify.mjs'); const cases = [ ['architecture-boundaries', 'architecture', 'production-deployment.architecture.json'], ['architecture-issue-110', 'architecture', 'brand-aware-delivery.architecture.json'], ['workflow', 'workflow', 'agent-tool-call.workflow.json'], ['sequence', 'sequence', 'cache-miss-request.sequence.json'], ['dataflow', 'dataflow', 'product-analytics.dataflow.json'], ['lifecycle', 'lifecycle', 'agent-run.lifecycle.json'], ]; for (const [label, type, example] of cases) { const input = path.join(installedRoot, 'examples', example); const out = path.join(tmp, `installed-${label}-delivery.html`); const result = spawnSync(process.execPath, [installedCli, 'deliver', type, input, out, '--json'], { cwd: installedRoot, encoding: 'utf8', }); assert.equal(result.status, 0, `${label}: ${result.stderr}`); assert.equal(JSON.parse(result.stdout).validation.checkCount, 9, label); assert.equal(fs.existsSync(out), true, label); const extracted = extractSvgs(fs.readFileSync(out, 'utf8')); assert.equal(extracted.direct.length, 1, `${label}: expected one delivered SVG`); assert.doesNotThrow( () => parseXml(extracted.direct[0]), `${label}: delivered SVG must be well-formed XML`, ); } }); test('cli: deliver XML guard parses markup instead of scanning attribute-like text', () => { assert.doesNotThrow(() => parseXml( '', )); assert.throws( () => parseXml(''), /attribute without value/i, ); assert.throws( () => parseXml(''), /unexpected close tag/i, ); }); test('cli: preview runs from an installed skill without node_modules and exits cleanly', { timeout: 30000 }, async () => { const installedRoot = path.join(tmp, 'installed-preview-skill'); copyInstalledSkill(installedRoot); const installedCli = path.join(installedRoot, 'bin/archify.mjs'); const input = path.join(installedRoot, 'examples/web-app.architecture.json'); const output = path.join(tmp, 'installed-preview.html'); const child = spawn(process.execPath, [installedCli, 'preview', 'architecture', input, output, '--quality', 'showcase', '--no-open'], { cwd: installedRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8'); child.stdout.on('data', (chunk) => { stdout += chunk; }); child.stderr.on('data', (chunk) => { stderr += chunk; }); let previewUrl; const started = Date.now(); while (!previewUrl && Date.now() - started < 8000) { previewUrl = stdout.match(/preview (http:\/\/127\.0\.0\.1:\d+\/)/)?.[1]; if (!previewUrl) await new Promise((resolve) => setTimeout(resolve, 40)); } assert.ok(previewUrl, `preview URL missing; stdout=${stdout}; stderr=${stderr}`); let state; while (Date.now() - started < 15000) { state = await fetch(new URL('/state', previewUrl)).then((response) => response.json()); if (state.status === 'verified') break; await new Promise((resolve) => setTimeout(resolve, 50)); } assert.equal(state?.status, 'verified', `preview did not verify; stdout=${stdout}; stderr=${stderr}`); assert.equal(state.revision, 1); assert.equal(fs.existsSync(output), true); child.kill('SIGTERM'); const exit = await new Promise((resolve) => child.once('close', (code, signal) => resolve({ code, signal }))); assert.deepEqual(exit, { code: 0, signal: null }); assert.match(stdout, /stopping preview/); await assert.rejects(fetch(previewUrl)); assert.deepEqual(fs.readdirSync(path.dirname(output)).filter((name) => name.startsWith('.archify-preview-')), []); }); test('cli: deliver preserves the previous artifact when the final check fails', () => { const installedRoot = path.join(tmp, 'broken-deliver-skill'); copyInstalledSkill(installedRoot); const installedCli = path.join(installedRoot, 'bin/archify.mjs'); const templatePath = path.join(installedRoot, 'assets/template.html'); const template = fs.readFileSync(templatePath, 'utf8'); fs.writeFileSync(templatePath, template.replace('', '\n')); const input = path.join(installedRoot, 'examples/web-app.architecture.json'); const out = path.join(tmp, 'preserved-delivery.html'); const trustedPriorArtifact = 'trusted prior artifact\n'; fs.writeFileSync(out, trustedPriorArtifact); const result = spawnSync(process.execPath, [installedCli, 'deliver', 'architecture', input, out, '--json'], { cwd: installedRoot, encoding: 'utf8', }); assert.equal(result.status, 1); const failure = JSON.parse(result.stdout); assert.equal(failure.ok, false); assert.equal(failure.stage, 'check'); assert.equal(failure.diagnostics[0].code, 'artifact/single-svg'); assert.equal(failure.diagnostics[0].subject.check, 'single_svg'); assert.ok(failure.diagnostics[0].supportedFixes.some((fix) => fix.includes('exactly one diagram SVG'))); assert.equal(failure.checker.checks.find((entry) => entry.name === 'single_svg').ok, false); assert.equal(fs.readFileSync(out, 'utf8'), trustedPriorArtifact); assert.deepEqual( fs.readdirSync(path.dirname(out)).filter((name) => name.includes('.archify-delivery-')), [], ); }); test('cli: deliver reports renderer failure as json and preserves the previous artifact', () => { const input = path.join(tmp, 'invalid-delivery.workflow.json'); const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/agent-tool-call.workflow.json'), 'utf8')); source.nodes[0].unexpected = true; fs.writeFileSync(input, JSON.stringify(source)); const out = path.join(tmp, 'renderer-failure-preserved.html'); const trustedPriorArtifact = 'last known good\n'; fs.writeFileSync(out, trustedPriorArtifact); const result = run(['deliver', 'workflow', input, out, '--json']); assert.equal(result.status, 1); const failure = JSON.parse(result.stdout); assert.equal(failure.ok, false); assert.equal(failure.stage, 'render'); assert.match(failure.error, /schema validation failed/i); assert.equal(fs.readFileSync(out, 'utf8'), trustedPriorArtifact); }); test('cli: deliver reports unreadable input as json without touching the target', () => { const input = path.join(tmp, 'malformed-delivery.json'); fs.writeFileSync(input, '{not valid json'); const out = path.join(tmp, 'malformed-input-preserved.html'); const trustedPriorArtifact = 'still trusted\n'; fs.writeFileSync(out, trustedPriorArtifact); const result = run(['deliver', 'architecture', input, out, '--json']); assert.equal(result.status, 1); const failure = JSON.parse(result.stdout); assert.equal(failure.ok, false); assert.equal(failure.stage, 'input'); assert.match(failure.error, /Could not read delivery input/); assert.equal(fs.readFileSync(out, 'utf8'), trustedPriorArtifact); }); test('cli: invalid source output metadata still fails inside the renderer', () => { const workingDirectory = path.join(tmp, 'invalid-output-metadata'); fs.mkdirSync(workingDirectory, { recursive: true }); const input = path.join(workingDirectory, 'source.architecture.json'); const source = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/web-app.architecture.json'), 'utf8')); source.meta.output = 17; fs.writeFileSync(input, JSON.stringify(source)); const out = path.join(workingDirectory, 'architecture.html'); const trustedPriorArtifact = 'metadata did not replace me\n'; fs.writeFileSync(out, trustedPriorArtifact); const result = run(['deliver', 'architecture', input, '--json'], { cwd: workingDirectory }); assert.equal(result.status, 1); const failure = JSON.parse(result.stdout); assert.equal(failure.stage, 'render'); assert.match(failure.error, /schema validation failed/i); assert.equal(fs.readFileSync(out, 'utf8'), trustedPriorArtifact); }); test('cli: deliver reports commit failure without a false success receipt', () => { const input = path.join(skillRoot, 'examples/web-app.architecture.json'); const outputDirectory = path.join(tmp, 'commit-target-is-a-directory'); fs.mkdirSync(outputDirectory, { recursive: true }); const result = run(['deliver', 'architecture', input, outputDirectory, '--json']); assert.equal(result.status, 1); const failure = JSON.parse(result.stdout); assert.equal(failure.ok, false); assert.equal(failure.stage, 'commit'); assert.match(failure.error, /Could not commit verified delivery/); assert.equal(fs.statSync(outputDirectory).isDirectory(), true); assert.equal(fs.readdirSync(outputDirectory).length, 0); }); test('cli: deliver reports preparation failure as json without touching the blocker', () => { const input = path.join(skillRoot, 'examples/web-app.architecture.json'); const blockingFile = path.join(tmp, 'delivery-parent-is-a-file'); fs.writeFileSync(blockingFile, 'do not replace me'); const out = path.join(blockingFile, 'cannot-write.html'); const result = run(['deliver', 'architecture', input, out, '--json']); assert.equal(result.status, 1); const failure = JSON.parse(result.stdout); assert.equal(failure.ok, false); assert.equal(failure.stage, 'prepare'); assert.match(failure.error, /Could not create delivery directory/); assert.equal(fs.readFileSync(blockingFile, 'utf8'), 'do not replace me'); }); test('cli: check validates rendered html', () => { const out = path.join(tmp, 'workflow-check.html'); const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json'); assert.equal(run(['render', 'workflow', input, out]).status, 0); const result = run(['check', out]); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /"ok": true/); }); test('cli: validate emits structured json without keeping html output', () => { const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json'); const before = new Set(fs.readdirSync(tmp)); const result = run(['validate', 'workflow', input, '--json']); assert.equal(result.status, 0, result.stderr); const parsed = JSON.parse(result.stdout); assert.equal(parsed.ok, true); assert.equal(parsed.type, 'workflow'); assert.equal(parsed.checks.length, 9); assert.equal(parsed.composition.profile, 'showcase'); assert.deepEqual(parsed.composition.summary, { errors: 0, warnings: 0 }); assert.equal(parsed.composition.metrics.containerBorderRuns, 0); assert.equal(parsed.composition.metrics.ambiguousCorridors, 0); assert.deepEqual(new Set(fs.readdirSync(tmp)), before); }); test('cli: validate JSON exposes only the primary v1 column-capacity diagnostic', () => { const input = path.join(tmp, 'pinned-column-capacity.workflow.json'); fs.writeFileSync(input, `${JSON.stringify({ schema_version: 1, diagram_type: 'workflow', meta: { title: 'Pinned issue 126 diagnostic boundary', viewBox: [720, 400], legend: { mode: 'hidden' }, }, lanes: [{ id: 'main', label: 'Main' }], nodes: [ { id: 'a', lane: 'main', col: 1, type: 'backend', label: 'A' }, { id: 'b', lane: 'main', col: 2, type: 'backend', label: 'B' }, ], edges: [{ id: 'ab', from: 'a', to: 'b', fromSide: 'top', toSide: 'top', via: [[220, 60], [300, 60]], }], }, null, 2)}\n`); const result = run(['validate', 'workflow', input, '--json'], { env: { ...process.env, ARCHIFY_DIAGNOSTIC_FORMAT: 'json' }, }); assert.equal(result.status, 1, result.stderr || result.stdout); assert.equal(result.stderr, ''); const failure = JSON.parse(result.stdout); assert.equal(failure.ok, false); assert.equal(failure.command, 'validate'); assert.equal(failure.stage, 'render'); assert.equal(failure.type, 'workflow'); assert.equal(failure.diagnostics.length, 1, JSON.stringify(failure.diagnostics, null, 2)); const [primary] = failure.diagnostics; assert.equal(primary.code, 'workflow/column-capacity'); assert.equal(primary.subject.edge, 'ab'); assert.equal(primary.subject.fromCol, 1); assert.equal(primary.subject.toCol, 2); assert.ok(primary.supportedFixes.length > 0); assert.ok(failure.diagnostics.every(({ code }) => ( code !== 'workflow/explicit-pin-conflict' && code !== 'workflow/viewbox-capacity' ))); }); test('cli: --quality overrides the source profile for render, validate, and deliver', () => { const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json'); const out = path.join(tmp, 'workflow-standard.html'); const rendered = run(['render', 'workflow', input, out, '--quality', 'standard']); assert.equal(rendered.status, 0, rendered.stderr); assert.match(fs.readFileSync(out, 'utf8'), /data-quality-profile="standard"/); const validated = run(['validate', 'workflow', input, '--quality=standard', '--json']); assert.equal(validated.status, 0, validated.stderr); assert.equal(JSON.parse(validated.stdout).composition.profile, 'standard'); const deliveredOut = path.join(tmp, 'workflow-delivered-standard.html'); const delivered = run(['deliver', 'workflow', input, deliveredOut, '--quality=standard', '--json']); assert.equal(delivered.status, 0, delivered.stderr); assert.equal(JSON.parse(delivered.stdout).validation.compositionProfile, 'standard'); }); test('cli: rejects an unknown quality profile', () => { const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json'); const result = run(['validate', 'workflow', input, '--quality', 'hero']); assert.equal(result.status, 2); assert.match(result.stderr, /Expected standard or showcase/); }); test('cli: rejects a quality flag without a value', () => { const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json'); for (const args of [ ['validate', 'workflow', input, '--json', '--quality'], ['validate', 'workflow', input, '--quality', '--json'], ['validate', 'workflow', input, '--quality='], ]) { const result = run(args); assert.equal(result.status, 2); assert.match(result.stderr, /--quality requires standard or showcase/); } }); test('cli: validate rejects unknown flags, layout-json assignment typos, and extra positionals', () => { const input = path.join(skillRoot, 'examples/agent-tool-call.workflow.json'); const cases = [ { args: ['validate', 'workflow', input, '--layout-json', '--bogus'], pattern: /Unknown validate option "--bogus"/, }, { args: ['validate', 'workflow', input, '--layout-json=true'], pattern: /Unknown validate option "--layout-json=true"/, }, { args: ['validate', 'workflow', input, '--layout-json=true', '--json'], pattern: /Unknown validate option "--layout-json=true"/, }, { args: ['validate', 'workflow', input, 'unexpected-output.html', '--layout-json'], pattern: /Usage:/, }, ]; for (const { args, pattern } of cases) { const result = run(args); assert.equal(result.status, 2, `${args.join(' ')}\n${result.stderr}\n${result.stdout}`); assert.equal(result.stdout, ''); assert.match(result.stderr, pattern); } }); test('cli: inspect emits architecture layout json', () => { const input = path.resolve(skillRoot, '../examples/archify-repo-grid.architecture.json'); const result = run(['inspect', 'architecture', input]); assert.equal(result.status, 0, result.stderr); const parsed = JSON.parse(result.stdout); assert.equal(parsed.ok, true); assert.equal(parsed.diagram_type, 'architecture'); assert.equal(parsed.layout.mode, 'grid'); assert.ok(parsed.components.length >= 5); assert.ok(parsed.connections.length >= 1); }); test('cli: inspect remains architecture-only while workflow uses validate --layout-json', () => { const input = path.join(skillRoot, 'examples', 'agent-tool-call.workflow.json'); const result = run(['inspect', 'workflow', input]); assert.equal(result.status, 2); assert.match(result.stderr, /inspect is currently supported for architecture diagrams only/); assert.equal(result.stdout, ''); }); test('cli: validate returns renderer errors for bad input', () => { const input = path.join(tmp, 'bad.workflow.json'); const validateTmp = path.join(tmp, 'validate-failure-tmp'); const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples/agent-tool-call.workflow.json'), 'utf8')); doc.edges[0].to = 'ghost'; fs.writeFileSync(input, JSON.stringify(doc)); fs.mkdirSync(validateTmp); const result = run(['validate', 'workflow', input], { env: { ...process.env, TMPDIR: validateTmp }, }); assert.notEqual(result.status, 0); assert.match(result.stderr, /unknown target "ghost"/); assert.deepEqual(fs.readdirSync(validateTmp), []); }); test('cli: validate rejects an unknown type without leaking a temp directory', () => { const validateTmp = path.join(tmp, 'validate-unknown-type-tmp'); fs.mkdirSync(validateTmp); const result = run(['validate', 'unknown', 'ignored.json'], { env: { ...process.env, TMPDIR: validateTmp, TMP: validateTmp, TEMP: validateTmp, }, }); assert.equal(result.status, 2); assert.match(result.stderr, /Unknown diagram type "unknown"/); assert.deepEqual(fs.readdirSync(validateTmp), []); }); process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));