- Added a new Archify skill, enabling users to create polished architecture, workflow, sequence, data-flow, and lifecycle diagrams. - Implemented comprehensive functionality including rendering, validation, and delivery of diagrams in various formats. - Integrated a user-friendly command-line interface for generating and previewing diagrams. - Developed supporting files including package.json, LICENSE, and SKILL.md for documentation and licensing. - Added unit tests to ensure reliability and functionality of the new skill. These changes enhance the application by providing a structured approach to visualizing system architecture and workflows, improving user experience and data representation.
98 lines
5.2 KiB
JavaScript
98 lines
5.2 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { execFileSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { animateAttr } from '../renderers/shared/cli.mjs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const skillRoot = path.resolve(__dirname, '..');
|
|
const template = fs.readFileSync(path.join(skillRoot, 'assets', 'template.html'), 'utf8');
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-settled-flow-'));
|
|
|
|
const CASES = {
|
|
architecture: 'web-app.architecture.json',
|
|
workflow: 'agent-tool-call.workflow.json',
|
|
sequence: 'cache-miss-request.sequence.json',
|
|
dataflow: 'product-analytics.dataflow.json',
|
|
lifecycle: 'agent-run.lifecycle.json',
|
|
};
|
|
|
|
function render(mode, example) {
|
|
const doc = JSON.parse(fs.readFileSync(path.join(skillRoot, 'examples', example), 'utf8'));
|
|
doc.meta = { ...doc.meta, animation: 'trace' };
|
|
const input = path.join(tmp, `${mode}.json`);
|
|
const output = path.join(tmp, `${mode}.html`);
|
|
fs.writeFileSync(input, JSON.stringify(doc));
|
|
execFileSync('node', [path.join(skillRoot, `renderers/${mode}/render-${mode}.mjs`), input, output], {
|
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
});
|
|
return fs.readFileSync(output, 'utf8');
|
|
}
|
|
|
|
test('all five renderers inherit one finite running-to-settled ambient contract', () => {
|
|
for (const [mode, example] of Object.entries(CASES)) {
|
|
const html = render(mode, example);
|
|
assert.match(html, /data-ambient-motion/, mode);
|
|
assert.match(html, /function startAmbient\(\)/, mode);
|
|
assert.match(html, /function settleAmbient\(reason\)/, mode);
|
|
assert.match(html, /animation: archify-edge-flow [^;]+ 1;/, mode);
|
|
assert.match(html, /animation: archify-node-pulse [^;]+ 1;/, mode);
|
|
assert.doesNotMatch(html, /animation: archify-edge-flow [^;]+ infinite/, mode);
|
|
assert.doesNotMatch(html, /animation: archify-node-pulse [^;]+ infinite/, mode);
|
|
}
|
|
});
|
|
|
|
test('settled flow restores authored security and async dash semantics', () => {
|
|
assert.match(template, /\.a-security\s*\{[^}]*stroke-dasharray:\s*5,5/);
|
|
assert.match(template, /\.a-dashed\s*\{[^}]*stroke-dasharray:\s*4,4/);
|
|
assert.match(template, /@keyframes archify-edge-flow\s*\{[\s\S]*?stroke-dasharray:\s*10 8/);
|
|
assert.match(template, /100%\s*\{\s*stroke-dashoffset:\s*0;\s*opacity:\s*1;\s*\}/);
|
|
const runningRule = template.match(/html\[data-ambient-motion="running"\][^{]+\[data-animate="edge"\][^{]*\{([^}]*)\}/)?.[1] || '';
|
|
assert.ok(runningRule, 'running edge rule missing');
|
|
assert.doesNotMatch(runningRule, /stroke-dasharray|stroke-dashoffset/);
|
|
});
|
|
|
|
test('ambient ownership is generation-bounded and cannot replay after settle', () => {
|
|
assert.match(template, /var ambientStarted = false/);
|
|
assert.match(template, /var ambientPending = new Set\(\)/);
|
|
assert.match(template, /if \(ambientStarted \|\| !capable\) return false/);
|
|
assert.match(template, /ambientStarted = true;[\s\S]*?html\.setAttribute\('data-ambient-motion', 'running'\)/);
|
|
assert.match(template, /ambientPending\.delete\(event\.target\)/);
|
|
assert.match(template, /if \(!ambientPending\.size\) settleAmbient\('complete'\)/);
|
|
assert.match(template, /svg\.addEventListener\('animationend', onAmbientBoundary, true\)/);
|
|
assert.match(template, /svg\.addEventListener\('animationcancel', onAmbientBoundary, true\)/);
|
|
assert.match(template, /if \(paused \|\| owner \|\| html\.hasAttribute\('data-embed'\)/);
|
|
assert.doesNotMatch(template, /setInterval\([^)]*ambient|addEventListener\('scroll'[^)]*ambient/);
|
|
});
|
|
|
|
test('animation delay is capped without changing normal authored order', () => {
|
|
assert.equal(animateAttr({ animation: 'trace' }, 'edge', 0), ' data-animate="edge" style="--step:0"');
|
|
assert.equal(animateAttr({ animation: 'trace' }, 'node', 8), ' data-animate="node" style="--step:8"');
|
|
assert.equal(animateAttr({ animation: 'trace' }, 'edge', 99), ' data-animate="edge" style="--step:12"');
|
|
assert.equal(animateAttr({}, 'edge', 99), '');
|
|
});
|
|
|
|
test('only the WebM canvas scene opts into a repeatable finite motion timeline', () => {
|
|
assert.match(template, /var motionScene = createMotionScene\(svg\)/);
|
|
assert.match(template, /drawMotionFrame\(ctx, backgroundImage, motionScene, elapsed\)/);
|
|
assert.match(template, /var data = serializeSvg\(scale\);/);
|
|
assert.match(template, /getPointAtLength/);
|
|
assert.doesNotMatch(template, /serializeSvg\(1, \{ autoTheme: true, motion: true \}\)/);
|
|
});
|
|
|
|
test('Still, reduced motion, embed, share, hidden state, and stronger intent settle ambient flow', () => {
|
|
assert.match(template, /reducedMotion\(\)/);
|
|
assert.match(template, /html\.hasAttribute\('data-embed'\)/);
|
|
assert.match(template, /html\.hasAttribute\('data-share-playback'\)/);
|
|
assert.match(template, /html\.hasAttribute\('data-document-hidden'\)/);
|
|
assert.match(template, /paused \|\| owner/);
|
|
assert.match(template, /settleAmbient\('suppressed'\)/);
|
|
assert.match(template, /html\[data-motion="still"\] svg\[data-animation="trace"\] \[data-animate\]/);
|
|
assert.match(template, /@media \(prefers-reduced-motion: reduce\)/);
|
|
});
|
|
|
|
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
|