@kkeve/suite 0.0.20 → 0.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,6 +55,82 @@ const path = __importStar(require("node:path"));
55
55
  const screenshotDiff_js_1 = require("../core/judge/screenshotDiff.js");
56
56
  const confidence_js_1 = require("../core/judge/confidence.js");
57
57
  const reportData_js_1 = require("../core/generateReport/reportData.js");
58
+ /** Cypress 最终状态 → 报告状态;pending 明确是跳过,其余非 passed 均按失败处理 */
59
+ function mapCypressState(rawState) {
60
+ const state = String(rawState || '').toLowerCase();
61
+ if (state === 'passed')
62
+ return 'passed';
63
+ if (state === 'pending' || state === 'skipped')
64
+ return 'skipped';
65
+ if (state === 'timedout' || state === 'timed_out')
66
+ return 'timedOut';
67
+ return 'failed';
68
+ }
69
+ /** 从 Cypress after:spec / after:run 的 TestResult 提取开始时间与耗时 */
70
+ function timingOfCypressTest(test) {
71
+ const attempts = Array.isArray(test?.attempts) ? test.attempts : [];
72
+ const last = attempts[attempts.length - 1] || {};
73
+ const duration = Number(last.wallClockDuration ?? test?.duration ?? 0);
74
+ const startedAt = last.wallClockStartedAt || test?.wallClockStartedAt;
75
+ if (startedAt) {
76
+ const parsed = new Date(startedAt);
77
+ if (!Number.isNaN(parsed.getTime()))
78
+ return { duration, startTime: parsed.toISOString() };
79
+ }
80
+ return { duration, startTime: new Date(Date.now() - (Number.isFinite(duration) ? duration : 0)).toISOString() };
81
+ }
82
+ /**
83
+ * 用 Cypress 引擎掌握的最终测试结果补录 / 校正状态。
84
+ *
85
+ * 浏览器侧 `/report/test` 依赖全局 afterEach 与场景状态;beforeEach 等钩子
86
+ * 直接失败时状态可能拿不到,导致用例没有任何上报,最终被 reportData 补成
87
+ * `missing`。Cypress 的 after:spec / after:run 仍能提供权威结果,这里是唯一
88
+ * 不依赖用例内钩子的兜底入口。
89
+ */
90
+ function mergeCypressRunTests(state, tests, specFile = '') {
91
+ let merged = 0;
92
+ for (const test of Array.isArray(tests) ? tests : []) {
93
+ const titlePath = Array.isArray(test?.title) ? test.title.map((part) => String(part)) : [];
94
+ const title = (titlePath[titlePath.length - 1] || String(test?.title || '')).trim();
95
+ if (!title)
96
+ continue;
97
+ const suiteTitle = titlePath.length >= 2 ? String(titlePath[titlePath.length - 2] || '').trim() : '';
98
+ const status = mapCypressState(test?.state);
99
+ const { duration, startTime } = timingOfCypressTest(test);
100
+ const error = status === 'passed' || status === 'skipped'
101
+ ? undefined
102
+ : String(test?.displayError || test?.attempts?.[test.attempts.length - 1]?.error?.message || '');
103
+ const existing = state.results.find((r) => r.title === title && (!specFile || !r.file || r.file === specFile));
104
+ if (existing) {
105
+ // Cypress 的最终状态优先于浏览器侧可能拿到的中间状态;日志与附件保留。
106
+ existing.status = status;
107
+ existing.duration = duration || existing.duration;
108
+ existing.startTime = startTime || existing.startTime;
109
+ // Cypress 最终结论权威:失败才写错误,最终 passed/skipped 必须清掉
110
+ // 浏览器侧中间状态可能留下的误报错误,避免报告出现「通过但带错误」。
111
+ existing.error = error;
112
+ if (!existing.suiteTitle && suiteTitle)
113
+ existing.suiteTitle = suiteTitle;
114
+ if (!existing.file && specFile)
115
+ existing.file = specFile;
116
+ }
117
+ else {
118
+ state.results.push({
119
+ title,
120
+ suiteTitle,
121
+ file: specFile,
122
+ status,
123
+ duration,
124
+ startTime,
125
+ error,
126
+ logs: [],
127
+ attachments: [],
128
+ });
129
+ }
130
+ merged++;
131
+ }
132
+ return merged;
133
+ }
58
134
  function resultDirOf() {
59
135
  // 优先使用 runner 注入的报告目录;缺失时回退旧布局,兼容历史执行环境。
60
136
  if (process.env.KEVE_RESULT_DIR)
@@ -300,7 +376,10 @@ async function setupKeveBridge(on, config) {
300
376
  }
301
377
  catch { /* 软链失败不影响报告生成 */ }
302
378
  }
