Files
mini-openclaw-v2/scripts/test_module_shell_ui.cjs
T
qiaoqichaoandClaude Code f775c6cbe4 feat(v2): 纠偏第四批——会议结果录入与改进发布接真实知识发布入口
- api/prescreen.py:transition_improvement 注入 _knowledge_publisher,
  release 委托 knowledge_cards.publish → component_registry.release;
  按 proposed_revision_ref 以「组件id@版本」精确解析草稿(不取"最新草稿",
  否则会出现"审核的是 A、发布的是 B");客户端自报 released_ref 一律 422;
  非 knowledge 目标 409 诚实拒绝;发布与候选状态同一事务,失败保留旧版。
- TrajectoryPanel:新增会议结果录入(追加式更正走 supersedes_id;
  未匹配单列且不计入分母)。
- ImprovementPanel(新增):偏差→候选→两道评测门→独立审核→发布;
  请求体不含版本号,无自动上线,不自批。
- 界面并入「轨迹复盘」,不新增第四个导航入口(AC-53)。
- 守卫:新增 scripts/test_improvement_panel.cjs(verify.py 13→14 项);
  test_module_shell_ui.cjs 的「只读」断言改为分别锁死「回放只读」与「结果只可追加」。
- 测试:新增 backend/test_improvement_release.py(AC-57 端到端 + 三条反例);
  test_improvements.py 补「自报版本号被拒」。
- 文档:17 第四批交付记录、15 §7 交接、03 §9.4 发布事务边界。

验证:scripts/verify.py 14/14 PASS(backend-unit 572 项),证据
test-evidence/20261001-013517-3e15bda/;scripts/xingbang_batch4_smoke.py
对真实服务器实跑通过(发布指针实测移到新版本)。

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

