fe-task3-mutate.mjs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. /**
  2. * Task 3 新逻辑的**两向变异**驱动器。
  3. *
  4. * 形状抄自同目录 `fe-update-mutate.mjs`(那里有「先看 git status 干净」那段注意事项)。
  5. *
  6. * 为什么需要它:`fe-update-harness.mjs` 绿 90 条**本身不证明**那些断言有牙 ——
  7. * 一条恒真的断言与一条真钉子在这份读数上长得一模一样。这里按两向走:
  8. * · 该红的必须红(三个真变异各打一处本轮新写的判定)
  9. * · **无害改动必须实测仍绿**(换两行没有依赖关系的声明顺序)—— 否则红可能只是
  10. * 「台子对任何改动都敏感」,而不是「这条断言钉住了这件事」
  11. *
  12. * ★ 每一轮都:改前存字节 → 断言替换**确实生效**(命中次数)→ 跑台子 → **按字节还原**
  13. * → 复核 sha256 回到改前。还原动作本身也要被验证(见 memory「变异验证要两向」第三种)。
  14. *
  15. * 用法:node fe-task3-mutate.mjs
  16. * 退出码:只有「还原后 sha 不符」或「无害变异竟然红了」才非 0。
  17. */
  18. import fs from "node:fs";
  19. import crypto from "node:crypto";
  20. import { spawnSync } from "node:child_process";
  21. import { fileURLToPath } from "node:url";
  22. const HERE = fileURLToPath(new URL(".", import.meta.url));
  23. const HARNESS = HERE + "fe-update-harness.mjs";
  24. const PAGE = "D:/project2/payment-platform/frontend/src/views/module_payment/collection/onboarding/index.vue";
  25. const sha = (buf) => crypto.createHash("sha256").update(buf).digest("hex");
  26. const MUTATIONS = [
  27. {
  28. id: "M-APPROVED-ALSO-SUBMIT",
  29. why: "把「可以收款了」那一档也挂上提交审核入口",
  30. expectRed: ["A7", "A8"],
  31. find: ` case "APPROVED":\n return [{ ...edit, primary: true }];`,
  32. repl: ` case "APPROVED":\n return [{ ...edit, primary: true }, submit];`,
  33. },
  34. {
  35. id: "M-409-CODE-CHANGED",
  36. why: "把 409 那个码值换成表外的值 ⇒ 409 那条分支永不进入",
  37. // ★ 起初这里写的是 ["S2","S3","S4"],实测 **S3 没红** ⇒ 追下去发现 S3 是条恒真的断言
  38. // (理由写在台子里 S3 被删处)。这条 `expectRed` 按**实测**改,不是按我当初的预期改。
  39. expectRed: ["S2", "S4"],
  40. find: `if (e?.data?.code === 409) {`,
  41. repl: `if (e?.data?.code === 999999) {`,
  42. },
  43. {
  44. id: "M-LIST-ONE-LAYER",
  45. why: "列表只解一层 `.data`(把 payload 抬高一层的读法)",
  46. expectRed: ["L1", "L2"],
  47. // ★ 锚点必须**唯一**:`res.data.data?.list` 这个串在同一文件的 javadoc 里也出现过一次
  48. // ⇒ 裸用它会让替换打到注释上(驱动器的命中次数护栏第一次就是这么把它拦下来的)。
  49. find: `list.value = res.data.data?.list || [];`,
  50. repl: `list.value = res.data?.list || [];`,
  51. },
  52. {
  53. id: "H-REORDER-DECLS",
  54. why: "无害:把两个没有依赖关系的声明换个顺序(行数不变)",
  55. expectRed: [],
  56. find: ` const submit: RowAction = { text: "提交审核", primary: false, run: openCommit };\n const edit: RowAction = { text: "改资料", primary: false, run: (r) => openUpdate(r.mch_no) };`,
  57. repl: ` const edit: RowAction = { text: "改资料", primary: false, run: (r) => openUpdate(r.mch_no) };\n const submit: RowAction = { text: "提交审核", primary: false, run: openCommit };`,
  58. },
  59. ];
  60. const original = fs.readFileSync(PAGE);
  61. const baseSha = sha(original);
  62. console.log(`基线 sha256 = ${baseSha}`);
  63. console.log(`基线字节 = ${original.length}`);
  64. function runHarness() {
  65. // ★ 用 spawnSync 而不是 execFileSync:变异体**有可能把台子打崩**,而崩掉的读数依然有用
  66. // (「预期该红的」可以从崩溃前的 FAIL 行里认出来)。execFileSync 遇到非 0 退出码会**抛异常**
  67. // ⇒ 驱动器自己死掉,后面几个变异一个都不跑。实测踩过:M-LIST-ONE-LAYER 让 L2 抛了
  68. // `TypeError: Cannot read properties of undefined`,驱动器当场终止。
  69. const r = spawnSync(process.execPath, [HARNESS, PAGE], {
  70. encoding: "utf8",
  71. stdio: ["ignore", "pipe", "ignore"], // stderr 丢掉(桩的告警都在那边)
  72. });
  73. const out = r.stdout || "";
  74. const m = out.match(/^SUMMARY .*$/m);
  75. const fail = (out.match(/^ FAIL {2}(.+?) {2}\(actual=/gm) || []).map((s) =>
  76. s.replace(/^ FAIL {2}/, "").replace(/ {2}\(actual=$/, "")
  77. );
  78. return {
  79. summary: m ? m[0] : `**台子没跑完(status=${r.status})⇒ 没打 SUMMARY**`,
  80. fail,
  81. complete: !!m,
  82. };
  83. }
  84. let bad = 0;
  85. for (const mut of MUTATIONS) {
  86. const src = original.toString("utf8");
  87. const hits = src.split(mut.find).length - 1;
  88. if (hits !== 1) {
  89. console.log(`\n[${mut.id}] **变异体没生效**(锚点命中 ${hits} 次,应为 1)—— 读数作废`);
  90. bad++;
  91. continue;
  92. }
  93. fs.writeFileSync(PAGE, src.replace(mut.find, mut.repl), "utf8");
  94. const mutatedSha = sha(fs.readFileSync(PAGE));
  95. let res;
  96. try {
  97. res = runHarness();
  98. } finally {
  99. fs.writeFileSync(PAGE, original); // 按字节还原
  100. }
  101. const backSha = sha(fs.readFileSync(PAGE));
  102. const restored = backSha === baseSha;
  103. const missing = mut.expectRed.filter((tag) => !res.fail.some((f) => f.startsWith(tag)));
  104. // ★ 「没跑完」必须自成一档:那时 `fail` 是从崩溃前的残缺 stdout 里抠出来的,
  105. // 拿它去判「无害改动仍绿」会把「台子崩了」读成「没红 ⇒ 通过」。
  106. let verdict;
  107. if (!restored) verdict = "**还原失败**";
  108. else if (!res.complete) verdict = "**台子没跑完 ⇒ 读数不完整(先看它崩在哪)**";
  109. else if (mut.expectRed.length === 0)
  110. verdict = res.fail.length === 0 ? "OK(无害改动仍绿)" : "**无害改动竟红了 ⇒ 台子对无关改动也敏感**";
  111. else verdict = missing.length === 0 ? "OK(该红的都红了)" : `**预期该红却没红:${missing.join(", ")}**`;
  112. if (verdict.includes("**")) bad++;
  113. console.log(`\n[${mut.id}] ${mut.why}`);
  114. console.log(` 变异后 sha = ${mutatedSha}`);
  115. console.log(` ${res.summary}`);
  116. console.log(` 红的用例 = ${res.fail.length ? res.fail.join(" | ") : "(无)"}`);
  117. console.log(` 还原后 sha = ${backSha} ${restored ? "== 基线 OK" : "**!= 基线**"}`);
  118. console.log(` ⇒ ${verdict}`);
  119. }
  120. console.log(`\n基线复核 sha = ${sha(fs.readFileSync(PAGE))} ${sha(fs.readFileSync(PAGE)) === baseSha ? "(未被留脏)" : "**留脏**"}`);
  121. console.log(bad === 0 ? "\nMUTATE-SUMMARY: 全绿" : `\nMUTATE-SUMMARY: ${bad} 处不合格`);
  122. process.exitCode = bad === 0 ? 0 : 1;