- 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.
621 lines
25 KiB
JavaScript
621 lines
25 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { 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 {
|
|
createHorizontalRankMapper,
|
|
migrateWorkflowDocument,
|
|
} from '../migrations/workflow-v2.mjs';
|
|
import {
|
|
createMappedWorkflowCandidate,
|
|
intrinsicWorkflow,
|
|
planningWorkflow,
|
|
} from '../renderers/workflow/workflow-migration-geometry.mjs';
|
|
import { compileWorkflow } from '../renderers/workflow/workflow-compiler.mjs';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const skillRoot = path.resolve(__dirname, '..');
|
|
const cli = path.join(skillRoot, 'bin', 'archify.mjs');
|
|
const fixture = path.join(
|
|
__dirname,
|
|
'fixtures',
|
|
'v1-workflow-explicit-coordinates.workflow.json',
|
|
);
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-workflow-migration-'));
|
|
|
|
function sha256(value) {
|
|
return createHash('sha256').update(value).digest('hex');
|
|
}
|
|
|
|
function runMigration(source, destination, { importModule, env } = {}) {
|
|
return spawnSync(process.execPath, [
|
|
...(importModule ? ['--import', importModule] : []),
|
|
cli,
|
|
'migrate',
|
|
'workflow',
|
|
source,
|
|
destination,
|
|
'--to-schema',
|
|
'2',
|
|
'--json',
|
|
], {
|
|
encoding: 'utf8',
|
|
...(env ? { env: { ...process.env, ...env } } : {}),
|
|
});
|
|
}
|
|
|
|
function runValidation(source) {
|
|
return spawnSync(process.execPath, [cli, 'validate', 'workflow', source, '--json'], {
|
|
encoding: 'utf8',
|
|
});
|
|
}
|
|
|
|
function parseJsonOutput(result) {
|
|
assert.doesNotThrow(
|
|
() => JSON.parse(result.stdout),
|
|
`expected JSON stdout, received:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
|
);
|
|
return JSON.parse(result.stdout);
|
|
}
|
|
|
|
function copyFixture(name, mutate = (value) => value) {
|
|
const source = path.join(tmp, name);
|
|
const value = mutate(JSON.parse(fs.readFileSync(fixture, 'utf8')));
|
|
fs.writeFileSync(source, `${JSON.stringify(value, null, 2)}\n`);
|
|
return source;
|
|
}
|
|
|
|
function explicitPinConflictWorkflow() {
|
|
return {
|
|
schema_version: 1,
|
|
diagram_type: 'workflow',
|
|
meta: {
|
|
title: 'Unmappable explicit label pin',
|
|
viewBox: [900, 420],
|
|
legend: { mode: 'hidden' },
|
|
},
|
|
lanes: [{ id: 'main', label: 'Main' }],
|
|
nodes: [
|
|
{ id: 'a', lane: 'main', col: 0, type: 'backend', label: 'A' },
|
|
{ id: 'b', lane: 'main', col: 5, type: 'backend', label: 'B' },
|
|
],
|
|
edges: [{
|
|
id: 'ab',
|
|
from: 'a',
|
|
to: 'b',
|
|
label: 'outside',
|
|
labelAt: [365, -20],
|
|
}],
|
|
};
|
|
}
|
|
|
|
function heightConstrainedWorkflow() {
|
|
return {
|
|
schema_version: 1,
|
|
diagram_type: 'workflow',
|
|
meta: {
|
|
title: 'Legacy height capacity',
|
|
viewBox: [720, 240],
|
|
legend: { mode: 'hidden' },
|
|
},
|
|
lanes: [
|
|
{ id: 'first', label: 'First' },
|
|
{ id: 'second', label: 'Second' },
|
|
],
|
|
nodes: [
|
|
{ id: 'a', lane: 'first', col: 0, type: 'backend', label: 'A' },
|
|
{ id: 'b', lane: 'second', col: 2, type: 'database', label: 'B' },
|
|
],
|
|
edges: [],
|
|
};
|
|
}
|
|
|
|
function profileDivergenceWorkflow() {
|
|
return {
|
|
schema_version: 1,
|
|
diagram_type: 'workflow',
|
|
meta: {
|
|
title: 'profile divergence',
|
|
quality_profile: 'showcase',
|
|
legend: { mode: 'hidden' },
|
|
},
|
|
lanes: [
|
|
{ id: 'top', label: 'Top' },
|
|
{ id: 'middle', label: 'Middle' },
|
|
{ id: 'bottom', label: 'Bottom' },
|
|
{ id: 'issue', label: 'Issue' },
|
|
],
|
|
nodes: [
|
|
{ id: 'left', lane: 'middle', col: 0, type: 'backend', label: 'Left' },
|
|
{ id: 'right', lane: 'middle', col: 4, type: 'backend', label: 'Right' },
|
|
{ id: 'above', lane: 'top', col: 2, type: 'backend', label: 'Above' },
|
|
{ id: 'below', lane: 'bottom', col: 2, type: 'backend', label: 'Below' },
|
|
{ id: 'a', lane: 'issue', col: 1, type: 'backend', label: 'A' },
|
|
{ id: 'b', lane: 'issue', col: 2, type: 'backend', label: 'B' },
|
|
],
|
|
edges: [
|
|
{ id: 'ab', from: 'a', to: 'b' },
|
|
{ id: 'horizontal', from: 'left', to: 'right', via: [[244, 243]] },
|
|
{ id: 'vertical', from: 'above', to: 'below', via: [[300, 200]] },
|
|
],
|
|
};
|
|
}
|
|
|
|
test('migration geometry helpers construct mapped v2 candidates without mutating the source', () => {
|
|
const source = {
|
|
schema_version: 1,
|
|
diagram_type: 'workflow',
|
|
meta: { title: 'Pure migration geometry', 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' },
|
|
],
|
|
mainPath: ['a', 'b'],
|
|
edges: [{
|
|
id: 'ab',
|
|
from: 'a',
|
|
to: 'b',
|
|
label: 'pin',
|
|
fromSide: 'top',
|
|
toSide: 'top',
|
|
via: [[220, 60], [260, 60], [300, 60]],
|
|
labelAt: [260, 42],
|
|
channelX: 260,
|
|
}],
|
|
};
|
|
const sourceBefore = JSON.parse(JSON.stringify(source));
|
|
|
|
const intrinsic = intrinsicWorkflow(source);
|
|
assert.equal(intrinsic.schema_version, 2);
|
|
assert.equal('viewBox' in intrinsic.meta, false);
|
|
assert.deepEqual(intrinsic.edges, source.edges);
|
|
|
|
const planning = planningWorkflow(source);
|
|
assert.deepEqual(planning.edges, []);
|
|
assert.equal('mainPath' in planning, false);
|
|
|
|
const mapX = createHorizontalRankMapper([88, 220, 300], [94, 214, 334]);
|
|
assert.equal(mapX(260), 274, 'the migration module must preserve the mapper re-export');
|
|
|
|
const mapped = createMappedWorkflowCandidate(
|
|
source,
|
|
[88, 220, 300],
|
|
[94, 214, 334],
|
|
);
|
|
assert.equal(mapped.document.schema_version, 2);
|
|
assert.deepEqual(mapped.document.meta.viewBox, [720, 400]);
|
|
assert.deepEqual(mapped.document.edges[0].via, [[214, 60], [274, 60], [334, 60]]);
|
|
assert.deepEqual(mapped.document.edges[0].labelAt, [274, 42]);
|
|
assert.equal(mapped.document.edges[0].channelX, 274);
|
|
assert.deepEqual(mapped.changedCoordinates, [
|
|
{ path: '/edges/0/via/0/0', from: 220, to: 214 },
|
|
{ path: '/edges/0/via/1/0', from: 260, to: 274 },
|
|
{ path: '/edges/0/via/2/0', from: 300, to: 334 },
|
|
{ path: '/edges/0/labelAt/0', from: 260, to: 274 },
|
|
{ path: '/edges/0/channelX', from: 260, to: 274 },
|
|
]);
|
|
assert.deepEqual(source, sourceBefore);
|
|
});
|
|
|
|
test('document migration treats a legacy explicit viewBox as capacity and expands it monotonically', () => {
|
|
const source = heightConstrainedWorkflow();
|
|
const sourceBefore = JSON.parse(JSON.stringify(source));
|
|
|
|
const migration = migrateWorkflowDocument(source);
|
|
|
|
assert.equal(migration.ok, true, JSON.stringify(migration, null, 2));
|
|
assert.deepEqual(source, sourceBefore, 'the document migration must not mutate its input');
|
|
assert.deepEqual(migration.document.meta.viewBox, [768, 404]);
|
|
assert.deepEqual(migration.document.meta.viewBox, migration.newRequiredViewBox);
|
|
assert.ok(migration.document.meta.viewBox[0] >= source.meta.viewBox[0]);
|
|
assert.ok(migration.document.meta.viewBox[1] >= source.meta.viewBox[1]);
|
|
assert.ok(migration.preExistingDiagnostics.some(({ message }) => (
|
|
/viewBox height 240/.test(message)
|
|
)), JSON.stringify(migration.preExistingDiagnostics, null, 2));
|
|
assert.deepEqual(migration.migrationDiagnostics, []);
|
|
assert.deepEqual(migration.newSchemaDiagnostics, []);
|
|
});
|
|
|
|
test('CLI atomically commits a capacity-expanded migration and preserves source bytes', () => {
|
|
const source = path.join(tmp, 'height-capacity-source.workflow.json');
|
|
const destination = path.join(tmp, 'height-capacity-destination.workflow.json');
|
|
const sourceBytes = Buffer.from(`${JSON.stringify(heightConstrainedWorkflow(), null, 2)}\n`);
|
|
fs.writeFileSync(source, sourceBytes);
|
|
fs.writeFileSync(destination, 'destination sentinel\n');
|
|
|
|
const result = runMigration(source, destination);
|
|
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
assert.deepEqual(fs.readFileSync(source), sourceBytes, 'the CLI must preserve source bytes');
|
|
const destinationBytes = fs.readFileSync(destination);
|
|
const migrated = JSON.parse(destinationBytes);
|
|
const report = parseJsonOutput(result);
|
|
assert.equal(report.ok, true);
|
|
assert.equal(migrated.schema_version, 2);
|
|
assert.deepEqual(migrated.meta.viewBox, [768, 404]);
|
|
assert.deepEqual(migrated.meta.viewBox, report.newRequiredViewBox);
|
|
assert.ok(migrated.meta.viewBox[0] >= 720);
|
|
assert.ok(migrated.meta.viewBox[1] >= 240);
|
|
assert.deepEqual(report.destination, {
|
|
path: path.resolve(destination),
|
|
sha256: sha256(destinationBytes),
|
|
bytes: destinationBytes.length,
|
|
});
|
|
assert.deepEqual(report.migrationDiagnostics, []);
|
|
assert.deepEqual(report.newSchemaDiagnostics, []);
|
|
});
|
|
|
|
test('column-capacity diagnostics do not advertise migration across a quality-profile divergence', () => {
|
|
const document = profileDivergenceWorkflow();
|
|
const standard = compileWorkflow({ workflow: document, qualityProfile: 'standard' });
|
|
assert.equal(standard.ok, false);
|
|
const capacity = standard.diagnostics.find(({ code }) => code === 'workflow/column-capacity');
|
|
assert.ok(capacity, JSON.stringify(standard.diagnostics, null, 2));
|
|
|
|
const migration = migrateWorkflowDocument(document);
|
|
assert.equal(migration.ok, false, 'the authored showcase migration must reject its pinned crossing');
|
|
assert.ok(migration.newSchemaDiagnostics.some(({ code, evidence }) => (
|
|
code === 'workflow/explicit-pin-conflict'
|
|
&& evidence?.invariant === 'explicit route-route crossing'
|
|
)), JSON.stringify(migration, null, 2));
|
|
assert.ok(
|
|
capacity.supportedFixes.every((fix) => !/migrate this workflow/i.test(fix)),
|
|
`a standard-only verification must not advertise the failing authored showcase migration: ${capacity.supportedFixes}`,
|
|
);
|
|
});
|
|
|
|
test('document migration uses the authored profile and returns an independently compilable document', () => {
|
|
const document = profileDivergenceWorkflow();
|
|
document.meta.quality_profile = 'standard';
|
|
|
|
const migration = migrateWorkflowDocument(document);
|
|
|
|
assert.equal(migration.ok, true, JSON.stringify(migration, null, 2));
|
|
assert.deepEqual(migration.migrationDiagnostics, []);
|
|
assert.deepEqual(migration.newSchemaDiagnostics, []);
|
|
const standalone = compileWorkflow({ workflow: migration.document });
|
|
assert.equal(standalone.ok, true, JSON.stringify(standalone.diagnostics, null, 2));
|
|
});
|
|
|
|
test('document migration defaults an omitted profile to standard', () => {
|
|
const document = profileDivergenceWorkflow();
|
|
delete document.meta.quality_profile;
|
|
|
|
const migration = migrateWorkflowDocument(document);
|
|
|
|
assert.equal(migration.ok, true, JSON.stringify(migration, null, 2));
|
|
const standalone = compileWorkflow({ workflow: migration.document });
|
|
assert.equal(standalone.ok, true, JSON.stringify(standalone.diagnostics, null, 2));
|
|
});
|
|
|
|
test('CLI migration ignores ambient standard for an authored showcase workflow', () => {
|
|
const source = path.join(tmp, 'authored-showcase-source.workflow.json');
|
|
const destination = path.join(tmp, 'authored-showcase-destination.workflow.json');
|
|
const sourceBytes = Buffer.from(`${JSON.stringify(profileDivergenceWorkflow(), null, 2)}\n`);
|
|
fs.writeFileSync(source, sourceBytes);
|
|
|
|
const migration = runMigration(source, destination, {
|
|
env: { ARCHIFY_QUALITY_PROFILE: 'standard' },
|
|
});
|
|
|
|
assert.notEqual(migration.status, 0);
|
|
assert.deepEqual(fs.readFileSync(source), sourceBytes);
|
|
assert.equal(fs.existsSync(destination), false);
|
|
const report = parseJsonOutput(migration);
|
|
assert.equal(report.ok, false);
|
|
assert.ok(report.newSchemaDiagnostics.some(({ code, evidence }) => (
|
|
code === 'workflow/explicit-pin-conflict'
|
|
&& evidence?.invariant === 'explicit route-route crossing'
|
|
)), JSON.stringify(report, null, 2));
|
|
});
|
|
|
|
test('CLI migration ignores ambient showcase for an authored standard workflow', () => {
|
|
const source = path.join(tmp, 'authored-standard-source.workflow.json');
|
|
const destination = path.join(tmp, 'authored-standard-destination.workflow.json');
|
|
const document = profileDivergenceWorkflow();
|
|
document.meta.quality_profile = 'standard';
|
|
const sourceBytes = Buffer.from(`${JSON.stringify(document, null, 2)}\n`);
|
|
fs.writeFileSync(source, sourceBytes);
|
|
|
|
const migration = runMigration(source, destination, {
|
|
env: { ARCHIFY_QUALITY_PROFILE: 'showcase' },
|
|
});
|
|
|
|
assert.equal(migration.status, 0, migration.stderr || migration.stdout);
|
|
assert.deepEqual(fs.readFileSync(source), sourceBytes);
|
|
assert.equal(fs.existsSync(destination), true);
|
|
const report = parseJsonOutput(migration);
|
|
assert.equal(report.ok, true);
|
|
const migrated = JSON.parse(fs.readFileSync(destination, 'utf8'));
|
|
const standalone = compileWorkflow({ workflow: migrated });
|
|
assert.equal(standalone.ok, true, JSON.stringify(standalone.diagnostics, null, 2));
|
|
});
|
|
|
|
test('CLI migration makes the blocking new-schema diagnostic primary over pre-existing findings', () => {
|
|
const source = path.join(tmp, 'blocking-diagnostic-source.workflow.json');
|
|
const destination = path.join(tmp, 'blocking-diagnostic-destination.workflow.json');
|
|
fs.writeFileSync(source, `${JSON.stringify(profileDivergenceWorkflow(), null, 2)}\n`);
|
|
|
|
const migration = runMigration(source, destination, {
|
|
env: { ARCHIFY_QUALITY_PROFILE: 'showcase' },
|
|
});
|
|
|
|
assert.notEqual(migration.status, 0);
|
|
assert.equal(fs.existsSync(destination), false);
|
|
const report = parseJsonOutput(migration);
|
|
assert.equal(report.ok, false);
|
|
assert.equal(report.preExistingDiagnostics[0].code, 'workflow/column-capacity');
|
|
const blocker = report.newSchemaDiagnostics.find(({ code }) => code === 'workflow/explicit-pin-conflict');
|
|
assert.ok(blocker, JSON.stringify(report, null, 2));
|
|
assert.equal(report.error, blocker.message);
|
|
assert.deepEqual(report.diagnostics[0], blocker);
|
|
assert.ok(report.diagnostics.some(({ code }) => code === 'workflow/column-capacity'));
|
|
});
|
|
|
|
test('workflow migration remaps absolute x coordinates, expands only the constrained viewBox axis, and reports hashes', () => {
|
|
const source = copyFixture('pinned-source.workflow.json');
|
|
const destination = path.join(tmp, 'pinned-destination.workflow.json');
|
|
const sourceBefore = fs.readFileSync(source);
|
|
|
|
const result = runMigration(source, destination);
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
assert.deepEqual(fs.readFileSync(source), sourceBefore, 'migration must not mutate its source bytes');
|
|
assert.equal(fs.existsSync(destination), true);
|
|
|
|
const migratedBytes = fs.readFileSync(destination);
|
|
const migrated = JSON.parse(migratedBytes);
|
|
assert.equal(migrated.schema_version, 2);
|
|
assert.deepEqual(migrated.meta.viewBox, [768, 700], 'width grows to fit v2 while sufficient height is preserved');
|
|
assert.deepEqual(migrated.edges[0].via, [[214, 119]]);
|
|
assert.deepEqual(migrated.edges[1].labelAt, [394, 203]);
|
|
assert.equal(migrated.edges[2].channelX, 574);
|
|
|
|
const report = parseJsonOutput(result);
|
|
assert.equal(report.ok, true);
|
|
assert.equal(report.command, 'migrate');
|
|
assert.equal(report.type, 'workflow');
|
|
assert.equal(report.fromSchemaVersion, 1);
|
|
assert.equal(report.toSchemaVersion, 2);
|
|
assert.deepEqual(report.source, {
|
|
path: path.resolve(source),
|
|
sha256: sha256(sourceBefore),
|
|
bytes: sourceBefore.length,
|
|
});
|
|
assert.deepEqual(report.destination, {
|
|
path: path.resolve(destination),
|
|
sha256: sha256(migratedBytes),
|
|
bytes: migratedBytes.length,
|
|
});
|
|
assert.deepEqual(report.preExistingDiagnostics, []);
|
|
assert.ok(Array.isArray(report.migrationDiagnostics));
|
|
assert.deepEqual(report.newSchemaDiagnostics, []);
|
|
assert.deepEqual(report.changedCoordinates, [
|
|
{ path: '/edges/0/via/0/0', from: 220, to: 214 },
|
|
{ path: '/edges/1/labelAt/0', from: 365, to: 394 },
|
|
{ path: '/edges/2/channelX', from: 500, to: 574 },
|
|
]);
|
|
assert.deepEqual(report.oldRequiredViewBox, [720, 652]);
|
|
assert.deepEqual(report.newRequiredViewBox, [768, 652]);
|
|
});
|
|
|
|
test('workflow migration never shrinks an already spacious explicit viewBox', () => {
|
|
const source = copyFixture('spacious-source.workflow.json', (workflow) => {
|
|
workflow.meta.viewBox = [1600, 900];
|
|
return workflow;
|
|
});
|
|
const destination = path.join(tmp, 'spacious-destination.workflow.json');
|
|
|
|
const result = runMigration(source, destination);
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
const migrated = JSON.parse(fs.readFileSync(destination, 'utf8'));
|
|
assert.deepEqual(migrated.meta.viewBox, [1600, 900]);
|
|
assert.equal(parseJsonOutput(result).destination.sha256, sha256(fs.readFileSync(destination)));
|
|
});
|
|
|
|
test('workflow migration reports the measured legacy requirement separately from authored capacity', () => {
|
|
const source = {
|
|
schema_version: 1,
|
|
diagram_type: 'workflow',
|
|
meta: {
|
|
title: 'Spacious legacy capacity',
|
|
viewBox: [1600, 900],
|
|
legend: { mode: 'hidden' },
|
|
},
|
|
lanes: [{ id: 'main', label: 'Main' }],
|
|
nodes: [{ id: 'only', lane: 'main', col: 0, type: 'backend', label: 'Only' }],
|
|
edges: [],
|
|
};
|
|
|
|
const migration = migrateWorkflowDocument(source);
|
|
|
|
assert.equal(migration.ok, true, JSON.stringify(migration, null, 2));
|
|
assert.deepEqual(migration.oldRequiredViewBox, [720, 280]);
|
|
assert.deepEqual(migration.document.meta.viewBox, [1600, 900]);
|
|
assert.deepEqual(migration.newRequiredViewBox, [768, 280]);
|
|
});
|
|
|
|
test('workflow migration staging never aliases a destination named like its verification artifact', () => {
|
|
const source = copyFixture('artifact-name-source.workflow.json');
|
|
const destination = path.join(tmp, 'migration-check.html');
|
|
|
|
const result = runMigration(source, destination);
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
assert.equal(JSON.parse(fs.readFileSync(destination, 'utf8')).schema_version, 2);
|
|
assert.equal(parseJsonOutput(result).destination.path, path.resolve(destination));
|
|
});
|
|
|
|
test('workflow migration cleanup failure warns without reversing a successful commit', () => {
|
|
const source = copyFixture('cleanup-warning-source.workflow.json');
|
|
const destination = path.join(tmp, 'cleanup-warning-destination.workflow.json');
|
|
const importModule = path.join(__dirname, 'fixtures', 'fail-migration-cleanup.mjs');
|
|
|
|
const result = runMigration(source, destination, { importModule });
|
|
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
assert.equal(JSON.parse(fs.readFileSync(destination, 'utf8')).schema_version, 2);
|
|
assert.equal(parseJsonOutput(result).ok, true);
|
|
assert.match(result.stderr, /Warning: could not remove workflow migration staging directory/);
|
|
assert.match(result.stderr, /simulated migration cleanup failure/);
|
|
});
|
|
|
|
test('workflow migration is idempotent when its v2 destination is migrated again', () => {
|
|
const source = copyFixture('idempotent-source.workflow.json');
|
|
const firstDestination = path.join(tmp, 'idempotent-first.workflow.json');
|
|
const secondDestination = path.join(tmp, 'idempotent-second.workflow.json');
|
|
|
|
const first = runMigration(source, firstDestination);
|
|
assert.equal(first.status, 0, first.stderr || first.stdout);
|
|
const second = runMigration(firstDestination, secondDestination);
|
|
assert.equal(second.status, 0, second.stderr || second.stdout);
|
|
|
|
const firstBytes = fs.readFileSync(firstDestination);
|
|
const secondBytes = fs.readFileSync(secondDestination);
|
|
assert.deepEqual(secondBytes, firstBytes);
|
|
|
|
const report = parseJsonOutput(second);
|
|
assert.equal(report.fromSchemaVersion, 2);
|
|
assert.equal(report.toSchemaVersion, 2);
|
|
assert.deepEqual(report.changedCoordinates, []);
|
|
assert.deepEqual(report.preExistingDiagnostics, []);
|
|
assert.deepEqual(report.migrationDiagnostics, []);
|
|
assert.deepEqual(report.newSchemaDiagnostics, []);
|
|
assert.equal(report.destination.sha256, report.source.sha256);
|
|
assert.deepEqual(report.newRequiredViewBox, report.oldRequiredViewBox);
|
|
});
|
|
|
|
test('fallback planning preserves straight-edge rank constraints when mapping absolute pins', () => {
|
|
const source = path.join(tmp, 'fallback-rank-source.workflow.json');
|
|
const destination = path.join(tmp, 'fallback-rank-destination.workflow.json');
|
|
const document = {
|
|
schema_version: 1,
|
|
diagram_type: 'workflow',
|
|
meta: { title: 'Fallback rank mapping', viewBox: [720, 520], legend: { mode: 'hidden' } },
|
|
lanes: [
|
|
{ id: 'pin', label: 'Pinned label' },
|
|
{ id: 'straight', label: 'Straight constraint' },
|
|
{ id: 'blocker', label: 'Blocker' },
|
|
],
|
|
nodes: [
|
|
{ id: 'a', lane: 'pin', col: 0, type: 'backend', label: 'A' },
|
|
{ id: 'b', lane: 'pin', col: 1, type: 'backend', label: 'B' },
|
|
{ id: 'c', lane: 'straight', col: 0, type: 'backend', label: 'C' },
|
|
{ id: 'd', lane: 'straight', col: 1, type: 'backend', label: 'D' },
|
|
{ id: 'obstacle', lane: 'blocker', col: 4, type: 'database', label: 'Obstacle' },
|
|
],
|
|
mainPath: ['a', 'b'],
|
|
edges: [
|
|
{
|
|
id: 'pin-edge',
|
|
from: 'a',
|
|
to: 'b',
|
|
label: 'p',
|
|
labelAt: [562, 367],
|
|
route: 'bottom-channel',
|
|
fromSide: 'bottom',
|
|
toSide: 'bottom',
|
|
},
|
|
{ id: 'straight-edge', from: 'c', to: 'd', label: 'x', route: 'straight' },
|
|
],
|
|
};
|
|
fs.writeFileSync(source, `${JSON.stringify(document, null, 2)}\n`);
|
|
|
|
const result = runMigration(source, destination);
|
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
|
const migrated = JSON.parse(fs.readFileSync(destination, 'utf8'));
|
|
assert.deepEqual(migrated.edges[0].labelAt, [643.52, 367]);
|
|
const report = parseJsonOutput(result);
|
|
assert.deepEqual(report.changedCoordinates, [{
|
|
path: '/edges/0/labelAt/0',
|
|
from: 562,
|
|
to: 643.52,
|
|
}]);
|
|
assert.deepEqual(report.newSchemaDiagnostics, []);
|
|
});
|
|
|
|
for (const { example, expectedEdge } of [
|
|
{ example: 'incident-response.workflow.json', expectedEdge: ['alert', 'page'] },
|
|
{ example: 'release-delivery.workflow.json', expectedEdge: ['pull_request', 'build'] },
|
|
]) {
|
|
test(`workflow migration preserves causal route diagnostics for packaged ${example}`, () => {
|
|
const source = path.join(skillRoot, 'examples', example);
|
|
const destination = path.join(tmp, `migrated-${example}`);
|
|
const sourceBefore = fs.readFileSync(source);
|
|
|
|
const validation = runValidation(source);
|
|
assert.equal(validation.status, 0, validation.stderr || validation.stdout);
|
|
const result = runMigration(source, destination);
|
|
assert.notEqual(result.status, 0);
|
|
assert.equal(fs.existsSync(destination), false);
|
|
assert.deepEqual(fs.readFileSync(source), sourceBefore);
|
|
|
|
const failure = parseJsonOutput(result);
|
|
assert.equal(failure.diagnostics.length, 1, JSON.stringify(failure.diagnostics, null, 2));
|
|
assert.ok(failure.diagnostics.every(({ message }) => !/mainPath step .* no matching edge/.test(message)));
|
|
const [diagnostic] = failure.diagnostics;
|
|
assert.equal(diagnostic.code, 'workflow/route-preset-conflict');
|
|
assert.deepEqual([diagnostic.subject?.from, diagnostic.subject?.to], expectedEdge);
|
|
assert.equal(diagnostic.subject?.route, 'drop');
|
|
});
|
|
}
|
|
|
|
test('workflow migration rejects the source path as its destination without changing the file', () => {
|
|
const source = copyFixture('same-path.workflow.json');
|
|
const sourceBefore = fs.readFileSync(source);
|
|
|
|
const result = runMigration(source, source);
|
|
assert.notEqual(result.status, 0);
|
|
assert.deepEqual(fs.readFileSync(source), sourceBefore);
|
|
|
|
const failure = parseJsonOutput(result);
|
|
assert.equal(failure.ok, false);
|
|
assert.equal(failure.command, 'migrate');
|
|
assert.ok(Array.isArray(failure.diagnostics));
|
|
assert.equal(failure.diagnostics.length, 1);
|
|
assert.equal(failure.diagnostics[0].code, 'migration/source-destination');
|
|
assert.match(failure.error, /source|destination/i);
|
|
assert.ok(failure.diagnostics[0].supportedFixes.some((fix) => /different.*destination/i.test(fix)));
|
|
});
|
|
|
|
test('workflow migration reports a cyclic-symlink destination as structured JSON', () => {
|
|
const source = copyFixture('symlink-cycle-source.workflow.json');
|
|
const destination = path.join(tmp, 'migration-cycle-a.workflow.json');
|
|
const otherLink = path.join(tmp, 'migration-cycle-b.workflow.json');
|
|
fs.symlinkSync(otherLink, destination, 'file');
|
|
fs.symlinkSync(destination, otherLink, 'file');
|
|
|
|
const result = runMigration(source, destination);
|
|
assert.notEqual(result.status, 0);
|
|
assert.equal(result.stderr, '');
|
|
const failure = parseJsonOutput(result);
|
|
assert.equal(failure.ok, false);
|
|
assert.equal(failure.diagnostics[0].code, 'output/symlink-cycle');
|
|
assert.equal(failure.diagnostics[0].subject.output, path.resolve(destination));
|
|
});
|
|
|
|
test('failed workflow migration emits diagnostics and never writes its destination', () => {
|
|
const source = path.join(tmp, 'pin-conflict-source.workflow.json');
|
|
const destination = path.join(tmp, 'pin-conflict-destination.workflow.json');
|
|
fs.writeFileSync(source, `${JSON.stringify(explicitPinConflictWorkflow(), null, 2)}\n`);
|
|
assert.equal(fs.existsSync(destination), false);
|
|
|
|
const result = runMigration(source, destination);
|
|
assert.notEqual(result.status, 0);
|
|
assert.equal(fs.existsSync(destination), false, 'a failed migration must not leave a partial destination');
|
|
|
|
const failure = parseJsonOutput(result);
|
|
assert.equal(failure.ok, false);
|
|
assert.equal(failure.command, 'migrate');
|
|
assert.ok(Array.isArray(failure.diagnostics));
|
|
assert.ok(
|
|
failure.diagnostics.some(({ code }) => code === 'workflow/explicit-pin-conflict'),
|
|
JSON.stringify(failure.diagnostics, null, 2),
|
|
);
|
|
});
|
|
|
|
process.on('exit', () => fs.rmSync(tmp, { recursive: true, force: true }));
|