mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-09-14 13:48:56 +02:00
140 lines
7.9 KiB
JavaScript
140 lines
7.9 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const monitor = fs.readFileSync('web/static/js/monitor.js', 'utf8');
|
|
const chat = fs.readFileSync('web/static/js/chat.js', 'utf8');
|
|
const reason = '工具调用已被安全规则拦截:识别到 example.gov,禁止访问。\n规则: 政府网站保护';
|
|
|
|
function sourceFunction(source, name) {
|
|
const start = source.indexOf(`function ${name}(`);
|
|
assert.notEqual(start, -1, name);
|
|
const rest = source.slice(start);
|
|
const next = rest.slice(1).search(/\n(?:async )?function /);
|
|
return (next === -1 ? rest : rest.slice(0, next + 1)).split(/\nwindow\.|\nconst toolCallDetailStateByItemId/)[0];
|
|
}
|
|
|
|
class Element {
|
|
constructor() {
|
|
this.dataset = {};
|
|
this.children = [];
|
|
this.className = '';
|
|
this.classList = {
|
|
contains: (value) => this.className.split(' ').includes(value),
|
|
add: (...values) => { this.className = [...new Set([...this.className.split(' ').filter(Boolean), ...values])].join(' '); },
|
|
remove: (...values) => { this.className = this.className.split(' ').filter((value) => !values.includes(value)).join(' '); }
|
|
};
|
|
}
|
|
set innerHTML(value) {
|
|
this.html = value;
|
|
this.title = new Element();
|
|
this.title.className = 'timeline-item-title';
|
|
}
|
|
get innerHTML() { return this.html; }
|
|
appendChild(child) { child.parent = this; this.children.push(child); }
|
|
remove() { if (this.parent) this.parent.children = this.parent.children.filter((child) => child !== this); }
|
|
querySelector(selector) {
|
|
if (selector === '.timeline-item-title') return this.title || null;
|
|
if (selector === '.tool-status-badge') return this.children.find((child) => child.classList.contains('tool-status-badge')) || null;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function runtime() {
|
|
const ctx = {
|
|
window: {}, document: { createElement: () => new Element() },
|
|
toolCallDetailStateByItemId: new Map(),
|
|
updateToolDetailToggleLabel() {}, applyEinoTimelineRole() {}, pruneLiveTimelineIfNeeded() {},
|
|
getCurrentTimeLocale: () => 'en-US', getTimeFormatOptions: () => ({}),
|
|
escapeHtml: (value) => String(value).replaceAll('&', '&').replaceAll('<', '<'),
|
|
};
|
|
const funcs = ['collectToolResultTextParts', 'isToolGuardBlockedResult', 'getToolExecutionDisplayStatus',
|
|
'getToolResultDisplayState', 'toolDisplayStatusFromState', 'getBackgroundRunningToolLabel',
|
|
'getToolCallStatusPresentation', 'applyToolCallStatus', 'parseToolCallArgsFromData', 'toolCallArgsEmpty',
|
|
'setToolCallDetailState', 'mergeToolResultIntoCallItem', 'coalesceProcessDetailsToolPairs',
|
|
'buildToolResultSectionHtml', 'addTimelineItem'];
|
|
vm.createContext(ctx);
|
|
vm.runInContext(funcs.map((name) => sourceFunction(monitor, name)).join('\n'), ctx);
|
|
ctx.window.getToolExecutionDisplayStatus = ctx.getToolExecutionDisplayStatus;
|
|
vm.runInContext(['normalizeToolExecutionSummary', 'getToolExecutionStatusLabel', 'formatMCPResultJsonForDisplay'].map((name) => sourceFunction(chat, name)).join('\n'), ctx);
|
|
return ctx;
|
|
}
|
|
|
|
test('structured block markers have priority over generic failure and running states', () => {
|
|
const ctx = runtime();
|
|
for (const payload of [
|
|
{ blocked: true, success: false, isError: true },
|
|
{ status: 'blocked', isError: true },
|
|
{ success: false, result: { blocked: true, isError: true, content: [] } },
|
|
{ success: false, result: JSON.stringify({ _meta: { 'cyberstrike.ai/blocked': true }, isError: true, content: [] }) },
|
|
{ blocked: true, displayStatus: 'background_running', success: true },
|
|
]) {
|
|
const state = ctx.getToolResultDisplayState(payload);
|
|
assert.equal(state.kind, 'blocked');
|
|
assert.equal(state.success, false);
|
|
assert.equal(ctx.toolDisplayStatusFromState(state), 'blocked');
|
|
}
|
|
});
|
|
|
|
test('legacy guard failures are recognized in raw, nested, serialized and deferred history results', () => {
|
|
const ctx = runtime();
|
|
for (const result of [reason, { isError: true, content: [{ type: 'text', text: reason }] }, JSON.stringify({ isError: true, content: [{ type: 'text', text: reason }] })]) {
|
|
assert.equal(ctx.getToolResultDisplayState({ result, success: false }).kind, 'blocked');
|
|
}
|
|
assert.equal(ctx.getToolResultDisplayState({ resultPreview: reason, success: false, _payloadDeferred: true }).kind, 'blocked');
|
|
assert.equal(ctx.getToolResultDisplayState({ success: false }, { rawText: reason }).kind, 'blocked');
|
|
});
|
|
|
|
test('quoted mentions, prefix lookalikes and explicitly successful output stay ordinary results', () => {
|
|
const ctx = runtime();
|
|
for (const result of ['示例:' + reason, '"' + reason + '"', '工具调用已被安全规则拦截说明文档']) {
|
|
assert.equal(ctx.getToolResultDisplayState({ result, success: false }).kind, 'error');
|
|
}
|
|
for (const data of [{ success: true, result: reason }, { isError: false, content: [{ text: reason }] }, { status: 'completed', result: reason }]) {
|
|
assert.equal(ctx.getToolResultDisplayState(data, { rawText: reason }).kind, 'success');
|
|
}
|
|
assert.equal(ctx.getToolResultDisplayState({ success: false, result: 'connection refused' }).kind, 'error');
|
|
});
|
|
|
|
test('live merge replaces a red failure badge with the distinct block badge and keeps the reason', () => {
|
|
const ctx = runtime();
|
|
const timeline = new Element();
|
|
ctx.addTimelineItem(timeline, 'tool_call', { title: 'http_request', data: { toolName: 'http_request' }, toolStatus: 'failed' });
|
|
const item = timeline.children[0];
|
|
assert.equal(item.classList.contains('tool-call-failed'), true);
|
|
ctx.mergeToolResultIntoCallItem(item, { blocked: true, success: false, result: reason });
|
|
assert.equal(item.dataset.toolDisplayStatus, 'blocked');
|
|
assert.equal(item.dataset.toolSuccess, '0');
|
|
assert.equal(item.classList.contains('tool-call-blocked'), true);
|
|
assert.equal(item.classList.contains('tool-call-failed'), false);
|
|
assert.equal(item.title.children.length, 1);
|
|
assert.match(item.title.children[0].textContent, /已拦截/);
|
|
assert.equal(ctx.toolCallDetailStateByItemId.get(item.id).rawText, reason);
|
|
});
|
|
|
|
test('refresh coalescing preserves blocks even when the old execution summary says failed', () => {
|
|
const ctx = runtime();
|
|
const details = ctx.coalesceProcessDetailsToolPairs([
|
|
{ id: 'call', eventType: 'tool_call', data: { toolCallId: 'id', toolName: 'http_request' } },
|
|
{ id: 'result', eventType: 'tool_result', data: { toolCallId: 'id', success: false, result: reason } }
|
|
]);
|
|
assert.equal(details.length, 1);
|
|
const timeline = new Element();
|
|
ctx.addTimelineItem(timeline, 'tool_call', { data: details[0].data, toolStatus: 'failed' });
|
|
const item = timeline.children[0];
|
|
assert.equal(item.dataset.toolDisplayStatus, 'blocked');
|
|
assert.match(item.title.children[0].className, /tool-status-blocked/);
|
|
assert.equal(item.classList.contains('tool-call-failed'), false);
|
|
const resultData = ctx.toolCallDetailStateByItemId.get(item.id).resultData;
|
|
assert.match(ctx.buildToolResultSectionHtml(resultData), /tool-result-section blocked/);
|
|
assert.doesNotMatch(ctx.buildToolResultSectionHtml(resultData), /tool-result-section error/);
|
|
});
|
|
|
|
test('execution summary buttons and raw detail preserve the separate blocked status', () => {
|
|
const ctx = runtime();
|
|
assert.equal(ctx.normalizeToolExecutionSummary({ toolName: 'http_request', status: 'blocked' }).status, 'blocked');
|
|
assert.equal(ctx.normalizeToolExecutionSummary({ toolName: 'http_request', status: 'failed', error: reason }).status, 'blocked');
|
|
assert.equal(ctx.getToolExecutionStatusLabel('blocked'), '已拦截');
|
|
assert.equal(JSON.parse(ctx.formatMCPResultJsonForDisplay({ blocked: true, isError: true, content: [] })).blocked, true);
|
|
});
|