feat(v2): S1d 最薄界面——工作台内的材料集合导入面板
T02 收尾子卡。挂在既有 /xingbang-workbench「材料与问题」页内,**不新建路由**, 复用 workspace.css 既有 class(无新增样式表)。 - src/components/xingbang/ImportBatchPanel.tsx:登记清单 → 逐项上传 → 重试/取消 → 冲突并列展示与裁定。浏览器端用 crypto.subtle 算 sha256 生成清单(服务端仍重算核对) - src/lib/prescreen.ts:错误信封解包 + 契约类型(**手工对齐**,OpenAPI→TS 生成链 未建设,已在文件头标注为临时)+ 服务端状态标签 - page.tsx 仅三处小改:导入面板、挂载面板、request() 改用 messageOf (兼容信封与既有 detail 的 string/array/object 三种形态,不丢既有信息量) 面向用户的诚实性约束(有源码守卫锁定):明确写出"未上传或未确认的文件不会进入 项目事实""不会按上传先后覆盖""不会自动挑一个";空批次用既有空状态,不绘制虚构行; 面板不含任何审批概率。 验证: - npx tsc --noEmit 通过;npm run build 通过 - 新增 scripts/test_prescreen_panel.cjs(10 条源码守卫),已加入 verify.py 用例 - scripts/verify.py 10/10 PASS(exit 0) **未验证**:面板未经浏览器端到端验证。它依赖客户端数据加载,SSR HTML 不含它, 且本项目没有浏览器自动化依赖。目前只有类型检查、构建与源码断言三层证据。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* S1d 界面源码守卫。
|
||||
*
|
||||
* 这是**源码断言**,不是浏览器端到端测试(docs/v2/06-validation.md §2 已明确
|
||||
* 现有 UI 脚本的性质)。它锁住的是可机械核对的约束:不新建路由、复用既有样式、
|
||||
* 上传必须带批次标识、冲突裁定必须留痕、不得展示任何审批概率。
|
||||
*/
|
||||
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/ImportBatchPanel.tsx");
|
||||
const workbench = read("src/app/xingbang-workbench/page.tsx");
|
||||
const lib = read("src/lib/prescreen.ts");
|
||||
|
||||
// 1. 挂载在既有工作台内,不新建路由(docs/v2/04-ui-spec.md §1:不恢复已合并的旧页面)
|
||||
assert.match(workbench, /import ImportBatchPanel from "@\/components\/xingbang\/ImportBatchPanel"/, "工作台应导入导入面板");
|
||||
assert.match(workbench, /<ImportBatchPanel /, "工作台应渲染导入面板");
|
||||
for (const banned of ["prescreen", "import-batch", "material-import"]) {
|
||||
assert.ok(!fs.existsSync(path.join(root, "src/app", banned)), `不得为 S1 新建路由目录 src/app/${banned}`);
|
||||
}
|
||||
|
||||
// 2. 复用既有样式,不新增样式表
|
||||
assert.ok(!/^import .*\.css";/m.test(panel), "面板应复用 workspace.css 既有 class,不新增样式表");
|
||||
|
||||
// 3. 上传必须带批次标识(否则服务端无法回写条目状态)
|
||||
assert.match(panel, /form\.append\("batch_id", batch\.id\)/, "上传必须提交 batch_id");
|
||||
assert.match(panel, /form\.append\("item_id", itemId\)/, "上传必须提交 item_id");
|
||||
|
||||
// 4. 冲突裁定必须显式选择 + 填写说明,且未填说明时按钮禁用
|
||||
assert.match(panel, /resolution_note: note/, "裁定必须提交 resolution_note");
|
||||
assert.match(panel, /selected_evidence_ids: \[evidenceId\]/, "裁定必须显式选择来源");
|
||||
assert.match(panel, /disabled=\{busy \|\| !note\.trim\(\)\}/, "未填裁定说明时不得可提交");
|
||||
|
||||
// 5. 必须向用户说明"未确认不入事实"与"冲突不覆盖"(BR-01 / BR-23 的用户可见面)
|
||||
assert.match(panel, /未上传或未确认的文件不会进入项目事实/, "必须说明未确认不入事实");
|
||||
assert.match(panel, /不会按上传先后覆盖/, "必须说明冲突不按上传先后覆盖");
|
||||
assert.match(panel, /不会自动挑一个/, "必须说明系统不会自动挑选冲突来源");
|
||||
|
||||
// 6. 不得展示任何审批概率(BR-12 / AC-12:未校准即无概率)
|
||||
assert.ok(!/通过率|probability|概率环/.test(panel), "S1 面板不得展示任何审批概率");
|
||||
assert.ok(!/通过率|probability/.test(lib), "S1 客户端不得引入概率字段");
|
||||
|
||||
// 7. 不得在前端编造判定结果:值必须来自服务端字段
|
||||
assert.ok(!/value:\s*["']?\d/.test(panel), "面板不得硬编码业务数值");
|
||||
|
||||
// 8. 客户端必须同时吃下两种错误形状,且不丢既有 detail 的信息量
|
||||
assert.match(lib, /data\?\.error/, "客户端必须能解包统一错误信封");
|
||||
assert.match(lib, /JSON\.stringify\(detail\)/, "既有 detail 数组形态不得丢信息");
|
||||
|
||||
// 9. 空状态不得绘制虚构行(UI Spec §3)
|
||||
assert.match(panel, /ra-empty/, "空批次应使用既有空状态样式");
|
||||
assert.match(panel, /尚无导入批次/, "空批次应给出明确的空状态文案");
|
||||
|
||||
console.log("prescreen-panel-ui: PASS");
|
||||
@@ -212,6 +212,7 @@ def main() -> int:
|
||||
("identity-recovery-ui", [executable("node"), "scripts/test_identity_recovery.cjs"], ROOT),
|
||||
("cloud-assistant-access-ui", [executable("node"), "scripts/test_cloud_assistant_access.cjs"], ROOT),
|
||||
("ui-readability", [executable("node"), "scripts/test_ui_readability.cjs"], ROOT),
|
||||
("prescreen-panel-ui", [executable("node"), "scripts/test_prescreen_panel.cjs"], ROOT),
|
||||
("frontend-build", [npm, "run", "build"], ROOT),
|
||||
("compose-local-config", [docker, "compose", "-f", "docker-compose.yml", "config", "--quiet"], ROOT),
|
||||
("compose-aliyun-config", [docker, "compose", "--env-file", "deploy/aliyun.env.example", "-f", "deploy/docker-compose.aliyun.yml", "config", "--quiet"], ROOT),
|
||||
|
||||
Reference in New Issue
Block a user