按"不考虑时间、考虑正确合理"原则逐块实施。
P0-B 放开 select
- chat_state.js: shouldHideUploadSelect 始终 false; resolveUploadType 尊重用户选择
- chat.html: 选项目时 select 不再隐藏 + 不再强改 file_type=requirements
- 客户端 file_type↔扩展名校验(existing_system=.zip, requirements=.xlsx 等)
- 项目已预置同类型时 confirm() 显式覆盖确认(不藏起入口)
P0-C 高级字段折叠 + 字段级红框
- 项目抽屉主面板仅留 项目名/显示名;4 个服务器路径字段收进 details.advanced
- saveDrawerProject 错误时按 ProjectConfigError label 关键字(模板/做成说明书/
既有系统代码库/既有设计文档目录/项目名)给对应输入加 .invalid 3s 清除
- 删除按钮 pf-delete 改用 hidden 而非 style.display
P0-A RAG 入口产品化
- 后端 RagStore.count(scope) 线程安全读加锁
- GenesisService.rag_stats(sid) 含 except 兜底(count 抛错返回 0)
- GET /api/sessions/{sid}/rag-stats 端点
- 前端顶栏:RAG 开关 + RAG 已索引 N 片段 状态徽标 + 开始影响调查按钮
- 上传 existing_system 后自动 refreshRagStats 刷新徽标
- 开关持久化到 localStorage(genesis_rag_enabled)
P1-D loadSession 不再隐式覆盖 draftProject
- activeProject vs draftProject 分层;不一致时由 renderProjectMismatchHint
提示用户主动"切到该项目"或"保留当前项目"
- 保留 写入 sessionStorage 标记,避免每次 load 都提示
P1-F 删除抽屉项目 fallback 收敛
- 先记 deleted 再清 drawerSelected,修复"先 null 后比较"恒假 bug
- buildWelcome 唯一来源(chat_state.js 单点,前端内联重复移除)
P2-H 杂项
- send() in-flight 锁防双击
- input maxlength=2000
- 启动恢复前校验项目存在 + loading 占位
- select 关联 label for
- esc 转义加 引号
- sid 全程 encodeURIComponent
- avatar 按 name hash 选色 + 中文首字(Array.from)+ aria-label 含名字
测试:619 passed / 99.01% 99.0% 达标(+8 覆盖 store.count/service.rag_stats/
rag-stats 端点/rag_stats 异常);Node chat_state 9 passed
50 lines
2.0 KiB
JavaScript
50 lines
2.0 KiB
JavaScript
// 前端核心状态逻辑单测(Node 原生 test runner,UMD 模块可在 Node 下 require)
|
||
// 对应方案 docs/frontend_audit_plan.md §7.1 / §4.4
|
||
const test = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const S = require('../src/genesis/server/static/chat_state.js');
|
||
|
||
test('autoBindProject: 启动无项目且有项目时自动绑首项', () => {
|
||
assert.equal(S.autoBindProject(null, [{ name: 'A' }, { name: 'B' }]), 'A');
|
||
});
|
||
|
||
test('autoBindProject: 指向已删除项目时回退首项', () => {
|
||
assert.equal(S.autoBindProject('X', [{ name: 'A' }]), 'A');
|
||
});
|
||
|
||
test('autoBindProject: 已选有效项目保持不变', () => {
|
||
assert.equal(S.autoBindProject('A', [{ name: 'A' }]), 'A');
|
||
});
|
||
|
||
test('shouldHideUploadSelect: 始终 false(选项目时也允许补传其它类型)', () => {
|
||
assert.equal(S.shouldHideUploadSelect(null, null), false);
|
||
assert.equal(S.shouldHideUploadSelect(null, 'A'), false);
|
||
assert.equal(S.shouldHideUploadSelect('A', null), false);
|
||
assert.equal(S.shouldHideUploadSelect('A', 'A'), false);
|
||
});
|
||
|
||
test('resolveUploadType: 始终尊重用户下拉选择(不再被项目绑定强制为 requirements)', () => {
|
||
assert.equal(S.resolveUploadType(null, 'A', 'template'), 'template');
|
||
assert.equal(S.resolveUploadType(null, null, 'template'), 'template');
|
||
assert.equal(S.resolveUploadType('A', null, 'existing_system'), 'existing_system');
|
||
});
|
||
|
||
test('computeDrawerSnapshot: 去空白并以单元分隔符连接', () => {
|
||
assert.equal(S.computeDrawerSnapshot([' a ', 'b']), 'a\u0001b');
|
||
});
|
||
|
||
test('isDrawerDirty: 快照不同为脏,相同为干净', () => {
|
||
assert.equal(S.isDrawerDirty('x', 'y'), true);
|
||
assert.equal(S.isDrawerDirty('x', 'x'), false);
|
||
});
|
||
|
||
test('buildWelcome: 无项目提示新建/选择项目', () => {
|
||
assert.match(S.buildWelcome(null), /请新建项目|选择项目/);
|
||
});
|
||
|
||
test('buildWelcome: 有项目时提示由项目提供模板/规则/代码库', () => {
|
||
const txt = S.buildWelcome('A');
|
||
assert.match(txt, /项目「A」/);
|
||
assert.match(txt, /要件定义/);
|
||
});
|