mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 14:07:36 +02:00
fix: validate workflows and preserve execution outcomes
This commit is contained in:
1 parent
bdefa535b0
commit
8550325dfe
16 files changed
+494
-35
No files matched your search
@@ -39375,6 +39375,12 @@ html[data-theme="dark"] .form-group select {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.workflow-status-toggle.is-invalid {
|
||||
color: var(--error-color, #dc3545);
|
||||
border-color: currentColor;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.workflow-status-toggle:hover {
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
|
||||
@@ -3778,6 +3778,7 @@
|
||||
"processLibrary": "Workflows",
|
||||
"nodeLibrary": "Node library",
|
||||
"emptyList": "No workflows yet",
|
||||
"statusInvalid": "Invalid workflow",
|
||||
"statusEnabled": "Enabled",
|
||||
"statusDisabled": "Disabled",
|
||||
"metaId": "ID",
|
||||
|
||||
@@ -3790,6 +3790,7 @@
|
||||
"processLibrary": "Сценарии",
|
||||
"nodeLibrary": "Библиотека узлов",
|
||||
"emptyList": "Сценариев пока нет",
|
||||
"statusInvalid": "Ошибка проверки",
|
||||
"statusEnabled": "Включён",
|
||||
"statusDisabled": "Выключен",
|
||||
"metaId": "ID",
|
||||
|
||||
@@ -3766,6 +3766,7 @@
|
||||
"processLibrary": "工作流列表",
|
||||
"nodeLibrary": "节点库",
|
||||
"emptyList": "暂无工作流",
|
||||
"statusInvalid": "校验未通过",
|
||||
"statusEnabled": "启用",
|
||||
"statusDisabled": "禁用",
|
||||
"metaId": "ID",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
const fs=require('node:fs');
|
||||
const vm=require('node:vm');
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const source=fs.readFileSync('web/static/js/audit.js','utf8');
|
||||
test('reset clears the keyword and requests an unfiltered first page',() => {
|
||||
const fields={
|
||||
'audit-filter-category':{value:'auth'},'audit-filter-action':{value:'login',disabled:false},
|
||||
'audit-filter-result':{value:'failure'},'audit-filter-q':{value:'does-not-exist'},
|
||||
'audit-filter-since':{value:'2026-09-01T00:00'},'audit-filter-until':{value:'2026-09-02T00:00'}
|
||||
};
|
||||
let requested,cleared=false;
|
||||
const ctx={auditLogsPage:4,auditLogsPageSize:20,URLSearchParams,
|
||||
document:{getElementById:id => fields[id]},
|
||||
window:{AuditDatetimePicker:{clearAll(){cleared=true;fields['audit-filter-since'].value='';fields['audit-filter-until'].value='';}}},
|
||||
getAuditFilterDatetimeValue:id => fields[id].value,auditDatetimeLocalToRFC3339:x => x,
|
||||
rebuildAuditActionSelect(){fields['audit-filter-action'].value='';fields['audit-filter-action'].disabled=true;},
|
||||
syncAuditCustomSelect(){},loadAuditLogs(){requested=ctx.buildAuditQueryParams(false);}};
|
||||
vm.createContext(ctx);
|
||||
for (const [startMarker,endMarker] of [
|
||||
['function buildAuditQueryParams(','async function loadAuditSummary('],
|
||||
['function filterAuditLogs(','function applyAuditTimePreset(']
|
||||
]) {
|
||||
const start=source.indexOf(startMarker);
|
||||
vm.runInContext(source.slice(start,source.indexOf(endMarker,start)),ctx);
|
||||
}
|
||||
ctx.resetAuditLogFilters();
|
||||
const params=new URLSearchParams(requested);
|
||||
assert.equal(fields['audit-filter-q'].value,'');
|
||||
assert.equal(params.get('page'),'1');
|
||||
for (const key of ['q','category','action','result','since','until']) assert.equal(params.has(key),false);
|
||||
assert.equal(cleared,true);
|
||||
const html=fs.readFileSync('web/templates/index.html','utf8');
|
||||
assert.match(html,/<button[^>]*onclick="resetAuditLogFilters\(\)"[^>]*data-i18n="settingsAudit.resetBtn"/);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
const fs=require('node:fs');
|
||||
const vm=require('node:vm');
|
||||
const test=require('node:test');
|
||||
const assert=require('node:assert/strict');
|
||||
const source=fs.readFileSync('web/static/js/workflows.js','utf8');
|
||||
function harness(workflows,cy=null,errors=[]) {
|
||||
const list={innerHTML:''};
|
||||
const ctx={workflows,cy,currentWorkflowId:'selected',
|
||||
document:{getElementById:() => list},esc:x => String(x||''),_t:x => x,WORKFLOW_EDIT_ICON:'edit',
|
||||
elementsToGraph:() => ({}),validateWorkflowGraph:() => errors};
|
||||
vm.createContext(ctx);
|
||||
let start=source.indexOf(' function workflowValidationError(');
|
||||
vm.runInContext(source.slice(start,source.indexOf(' function updateWorkflowCanvasTitle(',start)),ctx);
|
||||
start=source.indexOf(' function renderWorkflowList(');
|
||||
vm.runInContext(source.slice(start,source.indexOf(' function nextNodeId(',start)),ctx);
|
||||
return {ctx,list};
|
||||
}
|
||||
test('invalid saved graph is labeled as invalid and never gets a green enabled badge',() => {
|
||||
const h=harness([{id:'invalid',name:'Invalid',enabled:true,validation_error:'missing start'}]);
|
||||
h.ctx.renderWorkflowList();
|
||||
assert.match(h.list.innerHTML,/workflows.statusInvalid/);
|
||||
assert.match(h.list.innerHTML,/is-invalid/);
|
||||
assert.doesNotMatch(h.list.innerHTML,/is-enabled/);
|
||||
assert.match(h.list.innerHTML,/title="missing start"/);
|
||||
});
|
||||
test('a selected invalid draft updates the list status',() => {
|
||||
const h=harness([{id:'selected',name:'Draft',enabled:true}],{},['missing output']);
|
||||
h.ctx.renderWorkflowList();
|
||||
assert.match(h.list.innerHTML,/workflows.statusInvalid/);
|
||||
});
|
||||
test('valid enabled and disabled definitions retain their configured status',() => {
|
||||
const h=harness([{id:'valid',enabled:true},{id:'off',enabled:false}]);
|
||||
h.ctx.renderWorkflowList();
|
||||
assert.match(h.list.innerHTML,/workflows.statusEnabled/);
|
||||
assert.match(h.list.innerHTML,/workflows.statusDisabled/);
|
||||
assert.doesNotMatch(h.list.innerHTML,/statusInvalid/);
|
||||
});
|
||||
@@ -350,6 +350,10 @@
|
||||
}
|
||||
});
|
||||
cy.on('add remove', updateEmptyState);
|
||||
cy.on('add remove data', () => {
|
||||
updateWorkflowCanvasTitle();
|
||||
renderWorkflowList();
|
||||
});
|
||||
document.addEventListener('keydown', event => {
|
||||
const active = document.activeElement;
|
||||
const editing = active && ['INPUT', 'TEXTAREA', 'SELECT'].includes(active.tagName);
|
||||
@@ -778,6 +782,19 @@
|
||||
};
|
||||
}
|
||||
|
||||
function workflowValidationError(wf) {
|
||||
if (cy && wf && wf.id === currentWorkflowId) {
|
||||
const errors = validateWorkflowGraph(elementsToGraph());
|
||||
if (errors.length) return errors.join(';');
|
||||
}
|
||||
return wf && wf.validation_error || '';
|
||||
}
|
||||
|
||||
function workflowStatusKey(wf, enabled) {
|
||||
return workflowValidationError(wf) ? 'workflows.statusInvalid'
|
||||
: (enabled ? 'workflows.statusEnabled' : 'workflows.statusDisabled');
|
||||
}
|
||||
|
||||
function updateWorkflowCanvasTitle() {
|
||||
const titleEl = document.getElementById('workflow-canvas-title');
|
||||
const subtitleEl = document.getElementById('workflow-canvas-subtitle');
|
||||
@@ -795,7 +812,7 @@
|
||||
const parts = [];
|
||||
if (meta.id) parts.push(meta.id);
|
||||
if (wf && wf.version) parts.push(`v${wf.version}`);
|
||||
parts.push(meta.enabled ? _t('workflows.statusEnabled') : _t('workflows.statusDisabled'));
|
||||
parts.push(_t(workflowStatusKey(wf || {id: currentWorkflowId}, meta.enabled)));
|
||||
subtitleEl.textContent = parts.join(' · ');
|
||||
subtitleEl.hidden = !parts.length;
|
||||
}
|
||||
@@ -848,10 +865,11 @@
|
||||
list.innerHTML = workflows.map(wf => {
|
||||
const encodedId = encodeURIComponent(wf.id);
|
||||
const isActive = wf.id === currentWorkflowId;
|
||||
const toggleTitle = esc(_t('workflows.toggleEnabled'));
|
||||
const validationError = workflowValidationError(wf);
|
||||
const toggleTitle = esc(validationError || _t('workflows.toggleEnabled'));
|
||||
const editTitle = esc(_t('workflows.editMeta'));
|
||||
const enabled = wf.enabled !== false;
|
||||
const statusText = esc(_t(enabled ? 'workflows.statusEnabled' : 'workflows.statusDisabled'));
|
||||
const statusText = esc(_t(workflowStatusKey(wf, enabled)));
|
||||
return `
|
||||
<div class="workflow-list-item ${isActive ? 'is-active' : ''}">
|
||||
<button type="button" class="workflow-list-main" onclick="selectWorkflow(decodeURIComponent('${encodedId}'))">
|
||||
@@ -859,7 +877,7 @@
|
||||
<span class="workflow-list-meta">${esc(wf.id)} · v${wf.version || 1}</span>
|
||||
</button>
|
||||
<div class="workflow-list-actions">
|
||||
<button type="button" class="workflow-status-toggle ${enabled ? 'is-enabled' : 'is-disabled'}" title="${toggleTitle}" aria-label="${toggleTitle}" aria-pressed="${enabled ? 'true' : 'false'}" onclick="event.stopPropagation(); toggleWorkflowEnabled(decodeURIComponent('${encodedId}'), ${enabled ? 'false' : 'true'})">
|
||||
<button type="button" class="workflow-status-toggle ${validationError ? 'is-invalid' : (enabled ? 'is-enabled' : 'is-disabled')}" title="${toggleTitle}" aria-label="${toggleTitle}" aria-pressed="${enabled ? 'true' : 'false'}" onclick="event.stopPropagation(); toggleWorkflowEnabled(decodeURIComponent('${encodedId}'), ${enabled ? 'false' : 'true'})">
|
||||
<span class="workflow-status-dot" aria-hidden="true"></span>
|
||||
<span>${statusText}</span>
|
||||
</button>
|
||||
|
||||
Reference in new issue
Block a user