// 从 chain6 的 allure-results 里取出「本轮真正跑过的用例」按状态分组, // 目的:给 ② 的 28 skipped 一份**具名**证据(哪些用例、在哪个 project、以什么理由跳过), // 而不是只引用汇总行里的计数。汇总计数只能证明「数量」,证明不了「没有把失败洗成跳过」。 import { readdirSync, statSync, readFileSync, writeFileSync } from 'fs'; import { resolve } from 'path'; const DIR = resolve(process.cwd() ?? '.', 'e2e/allure-results'); // chain6 功能阶段:03:52:08Z → 04:20:43Z;视觉阶段紧接其后。留出余量按 UTC 当日 03:50–04:25 过滤。 const FROM = Date.parse('2026-09-22T03:50:00Z'); const TO = Date.parse('2026-09-22T04:25:00Z'); const files = readdirSync(DIR).filter((f) => f.endsWith('-result.json')); const rows = []; for (const f of files) { const p = resolve(DIR, f); const m = statSync(p).mtimeMs; if (m < FROM || m > TO) continue; let j; try { j = JSON.parse(readFileSync(p, 'utf8')); } catch { continue; } const labels = {}; for (const l of j.labels || []) labels[l.name] = l.value; rows.push({ file: f, mtime: new Date(m).toISOString(), status: j.status, reason: j.statusDetails?.message || j.statusDetails?.trace || '', project: labels.device || labels.suiteParent || '', suite: labels.suite || '', fullName: j.fullName || '', name: j.name || '', }); } const byStatus = {}; for (const r of rows) byStatus[r.status] = (byStatus[r.status] || 0) + 1; const skipped = rows.filter((r) => r.status === 'skipped').sort((a, b) => (a.fullName + a.project).localeCompare(b.fullName + b.project)); const out = { generatedAt: new Date().toISOString(), source: 'e2e/allure-results(chain6 最终代码树 `npm run test` 功能+视觉两阶段的 allure 落盘)', window: { from: new Date(FROM).toISOString(), to: new Date(TO).toISOString() }, totalResults: rows.length, byStatus, skippedCount: skipped.length, // 按 (suite, reason) 聚合,便于核对「每处 test.skip 条件命中了多少条」 skipTally: Object.entries( skipped.reduce((acc, r) => { const k = `${r.suite} :: ${r.reason.slice(0, 80)}`; acc[k] = (acc[k] || 0) + 1; return acc; }, {}), ).map(([k, n]) => ({ group: k, count: n })), skipped, }; writeFileSync( resolve(process.cwd() ?? '.', 'docs/acceptance/2026-09-21-gates/skipped-tests-final-tree.json'), JSON.stringify(out, null, 2), ); console.log(`window results=${rows.length} byStatus=${JSON.stringify(byStatus)}`); console.log(`skipped=${skipped.length}`); for (const t of out.skipTally) console.log(` x${t.count} ${t.group}`); const nonPass = rows.filter((r) => r.status !== 'passed' && r.status !== 'skipped'); console.log(`otherNonPassed=${nonPass.length}`); for (const r of nonPass.slice(0, 10)) console.log(` ${r.status} ${r.project} ${r.fullName} :: ${r.reason.slice(0, 120)}`);