mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-15 23:50:32 +02:00
186 lines
5.8 KiB
JavaScript
186 lines
5.8 KiB
JavaScript
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const chat = fs.readFileSync('web/static/js/chat.js', 'utf8');
|
|
|
|
function timingSource(source) {
|
|
const start = source.indexOf('function formatAssistantTurnDuration(');
|
|
const end = source.indexOf('window.setAssistantTurnTiming = setAssistantTurnTiming;', start);
|
|
assert.notEqual(start, -1, 'formatAssistantTurnDuration should exist');
|
|
assert.notEqual(end, -1, 'timing exports should exist');
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
function processDetailFilterSource(source) {
|
|
const start = source.indexOf('function isEinoAgentHeartbeatProgress(');
|
|
const end = source.indexOf('function dedupeConsecutiveProcessDetailRows(', start);
|
|
assert.notEqual(start, -1, 'process detail filter should exist');
|
|
assert.notEqual(end, -1, 'dedupe boundary should exist');
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
function createClassList() {
|
|
return {
|
|
toggle() {},
|
|
};
|
|
}
|
|
|
|
function createMessage() {
|
|
const label = {
|
|
innerHTML: '',
|
|
classList: createClassList(),
|
|
setAttribute() {},
|
|
};
|
|
return {
|
|
dataset: {},
|
|
querySelector(selector) {
|
|
if (selector === '.mcp-call-label.turn-process-summary') return label;
|
|
return null;
|
|
},
|
|
label,
|
|
};
|
|
}
|
|
|
|
function createHarness(nowMs) {
|
|
const RealDate = Date;
|
|
class TestDate extends RealDate {
|
|
static now() {
|
|
return nowMs;
|
|
}
|
|
}
|
|
const context = {
|
|
Date: TestDate,
|
|
Number,
|
|
Math,
|
|
String,
|
|
document: {
|
|
querySelector() { return null; },
|
|
querySelectorAll() { return []; },
|
|
getElementById() { return null; },
|
|
},
|
|
window: {},
|
|
escapeHtml(value) { return String(value); },
|
|
setInterval() { return 1; },
|
|
clearInterval() {},
|
|
};
|
|
vm.runInNewContext(
|
|
`${timingSource(chat)}; this.setAssistantTurnTiming = setAssistantTurnTiming;`,
|
|
context
|
|
);
|
|
return context;
|
|
}
|
|
|
|
test('刷新运行中任务时忽略摘要中的零耗时并按开始时间恢复', () => {
|
|
const startedAt = '2026-08-12T02:00:00.000Z';
|
|
const startedMs = Date.parse(startedAt);
|
|
const context = createHarness(startedMs + 65_000);
|
|
const message = createMessage();
|
|
|
|
context.setAssistantTurnTiming(message, {
|
|
startedAt,
|
|
durationMs: 0,
|
|
status: 'running',
|
|
});
|
|
|
|
assert.equal(message.dataset.turnDurationMs, undefined);
|
|
assert.match(message.label.innerHTML, /已处理 1 分钟 5 秒/);
|
|
});
|
|
|
|
test('已完成任务仍优先使用持久化耗时', () => {
|
|
const context = createHarness(Date.parse('2026-08-12T02:05:00.000Z'));
|
|
const message = createMessage();
|
|
|
|
context.setAssistantTurnTiming(message, {
|
|
startedAt: '2026-08-12T02:00:00.000Z',
|
|
completedAt: '2026-08-12T02:01:05.000Z',
|
|
durationMs: 65_000,
|
|
status: 'completed',
|
|
});
|
|
|
|
assert.equal(message.dataset.turnDurationMs, '65000');
|
|
assert.match(message.label.innerHTML, /耗时 1 分钟 5 秒/);
|
|
});
|
|
|
|
test('已中断任务使用固定终态耗时且不再按当前时间增长', () => {
|
|
const context = createHarness(Date.parse('2026-08-13T12:00:00.000Z'));
|
|
const message = createMessage();
|
|
|
|
context.setAssistantTurnTiming(message, {
|
|
startedAt: '2026-08-11T09:47:14.000Z',
|
|
completedAt: '2026-08-11T09:51:39.000Z',
|
|
durationMs: 265_000,
|
|
status: 'cancelled',
|
|
});
|
|
|
|
assert.equal(message.dataset.turnDurationMs, '265000');
|
|
assert.match(message.label.innerHTML, /已中断 · 耗时 4 分钟 25 秒/);
|
|
assert.doesNotMatch(message.label.innerHTML, /已处理/);
|
|
});
|
|
|
|
test('历史占位消息存在取消事件时不会再判定为运行中', () => {
|
|
assert.match(chat, /function assistantTurnTerminalState\(processDetails\)/);
|
|
assert.match(chat, /const isRunning = isAssistantPlaceholder && !terminalState/);
|
|
assert.match(chat, /status: status/);
|
|
});
|
|
|
|
test('过程详情隐藏 Eino 内部诊断但保留真实工具调用', () => {
|
|
const context = {};
|
|
vm.runInNewContext(
|
|
`${processDetailFilterSource(chat)}; this.filterNoiseProcessDetails = filterNoiseProcessDetails;`,
|
|
context
|
|
);
|
|
|
|
const filtered = context.filterNoiseProcessDetails([
|
|
{
|
|
eventType: 'tool_call',
|
|
data: {
|
|
toolName: 'task',
|
|
argumentsObj: {
|
|
_cyberstrike_model_output_recovery: {
|
|
reason: 'invalid_tool_arguments_json',
|
|
repair_attempt: 1,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
eventType: 'model_output_rejected',
|
|
message: '模型工具调用不完整或参数不安全,已阻止执行并要求重写。',
|
|
data: { reason: 'invalid_tool_arguments_json' },
|
|
},
|
|
{
|
|
eventType: 'progress',
|
|
message: 'Eino TurnLoop 常驻多轮 runtime 已接管本轮会话。',
|
|
data: { kind: 'turn_loop_takeover' },
|
|
},
|
|
{
|
|
eventType: 'tool_call',
|
|
data: {
|
|
toolName: 'task',
|
|
argumentsObj: { _raw: 'command' },
|
|
arguments: 'command',
|
|
},
|
|
},
|
|
{
|
|
eventType: 'tool_call',
|
|
data: {
|
|
toolName: 'task',
|
|
argumentsObj: { _raw: '"' },
|
|
arguments: '"',
|
|
},
|
|
},
|
|
{
|
|
eventType: 'tool_call',
|
|
data: {
|
|
toolName: 'exec',
|
|
arguments: '{"command":"ls"}',
|
|
},
|
|
},
|
|
]);
|
|
|
|
assert.equal(filtered.length, 1);
|
|
assert.equal(filtered[0].data.toolName, 'exec');
|
|
});
|