| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179 |
- /**
- * 票 75 变异测试:证明 90 天分窗合并的每条关键断言抓得住它要防的缺陷。
- *
- * M1 合并丢趋势(只留首窗) → A2 必须红(90 天只显示 30 天)
- * M2 计数不求和(取首窗) → A1 必须红(13 变 0)
- * M3 处置率不重算(抄首窗百分比) → A3 必须红(0% 冒充 44%)
- * M4 分析 bucket 求和丢失 → B5 必须红(类型合计变 1 窗)
- * M5 百分比相加不重算 → B4 必须红(50%+25%≠75% 的老错误)
- * M6 分窗被拆掉(>31 天仍单请求) → C1 必须红(撞 400,界面「最多31天」复现)
- * M7 一窗失败仍合并其余 → C4 必须红(3/4 窗冒充 90 天)
- * M8 31 天提示复活 → D1 必须红(误导性提示回屏)
- *
- * 运行:node .scratch/cockpit-real-data-wayfinding/verify/t75-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('t75-90d-multi-window.mjs', import.meta.url))
- const srcPath = (rel) => `${repoRoot}src/modules/cockpit/${rel}`
- const reportDir = mkdtempSync(join(tmpdir(), 't75-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 }
- }
- function failingNames(output) {
- return [...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')
- runAndJudge(label, original, path, expectRed)
- }
- function runAndJudge(label, original, path, expectRed) {
- 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 && /does not provide an export|SyntaxError|Cannot find (module|export)/.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}: 还原失败`)
- }
- }
- // ---- 效率合并(services/warningResponseApi.js 的 mergeEfficiencyPayloads)----
- mutate(
- 'M1-efficiency-merge-drops-trend',
- 'services/warningResponseApi.js',
- ` for (const row of Array.isArray(payload.durationTrend) ? payload.durationTrend : []) {
- if (seenBuckets.has(row.timeBucket)) continue
- seenBuckets.add(row.timeBucket)
- durationTrend.push(row)
- }`,
- ``,
- 'A2 趋势按时序拼接且去重(720×3+17=2177 个小时桶,升序)',
- )
- mutate(
- 'M2-efficiency-counts-take-last-window',
- 'services/warningResponseApi.js',
- ` totalEvents += countOf(summary.totalEvents)`,
- ` totalEvents = countOf(summary.totalEvents)`,
- // 注:`+=` 变 `||=` 的写法对本数据是**等价变异**(前两窗为 0),不采用;取「末窗覆盖」才可观测。
- 'A1 计数跨窗求和(totalEvents=0+0+13+0=13,不由单窗冒充 90 天)',
- )
- mutate(
- 'M3-handling-rate-copied-not-recomputed',
- 'services/warningResponseApi.js',
- ` const numerator = stateCounts.filter((row) => row.stateCode === 5).reduce((sum, row) => sum + row.count, 0)`,
- ` const numerator = countOf((list[0].summary && list[0].summary.handlingEfficiency || {}).numerator)`,
- 'A3 处置率用合并后状态计数重算(分子=已结案4,分母=可计入9 → 44.44%)',
- )
- // ---- 分析合并(mergeAnalysisPayloads)----
- mutate(
- 'M4-analysis-total-count-takes-last-window',
- 'services/warningResponseApi.js',
- ` totalCount += countOf(payload.totalCount)`,
- ` totalCount = countOf(payload.totalCount)`,
- // 注 1:维度级 `entry.totalCount` 的「末窗覆盖」对本数据等价(w3=13、w4 无该维度),
- // 不可观测;B1 断言的是**载荷级** totalCount(末窗 w4=0 → 0 ≠ 13,才可观测)。
- // 注 2:w1/w2 的维度桶为空数组,"丢弃后窗同码桶"同样不可观测。
- 'B1 totalCount 跨窗求和(13)',
- )
- mutate(
- 'M5-analysis-percentage-added-not-recomputed',
- 'services/warningResponseApi.js',
- ` percentage: entry.totalCount > 0 ? ((bucket.count / entry.totalCount) * 100).toFixed(2) : '0.00',`,
- ` percentage: bucket.percentage,`,
- 'B4 percentage 重算而不是各窗相加(EVENT_TYPE 首桶 = count/totalCount×100)',
- )
- // ---- 分窗机制 ----
- mutate(
- 'M6-window-splitting-removed',
- 'services/warningResponseApi.js',
- ` const windows = historyWindows(range.startTime, range.endTime)
- if (windows.length <= 1) {`,
- ` const windows = historyWindows(range.startTime, range.endTime)
- if (true) {`,
- 'C1 90 天触发分窗:恰好 4 次请求、每次 ≤30 天(不是一次打 90 天撞 400)',
- )
- mutate(
- 'M7-partial-failure-still-merges',
- 'services/warningResponseApi.js',
- ` const failed = results.find((result) => ![SECTION_STATE.SUCCESS, SECTION_STATE.STALE, SECTION_STATE.EMPTY].includes(result.state))
- if (failed) return failed`,
- ` // 变异:窗失败被吞掉,拿其余窗冒充全量
- const failed = null
- if (failed) return failed`,
- 'C4 一窗失败 → 整分区 FAILED(不拿 3/4 窗冒充 90 天)',
- )
- // ---- 提示 ----
- mutate(
- 'M8-31d-note-resurrected',
- 'components/WarningResponsePane.vue',
- ` <p v-if="!eventStore.loading && section && section.reason && /历史接口回显/.test(section.reason)" class="warning-response-pane__range-status" role="alert">{{ section.reason }}</p>`,
- ` <p v-if="!eventStore.loading && eventStore.range === '90d' && panelId !== 'historyAnalysis'" class="warning-response-pane__range-status" role="status">
- 当前接口最多支持31天统计,请选择近7天或近30天;近90天历史趋势可正常查询。
- </p>
- <p v-if="!eventStore.loading && section && section.reason && /历史接口回显/.test(section.reason)" class="warning-response-pane__range-status" role="alert">{{ section.reason }}</p>`,
- 'D1 「最多支持31天」提示已撤(不再成立;误导比没有提示更糟)',
- )
- console.log('')
- console.log(`T75 变异测试:${proved}/${injected} 处变异被对应断言抓住。`)
- rmSync(reportDir, { recursive: true, force: true })
- if (proved < injected) process.exit(1)
|