Files
mini-openclaw-v2/scripts/test_experience_center_ui.cjs
T
qiaoqichaoandClaude Code 2e7e19471c feat(v2): 纠偏第二批——接通历史经验加工闭环
覆盖 AC-50/51/52(BR-48/49、E-28)。审计确认的 P1-3 缺口"历史提炼与卡片
不在实时 API 调用图上"由此关闭:提炼、候选治理、卡片进方案全部有真实调用方。

后端:
- contracts:新增 E-28 EXTRACTION_UNAVAILABLE(运行级,不进 HTTP 信封)与
  KnowledgeExtraction 记录类型。契约层强制"失败不得带候选",
  AC-50「模型不可用⇒不产生候选」是类型约束而不是文档约定。
- services/experience_extraction.py(新):真实提炼入口。模型不可用或调用失败
  → 记 E-28 且 candidate_id 为空;不产半成品候选。传输纪律同 assistant_model
  (禁进程内代理、零重试),且 tools=[] 使"越权读材料"在结构上不可能。
  自然语言修订同样必须真实调用模型,没有"把中文猜成条件"的回退路径。
- services/knowledge_cards.py:新增 current_draft/revise/list_cards 与
  registry_conditions。修订只产草稿、保留 origin 与来源、升版本记 parent_version;
  test_cases 纳入可改字段——它是 component_evaluation 对自定义 Skill 的发布门禁,
  不开放则提炼出的候选永远卡在评测门外。
- services/project_assistant.py:enqueue 改用 review_plan.assemble(),
  让 T06-b 的依赖预检(E-19 可定位文案)与维度解析不再被绕过。
- api/prescreen.py:case-timeline、knowledge-extractions、knowledge-candidates
  (创建/修订/状态机)、review-plans(装配/状态机)。角色门禁与既有
  /components 入口一致;id 与数据域由服务端生成;按 id 的路由一律在 run() 内
  查归属,否则 KeyError 会变成 500 而不是 404。
- component_registry/review_plan:下沉 current_draft/version_key 与 list_plans。

前端:
- 新增 /experience-center 独立入口(BR-50)与 ExperienceCenter 组件;
  AgentNav 增加「历史经验加工」。
- originBadge 此前零调用方、.origin-* 无任何 CSS——守卫只 grep 库文件字符串,
  于是"测试通过但功能不存在"。本批补齐消费方与样式,并用左边框样式区分四种来源,
  保证去色仍可区分(AC-42/BR-48)。
- 守卫语义按 AC-55 调整:由"禁止新建路由"改为"禁止重复页面"
  (面板不得搬到独立路由、已合并页必须保持重定向),并新增
  experience-center-ui 守卫(已登记 verify.py,共 12 项检查)。

测试:新增 36 项(提炼 15 / 候选治理 15 / 卡片到运行 6),全量 unittest 无回归;
两次同项目运行对照证明卡片确实改变了核查结果,且 basis_ids 指向 卡片id@版本。

未闭合(记录在 17 的剩余缺口):真实模型提炼尚未实测(需端点);
界面未经浏览器点击验证;例外边界未进入可求值语言;规则类候选不在本批。

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

78 lines
5.2 KiB
JavaScript
Raw 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 命名空间");
console.log("experience-center-ui: PASS");