| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- /**
- * 票 38 变异闸:把本票要防的错法逐一注入,确认 t38 真的转红;每次变异后按字节还原并校验哈希。
- *
- * 判据只看退出码(不用管道捕获子进程输出——那是本环境的已知边界,票 37 记过)。
- * 用法:先单跑 t38 看绿,再跑本脚本看每条变异都被抓住。
- * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t38-mutations.mjs
- */
- import { spawnSync } from 'node:child_process'
- import { readFileSync, writeFileSync } from 'node:fs'
- import { createHash } from 'node:crypto'
- import { fileURLToPath } from 'node:url'
- const ROOT = fileURLToPath(new URL('../../../', import.meta.url))
- const GATE = '.scratch/cockpit-real-data-wayfinding/verify/t38-infrastructure-panel-adjustments.mjs'
- const MUTATIONS = [
- {
- id: 'M1',
- why: '规模 KPI 回流到基础档案(第 2 条被撤销)→ A2/A6/D1 必须红',
- file: 'src/modules/cockpit/domain/infrastructurePanels.js',
- from: " { key: 'lengthKm', label: '管线里程', value: valueText(current.lengthKm), unit: 'km' },\n ]),",
- to: " { key: 'lengthKm', label: '管线里程', value: valueText(current.lengthKm), unit: 'km' },\n { key: 'scale', label: '规模', value: current.scale ? codeText(current.scale) : DETAIL_PLACEHOLDER },\n ]),",
- },
- {
- id: 'M2',
- why: '规模 KPI 回流到总体概览基础设施面板 → A5 必须红',
- file: 'src/modules/cockpit/domain/overviewPanels.js',
- from: " { key: 'lengthKm', label: '管线里程', value: valueText(regionData && regionData.lengthKm), unit: 'km', wide: true },\n ]),",
- to: " { key: 'lengthKm', label: '管线里程', value: valueText(regionData && regionData.lengthKm), unit: 'km', wide: true },\n { key: 'scale', label: '规模', value: '中型' },\n ]),",
- },
- {
- id: 'M3',
- why: 'lengthKm 也被顺手删掉(票面明说保留)→ A1b/A2b/A5/D3b 必须红',
- file: 'src/modules/cockpit/domain/infrastructurePanels.js',
- from: " { key: 'facilityCount', label: '设施总数', value: valueText(current.facilityCount), unit: '处' },\n { key: 'lengthKm', label: '管线里程', value: valueText(current.lengthKm), unit: 'km' },\n ]),",
- to: " { key: 'facilityCount', label: '设施总数', value: valueText(current.facilityCount), unit: '处' },\n ]),",
- },
- {
- id: 'M4',
- why: '监测点定位退回只换中心的 setCameraFocus(「没反应」的相机根因复发)→ E9/F1 必须红',
- file: 'src/modules/cockpit/stores/infrastructureStore.js',
- from: ' const applied = plan.object.kind === MAP_OBJECT_KIND.MONITORING_POINT\n ? await flyPoint(plan.object)\n : focus(target)',
- to: ' const applied = focus(target)',
- },
- {
- id: 'M5',
- why: '相机下发结果不核对(把没飞说成飞了)→ F4/F5 必须红',
- file: 'src/modules/cockpit/stores/infrastructureStore.js',
- from: ' if (applied !== true) {\n locate.value = { result: LOCATE_RESULT.UNAVAILABLE, pointCode: plan.pointCode, reasonText: LOCATE_TEXT.UNAVAILABLE }\n return locate.value\n }',
- to: ' if (false) {\n locate.value = { result: LOCATE_RESULT.UNAVAILABLE, pointCode: plan.pointCode, reasonText: LOCATE_TEXT.UNAVAILABLE }\n return locate.value\n }',
- },
- {
- id: 'M6',
- why: '管理信息主图退回普通 bars(可切换维度的图表块被撤,第 6 条不成立)→ C1/C2 必须红',
- file: 'src/modules/cockpit/domain/infrastructurePanels.js',
- from: " dimensionBarsBlock('片区 × 行业', rows, {",
- to: " barsBlock('片区 × 行业', rows, {",
- },
- {
- id: 'M7',
- why: '维度聚合退回逐行直画(城南两行不合并)→ C3/C4 必须红',
- file: 'src/modules/cockpit/domain/panelBlocks.js',
- from: ' entry.count += Number(countOf(row)) || 0\n for (const [name, source] of Object.entries(extras)) entry[name] += Number(row[source]) || 0\n }\n const sorted = sortByCountDesc([...byValue.values()], (row) => row.count)',
- to: ' entry.count = Number(countOf(row)) || 0\n for (const [name, source] of Object.entries(extras)) entry[name] = Number(row[source]) || 0\n byValue.set(key, { ...entry })\n }\n const sorted = [...byValue.values()]',
- },
- {
- id: 'M8',
- why: 'lengthKm 整行独占**退回按 key 特判**(决定权应从 domain 的 wide 滑回渲染层)→ A3b 必须红',
- file: 'src/modules/cockpit/components/PanelBlocks.vue',
- from: " :class=\"{ 'panel-kpi--wide': item.wide === true }\"",
- to: " :class=\"{ 'panel-kpi--wide': item.key === 'lengthKm' }\"",
- },
- {
- id: 'M9',
- why: '浮层被重新门控在折叠状态上(「点了没反应」换写法复发)→ E6c 必须红',
- file: 'src/modules/cockpit/CockpitShell.vue',
- from: '<div v-if="locateFeedback" class="cockpit-shell__notice" data-locate-notice>',
- to: '<div v-if="locateFeedback" v-show="statusPanelExpanded" class="cockpit-shell__notice" data-locate-notice>',
- },
- {
- id: 'M10',
- why: '浮层不自动收起(常驻噪音 + 定时器纪律丢失)→ E8 必须红',
- // 锚点含驻留毫秒数(票 38 走查定为 8s):以后若调时长,这里要同步改。
- file: 'src/modules/cockpit/CockpitShell.vue',
- from: ' locateDismissTimer = window.setTimeout(() => {\n locateDismissTimer = null\n infrastructureStore.clearLocate()\n }, 8000)',
- to: ' locateDismissTimer = null',
- },
- {
- id: 'M11',
- why: '第二轮:关联设备回联被撤(`21119740` 又报「不在已加载范围」)→ B5/B5d 必须红',
- file: 'src/modules/cockpit/domain/mapLocate.js',
- from: ' found = findDeviceOwner(objects, code)',
- to: ' found = null',
- },
- {
- id: 'M12',
- why: '第二轮:概览区域分布又被硬写 null(管线里程再次对不上)→ E13 必须红',
- file: 'src/modules/cockpit/stores/overviewStore.js',
- from: ' regions: regionsLoaded ? regions.data : null,',
- to: ' regions: null,',
- },
- {
- id: 'M13',
- why: '第二轮:wide 标志从 domain 撤掉(独占整行的呈现意图丢失)→ A3c 必须红',
- file: 'src/modules/cockpit/domain/overviewPanels.js',
- from: "unit: 'km', wide: true },",
- to: "unit: 'km' },",
- },
- ]
- const hash = (path) => createHash('sha256').update(readFileSync(path)).digest('hex')
- /** 变异源文本按 CRLF 归一后再注入(票 37/43 的教训:PowerShell 写盘是 CRLF,锚点含 \\n 会假未命中)。 */
- const normalizeFor = (text, path) => (readFileSync(path, 'utf8').includes('\r\n') ? text.replace(/\n/g, '\r\n') : text)
- let failures = 0
- for (const m of MUTATIONS) {
- const path = ROOT + m.file.replaceAll('/', process.platform === 'win32' ? '\\' : '/')
- const before = hash(path)
- const source = readFileSync(path, 'utf8')
- const needle = normalizeFor(m.from, path)
- if (!source.includes(needle)) {
- console.log(`BAD ${m.id}: 变异锚点未命中(源码已变,需更新脚本)`)
- failures += 1
- continue
- }
- writeFileSync(path, source.replace(needle, normalizeFor(m.to, path)))
- const status = spawnSync(process.execPath, [GATE], { cwd: ROOT, stdio: 'ignore' }).status
- writeFileSync(path, source)
- const restored = hash(path) === before
- const caught = status !== 0
- console.log(`${caught && restored ? 'OK ' : 'BAD '} ${m.id} — 闸门${caught ? '转红' : '**没抓住**'};还原${restored ? '按字节一致' : '**失败(哈希不符)**'}。${m.why}`)
- if (!caught || !restored) failures += 1
- }
- console.log('')
- console.log(`T38 mutations: ${MUTATIONS.length - failures}/${MUTATIONS.length} caught`)
- if (failures) process.exit(1)
|