Files
mini-openclaw-v2/scripts/test_prescreen_ui.cjs
T
qiaoqichaoandClaude Code 22f8a37799 feat(v2): T09 预审与推进沙盘——运行只读视图、演示情景与界面守卫
覆盖 AC-11/12/29/30/31/42/44(BR-11/29/30/41/43、E-20/21/22)。

后端:
- contracts/simulation.py:Simulation/SimulationAssumption/ScenarioDimensionState。
  契约层直接禁止 production 域携带演示估计(E-21);ready 必须有结果、
  failed 必须带错误码且不得显示结果、计算中不得预告乐观增幅。
- services/prescreen_sandbox.py:演示情景表(DEMO-TABLE-v1)与推演入口。
  组合查表不是单项加和(assert_not_additive 把这条写成可执行断言);
  只改被假设的维度,其余维度原样保留;失败保留基线并标未完成。
- services/prescreen_store.py:运行视图只读存储,复用 assistant_objects 新 kind
  prescreen_run,不新建表;只提供保存与读取,要改结果必须产生新修订。
- api/prescreen.py:新增 3 个只读端点(运行列表、运行视图、可用假设动作)
  与 1 个情景端点;请求体里没有任何概率字段。越域返回 E-21、冲突假设 E-22、
  过期修订 409。
- contracts/prescreen.py:补 E-20/21/22 三个错误码与其 HTTP 映射。

前端(挂进既有工作台「材料与问题」页,不新建路由、不新增样式表):
- src/lib/prescreen-run.ts:客户端契约、呈现判定(同刻度 3–8 维才画雷达)、
  演示值域判定、来源与数据域徽标(文字+图标+样式,去色仍可区分)。
- src/components/xingbang/PrescreenPanel.tsx:主审意见、演示值(紧邻固定说明)、
  维度雷达与等价文字列表、维度依据抽屉、假设勾选即时推演。
  旧请求晚到或结果与当前选择不一致即丢弃;请求失败保留基线并标未完成。
- scripts/test_prescreen_ui.cjs:界面源码守卫,已注册进 scripts/verify.py。

未做:运行装配结果的写入方仍是执行链路(T10 范围),当前由测试写入;
沙盘尚未接入真实执行,界面数值全部为服务端演示表值。

验证:backend 全量 unittest 469 项 OK;前端 npm run build 通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-30 09:15:57 +08:00

79 lines
4.5 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.
/**
* T09 预审工作台与沙盘界面源码守卫。
*
* 这是**源码断言**,不是浏览器端到端测试(docs/v2/06-validation.md §2 已明确
* 现有 UI 脚本的性质)。它锁住的是可机械核对的约束:数值只能来自服务端、
* 演示值只在演示域、雷达只画 3–8 维、乱序响应丢弃、失败保留基线、
* 图的等价文字与键盘可达。
*/
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 panel = read("src/components/xingbang/PrescreenPanel.tsx");
const lib = read("src/lib/prescreen-run.ts");
const workbench = read("src/app/xingbang-workbench/page.tsx");
// 1. 挂载在既有工作台内,不新建路由(04-ui-spec §1:不恢复已合并的旧页面)
assert.match(workbench, /import PrescreenPanel from "@\/components\/xingbang\/PrescreenPanel"/, "工作台应导入预审面板");
assert.match(workbench, /<PrescreenPanel /, "工作台应渲染预审面板");
for (const banned of ["prescreen", "sandbox", "simulation"]) {
assert.ok(!fs.existsSync(path.join(root, "src/app", banned)), `不得为 T09 新建路由 src/app/${banned}`);
}
// 2. 复用既有样式,不新增样式表
assert.ok(!/^import .*\.css";/m.test(panel), "面板应复用 workspace.css 既有 class,不新增样式表");
// 3. 数值只能来自服务端:前端不得复制演示情景表(AC-30/31)
for (const [name, source] of [["panel", panel], ["lib", lib]]) {
assert.ok(!/0\.(42|58|51|64)\b/.test(source), `${name} 不得硬编码演示概率值`);
assert.ok(!/"(00|10|01|11)"\s*:/.test(source), `${name} 不得在客户端复制演示情景表`);
}
assert.match(panel, /simulation\.probability_view/, "情景数值必须取自服务端响应");
// 4. 演示值只在演示域显示,且紧邻"人工设定"说明(AC-12/30、DESIGN §38)
assert.match(panel, /demoValue\(run\?\.data_domain/, "演示值必须按数据域判定后才显示");
assert.match(panel, /未完成真实历史校准/, "未校准时必须说明为何不显示概率");
assert.match(panel, /正式审批由人工决定/, "演示值旁必须有用途边界说明");
assert.match(lib, /domain !== "demo"/, "lib 必须在非演示域返回 null");
// 5. 雷达只对同刻度 3–8 维绘制,其余给完整列表;空维度不补占位轴(AC-44)
assert.match(lib, /RADAR_MIN = 3/);
assert.match(lib, /RADAR_MAX = 8/);
assert.match(lib, /kind: "radar"/);
assert.match(lib, /EMPTY_DIMENSIONS = "当前方案尚未配置核查维度"/);
assert.match(panel, /shown\.kind === "radar" && <RadarChart/, "只有判定为雷达时才画雷达");
assert.match(panel, /shown\.kind === "empty" && <div className="ra-empty">/, "空维度用空状态,不补六轴");
assert.match(panel, /文字列表与图等价/, "图必须有等价文字列表");
// 6. 乱序与过期响应必须丢弃(AC-31)
assert.match(panel, /ticket !== seq\.current/, "旧请求晚到必须丢弃");
assert.match(panel, /returned !== \[\.\.\.keys\]\.sort\(\)\.join\(","\)/, "结果与当前选择不一致时必须丢弃");
// 7. 失败保留基线并标未完成;计算中不得先给乐观增幅
assert.match(panel, /该情景未完成。基线数值与原有意见保持不变/, "失败必须保留基线");
assert.match(panel, /不会先给一个乐观增幅/, "计算中不得预告结果");
assert.match(panel, /不是各项增量的简单相加/, "沙盘必须说明组合非加和");
// 8. 键盘可达与无障碍
assert.match(panel, /<input type="checkbox"/, "假设动作必须是原生可聚焦控件");
assert.match(panel, /aria-live="polite"/, "变化的数值区必须有 aria-live");
assert.match(panel, /aria-label=\{`查看 \$\{item\.id\} 的依据`\}/, "维度依据入口必须有可读名称");
assert.ok(/<svg[^>]*aria-label=/.test(panel.replace(/\n/g, " ")), "雷达必须有文字等价说明");
// 9. 来源与数据域用文字+图标共同区分,不靠颜色单独传意(AC-42)
assert.match(lib, /text: "历史提炼"/);
assert.match(lib, /text: "人工创建"/);
assert.match(lib, /text: "制度导入"/);
assert.match(lib, /icon: "history"/);
assert.match(panel, /domain\.text/, "数据域必须以文字呈现");
// 10. 阶段是序数,不得换算成概率
assert.match(lib, /雷达面积不换算成百分比|STAGE_LEGEND/, "必须说明阶段与概率无关");
assert.ok(!/probability\s*[:=]\s*[0-9]/.test(panel), "面板不得写入任何概率数值");
console.log("prescreen-ui: PASS");