Files
archify-vscode-ext/test/integration/suite.ts
T
root-at-skic 7a2b661afe Models: main.archify, registries, refs, links and the model page; 0.7.0
A folder with main.archify at its top is a model, a set of diagrams
that belong together. Opening main.archify shows the model page: the
model's name and description, its own overview diagram when the file
has one, the diagrams grouped by folder (click to open), and the
registry with where each element is used. The page is built by
traversing the folder tree and refreshes when files change.

*.archify-registry files define shared elements once; diagrams draw
them with `ref: <id>` plus position and may override label, sublabel
and tag. References are resolved before Archify runs, so Archify is
unchanged. Hover, go-to-definition and completion on ref; unknown
names, forbidden overrides, duplicate ids and broken registries in
Problems; registry edits re-render open diagrams. `diagram:` links on
elements and components open the linked diagram on double-click.

New command Archify | Open Model, a Model template for New Diagram…,
editor schemas for the new keys and for registries, docs/model.md and
examples/online-shop. Single diagrams behave exactly as before.
2026-10-19 11:37:20 +03:00

344 lines
18 KiB
TypeScript

import assert from 'node:assert/strict';
import path from 'node:path';
import * as vscode from 'vscode';
const EXTENSION_ID = 'root-at-skic.archify-vscode-ext';
function workspaceFile(name: string): vscode.Uri {
const folder = vscode.workspace.workspaceFolders?.[0];
assert.ok(folder, 'a workspace folder is open');
return vscode.Uri.joinPath(folder.uri, name);
}
async function waitFor<T>(what: string, probe: () => T | undefined | false, timeoutMs = 20_000): Promise<T> {
const start = Date.now();
for (;;) {
const value = probe();
if (value) return value;
if (Date.now() - start > timeoutMs) throw new Error(`Timed out waiting for ${what}`);
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
function webviewTabs(viewTypeSuffix: string): vscode.Tab[] {
return vscode.window.tabGroups.all
.flatMap((group) => group.tabs)
.filter((tab) =>
(tab.input instanceof vscode.TabInputWebview && tab.input.viewType.endsWith(viewTypeSuffix)) ||
(tab.input instanceof vscode.TabInputCustom && tab.input.viewType === viewTypeSuffix));
}
async function check(name: string, body: () => Promise<void>): Promise<void> {
try {
await body();
console.log(` ✔ ${name}`);
} catch (error) {
console.log(` ✖ ${name}\n ${error instanceof Error ? error.stack ?? error.message : String(error)}`);
throw error;
}
}
export async function run(): Promise<void> {
console.log('Archify extension integration suite');
await check('activates on a diagram source and sets the context key', async () => {
const doc = await vscode.workspace.openTextDocument(workspaceFile('production-deployment.architecture.json'));
await vscode.window.showTextDocument(doc);
const extension = vscode.extensions.getExtension(EXTENSION_ID);
assert.ok(extension, 'extension is installed');
await waitFor('activation', () => extension.isActive);
});
await check('clicking a .archify file opens the diagram, not the text', async () => {
const uri = workspaceFile('web-platform.archify');
await vscode.commands.executeCommand('vscode.open', uri);
const tab = await waitFor('diagram editor tab', () =>
vscode.window.tabGroups.all
.flatMap((group) => group.tabs)
.find((t) => t.input instanceof vscode.TabInputCustom && t.input.viewType === 'archify.diagram' && t.input.uri.toString() === uri.toString()),
);
assert.ok(tab.isActive, 'the diagram tab is active');
assert.equal(vscode.window.activeTextEditor?.document.uri.toString() === uri.toString(), false, 'no text editor was opened for it');
});
await check('.archify files are YAML and Show Source opens the text beside the diagram', async () => {
const uri = workspaceFile('web-platform.archify');
await vscode.commands.executeCommand('archify.showSource');
const editor = await waitFor('source text editor', () =>
vscode.window.visibleTextEditors.find((e) => e.document.uri.toString() === uri.toString()),
);
assert.equal(editor.document.languageId, 'yaml');
assert.match(editor.document.getText(), /^schema_version: 1/m);
});
await check('.archify validation problems are reported against the YAML', async () => {
const good = await vscode.workspace.openTextDocument(workspaceFile('cache-miss.archify'));
const broken = good.getText().replace(/^participants:/m, 'actors:');
const target = workspaceFile('broken.archify');
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(broken));
const doc = await vscode.workspace.openTextDocument(target);
await vscode.window.showTextDocument(doc);
const diagnostics = await waitFor('archify diagnostics on broken.archify', () => {
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify');
return found.length > 0 && found;
});
assert.ok(diagnostics.some((d) => d.severity === vscode.DiagnosticSeverity.Error));
});
await check('.archify YAML syntax errors are reported at their line', async () => {
const target = workspaceFile('syntax.archify');
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode('schema_version: 1\ndiagram_type: architecture\nmeta: [unclosed\n'));
const doc = await vscode.workspace.openTextDocument(target);
await vscode.window.showTextDocument(doc);
const diagnostics = await waitFor('yaml syntax diagnostics', () => {
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify' && d.message.startsWith('YAML:'));
return found.length > 0 && found;
});
assert.ok(diagnostics[0].range.start.line >= 2, `reported on line ${diagnostics[0].range.start.line + 1}`);
});
await check('Archify | New Diagram creates a .archify file from a template and opens the diagram', async () => {
// Menus show the bare title, so it must name Archify by itself.
const commands = vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON.contributes.commands as { command: string; title: string; category?: string }[];
const newDiagram = commands.find((item) => item.command === 'archify.newDiagram');
assert.equal(newDiagram?.title, 'Archify | New Diagram…');
assert.equal(newDiagram?.category, undefined);
const folder = workspaceFile('.');
// Stub the template pick and the name prompt; check the name validation on the way.
const window = vscode.window as unknown as Record<string, unknown>;
const originalPick = window.showQuickPick;
const originalInput = window.showInputBox;
let existingNameMessage: unknown;
window.showQuickPick = async (items: readonly { label: string }[]) => (await items).find((item) => item.label === 'Everyday Plan');
window.showInputBox = async (options: vscode.InputBoxOptions) => {
existingNameMessage = await options.validateInput?.('web-platform');
return 'summer-plan';
};
try {
await vscode.commands.executeCommand('archify.newDiagram', folder);
} finally {
window.showQuickPick = originalPick;
window.showInputBox = originalInput;
}
assert.match(String(existingNameMessage), /already exists/);
const uri = vscode.Uri.joinPath(folder, 'summer-plan.archify');
const text = new TextDecoder().decode(await vscode.workspace.fs.readFile(uri));
assert.match(text, /^diagram_type: workflow$/m);
assert.match(text, /^ {2}title: Summer plan$/m);
assert.match(text, /^ {2}output: summer-plan\.html$/m);
await waitFor('diagram editor for the new file', () =>
webviewTabs('archify.diagram').some((tab) => tab.input instanceof vscode.TabInputCustom && tab.input.uri.toString() === uri.toString()),
);
});
await check('suggests brand IDs after brand: in .archify files', async () => {
const target = workspaceFile('brands.archify');
const text = [
'schema_version: 1',
'diagram_type: architecture',
'meta:',
' title: Brands',
'components:',
' - id: db',
' type: database',
' label: DB',
' brand: ',
' - id: bus',
' type: messagebus',
' label: Bus',
' brand: kafka',
'',
].join('\n');
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(text));
const doc = await vscode.workspace.openTextDocument(target);
const labels = async (line: number) => {
const list = await vscode.commands.executeCommand<vscode.CompletionList>(
'vscode.executeCompletionItemProvider',
doc.uri,
new vscode.Position(line, doc.lineAt(line).text.length),
);
return list.items.map((item) => (typeof item.label === 'string' ? item.label : item.label.label));
};
const empty = await labels(8);
assert.ok(empty.includes('postgresql') && empty.includes('redis'), 'brand IDs offered after "brand: "');
const kafka = await labels(12);
assert.ok(kafka.includes('apache-kafka'), 'the alias "kafka" finds apache-kafka');
assert.equal((await labels(7)).includes('postgresql'), false, 'nothing offered for other fields');
});
await check('an unknown brand is reported on its own line', async () => {
const target = workspaceFile('unknown-brand.archify');
const text = new TextDecoder().decode(await vscode.workspace.fs.readFile(workspaceFile('web-platform.archify')))
.replace(/^( {2}- id: [^\n]+\n)/m, '$1 brand: not-a-brand\n');
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode(text));
const doc = await vscode.workspace.openTextDocument(target);
await vscode.window.showTextDocument(doc);
const diagnostic = await waitFor('brand/unknown diagnostic', () =>
vscode.languages.getDiagnostics(doc.uri).find((d) => d.source === 'archify' && d.code === 'brand/unknown'),
);
assert.match(doc.lineAt(diagnostic.range.start.line).text, /brand: not-a-brand/);
});
await check('opens a live preview beside the source', async () => {
await vscode.commands.executeCommand('archify.showPreviewToSide', workspaceFile('production-deployment.architecture.json'));
const tabs = await waitFor('preview tab', () => {
const found = webviewTabs('archify.preview').filter((tab) => /production-deployment/.test(tab.label));
return found.length > 0 && found;
});
assert.equal(tabs.length, 1);
assert.match(tabs[0].label, /Preview production-deployment\.architecture\.json/);
});
await check('reports validation problems in the Problems panel with JSON ranges', async () => {
const doc = await vscode.workspace.openTextDocument({
language: 'json',
content: JSON.stringify({ schema_version: 1, diagram_type: 'architecture', meta: { title: 'x' }, components: [{ id: 'a' }] }, null, 2),
});
await vscode.window.showTextDocument(doc);
const diagnostics = await waitFor('archify diagnostics', () => {
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify');
return found.length > 0 && found;
});
const component = diagnostics.find((d) => /components\/0/.test(d.message));
assert.ok(component, 'a diagnostic for /components/0');
assert.equal(doc.getText(component.range), '{', 'range points at the component object');
assert.equal(component.severity, vscode.DiagnosticSeverity.Error);
});
await check('clears problems once the document is fixed', async () => {
const good = await vscode.workspace.openTextDocument(workspaceFile('event-stream.dataflow.json'));
const doc = await vscode.workspace.openTextDocument({ language: 'json', content: good.getText() });
await vscode.window.showTextDocument(doc);
await new Promise((resolve) => setTimeout(resolve, 3000));
const errors = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify' && d.severity === vscode.DiagnosticSeverity.Error);
assert.deepEqual(errors.map((d) => d.message), []);
});
await check('renders to an HTML file and opens it in the Archify viewer', async () => {
const source = workspaceFile('cache-miss-request.sequence.json');
const output = vscode.Uri.file(path.join(path.dirname(source.fsPath), 'cache-miss-request.sequence.html'));
// Stub the save dialog and the follow-up notification.
const window = vscode.window as unknown as Record<string, unknown>;
const originalSave = window.showSaveDialog;
const originalInfo = window.showInformationMessage;
window.showSaveDialog = async () => output;
window.showInformationMessage = async () => 'Open in Viewer';
try {
await vscode.commands.executeCommand('archify.renderToHtml', source);
} finally {
window.showSaveDialog = originalSave;
window.showInformationMessage = originalInfo;
}
const html = new TextDecoder().decode(await vscode.workspace.fs.readFile(output));
assert.match(html, /<meta name="generator" content="archify/);
await waitFor('custom editor tab', () => webviewTabs('archify.htmlViewer').length > 0);
});
await check('source-backed diagram previews with the evidence fallback', async () => {
const uri = workspaceFile('mco-runtime.architecture.json');
await vscode.commands.executeCommand('archify.showPreview', uri);
await waitFor('mco preview tab', () => webviewTabs('archify.preview').some((tab) => /mco-runtime/.test(tab.label)));
const doc = await vscode.workspace.openTextDocument(uri);
const diagnostics = await waitFor('evidence diagnostics', () => {
const found = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify');
return found.length > 0 && found;
});
assert.ok(
diagnostics.every((d) => d.severity !== vscode.DiagnosticSeverity.Error),
'unverifiable evidence is a warning in fallback mode',
);
});
// --- models: a folder with main.archify, registries and diagrams that use `ref`
await check('a diagram inside a model resolves registry refs and validates clean', async () => {
const uri = workspaceFile('model/0200-containers/online-shop.archify');
const doc = await vscode.workspace.openTextDocument(uri);
await vscode.window.showTextDocument(doc);
// Validation runs through the Archify CLI; give it time, then expect no errors at all.
await new Promise((resolve) => setTimeout(resolve, 6000));
const errors = vscode.languages.getDiagnostics(doc.uri).filter((d) => d.source === 'archify' && d.severity === vscode.DiagnosticSeverity.Error);
assert.deepEqual(errors.map((d) => d.message), []);
});
await check('an unknown ref is reported at its line', async () => {
const target = workspaceFile('model/0300-components/ghost.archify');
await vscode.workspace.fs.writeFile(
target,
new TextEncoder().encode('schema_version: 1\ndiagram_type: architecture\nmeta: { title: Ghost, output: ghost.html }\ncomponents:\n - ref: ghost\n pos: [40, 40]\n size: [130, 60]\n'),
);
const doc = await vscode.workspace.openTextDocument(target);
await vscode.window.showTextDocument(doc);
const diagnostic = await waitFor('unknown ref diagnostic', () =>
vscode.languages.getDiagnostics(doc.uri).find((d) => d.source === 'archify' && /Unknown element "ghost"/.test(d.message)),
);
assert.equal(diagnostic.range.start.line, 4, 'reported on the ref line');
});
await check('hover and completion on ref come from the registry', async () => {
const uri = workspaceFile('model/0200-containers/online-shop.archify');
const doc = await vscode.workspace.openTextDocument(uri);
const line = doc.getText().split('\n').findIndex((l) => /ref: api$/.test(l));
assert.ok(line >= 0, 'the containers diagram refers to api');
const hovers = await vscode.commands.executeCommand<vscode.Hover[]>('vscode.executeHoverProvider', uri, new vscode.Position(line, 10));
const text = hovers.flatMap((h) => h.contents).map((c) => (typeof c === 'string' ? c : c.value)).join('\n');
assert.match(text, /Owns authentication/);
const defs = await vscode.commands.executeCommand<vscode.Location[]>('vscode.executeDefinitionProvider', uri, new vscode.Position(line, 10));
assert.match(defs[0].uri.path, /registry\/containers\.archify-registry$/);
const completions = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', uri, new vscode.Position(line, 9));
const labels = completions.items.map((i) => (typeof i.label === 'string' ? i.label : i.label.label));
assert.ok(labels.includes('api') && labels.includes('queue'), `completion lists registry elements: ${labels.join(', ')}`);
});
await check('a registry file reports duplicate ids and parse problems', async () => {
const target = workspaceFile('model/registry/extra.archify-registry');
await vscode.workspace.fs.writeFile(target, new TextEncoder().encode('schema_version: 1\nelements:\n api:\n type: backend\n label: API again\n'));
const doc = await vscode.workspace.openTextDocument(target);
await vscode.window.showTextDocument(doc);
const diagnostic = await waitFor('duplicate id diagnostic', () =>
vscode.languages.getDiagnostics(doc.uri).find((d) => d.source === 'archify' && /defined twice/.test(d.message)),
);
assert.equal(diagnostic.range.start.line, 2);
await vscode.workspace.fs.delete(target);
});
await check('main.archify opens as the model page and Open Model finds it from a diagram', async () => {
const main = workspaceFile('model/main.archify');
await vscode.commands.executeCommand('vscode.open', main);
await waitFor('model page tab', () =>
vscode.window.tabGroups.all.flatMap((g) => g.tabs).some((t) => t.input instanceof vscode.TabInputCustom && t.input.viewType === 'archify.diagram' && t.input.uri.toString() === main.toString()),
);
const diagram = workspaceFile('model/0100-context/online-shop.archify');
await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(diagram));
await vscode.commands.executeCommand('archify.openModel', diagram);
const tab = await waitFor('model page active', () =>
vscode.window.tabGroups.all.flatMap((g) => g.tabs).find((t) => t.isActive && t.input instanceof vscode.TabInputCustom && t.input.uri.toString() === main.toString()),
);
assert.ok(tab);
});
await check('Archify | New Diagram… → Model creates main.archify from the folder name', async () => {
const folder = workspaceFile('new-shop');
await vscode.workspace.fs.createDirectory(folder);
const { window } = vscode;
const originalPick = window.showQuickPick;
(window as { showQuickPick: unknown }).showQuickPick = async (items: Thenable<{ label: string }[]> | { label: string }[]) => (await items).find((i) => /^Model/.test(i.label));
try {
await vscode.commands.executeCommand('archify.newDiagram', folder);
} finally {
window.showQuickPick = originalPick;
}
const main = vscode.Uri.joinPath(folder, 'main.archify');
let content = '';
for (let i = 0; i < 50 && !content; i++) {
try {
content = new TextDecoder().decode(await vscode.workspace.fs.readFile(main));
} catch {
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
assert.match(content, /id: new-shop/);
assert.match(content, /name: New shop/);
});
}