303
- return { ok: true, resultsPath, reportDataPath, cases: reportData?.summary?.total ?? undefined };
379
+ // reportData.summary 是 summaryJson 本体,total 在其嵌套的 summary 字段下。
380
+ // 直接读 summary.total 恒为 undefined,日志会误报「用例 0」,掩盖真实用例数。
381
+ const cases = reportData?.summary?.summary?.total ?? reportData?.summary?.total;
382
+ return { ok: true, resultsPath, reportDataPath, cases: cases ?? undefined };
304
383
  };
305
384
  const handle = async (route, payload, signal) => {
306
385
  switch (route) {
@@ -551,8 +630,15 @@ async function setupKeveBridge(on, config) {
551
630
  console.log(`[keve-bridge] listening on 127.0.0.1:${port} → ${state.resultDir}`);
552
631
  // 报告收口放在 after:run:此时所有 spec 的 /report/test 都已落盘,
553
632
  // 且不依赖任何场景级的 afterAll(硬失败/中断也能出报告)。
554
- on('after:run', async () => {
633
+ on('after:run', async (runResults) => {
555
634
  try {
635
+ // 最后一次用 Cypress run 全量结果校正,防止个别 spec 的 after:spec 未触发。
636
+ let merged = 0;
637
+ for (const run of Array.isArray(runResults?.runs) ? runResults.runs : []) {
638
+ merged += mergeCypressRunTests(state, run?.tests || [], String(run?.spec?.relative || ''));
639
+ }
640
+ if (merged > 0)
641
+ console.log(`[keve-bridge] Cypress 最终结果校正 ${merged} 条`);
556
642
  const out = await finalize();
557
643
  console.log(`[keve-bridge] report-data.json → ${out.reportDataPath}(用例 ${out.cases ?? 0})`);
558
644
  }
@@ -572,6 +658,9 @@ async function setupKeveBridge(on, config) {
572
658
  * after:spec 早于 after:run 触发,此时 results 已由浏览器侧 /report/test 落盘。
573
659
  */
574
660
  on('after:spec', async (_spec, results) => {
661
+ const merged = mergeCypressRunTests(state, results?.tests || [], String(results?.spec?.relative || ''));
662
+ if (merged > 0)
663
+ console.log(`[keve-bridge] Cypress 用例结果校正 ${merged} 条`);
575
664
  const video = results?.video;
576
665
  if (video && fs.existsSync(video)) {
577
666
  // 相对 taskDir,与 Playwright 侧 videoPath 的语义一致
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kkeve/suite",
3
- "version": "0.0.20",
3
+ "version": "0.0.21",
4
4
  "description": "AI-powered full-stack test agent CLI - static analysis, unit test, component test, E2E test, execute, evaluate, and report",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -550,15 +550,17 @@ function rewriteOpenPageTargets(input, cases) {
550
550
 
551
551
  /**
552
552
  * 逐条统计 caseResults 状态。
553
- * 状态语义与 core/generateReport 保持一致:
554
- * passed / failed 之外的一律按 skipped 计入,保证 total = passed + failed + skipped。
553
+ *
554
+ * 只有显式 `skipped` 才算跳过;`missing` / `timedOut` / `unknown` / `error`
555
+ * 都代表用例没有得到有效执行结果或执行异常,必须计为失败。历史实现把它们
556
+ * 统一算成 skipped,导致 Cypress 已报 `1 failed` 时平台仍展示 0 失败。
555
557
  */
556
558
  function summarizeReport(reportData) {
557
559
  const cases = Array.isArray(reportData?.caseResults) ? reportData.caseResults : [];
558
560
  if (cases.length) {
559
561
  const passed = cases.filter((item) => item?.status === 'passed').length;
560
- const failed = cases.filter((item) => item?.status === 'failed').length;
561
- const skipped = cases.length - passed - failed;
562
+ const skipped = cases.filter((item) => item?.status === 'skipped').length;
563
+ const failed = cases.length - passed - skipped;
562
564
  return { passed, failed, skipped, total: cases.length };
563
565
  }
564
566
  const summary = reportData?.summary?.summary || reportData?.summary || {};