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): 修复刷新后流式输出停止粘底
125 lines
7.5 KiB
JavaScript
125 lines
7.5 KiB
JavaScript
const fs = require('node:fs');
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const projects = fs.readFileSync('web/static/js/projects.js', 'utf8');
|
|
const styles = fs.readFileSync('web/static/css/style.css', 'utf8');
|
|
const chat = fs.readFileSync('web/static/js/chat.js', 'utf8');
|
|
const html = fs.readFileSync('web/templates/index.html', 'utf8');
|
|
const rbac = fs.readFileSync('web/static/js/rbac-guards.js', 'utf8');
|
|
const zh = fs.readFileSync('web/static/i18n/zh-CN.json', 'utf8');
|
|
const en = fs.readFileSync('web/static/i18n/en-US.json', 'utf8');
|
|
|
|
function functionSource(source, name, nextName) {
|
|
const start = source.indexOf(`function ${name}(`);
|
|
const end = source.indexOf(`function ${nextName}(`, start);
|
|
assert.notEqual(start, -1, `${name} should exist`);
|
|
assert.notEqual(end, -1, `${nextName} should follow ${name}`);
|
|
return source.slice(start, end);
|
|
}
|
|
|
|
test('无项目文件夹与普通项目共用悬浮和键盘聚焦预览', () => {
|
|
const source = functionSource(projects, 'appendChatProjectFolderItem', 'appendChatProjectConversationItem');
|
|
|
|
assert.match(source, /row\.addEventListener\('mouseenter', \(\) => scheduleShowProjectFolderPreview/);
|
|
assert.match(source, /button\.addEventListener\('focus', \(\) => scheduleShowProjectFolderPreview/);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/if \(!isUnassigned\) \{\s*row\.addEventListener\('mouseenter', \(\) => scheduleShowProjectFolderPreview/
|
|
);
|
|
});
|
|
|
|
test('无项目预览隐藏测试范围和编辑入口', () => {
|
|
const source = functionSource(projects, 'showProjectFolderPreview', 'scheduleShowProjectFolderPreview');
|
|
|
|
assert.match(source, /preview\.classList\.toggle\('is-unassigned', isUnassigned\)/);
|
|
assert.match(source, /scopeRow\.hidden = isUnassigned \|\| !scope/);
|
|
assert.match(source, /editButton\.hidden = isUnassigned/);
|
|
assert.match(styles, /\.project-folder-preview\.is-unassigned \.project-folder-preview-edit\s*\{\s*display: none !important;/);
|
|
assert.match(styles, /\.project-folder-preview\.is-unassigned \.project-folder-preview-details\s*\{\s*border-bottom: 0;/);
|
|
});
|
|
|
|
test('项目标题提供受权限保护的新建项目入口', () => {
|
|
const source = functionSource(projects, 'showNewProjectModalFromChatSidebar', 'saveProjectModal');
|
|
|
|
assert.match(html, /class="add-group-btn project-folders-add-btn"[\s\S]*?onclick="showNewProjectModalFromChatSidebar\(\)"/);
|
|
assert.match(chat, /projectHeader\.querySelector\('\.project-folders-add-btn'\)/);
|
|
assert.match(source, /window\._projectModalFromChat = false/);
|
|
assert.match(source, /window\._projectModalFromChatSidebar = true/);
|
|
assert.match(rbac, /showNewProjectModalFromChatSidebar: 'project:write'/);
|
|
});
|
|
|
|
test('对话项目归属尚未加载时不会误展开无项目', () => {
|
|
const resolver = functionSource(projects, 'resolveChatProjectFolderSelection', 'renderChatProjectFolders');
|
|
const render = functionSource(projects, 'renderChatProjectFolders', 'refreshChatProjectFolders');
|
|
|
|
assert.match(resolver, /if \(!chatProjectFolderContext\.ready\) return null/);
|
|
assert.match(resolver, /if \(!conversation\) return null/);
|
|
assert.match(resolver, /conversation\.projectId \|\| conversation\.project_id \|\| ''/);
|
|
assert.match(render, /const selectedId = resolveChatProjectFolderSelection\(\)/);
|
|
assert.match(render, /selectedId !== null && chatProjectFolderLastSelectionId !== selectedId/);
|
|
});
|
|
|
|
test('项目按展开状态切换 Codex 风格的打开和关闭文件夹', () => {
|
|
const icon = functionSource(projects, 'projectFolderIconMarkup', 'clampProjectPreviewText');
|
|
const folder = functionSource(projects, 'appendChatProjectFolderItem', 'appendChatProjectConversationItem');
|
|
|
|
assert.match(icon, /const path = isExpanded/);
|
|
assert.match(icon, /M3\.5 18V6\.5/);
|
|
assert.match(icon, /M3\.5 7a2 2 0 0 1 2-2h4l2 2/);
|
|
assert.match(folder, /icon\.className = 'project-folder-icon';/);
|
|
assert.match(folder, /icon\.innerHTML = projectFolderIconMarkup\(isExpanded\);/);
|
|
});
|
|
|
|
test('项目名仅在界面按 12 个 Unicode 字符省略并保留完整悬浮信息', () => {
|
|
const formatterSource = functionSource(chat, 'formatProjectNameForDisplay', 'applyProjectNameDisplay');
|
|
const formatter = new Function(
|
|
'PROJECT_NAME_DISPLAY_MAX_CHARACTERS',
|
|
`${formatterSource}; return formatProjectNameForDisplay;`
|
|
)(12);
|
|
const folder = functionSource(projects, 'appendChatProjectFolderItem', 'appendChatProjectConversationItem');
|
|
const picker = functionSource(projects, 'appendChatProjectPanelItem', 'appendChatProjectPanelMessage');
|
|
const button = functionSource(projects, 'updateChatProjectButtonLabel', 'renderChatProjectPanel');
|
|
|
|
assert.equal(formatter('十二字符以内'), '十二字符以内');
|
|
assert.equal(formatter('这是一个非常非常长的项目名称'), '这是一个非常非常长的项目…');
|
|
assert.equal(formatter('😀😀😀😀😀😀😀😀😀😀😀😀😀'), '😀😀😀😀😀😀😀😀😀😀😀😀…');
|
|
assert.match(chat, /const PROJECT_NAME_DISPLAY_MAX_CHARACTERS = 12/);
|
|
assert.match(folder, /applyProjectNameDisplay\(title, project\.name/);
|
|
assert.match(projects, /applyProjectNameDisplay\(titleEl, text\)/);
|
|
assert.match(picker, /title="\$\{escapeAttr\(fullName\)\}"/);
|
|
assert.match(picker, /setAttribute\('aria-label', fullName\)/);
|
|
assert.match(button, /applyProjectNameDisplay/);
|
|
assert.match(styles, /\.project-selector-wrapper \.role-selector-text\s*\{[\s\S]*?max-width: 13em/);
|
|
});
|
|
|
|
test('项目文件夹首批显示 6 个并通过加载更多按批追加', () => {
|
|
const loadMore = functionSource(projects, 'loadMoreChatProjectFolders', 'renderChatProjectFolders');
|
|
const render = functionSource(projects, 'renderChatProjectFolders', 'refreshChatProjectFolders');
|
|
const search = functionSource(projects, 'handleProjectFolderSearch', 'clearProjectFolderSearch');
|
|
|
|
assert.match(projects, /const CHAT_PROJECT_FOLDER_PAGE_SIZE = 6/);
|
|
assert.match(loadMore, /chatProjectFolderVisibleCount \+= CHAT_PROJECT_FOLDER_PAGE_SIZE/);
|
|
assert.match(render, /const visibleFolders = folders\.slice\(0, chatProjectFolderVisibleCount\)/);
|
|
assert.match(render, /appendChatProjectFoldersLoadMore\(list, folders\.length - visibleFolders\.length\)/);
|
|
assert.match(render, /chatProjectFolderVisibleCount = selectedIndex \+ 1/);
|
|
assert.match(search, /renderChatProjectFolders\(projectsCacheAll\)/);
|
|
assert.match(styles, /\.project-folders-load-more\s*\{/);
|
|
assert.match(zh, /"projectFoldersLoadMoreRemaining": "加载更多,剩余 \{\{count\}\} 个项目"/);
|
|
assert.match(en, /"projectFoldersLoadMoreRemaining": "Load more, \{\{count\}\} projects remaining"/);
|
|
});
|
|
|
|
test('对话悬浮预览显示本地年月日时分', () => {
|
|
const age = functionSource(projects, 'formatProjectConversationPreviewAge', 'getProjectConversationModeLabel');
|
|
|
|
assert.match(age, /date\.getFullYear\(\)/);
|
|
assert.match(age, /date\.getMonth\(\) \+ 1/);
|
|
assert.match(age, /date\.getDate\(\)/);
|
|
assert.match(age, /date\.getHours\(\)/);
|
|
assert.match(age, /date\.getMinutes\(\)/);
|
|
assert.match(age, /chat\.conversationPreviewDateTime/);
|
|
assert.doesNotMatch(age, /elapsedMs|conversationPreviewDays|conversationPreviewHours/);
|
|
assert.match(zh, /"conversationPreviewDateTime": "\{\{year\}\}年\{\{month\}\}月\{\{day\}\}日 \{\{hour\}\}:\{\{minute\}\}"/);
|
|
assert.match(en, /"conversationPreviewDateTime": "\{\{year\}\}-\{\{month\}\}-\{\{day\}\} \{\{hour\}\}:\{\{minute\}\}"/);
|
|
});
|