Files
CyberStrikeAI/web/static/js/tool-blocked-display.test.cjs
T

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('&', '&amp;').replaceAll('<', '&lt;'),
};
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);
});