Files
mini-openclaw-v2/scripts/test_experience_center_ui.cjs
qiaoqichaoandClaude Code c880f40e4c fix(v2): 修掉预审面板不刷新与候选行同 id 连开多行的缺陷
真实 Chrome 端到端验证第二篇(/prescreen-workbench、/experience-center)发现两个
源码守卫与生产构建都看不见的缺陷:

- 发起 AI 预审、运行结束后 PrescreenPanel 不重载:新运行进不了"查看哪一次运行",
  面板继续显示上一次运行,而徽标写着"结果会自动刷新"。根因是 load() 只依赖
  project.id;补修时又发现 worker 先写 run.status、再另写 prescreen_run 视图
  (assistant_worker.process_next),两者不在同一事务,只读一次会读到视图还没
  落库的中间态,因此改为按运行集合重载并在最新运行进入视图前短时重试。
- 候选区 key={card.id} 撞 React key,且「自然语言修改适用范围」「补充独立测试」
  按 card.id 开合,点一行连开 4 行(同一 card.id 的草稿行与各版本行共享 id)。
  改为行级 rowKey(组件id@版本@状态)。

同时新增可选脚本 scripts/browser-e2e-modules.cjs(42/42 通过),并给
test_prescreen_ui.cjs / test_experience_center_ui.cjs 补上对应源码断言。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 09:35:29 +08:00

86 lines
5.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 纠偏第二批 · 历史经验加工界面源码守卫(BR-48 / BR-49 / BR-50、AC-42/50/51/52)。
*
* 这是**源码断言**,不是浏览器端到端测试(docs/v2/06-validation.md §2)。
* 它锁住四类可机械核对的约束:
*
* 1. **入口独立**:模块有自己的路由与导航项,不是旧页的第 7 个页签(BR-50)。
* 2. **来源徽标真的被用**:`originBadge` 必须被消费,且 .origin-* 样式与图标同时存在。
* ← 这条是补既有漏洞:此前该函数零调用方、无任何 CSS,而守卫只 grep 库文件字符串,
* 于是"测试通过但功能不存在"。
* 3. **失败如实呈现**:E-28 未完成原因必须渲染,不得只显示成功态。
* 4. **不诱导违规**:同一身份既编辑又发布时不得出现发布按钮(自批由后端强制,界面不应提供)。
*/
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const root = path.resolve(__dirname, "..");
const read = (file) => fs.readFileSync(path.join(root, file), "utf8");
const page = read("src/app/experience-center/page.tsx");
const panel = read("src/components/xingbang/ExperienceCenter.tsx");
const nav = read("src/components/xingbang/AgentNav.tsx");
const lib = read("src/lib/prescreen-run.ts");
const css = read("src/app/data-access/workspace.css");
// 1. 独立入口(BR-50:三个模块各有独立入口,不得只在旧页下叠加面板)
assert.ok(fs.existsSync(path.join(root, "src/app/experience-center/page.tsx")), "历史经验加工应有独立路由");
assert.match(page, /<ExperienceCenter /, "路由页应渲染加工中心组件");
assert.match(nav, /\/experience-center/, "导航应提供历史经验加工入口");
assert.match(nav, /active==="experience"/, "导航应能标记当前模块");
// 复用既有 shell,不另立视觉;也不得把已合并旧页恢复成独立页面(AC-55 的"禁止重复页面")
assert.match(page, /AgentNav/, "页面应复用既有导航组件");
assert.match(page, /\.\.\/data-access\/workspace\.css/, "页面应复用既有工作区样式");
for (const merged of ["trajectory", "review-workbench", "report-workbench", "collateral-workbench"]) {
assert.match(read(`src/app/${merged}/page.tsx`), /redirect\(/,
`已合并页 src/app/${merged} 必须保持重定向`);
}
assert.ok(!/^import .*\.css";/m.test(panel), "加工中心不得新增自己的样式表");
// 2. 来源徽标:文字 + 图标 + 样式三者齐全,去色仍可区分(BR-48 / AC-42)
assert.match(lib, /export function originBadge/, "来源徽标解析函数应存在");
assert.match(panel, /import \{[^}]*originBadge[^}]*\} from "@\/lib\/prescreen-run"/,
"加工中心必须真的消费 originBadge(此前它零调用方,属于'函数在、功能不在')");
assert.match(panel, /originBadge\(/, "必须调用 originBadge 而不是自己写一份来源映射");
assert.match(panel, /ORIGIN_ICONS/, "来源徽标必须配图标,不能只靠颜色");
for (const tone of ["origin-history", "origin-manual", "origin-policy", "origin-unknown"]) {
assert.ok(css.includes(`.${tone}{`), `workspace.css 必须定义 .${tone} 样式`);
}
// 去色仍可区分:四种来源用不同的左边框样式,而不只是不同颜色
const borderStyles = ["dashed", "solid", "double", "dotted"].filter((style) => css.includes(`border-left-style:${style}`));
assert.equal(borderStyles.length, 4, "四种来源应使用不同的左边框样式,保证去色后仍可区分");
// 3. 失败如实呈现:E-28 未完成原因必须渲染(AC-50 后半句)
assert.match(panel, /degradations/, "提炼记录必须渲染降级/未完成原因");
assert.match(panel, /不产生任何候选草稿|不会用一个固定返回值冒充提炼成功/, "界面须说明模型不可用时不产生候选");
assert.match(panel, /item\.code/, "未完成原因必须带错误码,便于定位");
// 4. 不自批:同一身份不得看到自己的发布按钮(BR-42 / AC-43)
assert.match(panel, /mayPublish/, "发布按钮必须经过「不自批」判定");
assert.match(panel, /card\.edited_by !== identity\?\.id|edited_by !== identity/, "不自批判据必须比较编辑人与当前身份");
assert.match(panel, /发布需要独立审核人/, "被隐藏发布按钮时应说明原因,而不是静默消失");
// 5. 系统沙盘/预审的既有禁令继续有效:这里也不得出现任何概率数值或演示情景表
for (const [name, source] of [["panel", panel], ["page", page]]) {
assert.ok(!/0\.(42|58|51|64)\b/.test(source), `${name} 不得硬编码演示概率值`);
assert.ok(!/"(00|10|01|11)"\s*:/.test(source), `${name} 不得复制演示情景表`);
assert.ok(!/probability\s*[:=]\s*[0-9]/.test(source), `${name} 不得写入任何概率数值`);
}
// 6. 加工动作必须落在已定义的端点上,不自行拼 SQL 或绕过服务端
for (const endpoint of ["case-timeline", "knowledge-extractions", "knowledge-candidates", "review-plans"]) {
assert.ok(panel.includes(endpoint), `加工中心应通过 ${endpoint} 端点读写`);
}
assert.match(panel, /PRESCREEN/, "必须走既有 prescreen 命名空间");
// 7. 候选行的标识必须唯一:列表里同一个 card.id 会出现多行(草稿行与版本行共享 id)
// 只用 card.id 作 key 会撞 React key,作展开标记会一点连开多行——两者都在真实浏览器里量到过
// (见 17 四批交付记录「浏览器验证发现的问题(第二篇)」)。
assert.match(panel, /const rowKey = \(card: any\) =>/, "候选行必须有行级标识");
assert.match(panel, /key=\{rowKey\(card\)\}/, "候选行的 React key 不得只用 card.id");
assert.ok(!/setReviseFor\([^)]*card\.id\b/.test(panel), "修订面板标记不得只用 card.id(会同开多行)");
assert.ok(!/setCasesFor\([^)]*card\.id\b/.test(panel), "测试样本面板标记不得只用 card.id");
console.log("experience-center-ui: PASS");