| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- /**
- * 票 67 变异测试:证明 t67 闸门的每条关键断言**抓得住它要防的缺陷**(t59/t60/t63/t64/t68 同一纪律)。
- *
- * 做法:对源文件做**单点**变异注入 → 跑 t67 → 断言**指定断言**转红 → 按字节还原并校验一致。
- * 「随便红一条就算」是不允许的:每条变异都点名它必须打红的那个断言名。
- *
- * 十四条变异对应本票的真实风险:
- * M1 多维分析(唯一真收片区的端点)不再下发 district → A1 红(片区静默失效,用户以为筛过了)
- * M2 当前事件被塞上 district(伪造一次后端不认的筛选) → A4 红(票面「不得伪造」)
- * M3 scope 不进条件键(换片区页码不复位) → B2 红(条文 5/6)
- * M4 watch(scope) 丢掉 eventStore(切片区四块面板不刷新) → C1 红(票 67 缺口 2 复发)
- * M5 历史分析那块不再声明片区缺口 → D-gap:historyAnalysis 红(如实说明缺失)
- * M6 缺口句里塞接口字段名 district → D5 红(票 05 文案纪律)
- * M7 districtApplied 恒 false(管线明明按片区筛了却说没筛)→ E3 红(语义退回半成品)
- * M8 管线说明写回「不作用于管线与构件图层」(旧失实结论) → E5 红(把能做的说成做不到)
- * M9 定时刷新漏传 scope(预警分析悄悄退回全域) → C2 红
- * M10 工单列表的行业缺口说明被摘掉 → D3b 红(同屏两数各说各话)
- * M11 当前预警面板的缺口说明被摘掉 → D-gap:currentEvents 红
- * M12 注册表出口绕过 withFilterCaliber(接缝被旁路) → D3 红(正向普查兜住)
- * M13 前缀映射错位(TC→0,太常的管线被当成城南) → E1/E2 红(推导正确率与筛选结果同时崩)
- * M14 推不出片区的对象被静默保留在具体片区结果里 → E4 红(既不硬塞也不该混进来)
- *
- * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t67-mutations.mjs
- */
- import assert from "node:assert/strict"
- import { closeSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs"
- import { spawnSync } from "node:child_process"
- import { tmpdir } from "node:os"
- import { join } from "node:path"
- import { fileURLToPath } from "node:url"
- const repoRoot = fileURLToPath(new URL("../../../", import.meta.url))
- const GATE = fileURLToPath(new URL("t67-filter-caliber.mjs", import.meta.url))
- const srcPath = (rel) => repoRoot + "src/modules/cockpit/" + rel
- /** 用文件重定向取子进程输出(Windows 沙箱下管道捕获是文档化边界,见 t60/t63 注释)。 */
- const reportDir = mkdtempSync(join(tmpdir(), "t67-mutations-"))
- function runGate() {
- const outPath = join(reportDir, "stdout.txt")
- const errPath = join(reportDir, "stderr.txt")
- const out = openSync(outPath, "w")
- const err = openSync(errPath, "w")
- let status = 0
- try {
- status = spawnSync(process.execPath, [GATE], { stdio: ["ignore", out, err], timeout: 300000 }).status
- } finally {
- closeSync(out)
- closeSync(err)
- }
- return { output: readFileSync(outPath, "utf8") + readFileSync(errPath, "utf8"), status }
- }
- /** 断言名形如 "D-gap:historyAnalysis"(含冒号),所以按「非空白」切,不按冒号切。 */
- function failingNames(output) {
- return [...output.matchAll(/^FAIL (\S+)/gm)].map((match) => match[1])
- }
- let injected = 0
- let proved = 0
- /** 文字替换式变异:from 找不到就报「源未命中」(文件形状变了,变异脚本要跟着更新)。 */
- function mutate(label, file, from, to, expectRed) {
- return mutateWith(label, file, expectRed, (text) => {
- const candidates = [from, from.replace(/\n/g, "\r\n")]
- const needle = candidates.find((candidate) => text.includes(candidate))
- if (!needle) return null
- return text.replace(needle, to)
- })
- }
- /** 通用的单点变异:transform 回 null 表示变异源未命中。 */
- function mutateWith(label, file, expectRed, transform) {
- const path = srcPath(file)
- const original = readFileSync(path, "utf8")
- const mutated = transform(original)
- injected += 1
- if (mutated === null || mutated === original) {
- console.error("FAIL " + label + ": 变异源未命中(文件形状已变,需人工更新变异脚本)")
- return
- }
- writeFileSync(path, mutated, "utf8")
- let caught = false
- let how = ""
- try {
- const result = runGate()
- const red = failingNames(result.output)
- const expected = Array.isArray(expectRed) ? expectRed : [expectRed]
- const hit = red.find((name) => expected.some((want) => name === want || name.startsWith(want)))
- if (hit) {
- caught = true
- how = expected.join(" 或 ") + " 转红(实际命中 " + hit + ")"
- } else if (result.status !== 0 && /does not provide an export|SyntaxError|Cannot find (module|export)/.test(result.output)) {
- caught = true
- how = "闸门整体加载失败(导出缺失/语法错)— 崩溃式抓住"
- } else if (result.status !== 0) {
- how = "退出码 " + result.status + " 但目标断言未红(红项:" + (red.join(", ") || "无") + ")— 判据不明,按未抓住记"
- } else {
- how = "未转红(假绿!闸门仍全绿)"
- }
- console.log((caught ? "PASS " : "FAIL ") + label + " → " + how)
- if (caught) proved += 1
- } finally {
- writeFileSync(path, original, "utf8")
- assert.equal(readFileSync(path, "utf8"), original, label + ": 还原失败")
- }
- }
- const WARNING_RESPONSE = "services/warningResponseApi.js"
- const EVENT_API = "services/eventApi.js"
- const EVENT_STORE = "stores/eventStore.js"
- const SHELL = "CockpitShell.vue"
- const CALIBER = "domain/filterCaliber.js"
- const PANEL_VIEWS = "domain/panelViews.js"
- const PANELS = "domain/warningResponsePanels.js"
- const PIPES = "domain/pipeNetwork.js"
- const EVENT_LIST = "domain/eventList.js"
- // M1 多维分析不再下发 district(事件族的片区接线整条消失,界面却仍写着「当前范围」)
- mutate(
- "M1-analysis-drops-district",
- WARNING_RESPONSE,
- " if (hooks.acceptsDistrict === true) base.district = districtQuery(options.scope)",
- " if (hooks.acceptsDistrict === true) base.district = ''",
- "A1",
- )
- // M2 当前事件被塞上 district(后端实测忽略该参数 —— 传了就是伪造一次筛选,票面明令禁止)
- mutateWith("M2-current-events-fakes-district", EVENT_API, "A4", (text) => {
- const anchor = " industry: industryQuery(options.industry),"
- if (!text.includes(anchor)) return null
- return text.replace(anchor, anchor + "\n district: 0,")
- })
- // M3 scope 不进条件键(换片区页码不回第 1 页,用户停在第 3 页看另一片区的第 3 页)
- mutateWith("M3-scope-not-a-condition-key", EVENT_STORE, "B2", (text) => {
- const matched = /(const CONDITION_KEYS = \[)([^\]]*)(\])/.exec(text)
- if (!matched) return null
- const inner = matched[2]
- .split(",")
- .map((item) => item.trim())
- .filter((item) => item && item !== "'scope'" && item !== '"scope"')
- .join(", ")
- return text.replace(matched[0], matched[1] + inner + matched[3])
- })
- // M4 watch(scope) 丢掉 eventStore(票 67 缺口 2 原样复发:切片区预警四块面板不刷新)
- mutateWith("M4-scope-watch-drops-event-store", SHELL, "C1", (text) => {
- const start = text.indexOf("watch(scope")
- if (start < 0) return null
- const open = text.indexOf("{", start)
- let depth = 0
- let end = -1
- for (let index = open; index < text.length; index += 1) {
- if (text[index] === "{") depth += 1
- else if (text[index] === "}") {
- depth -= 1
- if (depth === 0) { end = index; break }
- }
- }
- if (end < 0) return null
- const block = text.slice(start, end)
- const stripped = block.replace(/^[ \t]*eventStore\.loadPanels\([^)]*\)[ \t]*\r?\n/m, "")
- if (stripped === block) return null
- return text.slice(0, start) + stripped + text.slice(end)
- })
- // M5 历史分析那块不再声明片区缺口(读者只会以为「切了片区、数字没变」是数据问题)
- mutateWith("M5-history-gap-removed", CALIBER, "D-gap:historyAnalysis", (text) => {
- const stripped = text.replace(/^[ \t]*historyAnalysis: .*\r?\n/m, "")
- return stripped === text ? null : stripped
- })
- // M6 缺口句里塞进接口字段名(票 05 纪律:接口词汇不上大屏)
- mutate(
- "M6-gap-leaks-api-field-name",
- CALIBER,
- "const NO_DISTRICT_PARAM = '后端该端点未提供片区入参'",
- "const NO_DISTRICT_PARAM = '后端该端点未提供 district 入参'",
- "D5",
- )
- // M7 districtApplied 恒 false(管线明明已经按编码前缀筛过了,界面却退回「没筛」的说法)
- mutate(
- "M7-district-applied-forced-false",
- PIPES,
- " districtApplied: district !== '',",
- " districtApplied: false,",
- "E3",
- )
- // M8 管线片区说明被写回旧结论「不作用于管线与构件图层」(把已经能做到的又说成做不到)
- mutateWith("M8-pipe-note-back-to-not-applied", CALIBER, "E5", (text) => {
- const start = text.indexOf("export const PIPE_DISTRICT_NOTE =")
- if (start < 0) return null
- // 不写反引号字面量(本脚本自身用模板串拼装):用字符码定位模板串的收尾引号。
- const quote = String.fromCharCode(96)
- const firstQuote = text.indexOf(quote, start)
- const lastQuote = firstQuote < 0 ? -1 : text.indexOf(quote, firstQuote + 1)
- if (lastQuote < 0) return null
- const lineEnd = text.indexOf("\n", lastQuote)
- if (lineEnd < 0) return null
- const replacement = "export const PIPE_DISTRICT_NOTE = '「当前范围」不作用于管线与构件图层:几何数据里没有片区字段'"
- return text.slice(0, start) + replacement + text.slice(lineEnd)
- })
- // M9 定时刷新漏传 scope(预警分析悄悄退回全域,而筛选条仍显示某个片区)
- mutate(
- "M9-refresh-drops-scope",
- SHELL,
- " await eventStore.loadPanels({ mode: eventStore.mode, industry: industry.value, scope: scope.value })",
- " await eventStore.loadPanels({ mode: eventStore.mode, industry: industry.value })",
- "C2",
- )
- // M10 工单列表的行业缺口说明被摘掉(同屏上方分布随行业变、下方列表不变且不说原因)
- mutate(
- "M10-work-order-industry-gap-removed",
- PANELS,
- " const industryNote = industryGap('workOrderList')",
- " const industryNote = ''",
- "D3b",
- )
- // M11 当前预警面板的缺口说明被摘掉(字段没了=屏上那句也没了)
- mutateWith("M11-current-events-gap-removed", EVENT_LIST, "D-gap:currentEvents", (text) => {
- const stripped = text.replace(/^[ \t]*gap: districtGap\('currentEvents'\),\r?\n/m, "")
- return stripped === text ? null : stripped
- })
- // M12 注册表出口绕过 withFilterCaliber(接缝被旁路:面板自己没写缺口,缺口就整体消失)
- mutate(
- "M12-registry-bypasses-filter-caliber",
- PANEL_VIEWS,
- " return withFilterCaliber(build(data, section, extra), panelId)",
- " return build(data, section, extra)",
- "D3",
- )
- // M13 前缀映射错位(TC→城南 0:太常的 4116 条管线会被当成城南,推导与筛选同时崩)
- mutate(
- "M13-prefix-map-misaligned",
- PIPES,
- " TC: DISTRICT_CODES.taichang,",
- " TC: DISTRICT_CODES.south,",
- ["E1", "E2"],
- )
- // M14 推不出片区的对象被静默保留在具体片区结果里(既不硬塞进某片区,也不该混进来)
- mutate(
- "M14-unresolved-kept-under-concrete-district",
- PIPES,
- " return row.district === district",
- " return true",
- "E4",
- )
- rmSync(reportDir, { recursive: true, force: true })
- console.log("")
- console.log("T67 变异:" + proved + "/" + injected + " 条被闸门抓住。")
- if (proved !== injected) {
- console.error("有变异未被抓住(闸门有假绿风险)。")
- process.exit(1)
- }
- console.log("T67 变异全部打红并按字节还原。")
|