t65b-mutations.mjs 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. /**
  2. * 票 65 补口变异:证明工单列表翻页闸门抓得住这次真实缺陷形态。
  3. *
  4. * M27 面板退回「只认 historyAnalysis」 → S1 必须红(**本次真机缺陷原形**)
  5. * M28 store 不再导出 setWorkOrderPage → S2 必须红(组件调不到)
  6. * M29 翻页共用全局面板令牌 → S3 必须红(会作废四块面板在途响应)
  7. * M30 条件变化不重置工单页码 → P6 必须红(切范围还停在第 2 页)
  8. * M31 翻页不落分区(写了不发) → P2 必须红(点了没反应)
  9. * M32 令牌检查去掉(迟到响应照落) → P7 必须红(连点落到旧页)
  10. *
  11. * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t65b-mutations.mjs
  12. */
  13. import assert from 'node:assert/strict'
  14. import { closeSync, mkdtempSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  15. import { spawnSync } from 'node:child_process'
  16. import { tmpdir } from 'node:os'
  17. import { join } from 'node:path'
  18. import { fileURLToPath } from 'node:url'
  19. const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
  20. const GATE = fileURLToPath(new URL('t65b-work-order-paging.mjs', import.meta.url))
  21. const srcPath = (rel) => `${repoRoot}src/modules/cockpit/${rel}`
  22. const reportDir = mkdtempSync(join(tmpdir(), 't65b-mutations-'))
  23. function runGate() {
  24. const outPath = join(reportDir, 'stdout.txt')
  25. const errPath = join(reportDir, 'stderr.txt')
  26. const out = openSync(outPath, 'w')
  27. const err = openSync(errPath, 'w')
  28. let status = 0
  29. try {
  30. status = spawnSync(process.execPath, [GATE], { stdio: ['ignore', out, err] }).status
  31. } finally {
  32. closeSync(out)
  33. closeSync(err)
  34. }
  35. return { output: `${readFileSync(outPath, 'utf8')}${readFileSync(errPath, 'utf8')}`, status }
  36. }
  37. const failingNames = (output) => [...output.matchAll(/^FAIL ([^:]+):/gm)].map((match) => match[1])
  38. let injected = 0
  39. let proved = 0
  40. function mutate(label, file, from, to, expectRed) {
  41. const path = srcPath(file)
  42. const original = readFileSync(path, 'utf8')
  43. const candidates = [from, from.replace(/\n/g, '\r\n')]
  44. const needle = candidates.find((candidate) => original.includes(candidate))
  45. if (!needle) {
  46. console.error(`FAIL ${label}: 变异源未命中(文件形状已变,需人工更新变异脚本)`)
  47. injected += 1
  48. return
  49. }
  50. injected += 1
  51. writeFileSync(path, original.replace(needle, to), 'utf8')
  52. let caught = false
  53. let how = ''
  54. try {
  55. const { output, status } = runGate()
  56. const red = failingNames(output)
  57. if (red.includes(expectRed)) {
  58. caught = true
  59. how = `${expectRed} 转红`
  60. } else if (status !== 0 && /TypeError|is not a function|does not provide an export|SyntaxError/.test(output)) {
  61. // 删掉导出/方法这类变异会让闸门在「调到不存在的方法」处崩掉 —— 也算抓住(崩溃式)。
  62. caught = true
  63. how = '闸门在缺导出/缺方法处崩溃 — 崩溃式抓住'
  64. } else if (status !== 0) {
  65. how = `退出码 ${status} 但目标断言未红(红项:${red.join(', ') || '无'})`
  66. } else {
  67. how = '未转红(假绿!)'
  68. }
  69. console.log(`${caught ? 'PASS' : 'FAIL'} ${label} → ${how}`)
  70. if (caught) proved += 1
  71. } finally {
  72. writeFileSync(path, original, 'utf8')
  73. assert.equal(readFileSync(path, 'utf8'), original, `${label}: 还原失败`)
  74. }
  75. }
  76. // ---- 本次缺陷原形:面板只认 historyAnalysis ----
  77. mutate(
  78. 'M27-pane-only-routes-history-analysis',
  79. 'components/WarningResponsePane.vue',
  80. ` const target = payload && payload.page
  81. if (props.panelId === 'historyAnalysis') {
  82. eventStore.setHistoryPage(target)
  83. return
  84. }
  85. if (props.panelId === 'responseEfficiency') {
  86. eventStore.setWorkOrderPage(target)
  87. }`,
  88. ` if (props.panelId !== 'historyAnalysis') return
  89. eventStore.setHistoryPage(payload && payload.page)`,
  90. 'S1 面板把报警效率的翻页事件交给 store(不再只认 historyAnalysis)',
  91. )
  92. mutate(
  93. 'M28-store-does-not-export-action',
  94. 'stores/eventStore.js',
  95. ` setWorkOrderPage,`,
  96. ``,
  97. 'S2 store 暴露 setWorkOrderPage(组件才调得到)',
  98. )
  99. mutate(
  100. 'M29-paging-shares-global-token',
  101. 'stores/eventStore.js',
  102. ` workOrderToken += 1
  103. const mine = workOrderToken`,
  104. ` token += 1
  105. const mine = token`,
  106. 'S3 工单页码用独立令牌(翻工单页不作废四块面板的在途响应)',
  107. )
  108. mutate(
  109. 'M30-condition-change-keeps-work-order-page',
  110. 'stores/eventStore.js',
  111. ` // 票 65:条件变了,工单列表也回第 1 页(条文 5;定时刷新不带条件 ⇒ 页码保持)。
  112. workOrderPage.value = 1`,
  113. ``,
  114. 'P6 条件变化(切范围)后工单页码回第 1 页',
  115. )
  116. mutate(
  117. 'M31-paging-does-not-commit-section',
  118. 'stores/eventStore.js',
  119. ` sections.value = { ...sections.value, workOrderList: { ...result, loaded: true } }`,
  120. ``,
  121. 'P2 翻到第 2 页:请求带 page=2、分区换成 6–10 号工单',
  122. )
  123. mutate(
  124. 'M32-stale-response-committed',
  125. 'stores/eventStore.js',
  126. ` // 迟到响应丢弃:期间又翻过页/切过条件,这一页不落分区。
  127. if (mine !== workOrderToken) return sections.value.workOrderList`,
  128. ``,
  129. 'P7 快速连点只认最后一页(迟到响应丢弃,独立令牌)',
  130. )
  131. // ---- 时间窗(2026-10-22 维护者反馈「列表没按时间筛」)----
  132. mutate(
  133. 'M33-request-drops-time-window',
  134. 'stores/eventStore.js',
  135. ` fetchWorkOrderList(
  136. { page: workOrderPage.value, pageSize: WORK_ORDER_LIST_PAGE_SIZE, startTime: workOrderWindow.startTime, endTime: workOrderWindow.endTime },
  137. requestOptions,
  138. ),`,
  139. ` fetchWorkOrderList(
  140. { page: workOrderPage.value, pageSize: WORK_ORDER_LIST_PAGE_SIZE },
  141. requestOptions,
  142. ),`,
  143. 'P9 首屏工单列表请求带当前窗口(7 天的 startTime/endTime 都非空)',
  144. )
  145. mutate(
  146. 'M34-paging-recomputes-window',
  147. 'stores/eventStore.js',
  148. ` const window = workOrderWindow || activeConditions()`,
  149. ` const window = activeConditions()`,
  150. 'P10 翻页沿用同一时间窗(第 2 页的 startTime/endTime 与首屏完全一致)',
  151. )
  152. mutate(
  153. 'M35-range-change-keeps-old-window',
  154. 'stores/eventStore.js',
  155. ` workOrderWindow = { startTime: active.startTime, endTime: active.endTime }`,
  156. ``,
  157. 'P11 切到 30 天后请求换成 30 天窗口(且回第 1 页)',
  158. )
  159. console.log('')
  160. console.log(`T65B 变异测试:${proved}/${injected} 处变异被对应断言抓住。`)
  161. rmSync(reportDir, { recursive: true, force: true })
  162. if (proved < injected) process.exit(1)