| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- /**
- * 票 65 补口变异:证明工单列表翻页闸门抓得住这次真实缺陷形态。
- *
- * M27 面板退回「只认 historyAnalysis」 → S1 必须红(**本次真机缺陷原形**)
- * M28 store 不再导出 setWorkOrderPage → S2 必须红(组件调不到)
- * M29 翻页共用全局面板令牌 → S3 必须红(会作废四块面板在途响应)
- * M30 条件变化不重置工单页码 → P6 必须红(切范围还停在第 2 页)
- * M31 翻页不落分区(写了不发) → P2 必须红(点了没反应)
- * M32 令牌检查去掉(迟到响应照落) → P7 必须红(连点落到旧页)
- *
- * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t65b-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('t65b-work-order-paging.mjs', import.meta.url))
- const srcPath = (rel) => `${repoRoot}src/modules/cockpit/${rel}`
- const reportDir = mkdtempSync(join(tmpdir(), 't65b-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] }).status
- } finally {
- closeSync(out)
- closeSync(err)
- }
- return { output: `${readFileSync(outPath, 'utf8')}${readFileSync(errPath, 'utf8')}`, status }
- }
- const failingNames = (output) => [...output.matchAll(/^FAIL ([^:]+):/gm)].map((match) => match[1])
- let injected = 0
- let proved = 0
- function mutate(label, file, from, to, expectRed) {
- const path = srcPath(file)
- const original = readFileSync(path, 'utf8')
- const candidates = [from, from.replace(/\n/g, '\r\n')]
- const needle = candidates.find((candidate) => original.includes(candidate))
- if (!needle) {
- console.error(`FAIL ${label}: 变异源未命中(文件形状已变,需人工更新变异脚本)`)
- injected += 1
- return
- }
- injected += 1
- writeFileSync(path, original.replace(needle, to), 'utf8')
- let caught = false
- let how = ''
- try {
- const { output, status } = runGate()
- const red = failingNames(output)
- if (red.includes(expectRed)) {
- caught = true
- how = `${expectRed} 转红`
- } else if (status !== 0 && /TypeError|is not a function|does not provide an export|SyntaxError/.test(output)) {
- // 删掉导出/方法这类变异会让闸门在「调到不存在的方法」处崩掉 —— 也算抓住(崩溃式)。
- caught = true
- how = '闸门在缺导出/缺方法处崩溃 — 崩溃式抓住'
- } else if (status !== 0) {
- how = `退出码 ${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}: 还原失败`)
- }
- }
- // ---- 本次缺陷原形:面板只认 historyAnalysis ----
- mutate(
- 'M27-pane-only-routes-history-analysis',
- 'components/WarningResponsePane.vue',
- ` const target = payload && payload.page
- if (props.panelId === 'historyAnalysis') {
- eventStore.setHistoryPage(target)
- return
- }
- if (props.panelId === 'responseEfficiency') {
- eventStore.setWorkOrderPage(target)
- }`,
- ` if (props.panelId !== 'historyAnalysis') return
- eventStore.setHistoryPage(payload && payload.page)`,
- 'S1 面板把报警效率的翻页事件交给 store(不再只认 historyAnalysis)',
- )
- mutate(
- 'M28-store-does-not-export-action',
- 'stores/eventStore.js',
- ` setWorkOrderPage,`,
- ``,
- 'S2 store 暴露 setWorkOrderPage(组件才调得到)',
- )
- mutate(
- 'M29-paging-shares-global-token',
- 'stores/eventStore.js',
- ` workOrderToken += 1
- const mine = workOrderToken`,
- ` token += 1
- const mine = token`,
- 'S3 工单页码用独立令牌(翻工单页不作废四块面板的在途响应)',
- )
- mutate(
- 'M30-condition-change-keeps-work-order-page',
- 'stores/eventStore.js',
- ` // 票 65:条件变了,工单列表也回第 1 页(条文 5;定时刷新不带条件 ⇒ 页码保持)。
- workOrderPage.value = 1`,
- ``,
- 'P6 条件变化(切范围)后工单页码回第 1 页',
- )
- mutate(
- 'M31-paging-does-not-commit-section',
- 'stores/eventStore.js',
- ` sections.value = { ...sections.value, workOrderList: { ...result, loaded: true } }`,
- ``,
- 'P2 翻到第 2 页:请求带 page=2、分区换成 6–10 号工单',
- )
- mutate(
- 'M32-stale-response-committed',
- 'stores/eventStore.js',
- ` // 迟到响应丢弃:期间又翻过页/切过条件,这一页不落分区。
- if (mine !== workOrderToken) return sections.value.workOrderList`,
- ``,
- 'P7 快速连点只认最后一页(迟到响应丢弃,独立令牌)',
- )
- // ---- 时间窗(2026-10-22 维护者反馈「列表没按时间筛」)----
- mutate(
- 'M33-request-drops-time-window',
- 'stores/eventStore.js',
- ` fetchWorkOrderList(
- { page: workOrderPage.value, pageSize: WORK_ORDER_LIST_PAGE_SIZE, startTime: workOrderWindow.startTime, endTime: workOrderWindow.endTime },
- requestOptions,
- ),`,
- ` fetchWorkOrderList(
- { page: workOrderPage.value, pageSize: WORK_ORDER_LIST_PAGE_SIZE },
- requestOptions,
- ),`,
- 'P9 首屏工单列表请求带当前窗口(7 天的 startTime/endTime 都非空)',
- )
- mutate(
- 'M34-paging-recomputes-window',
- 'stores/eventStore.js',
- ` const window = workOrderWindow || activeConditions()`,
- ` const window = activeConditions()`,
- 'P10 翻页沿用同一时间窗(第 2 页的 startTime/endTime 与首屏完全一致)',
- )
- mutate(
- 'M35-range-change-keeps-old-window',
- 'stores/eventStore.js',
- ` workOrderWindow = { startTime: active.startTime, endTime: active.endTime }`,
- ``,
- 'P11 切到 30 天后请求换成 30 天窗口(且回第 1 页)',
- )
- console.log('')
- console.log(`T65B 变异测试:${proved}/${injected} 处变异被对应断言抓住。`)
- rmSync(reportDir, { recursive: true, force: true })
- if (proved < injected) process.exit(1)
|