/** * 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. 面板挂载在既有工作台内 assert.match(workbench, /import PrescreenPanel from "@\/components\/xingbang\/PrescreenPanel"/, "工作台应导入预审面板"); assert.match(workbench, //, "空维度用空状态,不补六轴"); assert.match(panel, /文字列表与图等价/, "图必须有等价文字列表"); // 纠偏第一批:面板必须展示本次核查结果与依据,缺证不得改写成通过 assert.match(panel, /核查结果(\{/, "面板必须展示本次核查结果"); assert.match(panel, /未完成项:\{item\.message\}/, "降级记录必须显示到界面"); assert.match(panel, /查看 \$\{item\.title \|\| item\.code\} 的依据/, "每条核查结果必须能打开依据"); assert.match(panel, /本次没有可展示的原文片段/, "没有证据时必须说明按缺证处理,不得留白或编造"); assert.match(panel, /没有发现问题不等于已经通过/, "空结果不得被读成通过"); assert.match(panel, /width=\{\d+\} height=\{\d+\} viewBox=/, "雷达必须给显式尺寸,否则会撑满容器(真渲染验证过的缺陷)"); assert.match(panel, / item\.assumed\)/, "没有勾选假设时不得画出实线情景轮廓"); assert.match(panel, /阶段 \{stage \?\? "—"\} · \{stageName\(stage\)\}/, "维度必须同时给出阶段序号与名称"); // 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, /]*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), "面板不得写入任何概率数值"); // 11. 从本页发起的运行必须能被面板读到(真实浏览器端到端验证发现的缺陷) // 面板原先只依赖 project.id 重载,于是从本页发起预审、运行跑完后,面板仍停在旧运行上, // 新运行也进不了"查看哪一次运行"——而徽标写着"结果会自动刷新"。 // 源码守卫与生产构建都看不出这类问题,只有真渲染会暴露;这里锁住修好的依赖关系。 assert.match(panel, /const runKeys = \(project\.runs \|\| \[\]\)/, "面板必须从项目的运行集合推导重载信号"); assert.match(panel, /\[project\.id, runKeys\]/, "运行集合变化时面板必须重载,否则发起的预审不会被读到"); assert.ok(!/\[project\.id\]\)/.test(panel), "不得退回只依赖 project.id 的重载(发起的运行会读不到)"); // run.status 与 prescreen_run 视图不是同一事务写入的:只读一次会读到视图还没落库的中间态 assert.match(panel, /item\.id === newestRun/, "重载前必须确认最新运行已出现在视图列表里,不能只赌一次"); assert.match(panel, /rounds < 5/, "重试必须有次数上限,不得变成无限轮询"); console.log("prescreen-ui: PASS");