| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143 |
- /**
- * 票 54 变异闸:把本票要防的错法逐一注入,确认 t54 真的一致红;
- * 每条变异后按字节还原并校验哈希。判据只看退出码(不管道捕获子进程输出,票 37 的沙箱教训)。
- *
- * node .scratch/cockpit-real-data-wayfinding/verify/t54-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 T54 = '.scratch/cockpit-real-data-wayfinding/verify/t54-monitoring-overview-grid.mjs'
- const PANELS = 'src/modules/cockpit/domain/monitoringPanels.js'
- const RENDERER = 'src/modules/cockpit/components/PanelBlocks.vue'
- const MUTATIONS = [
- {
- id: 'M1',
- file: PANELS,
- from: ' return { key: \'createdAt\', label: \'数据时间\', value: shortEventTime(value) || full, full }',
- to: ' return { key: \'createdAt\', label: \'数据时间\', value: full, full }',
- why: '屏上退回 ISO 全串(2×2 半格里必然省略成半截读数,验收第 1 条的原始诉求落空)→ G1a 必须红',
- },
- {
- id: 'M2',
- file: PANELS,
- from: ' return { key: \'createdAt\', label: \'数据时间\', value: shortEventTime(value) || full, full }',
- to: ' return { key: \'createdAt\', label: \'数据时间\', value: shortEventTime(value) || full }',
- why: '完整原文字段丢失(title 只剩短读法,「语义全部保持」被破坏)→ G1b/G1c 必须红',
- },
- {
- id: 'M3',
- file: PANELS,
- from: " if (full === DETAIL_PLACEHOLDER) return { key: 'createdAt', label: '数据时间', value: full, full }",
- to: " if (full === DETAIL_PLACEHOLDER) return { key: 'createdAt', label: '数据时间', value: full, full: '' }",
- why: '缺值分支的完整原文置空(占位符的 title 契约破坏)→ G1d 必须红',
- },
- {
- id: 'M4',
- file: PANELS,
- from: ' ], { layout: \'two-by-two\' }),',
- to: ' ]),',
- why: 'domain 不再声明 2×2(退回 auto-fit,宽面板又展开成一行)→ G2a 必须红',
- },
- {
- id: 'M5',
- file: PANELS,
- from: " createdAtKpi(current.createdAt),\n { key: 'offlineCount'",
- to: " { key: 'createdAt', label: '数据时间', value: valueText(current.createdAt) },\n { key: 'offlineCount'",
- why: '运行概况另起一套写法(两处数据时间早晚漂移)→ G1f 必须红',
- },
- {
- id: 'M6',
- file: RENDERER,
- from: ' return `${item.full || item.value}${unit}`',
- to: ' return `${item.value}${unit}`',
- why: 'title 退回只显示值(domain 的 full 没人消费,hover 读不到完整 ISO)→ G4c 必须红',
- },
- {
- id: 'M7',
- file: RENDERER,
- from: ":class=\"{ 'is-muted': isPlaceholder(item.value) }\"",
- to: ":class=\"{ 'is-muted': isPlaceholder(item.value), 'panel-kpi__value--time': item.key === 'createdAt' }\"",
- why: '渲染层重新按 item.key 特判(票 38 明令反对的第二份布局语义)→ G2d/G3c 必须红',
- },
- {
- id: 'M8',
- file: RENDERER,
- from: '.panel-kpi__value {\n min-width: 0;',
- to: '.panel-kpi__value--time {\n font-size: var(--fs-caption, 10.5px);\n}\n\n.panel-kpi__value {\n min-width: 0;',
- why: '时间降档规则被复活(数据时间又与四卡不同档)→ G3a/G3c 必须红',
- },
- {
- id: 'M9',
- file: RENDERER,
- from: ' grid-template-columns: repeat(2, minmax(0, 1fr));\n container-type: inline-size;',
- to: ' grid-template-columns: repeat(2, 1fr);\n container-type: inline-size;',
- why: '列宽去掉 minmax(0,·)(auto 最小尺寸把长读数顶出横向溢出,验收第 2 条)→ G2e 必须红',
- },
- {
- id: 'M10',
- file: RENDERER,
- from: ' grid-template-columns: repeat(2, minmax(0, 1fr));\n container-type: inline-size;',
- to: ' grid-template-columns: repeat(auto-fit, minmax(calc(78 * var(--u, 1px)), 1fr));\n container-type: inline-size;',
- why: '两列改回 auto-fit(宽面板不固定 2×2)→ G2e 必须红',
- },
- {
- id: 'M11',
- file: RENDERER,
- from: '@container panel-kpi-grid (max-width: 260px) {',
- to: '@media (max-width: 260px) {',
- why: '降级退回视口 media query(面板栏宽固定 280–460px,这条规则在大屏永不生效=假完成)→ G2f/G2g 必须红',
- },
- {
- id: 'M12',
- file: RENDERER,
- from: ' .panel-kpis--two-by-two .panel-kpi {\n grid-column: 1 / -1;\n }',
- to: ' .panel-kpis--two-by-two {\n grid-template-columns: minmax(0, 1fr);\n }',
- why: '把降级写在容器自身上(CSS containment 规定容器查不了自己的尺寸,规则永不命中)→ G2h 必须红',
- },
- {
- id: 'M13',
- file: RENDERER,
- from: '.panel-blocks {\n /* 图表 hover 详情浮层',
- to: '.panel-blocks {\n container-type: inline-size;\n /* 图表 hover 详情浮层',
- why: '给 .panel-blocks 根加 containment(contain:layout 让它成为绝对定位后代的包含块,.chart-tip 基准被换掉)→ G4f 必须红',
- },
- ]
- const hash = (path) => createHash('sha256').update(readFileSync(path)).digest('hex')
- /** 变异源文本按 CRLF 归一后再注入(票 37 的教训:锚点含 \n 而源文件是 CRLF 会假未命中)。 */
- 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 caught = spawnSync(process.execPath, [T54], { cwd: ROOT, stdio: 'ignore' }).status !== 0
- writeFileSync(path, source)
- const restored = hash(path) === before
- if (!caught) {
- console.log(`MISS ${m.id}: 注入后闸门仍全绿 → 守卫没有牙(${m.why})`)
- failures += 1
- } else if (!restored) {
- console.log(`BAD ${m.id}: 还原后哈希不一致,文件状态可疑,立刻人工核对`)
- failures += 1
- } else {
- console.log(`OK ${m.id} ${m.why} → 闸门转红;还原按字节一致`)
- }
- }
- console.log(`\nT54 MUTATIONS: ${MUTATIONS.length - failures}/${MUTATIONS.length} 全部被抓住`)
- process.exitCode = failures ? 1 : 0
|