# 批次三-B 的验证台 —— 怎么跑、看什么 这五支是**验收用**的:不连库、不连上游、**不发任何网络请求**,也不需要登录凭据。 它们直接编译真的 Vue SFC、跑 `frontend/` 里那份真的 `vue`、驱动页面的真 `setup()`, 只把 `@/api` 与上传子组件桩掉。 来源与搬迁经过见 `.claude/plan/2026-09-23-lsy-onboarding-B-交付说明.md` §四 4。 **本 README 的每一条命令与读数都是 2026-09-23 实跑取的,不是转述**,但取自**两个时点**: - 第 1~4 节里除 `fe-update-harness.mjs` 之外的读数,取自交付提交 `20a222b`; - `fe-update-harness.mjs` 与第 3 节的 `fe-task3-mutate.mjs`,是在**「乐收赢全流程傻瓜化」Task 3** (进件页重写成「我的商户」列表,计划见 `.claude/plan/2026-09-23-lsy-simple-flow.md`)里 扩写 / 新增的,读数取自该任务**尚未提交的工作区**:页面 `frontend/src/views/module_payment/collection/onboarding/index.vue` = 58650 字节 / sha256 `ef04f76615739c3712f38e874223e0fc3e0d13df76ac75afea3aca0e684d6356`(LF 形态, `git ls-files --eol` 实测 `i/lf w/lf`;CRLF 形态 60041 字节,见第 4 节)。 --- ## 0. 公共前提 - Node:实测 `v22.22.3`。 - 工作目录:README 里的命令以**仓库根** `D:/project2/payment-platform` 为 cwd 写。 台子内部用的是绝对路径,换 cwd 不影响结果(已实测:从 `D:\` 跑同样通过)。 - 依赖 `frontend/node_modules`:台子用 `createRequire` 指到 `frontend/package.json` 来加载 `vue` 与 `@vue/compiler-sfc` ⇒ 那个目录不存在时**加载这一关就过不去**,先 `pnpm i`。 - **跑之前 `git status -- frontend/` 应当是干净的**。第 1、2 节那三支**只读**,跑完仍干净; 第 3 节两台变异驱动**会临时改生产文件**(3a 自己还原,3b 要手动 `restore`)—— 所以「跑完还干净」这句只对前三支成立。 ## 1. 两支要传页面路径的台子(**不传会崩**) 这两支把目标页面当**命令行参数**读(`process.argv[2]`): ```bash PG="D:/project2/payment-platform/frontend/src/views/module_payment/collection/onboarding/index.vue" node .claude/plan/2026-09-23-lsy-onboarding-B-harness/task-9-fix1-harness.mjs "$PG" node .claude/plan/2026-09-23-lsy-onboarding-B-harness/fe-update-harness.mjs "$PG" ``` **忘了传参数会怎样**:立刻崩,`TypeError [ERR_INVALID_ARG_TYPE]: The "path" argument must be of type string ... Received undefined` (栈顶是 `compileSfc` 里的 `readFileSync`),**退出码 1**。看到这个就是漏了页面路径,不是台子坏了。 ### 判成败只看 `SUMMARY` 行 两支都会在最后打一行汇总。**不要看退出码**:台子**故意不设** `process.exitCode` (`fe-update-harness.mjs` 第 14 行的注释就是这么写的),所以**有 `FAIL` 也退 0**; 唯一会给出非 0 退出码的情况是上面那种「参数没传」的崩溃。 所以判据是这两行里的 `fail=`: ``` SUMMARY <页面路径>: pass=25 fail=0 ← task-9-fix1-harness.mjs SUMMARY <页面路径>: pass=89 fail=0 ← fe-update-harness.mjs(Task 3 之前是 43) ``` 有失败时同一行会追加失败用例名:`... fail=2 -> <用例名> | <用例名>`。 上面两个读数都是 2026-09-23 实跑的(各 1 次,`exit=0`):`pass=25` 取自交付提交 `20a222b`; `pass=89` 取自「傻瓜化」Task 3 的工作区(页面指纹见文首)。 ★ 顺手可以把 stderr 丢掉,屏幕只剩 PASS 与 `SUMMARY`(实测:`fe-update-harness` 加 `2>/dev/null` 后 stdout **91 行**、`grep -c "Vue warn"` = **0**;这个行数随用例数走,43 条那会儿是 27 行) —— 台子的告警与桩报错**都在 stderr**,stdout 是干净的判据流。 ### 各自钉什么 - **`task-9-fix1-harness.mjs`(25 条)**:进件页 Task 9 那处修复(商户号回显 / 警示)的判定逻辑, 含两条**正对照**(G4「正常商户号 ⇒ 显示出来」、G5「正常商户号 ⇒ 不置警示」) —— 没有正对照的话「一律不显示」也会全绿。 - **`fe-update-harness.mjs`(89 条)**:同一支台子,两批被测逻辑。原 43 条是 R-7c 的 「建号 / 更新」两个模式的报文形状 —— `U.` 更新腿键白名单(U1/U1b:不含 `merchant_name_business`)、 `V.` 建号腿正对照(一个都没被改坏)、`W.` 渲染投影、`Y.` 更新腿的失败与形状、 `Z.` 键集(Z5/Z6:更新集与建号集里都没有 `category` / `tenant_id` / `rate_code`)。 Task 3 新增的五段钉的是**重写后新出现的**判定: | 段 | 钉什么 | |---|---| | `X.` | 打开两张表单的入口 —— 新建从**干净表单**起步;「改资料」模式与商户号预填,但**不**把建号表单的值带过去 | | `P.` | 进度列:四个 `review_status` 逐字映射,**表外的值一律不猜**(正对照:未知值 ⇒ 空) | | `A.` | 每行**恰好一个**主按钮 + 至多一个文字按钮;A7 是正对照(APPROVED **没有**提交审核入口)、A5 是基数闸(五档各恰好一个主按钮) | | `C.` | 提交审核报文:6 个业务字段 + `mch_no`,空白整个省掉;12 个资质码表;大写 `L` 那个坑 | | `S.` | 409 **当成「已提交」**而不是失败(S2 关弹窗、S4 常驻警示);**另配一个 400 正对照**,证明它认的是那个码值而不是「任何失败都当 409」 | | `L.` | 列表读法要解**两层** `.data`(响应拦截器不解包);L5 是正对照 —— 只解一层时读不到行 | ★ 台子里有**一处已删的空断言**(原 `S3`)和**一处已改的脆弱写法**(`L2` 原用 `[0].mch_no`), 两处的理由都写在代码注释里,别当漏做补回去。详见 `.claude/plan/2026-09-23-lsy-simple-flow.md` Task 3 的「变异暴露出的两条我自己的问题」。 ### 已知局限(别把它的绿读大) - **模板不渲染** ⇒ 它证明的是**函数判定逻辑**,**不证明浏览器里的表现**。 - stderr 上会出现一条 `[Vue warn]: onMounted is called when there is no active component instance...` —— 这是桩环境没有组件实例导致的,**预期内**,不是失败。 - stderr 上还会有一段桩出来的报错(例如 `级联数据加载失败: Error: 网络挂了`), 那是**故意注入的网络失败**用例在走错误分支,同样不是失败。 (这两条都实测过只在 stderr:`2>/dev/null` 之后 stdout 里一个都没有。) ## 2. 静态键名核对台(不需要参数) ```bash node .claude/plan/2026-09-23-lsy-onboarding-B-harness/fe-update-keycheck.mjs ``` **退出码 0** + 末行 `SUMMARY keycheck: 全绿`。它比对前后端两侧的字段键名集合, 确认没有拼错的键(跨端键名拼错在运行时只会表现为「字段悄悄没发出去」)。 ★ 这支是五支里**唯一搬过家时改过内容**的一支:原先写死了兄弟台子的工作区绝对路径, 搬完就成了一个已删除的文件 ⇒ 改成按**自身位置**相对定位 (`fileURLToPath(new URL("./fe-update-harness.mjs", import.meta.url))`)。 所以它必须与 `fe-update-harness.mjs` **待在同一个目录**里。 ## 3. 变异驱动(跑之前先看清楚) **两台驱动形状不同,别互相套用**:`fe-update-mutate.mjs` 是「一次一个变异 + 手动 `restore`」, `fe-task3-mutate.mjs` 是「四轮一口气跑完 + 每轮自己还原」。 ### 3a. `fe-task3-mutate.mjs` —— Task 3 的**两向**变异,一条命令跑完 ```bash node .claude/plan/2026-09-23-lsy-onboarding-B-harness/fe-task3-mutate.mjs ``` 不需要参数,**也不需要事后 `restore`**:它每一轮都在 `finally` 里**按字节还原**, 还原后再复核 sha256 是否回到基线,末行还会再打一次「基线复核 sha …(未被留脏)」。 判据是最后那行:`MUTATE-SUMMARY: 全绿`。四个变异各自预期红的用例: | 变异 | 打的是 | 预期该红 | |---|---|---| | `M-APPROVED-ALSO-SUBMIT` | APPROVED 那档也挂提交审核入口 | A7、A8 | | `M-409-CODE-CHANGED` | 把 409 换成一个表外的码值 | S2、S4 | | `M-LIST-ONE-LAYER` | 列表只解一层 `.data` | L1、L2、**L5(正对照自己也红)** | | `H-REORDER-DECLS` | **无害**:换个声明顺序 | (无,必须仍 89/0) | 最后一行那格是这台子存在的理由:**没有它,「该红的都红了」也可能只是台子对任何改动都敏感**。 **几个会让读数作废、但驱动器自己会拦下来的情况**(都会打印出来并计入非 0 退出码): 锚点命中次数 ≠ 1(说明变异打偏了,**不会**去改文件)、还原后 sha ≠ 基线、台子**没跑完** (`spawnSync` ⇒ 变异把台子打崩时驱动器**不跟着死**,这一档单独判,不会被读成「没红 ⇒ 通过」)。 ### 3b. `fe-update-mutate.mjs` —— R-7c 的变异,**需要手动还原** ```bash node .claude/plan/2026-09-23-lsy-onboarding-B-harness/fe-update-mutate.mjs restore ``` - `restore`:把备份 `.mut-bak` 还原回生产文件。**没有备份时什么都不做,退出码 0。** - 传其它任何 id(`M-U1` / `M-U2` / `M-U3` / `M-U5` / `M-T1` / `M-T2`):**会真的改生产前端文件** (`frontend/src/views/module_payment/collection/onboarding/index.vue` 与 `frontend/src/api/module_payment/onboarding.ts`),并在同目录留 `.mut-bak`。 跑完**必须**用 `restore` 还原,再用 `git status -- frontend/` 确认干净。 **验收不需要跑变异。** - 跑 3a:一条命令,跑完看末行 `MUTATE-SUMMARY`,**不用再 `restore`**,末行会自己报「未被留脏」。 - 跑 3b:按「先 `git status` 干净 → 变异 → 跑第 1 节的两支台子看读数变化 → `restore` → 再 `git status` 干净」的顺序。 - 两台驱动都**只动工作区文件、不动索引**,所以跑完 `git status` 该与跑前**逐行相同**。 (跑 3a 实测过:跑前跑后 `git status --porcelain` `diff` **无差异**、`git diff --cached --stat` 空。) ## 4. 核指纹时注意行尾 本仓 `core.autocrlf = true` 且**没有 `.gitattributes`** ⇒ **重新 checkout 会把工作区文件抹成 CRLF**。 两种形态**都实测过**,不是按行数推算的(LF 即当前工作区形态): | 文件 | LF 字节 | LF sha256 | CRLF 字节 | CRLF sha256 | |---|---|---|---|---| | `task-9-fix1-harness.mjs` | 10796 | `7f07de4099c96a23c56c1238aeb002857ed8bc10c83458feab5b979cac5bc095` | 11049 | `81271e98d40456c3a594669e796ff3d49681eee9b2491bf8ba1766f09e4a362f` | | `fe-update-harness.mjs` | 24707 | `a93e0f8e4dd86340f8db29766ecc6336335e00a9b3e190e219135bb296805150` | 25189 | `17e1bd83b73123bd1a941c0f0f1cdc6396e0c513638cad47753ef86baa1ef81d` | | `fe-update-keycheck.mjs` | 6874 | `3d4033167ed8950cda71c3396d77fe4968f70aafef915f2616193a675eba1767` | 7002 | `2e1eb43d829ea7e4aabc330c77d4a5605c4b4672a580d21ace255fbfa26325e4` | | `fe-update-mutate.mjs` | 4197 | `a3e68dff6d58fd86a6b219c2de50f98493434bc929c544d9f77bb1af9565a330` | 4294 | `fafacc0c8e41091a870054098c2de20dc46f90670890983dcd5101f9d8d65621` | | `fe-task3-mutate.mjs` | 6575 | `4003028451629bcbef57cf40bfc99c67e7879084640cfdf4b5cd8cfa7abb42dc` | 6706 | `6640b0083e02066ebccc85d34c99064d7d2ec9af5ae85cedf9eff77228c87ddf` | 被测页面(Task 3 那两行的读数钉在它的这个版本上): | 文件 | LF 字节 | LF sha256 | CRLF 字节 | CRLF sha256 | |---|---|---|---|---| | `collection/onboarding/index.vue` | 58650 | `ef04f76615739c3712f38e874223e0fc3e0d13df76ac75afea3aca0e684d6356` | 60041 | `34e3cb98fd44c8835e26f79d7e3805d3a7a9b42be65fc01e5510dd12d90dfea8` | **核出 CRLF 那组数不是文件被改过,是行尾。** (上面两张表共 12 个读数,是用同一个换算脚本一次跑出来的:把读入的字节按 `\r\n` ↔ `\n` 互转后各自 sha256。 该脚本对 `task-9-fix1-harness.mjs` 复现出的 CRLF 值 `81271e98…` 与早先独立量到的一致 ⇒ 换算方式没跑偏。) --- ## 5. `lsy-keepalive-harness.mjs` —— keep-alive 组件名匹配(2026-09-28 新增) **要证的一句话**:`` 匹配的是**组件名**,而 `cachedViews` 里放的是 **路由名**(= `sys_menu.route_name`)⇒ 两者不逐字相同时,那页的 `keep_alive=true` **静默失效**(vue 不报错,只是不缓存;症状是「切走再切回来,筛选条件/分页/正在等的二维码全没了」)。 链路三跳(全部从源码量过,不是推的): `sys_menu.route_name` → `permission.store.ts` 造路由时 `name: item.route_name` → `tags-view.store.ts` 的 `addCachedView` 把**路由名**推进 `cachedViews` → `layouts/components/AppMain/index.vue:6` 的 `` 拿它去比**组件名**。 ```bash node .claude/plan/2026-09-23-lsy-onboarding-B-harness/lsy-keepalive-harness.mjs \ "D:/project2/payment-platform/frontend/src/views/module_payment/collection/order/index.vue" order \ [可选的 include 覆盖值] ``` 编**真** SFC(`@vue/compiler-sfc`)、用 `frontend/node_modules` 里那份**真** `vue` 挂进**真** ``,数 `setup()` 跑了几次 —— 缓存命中后再激活**不会**重跑 `setup`。 一次跑给**两向**读数:`include=[本页真名]` 期望 1;`include=[必然不匹配的串]` 期望 2(正对照, 没有它,`setups=1` 也可能只是计数器没在工作)。 ### 读数(2026-09-28 实跑) 五个页面各自自比,**5/5 PASS**(`hit=1 miss=2`): | 页面 | 组件名(修后) | 活库 `route_name` | |---|---|---| | `collection/order` | `CollectionOrder` | `CollectionOrder`(id=269) | | `collection/refund` | `CollectionRefund` | `CollectionRefund`(id=270) | | `collection/merchant` | `CollectionMerchant` | `CollectionMerchant`(id=271) | | `collection/cashier` | `CollectionCashier` | `CollectionCashier` | | `collection/onboarding` | `CollectionOnboarding` | `CollectionOnboarding`(id=272) | 活库那一列取自 `lsy-cashier-menu.sql` 头部记的 **2026-09-23 只读实测**(id 269–272)。 ★ 前三个页面**在 2026-09-28 之前**写的是 `Leshouying*`(`lsy-menu-rename.sql` 把库从 `Leshouying*` 改成 `Collection*` 时,SFC 的 `name` 没跟着改)—— 即**三个页面的 `keep_alive` 一直是失效的**。 `lsy-cashier-menu.sql:32-40` 早记过这条,当时按「不相关的不动」留下未修。 **变异(两向的另一半)**:把 order 页的 `name` 改回 `LeshouyingOrder`,`include` 仍喂 `CollectionOrder` ⇒ **`SUMMARY FAIL hit=2 miss=2`**(不缓存)。这证明上面那些 `PASS` 是 **行为差别**,不是文字差别。还原后 `git status -- frontend/` 只剩那三个待提交文件。 ### 已知局限(别把它的绿读大) - 模板**不渲染**(`inlineTemplate: false`)⇒ 它证明的是**缓存语义**,**不是**浏览器里长什么样。 - 用 `createRenderer` 接一套哑节点操作,**不引 jsdom**。有的页面 `onMounted` 会碰 `window` (如 cashier 页),台子里有一行 `globalThis.window = globalThis` 的垫片 —— ★ 那是**台子**的垫片,不是页面的缺陷,别去改页面。 - 桩是通用的(`@/api/*` 与 `element-plus` 都换成「取什么都是可调用、返回 Promise」的代理) ⇒ 它验不到任何接口契约。**残留 import 会被一句兜底断言当场炸掉**,不会悄悄跑出假绿 (实测:`qrcode` 与 `SingleImageUpload` 两条就是这么被发现并补上的)。 - 不设 `process.exitCode`(与同目录各台一致)⇒ 失败也退 0,**只认最后那行 `SUMMARY`**。 ### 指纹 | 文件 | LF 字节 | LF sha256 | CRLF 字节 | CRLF sha256 | |---|---|---|---|---| | `lsy-keepalive-harness.mjs` | 7718 | `2d5a9c9a677dafd1d57d84eae6e0afc8d11ce7526d0aae2e66dffa15327c7571` | 7888 | `cd97a29a36877aeb88656b2271d232e960850f7cc3c94329b256ed009a04d1fe` | | `collection/order/index.vue` | 10531 | `66b144354bb612081aeca65ef54b002c194f29ec2eab5d840fea7fa6ea7b9700` | 10789 | `1559fbb99ad173c7995f5491dfdd96def53b8b894dfc38fcb41ea30e73ef315a` | | `collection/refund/index.vue` | 4720 | `0a6c782061522baad7a97a794f51f78b214bcf960141aed6546e8a8291322b2b` | 4860 | `1c00b8b0f89f46adaf8d0a64999817415c16194bf2e5fe8218d3a72493df5ea7` | | `collection/merchant/index.vue` | 11178 | `2df3a00d5440b08487cd409d9923b72dd76fa5659fc75a436e4e900ebcb449b6` | 11431 | `bebaac71b14e2c331d32c96d7d55d83fbc255b8a9b4233307891bf1a3ab60942` | 三个 `.vue` 的 `git ls-files --eol` 实测 `i/lf w/crlf`(工作区即 CRLF 形态); `.mjs` 是 2026-09-28 新写的,工作区当前是 LF(CR 数 0)—— 提交后随 `core.autocrlf=true` 会变 CRLF。