Files
CyberStrikeAI/web/static/js/workflow-save.test.cjs
T

115 lines
5.2 KiB
JavaScript

const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const test = require('node:test');
const assert = require('node:assert/strict');
const source = fs.readFileSync(path.join(__dirname, 'workflows.js'), 'utf8');
function createEditor() {
const elements = {
'workflow-id': { value: '' },
'workflow-name': { value: '' },
'workflow-description': { value: '' },
'workflow-enabled': { checked: true },
'workflow-canvas': {}
};
const nodes = [
{ id: 'start', type: 'start', config: {} },
{ id: 'output', type: 'output', config: { output_key: 'result' } }
].map(data => ({ id: () => data.id, data: key => data[key], position: () => ({ x: 0, y: 0 }) }));
const state = { calls: [], notifications: [], workflows: [], modalOpen: false };
const context = {
document: { getElementById: id => elements[id] || null, querySelector: () => null, addEventListener() {} },
cytoscape: () => ({
nodes: () => nodes,
edges: () => [{ id: () => 'edge', source: () => nodes[0], target: () => nodes[1], data: () => '' }],
on() {}, resize() {}
}),
openAppModal: () => { state.modalOpen = true; },
closeAppModal: () => { state.modalOpen = false; },
showNotification: (message, type) => state.notifications.push({ message, type }),
apiFetch: async (url, options = {}) => {
const body = options.body ? JSON.parse(options.body) : undefined;
state.calls.push({ url, method: options.method || 'GET', body });
if (url === '/api/workflows/validate') {
if (state.validationGate) await state.validationGate;
return { ok: true };
}
if (options.method === 'POST' || options.method === 'PUT') {
const workflow = { ...body, graph_json: JSON.stringify(body.graph), version: 1 };
state.workflows = [workflow];
return { ok: true, json: async () => ({ workflow }) };
}
if (url.startsWith('/api/workflows?')) return { ok: true, json: async () => ({ workflows: state.workflows }) };
throw new Error('Unexpected API: ' + url);
}
};
context.window = context;
vm.createContext(context);
vm.runInContext(source, context, { filename: 'workflows.js' });
const fillMeta = () => {
elements['workflow-id'].value = 'save-regression';
elements['workflow-name'].value = 'Save regression';
};
const saves = () => state.calls.filter(call => call.url === '/api/workflows' && call.method === 'POST');
return { context, state, elements, fillMeta, saves };
}
test('Save resumes after metadata confirmation and submits the graph once', async () => {
const editor = createEditor();
await editor.context.saveWorkflowDraft();
assert.equal(editor.state.modalOpen, true);
assert.equal(editor.state.calls.length, 0);
editor.fillMeta();
let finishValidation;
editor.state.validationGate = new Promise(resolve => { finishValidation = resolve; });
const saving = editor.context.applyWorkflowMetaModal();
await editor.context.applyWorkflowMetaModal();
assert.equal(editor.state.calls.filter(call => call.url === '/api/workflows/validate').length, 1);
assert.equal(editor.saves().length, 0);
finishValidation();
await saving;
assert.equal(editor.saves().length, 1);
assert.equal(editor.saves()[0].body.name, 'Save regression');
assert.deepEqual(editor.saves()[0].body.graph.nodes.map(node => node.id), ['start', 'output']);
assert.equal(editor.state.modalOpen, false);
assert.equal(editor.state.notifications.at(-1).message, 'workflows.saved');
assert.ok(editor.state.calls.some(call => call.url.startsWith('/api/workflows?')));
});
test('Cancel clears a pending save; reopening metadata only edits the draft', async () => {
const editor = createEditor();
await editor.context.saveWorkflowDraft();
editor.context.closeWorkflowMetaModal();
editor.fillMeta();
await editor.context.applyWorkflowMetaModal();
assert.equal(editor.state.calls.length, 0);
await editor.context.saveWorkflowDraft();
assert.equal(editor.saves().length, 1);
const callCount = editor.state.calls.length;
editor.context.openWorkflowMetaModal();
await editor.context.applyWorkflowMetaModal();
assert.equal(editor.state.calls.length, callCount);
});
test('Opening metadata normally replaces pending save intent', async () => {
const editor = createEditor();
await editor.context.saveWorkflowDraft();
editor.context.openWorkflowMetaModal();
editor.fillMeta();
await editor.context.applyWorkflowMetaModal();
assert.equal(editor.state.calls.length, 0);
});
test('Invalid metadata keeps the pending save until corrected', async () => {
const editor = createEditor();
await editor.context.saveWorkflowDraft();
await editor.context.applyWorkflowMetaModal();
assert.equal(editor.state.modalOpen, true);
assert.equal(editor.state.calls.length, 0);
editor.fillMeta();
await editor.context.applyWorkflowMetaModal();
assert.equal(editor.saves().length, 1);
});