/** * Task 3 新逻辑的**两向变异**驱动器。 * * 形状抄自同目录 `fe-update-mutate.mjs`(那里有「先看 git status 干净」那段注意事项)。 * * 为什么需要它:`fe-update-harness.mjs` 绿 90 条**本身不证明**那些断言有牙 —— * 一条恒真的断言与一条真钉子在这份读数上长得一模一样。这里按两向走: * · 该红的必须红(三个真变异各打一处本轮新写的判定) * · **无害改动必须实测仍绿**(换两行没有依赖关系的声明顺序)—— 否则红可能只是 * 「台子对任何改动都敏感」,而不是「这条断言钉住了这件事」 * * ★ 每一轮都:改前存字节 → 断言替换**确实生效**(命中次数)→ 跑台子 → **按字节还原** * → 复核 sha256 回到改前。还原动作本身也要被验证(见 memory「变异验证要两向」第三种)。 * * 用法:node fe-task3-mutate.mjs * 退出码:只有「还原后 sha 不符」或「无害变异竟然红了」才非 0。 */ import fs from "node:fs"; import crypto from "node:crypto"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const HERE = fileURLToPath(new URL(".", import.meta.url)); const HARNESS = HERE + "fe-update-harness.mjs"; const PAGE = "D:/project2/payment-platform/frontend/src/views/module_payment/collection/onboarding/index.vue"; const sha = (buf) => crypto.createHash("sha256").update(buf).digest("hex"); const MUTATIONS = [ { id: "M-APPROVED-ALSO-SUBMIT", why: "把「可以收款了」那一档也挂上提交审核入口", expectRed: ["A7", "A8"], find: ` case "APPROVED":\n return [{ ...edit, primary: true }];`, repl: ` case "APPROVED":\n return [{ ...edit, primary: true }, submit];`, }, { id: "M-409-CODE-CHANGED", why: "把 409 那个码值换成表外的值 ⇒ 409 那条分支永不进入", // ★ 起初这里写的是 ["S2","S3","S4"],实测 **S3 没红** ⇒ 追下去发现 S3 是条恒真的断言 // (理由写在台子里 S3 被删处)。这条 `expectRed` 按**实测**改,不是按我当初的预期改。 expectRed: ["S2", "S4"], find: `if (e?.data?.code === 409) {`, repl: `if (e?.data?.code === 999999) {`, }, { id: "M-LIST-ONE-LAYER", why: "列表只解一层 `.data`(把 payload 抬高一层的读法)", expectRed: ["L1", "L2"], // ★ 锚点必须**唯一**:`res.data.data?.list` 这个串在同一文件的 javadoc 里也出现过一次 // ⇒ 裸用它会让替换打到注释上(驱动器的命中次数护栏第一次就是这么把它拦下来的)。 find: `list.value = res.data.data?.list || [];`, repl: `list.value = res.data?.list || [];`, }, { id: "H-REORDER-DECLS", why: "无害:把两个没有依赖关系的声明换个顺序(行数不变)", expectRed: [], find: ` const submit: RowAction = { text: "提交审核", primary: false, run: openCommit };\n const edit: RowAction = { text: "改资料", primary: false, run: (r) => openUpdate(r.mch_no) };`, repl: ` const edit: RowAction = { text: "改资料", primary: false, run: (r) => openUpdate(r.mch_no) };\n const submit: RowAction = { text: "提交审核", primary: false, run: openCommit };`, }, ]; const original = fs.readFileSync(PAGE); const baseSha = sha(original); console.log(`基线 sha256 = ${baseSha}`); console.log(`基线字节 = ${original.length}`); function runHarness() { // ★ 用 spawnSync 而不是 execFileSync:变异体**有可能把台子打崩**,而崩掉的读数依然有用 // (「预期该红的」可以从崩溃前的 FAIL 行里认出来)。execFileSync 遇到非 0 退出码会**抛异常** // ⇒ 驱动器自己死掉,后面几个变异一个都不跑。实测踩过:M-LIST-ONE-LAYER 让 L2 抛了 // `TypeError: Cannot read properties of undefined`,驱动器当场终止。 const r = spawnSync(process.execPath, [HARNESS, PAGE], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], // stderr 丢掉(桩的告警都在那边) }); const out = r.stdout || ""; const m = out.match(/^SUMMARY .*$/m); const fail = (out.match(/^ FAIL {2}(.+?) {2}\(actual=/gm) || []).map((s) => s.replace(/^ FAIL {2}/, "").replace(/ {2}\(actual=$/, "") ); return { summary: m ? m[0] : `**台子没跑完(status=${r.status})⇒ 没打 SUMMARY**`, fail, complete: !!m, }; } let bad = 0; for (const mut of MUTATIONS) { const src = original.toString("utf8"); const hits = src.split(mut.find).length - 1; if (hits !== 1) { console.log(`\n[${mut.id}] **变异体没生效**(锚点命中 ${hits} 次,应为 1)—— 读数作废`); bad++; continue; } fs.writeFileSync(PAGE, src.replace(mut.find, mut.repl), "utf8"); const mutatedSha = sha(fs.readFileSync(PAGE)); let res; try { res = runHarness(); } finally { fs.writeFileSync(PAGE, original); // 按字节还原 } const backSha = sha(fs.readFileSync(PAGE)); const restored = backSha === baseSha; const missing = mut.expectRed.filter((tag) => !res.fail.some((f) => f.startsWith(tag))); // ★ 「没跑完」必须自成一档:那时 `fail` 是从崩溃前的残缺 stdout 里抠出来的, // 拿它去判「无害改动仍绿」会把「台子崩了」读成「没红 ⇒ 通过」。 let verdict; if (!restored) verdict = "**还原失败**"; else if (!res.complete) verdict = "**台子没跑完 ⇒ 读数不完整(先看它崩在哪)**"; else if (mut.expectRed.length === 0) verdict = res.fail.length === 0 ? "OK(无害改动仍绿)" : "**无害改动竟红了 ⇒ 台子对无关改动也敏感**"; else verdict = missing.length === 0 ? "OK(该红的都红了)" : `**预期该红却没红:${missing.join(", ")}**`; if (verdict.includes("**")) bad++; console.log(`\n[${mut.id}] ${mut.why}`); console.log(` 变异后 sha = ${mutatedSha}`); console.log(` ${res.summary}`); console.log(` 红的用例 = ${res.fail.length ? res.fail.join(" | ") : "(无)"}`); console.log(` 还原后 sha = ${backSha} ${restored ? "== 基线 OK" : "**!= 基线**"}`); console.log(` ⇒ ${verdict}`); } console.log(`\n基线复核 sha = ${sha(fs.readFileSync(PAGE))} ${sha(fs.readFileSync(PAGE)) === baseSha ? "(未被留脏)" : "**留脏**"}`); console.log(bad === 0 ? "\nMUTATE-SUMMARY: 全绿" : `\nMUTATE-SUMMARY: ${bad} 处不合格`); process.exitCode = bad === 0 ? 0 : 1;