mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-14 07:00:23 +02:00
* feat(chat): add project-based conversation sidebar * feat(chat): refine Codex-style conversation UI * feat(chat): add Codex-style conversation workflow * feat(ui): 优化对话框与项目侧边栏交互 * fix(chat): 修复暗色输入框圆角填色 * fix(chat): 恢复输入区分层错位布局 * fix(hitl): isolate reviewer state per conversation * feat(hitl): 增加双入口审批与倒计时进度 * fix(hitl): 汇总项目审批并隔离对话状态 * fix(ui): 修复审批状态与无项目新任务 * fix(ui): 优化审批状态与对话切换性能 * fix(ui): 修复中断任务审批仍计时 * fix(ui): 修复多对话并发切换卡顿 * fix(hitl): 主动同步审批并关闭中断状态 * fix(ui): 固定项目审批汇总为绿色 * feat(ui): 同步系统模型与推理强度 * feat(ui): 完善项目侧栏预览与新建入口 * fix(ui): 防止无项目文件夹误展开 * fix(ui): 防止长历史对话滚动误触审批 * fix(ui): 修复对话操作并补充项目置顶 * fix(ui): 移除对话分组并调整项目置顶排序 * feat(ui): 优化迭代导航与审批交互 * fix(hitl): 将 write_file 加入内置免审批工具 * fix(chat): 支持回车发送与 Shift 回车换行 * fix(chat): 优化对话刷新与 Codex 风格交互 * fix(ui): 显示对话具体更新时间 * fix(ui): 优化对话刷新与项目加载 * fix(ui): 修复 Agent 审查文字裁切 * fix(chat): 修复刷新续流与多标签页同步 * fix(chat): 修复滚动跟随与中断任务终态 * fix(ui): 修复流式滚动跳动与工具状态恢复 * fix(ui): 修复刷新后流式输出停止粘底
329 lines
21 KiB
JavaScript
329 lines
21 KiB
JavaScript
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const monitor = fs.readFileSync('web/static/js/monitor.js', 'utf8');
|
|
const chatScroll = fs.readFileSync('web/static/js/chat-scroll.js', 'utf8');
|
|
const projects = fs.readFileSync('web/static/js/projects.js', 'utf8');
|
|
const chat = fs.readFileSync('web/static/js/chat.js', 'utf8');
|
|
const styles = fs.readFileSync('web/static/css/style.css', 'utf8');
|
|
const template = fs.readFileSync('web/templates/index.html', 'utf8');
|
|
const handler = fs.readFileSync('internal/handler/hitl.go', 'utf8');
|
|
const zh = JSON.parse(fs.readFileSync('web/static/i18n/zh-CN.json', 'utf8'));
|
|
const en = JSON.parse(fs.readFileSync('web/static/i18n/en-US.json', 'utf8'));
|
|
|
|
test('输入区提供独立审批入口并暴露可配置等待时限', () => {
|
|
assert.match(template, /id="chat-hitl-approval-dock"/);
|
|
assert.match(template, /id="hitl-timeout-select"/);
|
|
assert.match(template, /option value="300" selected/);
|
|
assert.match(chat, /DEFAULT_HITL_TIMEOUT_SECONDS = 300/);
|
|
assert.match(chat, /timeoutSeconds: normalizeHitlTimeoutForChat/);
|
|
assert.match(chat, /body\.hitl = \{[\s\S]*?timeoutSeconds: normalizeHitlTimeoutForChat\(hitlCfg\.timeoutSeconds/);
|
|
});
|
|
|
|
test('输入框可直接保存系统模型和系统推理强度且审批模型只出现在审计 Agent 入口', () => {
|
|
assert.match(chat, /function currentSystemModelLabel\(\)/);
|
|
assert.match(chat, /chatDefaultAIChannel \? chatAIChannels\[chatDefaultAIChannel\]/);
|
|
assert.match(chat, /function currentHitlAuditModelLabel\(\)/);
|
|
assert.match(chat, /const label = currentSystemModelLabel\(\)/);
|
|
assert.doesNotMatch(chat, /const label = data\.model \|\| currentChatModelLabel\(\)/);
|
|
assert.match(chat, /const approvalModel = auditAgent \? currentHitlAuditModelLabel\(\) : ''/);
|
|
assert.match(chat, /hitlAuditModel\.model\.trim\(\)/);
|
|
assert.match(template, /id="chat-model-shortcut"[^>]+onclick="openChatSystemModelPicker\(event\)"/);
|
|
assert.match(template, /id="chat-system-model-menu"[^>]+hidden/);
|
|
assert.doesNotMatch(template, /id="chat-reasoning-shortcut"/);
|
|
assert.match(template, /openChatSystemModelView\('model', event\)[\s\S]{0,1200}openChatSystemModelView\('effort', event\)/);
|
|
assert.match(chat, /function renderChatReasoningEffortOptions\(\)/);
|
|
assert.match(chat, /function currentSystemReasoningEffort\(\)[\s\S]{0,500}reasoning\.effort/);
|
|
assert.match(chat, /case 'low': return 'low'[\s\S]{0,300}case 'max': return 'max'/);
|
|
assert.match(chat, /chatTranslate\('chat\.reasoningEffortUnset', '不指定'\)/);
|
|
assert.match(chat, /function selectChatReasoningEffort\(effort\)[\s\S]{0,2400}reasoning: \{ \.\.\.\(state\.channel\.reasoning \|\| \{\}\), effort: chosen \}/);
|
|
assert.match(chat, /function selectChatReasoningEffort\(effort\)[\s\S]{0,4200}body: JSON\.stringify\(\{ ai: state\.ai \}\)[\s\S]{0,900}apiFetch\('\/api\/config\/apply'/);
|
|
assert.match(chat, /function openChatSystemModelPicker\(event\)[\s\S]{0,4200}apiFetch\('\/api\/config\/list-models'/);
|
|
assert.match(chat, /function selectChatSystemModel\(model\)[\s\S]{0,2600}method: 'PUT'[\s\S]{0,900}apiFetch\('\/api\/config\/apply'/);
|
|
assert.match(chat, /body: JSON\.stringify\(\{ ai: state\.ai \}\)/);
|
|
assert.equal(zh.chat.modelSettingsAria, '选择模型与推理强度');
|
|
assert.equal(en.chat.modelSettingsAria, 'Choose model and reasoning effort');
|
|
});
|
|
|
|
test('审批请求按浏览器、命令、文件和通用工具动态描述', () => {
|
|
assert.match(monitor, /function hitlApprovalTemplate/);
|
|
assert.match(monitor, /hitlApprovalTranslate\(key, fallback\)/);
|
|
assert.match(monitor, /replaceAll\('\{\{' \+ name \+ '\}\}'/);
|
|
assert.match(monitor, /function describeHitlApprovalRequest/);
|
|
assert.match(monitor, /requestVisitUrl/);
|
|
assert.match(monitor, /requestCommand/);
|
|
assert.match(monitor, /requestFile/);
|
|
assert.match(monitor, /requestGeneric/);
|
|
assert.match(monitor, /let displayTool = rawToolName/);
|
|
assert.doesNotMatch(monitor, /displayTool = 'Browser'/);
|
|
assert.doesNotMatch(monitor, /displayTool = hitlApprovalTranslate\('hitl\.toolTerminal'/);
|
|
assert.doesNotMatch(monitor, /displayTool = hitlApprovalTranslate\('hitl\.toolFiles'/);
|
|
});
|
|
|
|
test('Agent 审查不进入人工审批弹窗、倒计时和项目计数', () => {
|
|
const logsHandler = fs.readFileSync('internal/handler/hitl_logs.go', 'utf8');
|
|
const hitlPage = fs.readFileSync('web/static/js/hitl.js', 'utf8');
|
|
assert.match(handler, /CreatePendingInterrupt\([\s\S]{0,260}reviewer string/);
|
|
assert.match(handler, /reviewer != "audit_agent"[\s\S]{0,120}m\.pending\[id\] = p/);
|
|
assert.match(logsHandler, /ADD COLUMN reviewer TEXT NOT NULL DEFAULT 'human'/);
|
|
assert.match(logsHandler, /status = 'pending' AND COALESCE\(reviewer,'human'\) = 'human'/);
|
|
assert.match(monitor, /function isAgentReviewedHitl\(data\)/);
|
|
assert.match(monitor, /if \(!data\.resolved && !isAgentReviewedHitl\(data\)\)/);
|
|
assert.match(monitor, /if \(!isAgentReviewedHitl\(data\)\) \{[\s\S]{0,240}bindHitlApprovalCountdown/);
|
|
assert.match(monitor, /if \(isAgentReviewedHitl\(data\)\) return false/);
|
|
assert.match(projects, /filter\(isHumanProjectPendingApproval\)/);
|
|
assert.match(projects, /if \(!isHumanProjectPendingApproval\(details\)\) return/);
|
|
assert.match(hitlPage, /const items = rawItems\.filter/);
|
|
});
|
|
|
|
test('人工批准不要求输入备注,审查编辑仅发送真正修改过的参数', () => {
|
|
assert.match(monitor, /if \(!approveBtn \|\| !rejectBtn \|\| !statusEl\) return/);
|
|
assert.doesNotMatch(monitor, /!commentInput \|\| !statusEl/);
|
|
assert.match(monitor, /JSON\.stringify\(editedArgs\) === JSON\.stringify\(originalArgs\)/);
|
|
assert.match(monitor, /editedArgs = null/);
|
|
});
|
|
|
|
test('长历史对话的回到最新按钮不会把滚动点击穿透到审批操作', () => {
|
|
assert.match(chatScroll, /function isolateReturnLatestPointerEvent\(event\)/);
|
|
assert.match(chatScroll, /returnLatestButton\.addEventListener\('pointerdown', isolateReturnLatestPointerEvent\)/);
|
|
assert.match(chatScroll, /function onReturnLatestClick\(event\)[\s\S]{0,260}event\.preventDefault\(\)[\s\S]{0,180}event\.stopPropagation\(\)/);
|
|
assert.match(monitor, /const bindExplicitHitlAction = function \(button, decision\)/);
|
|
assert.match(monitor, /button\.addEventListener\('pointerdown'[\s\S]{0,900}pointerClick && !explicitlyPressed/);
|
|
assert.match(monitor, /bindExplicitHitlAction\(approveBtn, 'approve'\)/);
|
|
assert.match(monitor, /bindExplicitHitlAction\(rejectBtn, 'reject'\)/);
|
|
});
|
|
|
|
test('轮次导航使用连续大热区并允许鼠标平滑进入 Codex 风格预览卡', () => {
|
|
const styles = fs.readFileSync('web/static/css/style.css', 'utf8');
|
|
assert.match(styles, /\.chat-turn-rail-markers \{[\s\S]{0,260}gap: 0;/);
|
|
assert.match(styles, /\.chat-turn-rail-markers \{[\s\S]{0,420}overflow-x: hidden;/);
|
|
assert.match(styles, /\.chat-turn-rail-markers \{[\s\S]{0,520}touch-action: pan-y;/);
|
|
assert.match(styles, /\.chat-turn-rail-marker \{[\s\S]{0,260}width: 36px;[\s\S]{0,160}height: 11px;/);
|
|
assert.match(styles, /\.chat-turn-rail-marker::before \{[\s\S]{0,420}width: 12px;[\s\S]{0,120}height: 3px;/);
|
|
assert.match(styles, /\.chat-turn-rail-marker:hover::before \{[\s\S]{0,100}width: 22px;/);
|
|
assert.match(styles, /\.chat-turn-rail-preview \{[\s\S]{0,520}pointer-events: auto;/);
|
|
assert.match(chatScroll, /function scheduleHideTurnPreview\(\)/);
|
|
assert.match(chatScroll, /window\.setTimeout\(hideTurnPreview, 160\)/);
|
|
assert.match(chatScroll, /turnPreview\.addEventListener\('mouseenter'/);
|
|
assert.match(chatScroll, /marker\.addEventListener\('mouseleave', scheduleHideTurnPreview\)/);
|
|
});
|
|
|
|
test('倒计时由服务端时间驱动,到期时只锁定界面并等待服务端拒绝', () => {
|
|
assert.match(handler, /payload\["hitlApproval"\]/);
|
|
assert.match(handler, /"expiresAt":\s+approvalExpiresAt/);
|
|
assert.match(handler, /status = "timeout"/);
|
|
assert.match(handler, /decidedBy = "system"/);
|
|
assert.match(monitor, /function bindHitlApprovalCountdown/);
|
|
assert.match(monitor, /setInterval\(update, 250\)/);
|
|
assert.match(monitor, /expiredAutoRejected/);
|
|
assert.doesNotMatch(monitor, /remaining <= 0[\s\S]{0,240}submitHitlDecisionWithPayload/);
|
|
});
|
|
|
|
test('项目对话列表能同时显示等待批准与运行状态', () => {
|
|
assert.match(projects, /pendingApprovalByConversation: new Map/);
|
|
assert.match(projects, /statusKinds\.push\('approval'\)/);
|
|
assert.match(projects, /statusKinds\.push\('running'\)/);
|
|
assert.match(projects, /window\.setProjectConversationApprovalStatus/);
|
|
assert.match(projects, /api\/hitl\/pending\?page=1&pageSize=200/);
|
|
assert.match(projects, /function bindProjectApprovalProgress/);
|
|
assert.match(projects, /project-approval-progress-value/);
|
|
assert.match(projects, /PROJECT_APPROVAL_TICK_INTERVAL_MS = 1000/);
|
|
assert.match(projects, /function registerProjectApprovalTicker/);
|
|
assert.match(monitor, /function renderDirectHitlSidebarApproval/);
|
|
assert.match(monitor, /hitlSidebarApprovalSyncTimer = window\.setInterval/);
|
|
});
|
|
|
|
test('项目文件夹汇总始终为绿色且只有具体对话按剩余时间变色', () => {
|
|
assert.match(projects, /waitingApprovalCount/);
|
|
assert.match(projects, /aggregate: true, count: folderApprovals\.length/);
|
|
assert.match(projects, /project-task-status--approval-summary', 'is-urgency-normal'/);
|
|
assert.match(projects, /status\.dataset\.approvalUrgency = 'normal'/);
|
|
assert.match(projects, /if \(isApprovalSummary\)[\s\S]{0,520}else \{[\s\S]{0,160}bindProjectApprovalUrgency\(status, details, label\)/);
|
|
assert.doesNotMatch(projects, /currentExpiry < earliestExpiry/);
|
|
assert.match(projects, /PROJECT_APPROVAL_URGENCY_CLASSES/);
|
|
assert.match(projects, /remaining <= 60 \* 1000/);
|
|
assert.match(projects, /remaining <= 3 \* 60 \* 1000/);
|
|
assert.doesNotMatch(projects, /remaining <= 5 \* 60 \* 1000/);
|
|
assert.match(projects, /project-task-status--approval-summary/);
|
|
assert.equal(zh.hitl.waitingApprovalCount, '等待批准 {{count}}');
|
|
assert.equal(zh.hitl.approvalUrgencyMoreThanThree, '最早审批将在 3 分钟后到期');
|
|
assert.equal(typeof en.hitl.waitingApprovalCount, 'string');
|
|
const urgencyFunctionSource = projects.match(
|
|
/function projectApprovalUrgencyLevel\(remainingMilliseconds, hasDeadline\) \{[\s\S]*?\n\}/
|
|
);
|
|
assert.ok(urgencyFunctionSource, '应提供可测试的审批紧急程度函数');
|
|
const urgencyLevel = vm.runInNewContext(`(${urgencyFunctionSource[0]})`);
|
|
assert.equal(urgencyLevel(6 * 60 * 1000, true), 'normal');
|
|
assert.equal(urgencyLevel(4 * 60 * 1000, true), 'normal');
|
|
assert.equal(urgencyLevel(3 * 60 * 1000 + 1, true), 'normal');
|
|
assert.equal(urgencyLevel(3 * 60 * 1000, true), 'warning');
|
|
assert.equal(urgencyLevel(2 * 60 * 1000, true), 'warning');
|
|
assert.equal(urgencyLevel(30 * 1000, true), 'critical');
|
|
assert.equal(urgencyLevel(0, false), 'normal');
|
|
});
|
|
|
|
test('切换对话后主按钮只读取当前可见对话的运行状态', () => {
|
|
assert.match(chat, /function getVisibleChatConversationId\(\)/);
|
|
assert.match(chat, /function shouldTreatLiveChatTaskAsCurrent\(/);
|
|
assert.match(chat, /function isLiveChatTaskVisible\(/);
|
|
assert.match(chat, /if \(visibleConversationId\) return visibleConversationId/);
|
|
assert.match(chat, /isConversationTaskRunning\(visibleConversationId\)/);
|
|
assert.doesNotMatch(
|
|
chat,
|
|
/function getCurrentChatTaskConversationId\(\) \{[\s\S]{0,220}if \(live && live\.active && live\.conversationId\) \{[\s\S]{0,100}return String\(live\.conversationId\)/
|
|
);
|
|
const visibilityFunctionSource = chat.match(
|
|
/function shouldTreatLiveChatTaskAsCurrent\(liveConversationId, visibleConversationId, hasVisibleProgress\) \{[\s\S]*?\n\}/
|
|
);
|
|
assert.ok(visibilityFunctionSource, '应提供可测试的当前任务隔离函数');
|
|
const isCurrent = vm.runInNewContext(`(${visibilityFunctionSource[0]})`);
|
|
assert.equal(isCurrent('running-conversation', '', true), false);
|
|
assert.equal(isCurrent('running-conversation', 'new-conversation', true), false);
|
|
assert.equal(isCurrent('running-conversation', 'running-conversation', false), true);
|
|
assert.equal(isCurrent('', '', true), true);
|
|
assert.equal(isCurrent('', '', false), false);
|
|
});
|
|
|
|
test('无项目使用独立虚拟文件夹且顶部新任务继承当前项目', () => {
|
|
assert.match(projects, /CHAT_UNASSIGNED_PROJECT_FOLDER_ID/);
|
|
assert.match(projects, /_isUnassigned: true/);
|
|
assert.match(projects, /\[\.\.\.pinnedProjects, unassignedProject, \.\.\.regularProjects\]/);
|
|
assert.match(projects, /window\.startNewConversation\(\{ projectId: isUnassigned \? '' : project\.id \}\)/);
|
|
assert.match(chat, /Object\.prototype\.hasOwnProperty\.call\(options, 'projectId'\)/);
|
|
assert.match(chat, /typeof resolveChatProjectSelection === 'function'/);
|
|
assert.match(chat, /String\(inheritedProjectId \|\| ''\)\.trim\(\)/);
|
|
assert.match(chat, /typeof setActiveProjectId === 'function'\) setActiveProjectId\(requestedProjectId\)/);
|
|
assert.equal(zh.chat.newUnassignedConversation, '新建无项目对话');
|
|
assert.equal(typeof en.chat.newUnassignedConversation, 'string');
|
|
});
|
|
|
|
test('单个对话的审批徽标随倒计时同步切换紧急颜色', () => {
|
|
assert.match(projects, /bindProjectApprovalProgress\(status, details\);\s*bindProjectApprovalUrgency\(status, details, label\);/);
|
|
assert.match(fs.readFileSync('web/static/css/style.css', 'utf8'), /\.project-task-status--approval\.is-urgency-critical/);
|
|
});
|
|
|
|
test('项目状态刷新复用单一计时器且切换对话不重复请求完整项目上下文', () => {
|
|
assert.match(projects, /const projectApprovalTickerEntries = new Set\(\)/);
|
|
assert.match(projects, /if \(!changed && !approvalChanged\) return/);
|
|
assert.match(projects, /options\.reloadFolders !== false/);
|
|
assert.match(chat, /refreshChatProjectSelector\(\{ reloadFolders: false, renderFolders: false \}\)/);
|
|
assert.match(projects, /function selectChatProjectConversationItem/);
|
|
assert.match(projects, /options\.renderFolders !== false/);
|
|
assert.match(projects, /projectConversationPreviewSuppressedUntil = Date\.now\(\) \+ 700/);
|
|
assert.match(projects, /project-task-status-group--folder/);
|
|
assert.doesNotMatch(fs.readFileSync('web/static/css/style.css', 'utf8'), /project-task-status-group--folder \.project-task-status--running/);
|
|
assert.match(fs.readFileSync('web/static/css/style.css', 'utf8'), /\.active-tasks-bar \{[\s\S]*?padding: 13px 24px 14px;/);
|
|
});
|
|
|
|
test('运行中对话切换会取消旧事件流并仅恢复最新一页过程详情', () => {
|
|
assert.match(chat, /window\.cancelRunningTaskEventStream\(conversationId\)/);
|
|
assert.match(monitor, /function cancelRunningTaskEventStream/);
|
|
assert.match(monitor, /abortController\.abort\(\)/);
|
|
assert.match(monitor, /signal: abortController\.signal/);
|
|
assert.match(monitor, /initialLatest: true/);
|
|
assert.match(monitor, /autoLoadAll: false/);
|
|
});
|
|
|
|
test('多对话并发时释放隐藏主流且旧请求不能覆盖新对话状态', () => {
|
|
assert.match(chat, /function ownsLiveChatStream\(liveStream\)/);
|
|
assert.match(chat, /function clearLiveChatStreamIfOwned\(liveStream\)/);
|
|
assert.match(chat, /function detachLiveChatStreamForNavigation\(nextConversationId, force = false\)/);
|
|
assert.match(chat, /liveStream\.detached = true;[\s\S]{0,240}controller\.abort\(\)/);
|
|
assert.match(chat, /const requestAbortController = new AbortController\(\)/);
|
|
assert.match(chat, /signal: requestAbortController\.signal/);
|
|
assert.match(chat, /if \(!ownsLiveChatStream\(liveStreamState\) \|\| liveStreamState\.detached\)/);
|
|
assert.match(chat, /const clearedOwnedStream = clearLiveChatStreamIfOwned\(liveStreamState\)/);
|
|
assert.match(chat, /detachLiveChatStreamForNavigation\(conversationId\)/);
|
|
assert.match(chat, /detachLiveChatStreamForNavigation\('', true\)/);
|
|
assert.match(chat, /window\.clearChatHitlApprovalDock\(\)/);
|
|
assert.match(monitor, /if \(conversationId && conversationId !== currentId\) return false/);
|
|
assert.match(monitor, /function scrollProcessDetailsToLatest\(assistantMessageId, smooth = true\)/);
|
|
assert.match(monitor, /timeline\.scrollTop = targetTop/);
|
|
assert.match(chat, /let loadConversationAbortController = null/);
|
|
assert.match(chat, /cancelPendingConversationLoad\(\);[\s\S]{0,220}const conversationLoadController = new AbortController\(\)/);
|
|
assert.match(chat, /signal: conversationLoadController\.signal/);
|
|
assert.match(template, /monitor\.js\?v=20260813-9/);
|
|
assert.match(template, /chat-scroll\.js\?v=20260813-6/);
|
|
assert.match(template, /chat\.js\?v=20260813-3/);
|
|
assert.match(template, /style\.css\?v=20260813-5/);
|
|
});
|
|
|
|
test('输入区 Agent 审查文字保留足够行高且不会裁切字形', () => {
|
|
assert.match(styles, /\.chat-hitl-shortcut > span\s*\{[\s\S]*?display: block/);
|
|
assert.match(styles, /\.chat-hitl-shortcut > span\s*\{[\s\S]*?padding-block: 1px/);
|
|
assert.match(styles, /\.chat-hitl-shortcut > span\s*\{[\s\S]*?line-height: 1\.4/);
|
|
});
|
|
|
|
test('任务结束后对话内审批按钮会变灰并禁止继续操作', () => {
|
|
assert.match(monitor, /ready: false/);
|
|
assert.match(monitor, /function setHitlApprovalTaskAvailability/);
|
|
assert.match(monitor, /conversationExecutionTracker\.ready && !conversationExecutionTracker\.isRunning\(id\)/);
|
|
assert.match(monitor, /hitlPendingInterruptTracker\.ready/);
|
|
assert.match(monitor, /!hitlPendingInterruptTracker\.has\(interruptId\)/);
|
|
assert.match(monitor, /button\.disabled = true/);
|
|
assert.match(monitor, /function setHitlApprovalInterruptedVisualState/);
|
|
assert.match(monitor, /stopHitlApprovalCountdown\(panel\)/);
|
|
assert.match(monitor, /removeAttribute\('data-hitl-expires-at'\)/);
|
|
assert.match(monitor, /hitl\.interruptedApprovalCancelled/);
|
|
assert.match(monitor, /reconcileHitlApprovalStateWithActiveTasks\(normalizedTasks\)/);
|
|
assert.match(monitor, /syncHitlApprovalTaskAvailability\(\)/);
|
|
assert.match(fs.readFileSync('web/static/css/style.css', 'utf8'), /hitl-approval-task-closed/);
|
|
assert.equal(zh.hitl.taskClosedApprovalUnavailable, '任务已结束,审批不可用');
|
|
assert.equal(zh.hitl.interruptedApprovalCancelled, '任务已中断,审批已取消');
|
|
assert.equal(typeof en.hitl.taskClosedApprovalUnavailable, 'string');
|
|
assert.equal(typeof en.hitl.interruptedApprovalCancelled, 'string');
|
|
});
|
|
|
|
test('项目树只保留当前进程仍在运行任务的审批状态', () => {
|
|
assert.match(projects, /chatProjectFolderContext\.runningIds\.has\(conversationId\)/);
|
|
assert.match(projects, /pendingApprovalByConversation\.delete\(conversationId\)/);
|
|
assert.match(monitor, /conversationExecutionTracker\.ready && !conversationExecutionTracker\.isRunning\(conversationId\)/);
|
|
});
|
|
|
|
test('审批状态主动轮询并在服务不可用时立即关闭旧审批', () => {
|
|
assert.match(monitor, /ACTIVE_TASK_REFRESH_INTERVAL = 2000/);
|
|
assert.match(monitor, /apiFetch\('\/api\/hitl\/pending\?page=1&pageSize=200'\)/);
|
|
assert.match(monitor, /function reconcilePendingHitlState\(rawItems\)/);
|
|
assert.match(monitor, /renderChatHitlApprovalDock\(currentPending\)/);
|
|
assert.match(monitor, /restoreHitlInlineForConversation\(currentId\)/);
|
|
assert.match(monitor, /case 'conversation':[\s\S]{0,1800}window\.refreshChatProjectFolders\(\)/);
|
|
assert.match(monitor, /renderActiveTasks\(\[\]\);[\s\S]{0,260}hitlPendingInterruptTracker\.update\(\[\]\)/);
|
|
assert.match(projects, /function syncProjectConversationApprovalStatuses\(items\)/);
|
|
assert.match(projects, /window\.syncProjectConversationApprovalStatuses/);
|
|
assert.match(template, /projects\.js\?v=20260812-6/);
|
|
});
|
|
|
|
test('旧会话首次升级到五分钟默认审批时限,仍允许用户之后主动选择不限时', () => {
|
|
assert.match(fs.readFileSync('web/static/js/hitl.js', 'utf8'), /HITL_TIMEOUT_DEFAULT_MIGRATION_PREFIX/);
|
|
assert.match(fs.readFileSync('web/static/js/hitl.js', 'utf8'), /shouldMigrateLegacyHitlTimeout/);
|
|
assert.match(fs.readFileSync('web/static/js/hitl.js', 'utf8'), /timeoutSeconds: 300/);
|
|
assert.match(fs.readFileSync('web/static/js/hitl.js', 'utf8'), /markLegacyHitlTimeoutMigrated/);
|
|
});
|
|
|
|
test('审批体验文案具有完整中英文资源', () => {
|
|
const hitlKeys = [
|
|
'waitingApprovalShort',
|
|
'requestVisitUrl',
|
|
'requestCommand',
|
|
'viewRequestDetails',
|
|
'timeoutAutoReject',
|
|
'expiredRejected',
|
|
];
|
|
const chatKeys = [
|
|
'hitlTimeoutLabel',
|
|
'hitlTimeoutFiveMinutes',
|
|
'hitlTimeoutUnlimited',
|
|
'hitlTimeoutHint',
|
|
];
|
|
hitlKeys.forEach((key) => {
|
|
assert.equal(typeof zh.hitl[key], 'string');
|
|
assert.equal(typeof en.hitl[key], 'string');
|
|
});
|
|
chatKeys.forEach((key) => {
|
|
assert.equal(typeof zh.chat[key], 'string');
|
|
assert.equal(typeof en.chat[key], 'string');
|
|
});
|
|
});
|