152 lines
9.9 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-50;AC-53/54/55)。
*
* 这是**源码断言**,不是浏览器端到端测试(docs/v2/06-validation.md §2)。
* 它锁住四类可机械核对的约束:
*
* 1. **深色导航+浅色工作区**(AC-53):导航取原型 token,画布保持浅色。
* 2. **三个模块各有独立入口**(BR-50):AI 预审 / 历史经验加工 / 轨迹复盘,
* 各自有路由、各自渲染**复用**的组件——不是把面板塞进旧页当页签。
* 3. **防重复页面**(AC-55):已合并的旧页必须仍是重定向;不得复活。
* 4. **权限与数据隔离**(AC-55):导航按角色裁剪;模块以 ?project= 为上下文,
* 不把项目 id 硬编码,也不在前端自造授权。
*/
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 exists = (file) => fs.existsSync(path.join(root, file));
const navigation = read("src/components/xingbang/navigation.css");
const nav = read("src/components/xingbang/AgentNav.tsx");
const home = read("src/app/page.tsx");
// ---------------------------------------------------------------- 1. 深色导航
assert.match(navigation, /\.xb-agent-nav\{[^}]*background:#102B27/, "主导航应使用原型的深色 nav.bg #102B27");
assert.match(navigation, /\.xb-agent-nav a\.selected\{[^}]*background:#244D41/, "选中项应使用 nav.active #244D41");
assert.match(nav, /智审工作台/, "品牌区应显示「智审工作台」(原型文案)");
// 深色底的文字对比度**算出来**,而不是在文档里声称"应该够清楚"(G-6 机械约束优先)。
// 阈值取 WCAG AA 正文 4.5:1;四组配色都要过。
function luminance(hex) {
const channel = (value) => {
const c = value / 255;
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
};
const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
}
function contrast(foreground, background) {
const [a, b] = [luminance(foreground), luminance(background)];
return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
}
const NAV_BG = "#102B27", NAV_ACTIVE = "#244D41";
for (const [label, fg, bg] of [
["未选中导航项", "#DCE9E3", NAV_BG],
["选中导航项", "#FFFFFF", NAV_ACTIVE],
["次要文字(muted)", "#B1C6BE", NAV_BG],
["品牌标题", "#F3F7F5", NAV_BG],
]) {
assert.ok(contrast(fg, bg) >= 4.5, `深色导航「${label}」对比度应 ≥ 4.5:1,实测 ${contrast(fg, bg).toFixed(2)}:1`);
}
assert.match(nav, /业务工作区/, "导航应保留「业务工作区」分组标签");
// 导航是深色,身份选择器在导航内必须用深色变体,否则白底白字
assert.match(nav, /<IdentitySelector[^>]*dark/, "导航内的身份选择器应使用深色变体");
// 画布仍是浅色:不要把整页刷黑
assert.match(read("src/app/data-access/workspace.css"), /\.ra-app\{min-height:100vh;background:#f5f7f9/, "工作区画布应保持浅色");
// ---------------------------------------------------------------- 2. 三个独立入口
const MODULES = [
{key: "prescreen", href: "/prescreen-workbench", label: "AI 预审", component: "PrescreenPanel"},
{key: "experience", href: "/experience-center", label: "历史经验加工", component: "ExperienceCenter"},
{key: "trajectory", href: "/trajectory-center", label: "轨迹复盘", component: "TrajectoryPanel"},
];
for (const item of MODULES) {
assert.ok(exists(`src/app${item.href}/page.tsx`), `${item.label} 应有独立路由 ${item.href}`);
assert.ok(nav.includes(`"${item.href}"`), `导航应提供 ${item.label} 入口`);
assert.ok(nav.includes(item.label), `导航入口文案应为「${item.label}」`);
const page = read(`src/app${item.href}/page.tsx`);
// 复用组件而不是在路由里再写一份实现:import 自 @/components
assert.match(page, /from "@\/components\/xingbang\//, `${item.label} 路由应复用既有组件`);
assert.match(page, /AgentNav/, `${item.label} 路由应复用深色导航`);
// 数据隔离:项目上下文来自 URL,不硬编码项目 id
assert.match(page, /new URLSearchParams\(location\.search\)\.get\("project"\)/, `${item.label} 的项目上下文应来自 ?project=`);
assert.ok(!/DEMO-[ABC]/.test(page), `${item.label} 不得硬编码演示项目 id`);
}
// 首页必须呈现三个入口(AC-53:不能只保留旧版落地页布局)
for (const item of MODULES) {
assert.ok(home.includes(item.href), `业务首页应提供 ${item.label} 入口卡片`);
}
assert.match(home, /MODULES/, "首页应以模块清单驱动入口卡片,而不是写死一段静态文案");
// 旧版落地页内容保留为独立入口,而不是被删掉
assert.ok(exists("src/app/assistant/page.tsx"), "原落地页内容应保留在 /assistant");
assert.match(read("src/app/assistant/page.tsx"), /BusinessAssistant/, "/assistant 应仍渲染业务助手");
// ---------------------------------------------------------------- 3. 防重复页面(AC-55)
// 已合并的旧页必须仍是重定向,不得被重新实现成独立页面。
for (const merged of ["trajectory", "review-workbench", "report-workbench", "collateral-workbench"]) {
assert.match(read(`src/app/${merged}/page.tsx`), /redirect\(/,
`已合并页 src/app/${merged} 必须保持重定向,不得复活`);
}
// 不得把预审面板搬到与既有禁名冲突的目录(那会把旧的 S1/T09 结构重新引入)
for (const banned of ["prescreen", "sandbox", "simulation", "import-batch"]) {
assert.ok(!exists(`src/app/${banned}`), `不得新建与既有面板重名的路由 src/app/${banned}`);
}
// 同一个面板组件被两个宿主复用时,必须是同一份实现(import 复用),不是复制粘贴的第二份
const hosts = ["src/app/xingbang-workbench/page.tsx", "src/app/prescreen-workbench/page.tsx"];
for (const host of hosts) {
assert.match(read(host), /import PrescreenPanel from "@\/components\/xingbang\/PrescreenPanel"/,
`${host} 应复用同一个 PrescreenPanel 组件`);
}
// ---------------------------------------------------------------- 4. 权限(AC-55)
assert.match(nav, /canConfigure/, "导航应按角色裁剪配置治理入口");
assert.match(nav, /request\.state|identity\?\.role/, "导航的角色判据应来自服务端身份而不是硬编码");
assert.ok(!/localStorage\.setItem\("xingbang-demo-role"/.test(nav), "导航不得自行设定演示身份");
// ---------------------------------------------------------------- 5. 轨迹复盘的可操作与诚实性(AC-54)
const trajectory = read("src/components/xingbang/TrajectoryPanel.tsx");
assert.match(trajectory, /failed/, "轨迹面板必须能显示失败步骤(失败保留)");
assert.match(trajectory, /不填 0|未记录/, "成本缺失时必须留空,不得填 0");
assert.ok(!/cost\s*[:=]\s*0\b/.test(trajectory), "不得把缺失成本写成 0");
// 纠偏第四批把"会议结果录入"并入了本面板,因此这里不能再只断言"整页只读"——
// 要分别锁死两件事:**回放只读**(不改运行、不改历史结果)与**结果只可追加**(不覆盖历史)。
assert.match(trajectory, /回放是只读的|只读回放/, "回放必须声明只读(第四批加的是追加,不是改写)");
assert.match(trajectory, /supersedes_id/, "会议结果更正必须指向旧记录(supersedes_id)");
assert.match(trajectory, /不覆盖|只\*\*追加\*\*/, "界面必须声明更正只追加、不覆盖历史");
// 判据落在"是否渲染推理字段"上,而不是禁用词本身——否则解释规则的注释会把守卫自己绊倒。
assert.ok(!/\.\s*(reasoning|reasoning_content|thoughts?|chain_of_thought)\b/i.test(trajectory),
"不得渲染模型隐藏推理字段");
// 服务端已声明轨迹不含内部推理,界面必须把它显示出来(而不是自己编一句)
assert.match(trajectory, /trajectory\.note|note\}/, "应展示服务端对轨迹范围的说明");
// AC-56:既读又写——只读列表不再是本面板的全部职责
assert.match(trajectory, /review-outcomes/, "轨迹面板应消费会议结果端点");
assert.match(trajectory, /"POST",\s*`\$\{PRESCREEN\}\$\{path\}\/review-outcomes`/,
"会议结果必须真的能录入(POST 到 review-outcomes)");
assert.match(trajectory, /raw_code/, "录入必须围绕原始结果码,不得自行推断通过或否决");
assert.match(trajectory, /不计入分母/, "未匹配的结果必须标明不计入分母(单列)");
// 预审模块必须真的挂载预审面板(否则"AI 预审入口"是空壳)
assert.match(read("src/app/prescreen-workbench/page.tsx"), /<PrescreenPanel /, "AI 预审模块应挂载预审面板");
// ---------------------------------------------------------------- 6. 前后端路径一致
// 界面调用的端点必须在后端真的声明过——否则"入口可点"只是连到 404。
const prescreenApi = read("backend/api/prescreen.py");
for (const route of ['"/projects/{project_id}/runs/{run_id}/trajectory"',
'"/projects/{project_id}/review-outcomes"',
'"/projects/{project_id}/improvements"',
'"/improvements/{candidate_id}/transitions"']) {
assert.ok(prescreenApi.includes(route), `后端应声明路由 ${route}`);
}
assert.ok(read("backend/api/project_assistant.py").includes('"/projects/{project_id}/runs"'),
"后端应声明发起运行的路由");
// ---------------------------------------------------------------- 7. 既有禁令继续有效
for (const [name, source] of [["home", home], ["nav", nav], ["trajectory", trajectory]]) {
assert.ok(!/0\.(42|58|51|64)\b/.test(source), `${name} 不得硬编码演示概率值`);
assert.ok(!/"(00|10|01|11)"\s*:/.test(source), `${name} 不得复制演示情景表`);
}
console.log("module-shell-ui: PASS");