@kkeve/suite 0.0.1 → 0.0.2

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.
Files changed (152) hide show
  1. package/README.md +53 -0
  2. package/dist/bin/keve.d.ts +13 -0
  3. package/dist/bin/keve.js +164 -0
  4. package/dist/commands/auth-update.d.ts +16 -0
  5. package/dist/commands/auth-update.js +88 -0
  6. package/dist/commands/clean.d.ts +10 -0
  7. package/dist/commands/clean.js +78 -0
  8. package/dist/commands/config.d.ts +17 -0
  9. package/dist/commands/config.js +99 -0
  10. package/dist/commands/init.d.ts +15 -0
  11. package/dist/commands/init.js +292 -0
  12. package/dist/commands/report.d.ts +12 -0
  13. package/dist/commands/report.js +159 -0
  14. package/dist/commands/run.d.ts +29 -0
  15. package/dist/commands/run.js +417 -0
  16. package/dist/commands/test.d.ts +12 -0
  17. package/dist/commands/test.js +102 -0
  18. package/dist/commands/vitest.d.ts +14 -0
  19. package/dist/commands/vitest.js +166 -0
  20. package/dist/config.d.ts +109 -0
  21. package/dist/config.js +291 -0
  22. package/dist/core/action-log-writer.d.ts +47 -0
  23. package/dist/{tools/write-file.js → core/action-log-writer.js} +42 -26
  24. package/dist/core/auth.d.ts +17 -0
  25. package/dist/core/auth.js +224 -0
  26. package/dist/core/cdp-resolver.d.ts +5 -0
  27. package/dist/core/cdp-resolver.js +93 -0
  28. package/dist/core/generateReport/report.d.ts +19 -0
  29. package/dist/core/generateReport/report.js +91 -0
  30. package/dist/core/generateReport/reportData.d.ts +215 -0
  31. package/dist/core/generateReport/reportData.js +735 -0
  32. package/dist/core/global-setup.d.ts +6 -0
  33. package/dist/core/global-setup.js +113 -0
  34. package/dist/core/keve-aspect.d.ts +54 -0
  35. package/dist/core/keve-aspect.js +52 -0
  36. package/dist/core/keve-decorators.d.ts +51 -0
  37. package/dist/core/keve-decorators.js +95 -0
  38. package/dist/core/keve-registry.d.ts +38 -0
  39. package/dist/core/keve-registry.js +25 -0
  40. package/dist/core/keve-report.d.ts +27 -0
  41. package/dist/core/keve-report.js +357 -0
  42. package/dist/core/keve-test.d.ts +36 -0
  43. package/dist/core/keve-test.js +364 -0
  44. package/dist/core/learned-actions.d.ts +24 -0
  45. package/dist/core/learned-actions.js +65 -0
  46. package/dist/core/pw-config.d.ts +19 -0
  47. package/dist/core/pw-config.js +180 -0
  48. package/dist/index.d.ts +8 -23
  49. package/dist/index.js +36 -43
  50. package/dist/page-agent/agent.d.ts +129 -0
  51. package/dist/page-agent/agent.js +585 -0
  52. package/dist/page-agent/hooks.d.ts +13 -0
  53. package/dist/page-agent/hooks.js +218 -0
  54. package/dist/page-agent/tools.d.ts +29 -0
  55. package/dist/page-agent/tools.js +297 -0
  56. package/doc/README.md +124 -0
  57. package/doc/agents/keve-dev-test-agent.md +140 -0
  58. package/doc/agents/keve-test-agent.md +178 -0
  59. package/doc/asserts//346/265/213/350/257/225/345/220/204/351/230/266/346/256/265/350/257/264/346/230/216.png +0 -0
  60. package/doc/keve-agent-demo.html +370 -0
  61. package/doc/skills/keve-bddcase-gen/SKILL.md +332 -0
  62. package/doc/skills/keve-bddcase-gen/references/test-case-rules.md +91 -0
  63. package/doc/skills/keve-bddcase-gen/references/test-cases-example.md +103 -0
  64. package/doc/skills/keve-component-gen/SKILL.md +255 -0
  65. package/doc/skills/keve-component-gen/references/component-mock-strategy.md +74 -0
  66. package/doc/skills/keve-component-gen/references/component-model-template.md +98 -0
  67. package/doc/skills/keve-component-gen/references/component-test-template.md +100 -0
  68. package/doc/skills/keve-component-gen/references/vue-test-utils-guide.md +91 -0
  69. package/doc/skills/keve-e2e-gen/SKILL.md +179 -0
  70. package/doc/skills/keve-e2e-gen/references/code-analysis-guide.md +208 -0
  71. package/doc/skills/keve-e2e-gen/references/decisions.md +36 -0
  72. package/doc/skills/keve-e2e-gen/references/pom-generation-guide.md +140 -0
  73. package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +236 -0
  74. package/doc/skills/keve-e2e-gen/references/test-auditor-guide.md +189 -0
  75. package/doc/skills/keve-e2e-gen/scripts/extract-components.sh +85 -0
  76. package/doc/skills/keve-e2e-gen/scripts/inject-testids.sh +95 -0
  77. package/doc/skills/keve-static-analysis/SKILL.md +362 -0
  78. package/doc/skills/keve-static-analysis/references/codegraph-analysis-guide.md +108 -0
  79. package/doc/skills/keve-static-analysis/references/git-impact-analysis.md +81 -0
  80. package/doc/skills/keve-static-analysis/references/lint-fix-guide.md +69 -0
  81. package/doc/skills/keve-static-analysis/references/runtime-risk-patterns.md +92 -0
  82. package/doc/skills/keve-static-analysis/references/security-scan-guide.md +32 -0
  83. package/doc/skills/keve-static-analysis/references/ts-error-patterns.md +98 -0
  84. package/doc/skills/keve-static-analysis/references/vue-security-guide.md +72 -0
  85. package/doc/skills/keve-test-eval/SKILL.md +78 -0
  86. package/doc/skills/keve-test-eval/references/common-fix-guide.md +166 -0
  87. package/doc/skills/keve-test-eval/references/confidence-data-guide.md +78 -0
  88. package/doc/skills/keve-test-eval/references/fix-loop-guide.md +67 -0
  89. package/doc/skills/keve-unit-gen/SKILL.md +203 -0
  90. package/doc/skills/keve-unit-gen/references/boundary-conditions.md +78 -0
  91. package/doc/skills/keve-unit-gen/references/unit-mock-strategy.md +71 -0
  92. package/doc/skills/keve-unit-gen/references/unit-test-template.md +105 -0
  93. package/package.json +47 -25
  94. package/dist/agent/factory.d.ts +0 -25
  95. package/dist/agent/factory.js +0 -99
  96. package/dist/agent/index.d.ts +0 -3
  97. package/dist/agent/index.js +0 -7
  98. package/dist/agent/loader.d.ts +0 -22
  99. package/dist/agent/loader.js +0 -117
  100. package/dist/agent/types.d.ts +0 -9
  101. package/dist/agent/types.js +0 -2
  102. package/dist/agent-loop/complete-task.d.ts +0 -28
  103. package/dist/agent-loop/complete-task.js +0 -70
  104. package/dist/agent-loop/index.d.ts +0 -16
  105. package/dist/agent-loop/index.js +0 -26
  106. package/dist/agent-loop/loop.d.ts +0 -42
  107. package/dist/agent-loop/loop.js +0 -282
  108. package/dist/agent-loop/runtime.d.ts +0 -40
  109. package/dist/agent-loop/runtime.js +0 -163
  110. package/dist/agent-loop/types.d.ts +0 -161
  111. package/dist/agent-loop/types.js +0 -105
  112. package/dist/context/index.d.ts +0 -8
  113. package/dist/context/index.js +0 -29
  114. package/dist/context/tokenCalculation.d.ts +0 -55
  115. package/dist/context/tokenCalculation.js +0 -95
  116. package/dist/context/truncation.d.ts +0 -67
  117. package/dist/context/truncation.js +0 -142
  118. package/dist/context/types.d.ts +0 -7
  119. package/dist/context/types.js +0 -27
  120. package/dist/llm/OpenAIClient.d.ts +0 -31
  121. package/dist/llm/OpenAIClient.js +0 -455
  122. package/dist/llm/constants.d.ts +0 -2
  123. package/dist/llm/constants.js +0 -6
  124. package/dist/llm/errors.d.ts +0 -25
  125. package/dist/llm/errors.js +0 -51
  126. package/dist/llm/index.d.ts +0 -49
  127. package/dist/llm/index.js +0 -158
  128. package/dist/llm/types.d.ts +0 -124
  129. package/dist/llm/types.js +0 -2
  130. package/dist/llm/utils.d.ts +0 -29
  131. package/dist/llm/utils.js +0 -284
  132. package/dist/skill/frontmatter-parser.d.ts +0 -50
  133. package/dist/skill/frontmatter-parser.js +0 -152
  134. package/dist/skill/index.d.ts +0 -12
  135. package/dist/skill/index.js +0 -17
  136. package/dist/skill/loader.d.ts +0 -96
  137. package/dist/skill/loader.js +0 -256
  138. package/dist/tools/ask-user.d.ts +0 -8
  139. package/dist/tools/ask-user.js +0 -47
  140. package/dist/tools/bash.d.ts +0 -47
  141. package/dist/tools/bash.js +0 -213
  142. package/dist/tools/edit-file.d.ts +0 -9
  143. package/dist/tools/edit-file.js +0 -76
  144. package/dist/tools/glob.d.ts +0 -8
  145. package/dist/tools/glob.js +0 -101
  146. package/dist/tools/grep.d.ts +0 -10
  147. package/dist/tools/grep.js +0 -76
  148. package/dist/tools/index.d.ts +0 -24
  149. package/dist/tools/index.js +0 -47
  150. package/dist/tools/read-files.d.ts +0 -8
  151. package/dist/tools/read-files.js +0 -81
  152. package/dist/tools/write-file.d.ts +0 -8
@@ -0,0 +1,735 @@
1
+ "use strict";
2
+ /**
3
+ * KEVE Summary Report Data Generator
4
+ *
5
+ * Reads test-results.json, confidence-data.jsonl, and test-cases.yaml,
6
+ * merges them into a structured report-data.json.
7
+ *
8
+ * This file handles ONLY data processing — no HTML rendering.
9
+ * Rendering is handled by generate-html-renderer.ts.
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.decodeAttachmentBody = decodeAttachmentBody;
46
+ exports.parseKeveGoals = parseKeveGoals;
47
+ exports.parseKeveAsserts = parseKeveAsserts;
48
+ exports.parseKeveScreenshots = parseKeveScreenshots;
49
+ exports.parseGoalScreenshots = parseGoalScreenshots;
50
+ exports.generateReportData = generateReportData;
51
+ exports.parseCaseIdFromTitle = parseCaseIdFromTitle;
52
+ exports.extractAllSpecs = extractAllSpecs;
53
+ exports.buildCaseResultMap = buildCaseResultMap;
54
+ exports.parseYamlSimple = parseYamlSimple;
55
+ const fs = __importStar(require("node:fs"));
56
+ const path = __importStar(require("node:path"));
57
+ const keve_registry_1 = require("../keve-registry");
58
+ function resolveOptions(opts) {
59
+ const PROJECT_ROOT = opts.projectRoot;
60
+ const TEST_RESULTS_JSON = opts.resultsPath;
61
+ const CONFIDENCE_DATA_JSONL = opts.confidencePath || path.join(path.dirname(TEST_RESULTS_JSON), 'confidence-data.jsonl');
62
+ const TEST_CASES_YAML = opts.casesPath || path.join(PROJECT_ROOT, 'test-cases.yaml');
63
+ // REPORTS_DIR = parent of round-N dirs (e.g. .keve/{plan}/test-artifacts/)
64
+ // test-results.json is inside round-N dir, so go up one level
65
+ const RESULT_PARENT_DIR = path.dirname(path.dirname(TEST_RESULTS_JSON));
66
+ const SCREENSHOTS_DIR = path.join(RESULT_PARENT_DIR, 'screenshots');
67
+ const REPORTS_DIR = RESULT_PARENT_DIR;
68
+ return { PROJECT_ROOT, TEST_RESULTS_JSON, CONFIDENCE_DATA_JSONL, TEST_CASES_YAML, SCREENSHOTS_DIR, REPORTS_DIR };
69
+ }
70
+ // ─── Helpers ─────────────────────────────────────────────────────
71
+ function parseCaseIdFromTitle(title) {
72
+ const colonMatch = title.match(/^([A-Z]+(?:-[A-Z]+)*-\d+):/);
73
+ const bracketMatch = title.match(/\[([A-Z]+(?:-[A-Z]+)*-\d+)\]/);
74
+ const spaceMatch = title.match(/^([A-Z]+(?:-[A-Z]+)*-\d+)\s/);
75
+ return colonMatch?.[1] || bracketMatch?.[1] || spaceMatch?.[1] || null;
76
+ }
77
+ /**
78
+ * Strip surrounding quotes from YAML string values.
79
+ * YAML uses quotes like id: "AG-01" which parseYamlSimple captures as "AG-01"
80
+ * with the double quotes included. This strips them.
81
+ */
82
+ function stripYamlQuotes(value) {
83
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
84
+ return value.slice(1, -1);
85
+ }
86
+ return value;
87
+ }
88
+ /** Convert absolute path to relative path (relative to baseDir).
89
+ * Frontend uses /api/ai/task/:taskId/screenshot?path=... which rejects absolute paths,
90
+ * so all screenshot paths in report-data.json must be relative to the workspace root. */
91
+ function toRelativePath(absOrRelPath, baseDir) {
92
+ if (!absOrRelPath)
93
+ return '';
94
+ // Already relative? Return as-is
95
+ if (!path.isAbsolute(absOrRelPath))
96
+ return absOrRelPath;
97
+ // Convert absolute → relative
98
+ return path.relative(baseDir, absOrRelPath);
99
+ }
100
+ function findScreenshotsForCase(caseId, screenshotsDir, taskDir) {
101
+ if (!screenshotsDir)
102
+ return [];
103
+ const files = fs.existsSync(screenshotsDir) ? fs.readdirSync(screenshotsDir) : [];
104
+ const prefix = caseId.replace('-', '_');
105
+ return files
106
+ .filter(f => f.startsWith(prefix + '_') && f.endsWith('.png'))
107
+ .sort()
108
+ .map(f => {
109
+ // 保留相对路径(相对于 taskDir),前端通过 API 按需加载
110
+ const absPath = path.join(screenshotsDir, f);
111
+ return taskDir ? path.relative(taskDir, absPath) : absPath;
112
+ });
113
+ }
114
+ function parseYamlSimple(yamlText) {
115
+ const lines = yamlText.split('\n');
116
+ const result = {
117
+ version: '', mr_id: '', description: '', docs_url: '',
118
+ generated_at: '', modules: []
119
+ };
120
+ let currentModule = null;
121
+ let currentCase = null;
122
+ let currentStep = null;
123
+ let inSteps = false;
124
+ let inPrecondition = false;
125
+ for (const line of lines) {
126
+ const trimmed = line.trimEnd();
127
+ // Top-level fields
128
+ if (trimmed.startsWith('version:') && !trimmed.startsWith(' ')) {
129
+ result.version = trimmed.substring(trimmed.indexOf(':') + 1).trim();
130
+ }
131
+ else if (trimmed.startsWith('mr_id:')) {
132
+ result.mr_id = trimmed.substring(trimmed.indexOf(':') + 1).trim();
133
+ }
134
+ else if (trimmed.startsWith('description:')) {
135
+ result.description = trimmed.substring(trimmed.indexOf(':') + 1).trim();
136
+ }
137
+ else if (trimmed.startsWith('docs_url:')) {
138
+ result.docs_url = trimmed.substring(trimmed.indexOf(':') + 1).trim();
139
+ }
140
+ else if (trimmed.startsWith('generated_at:')) {
141
+ result.generated_at = trimmed.substring(trimmed.indexOf(':') + 1).trim();
142
+ }
143
+ // Module: YAML uses ` - name: "AG"` under `modules:`, which is 2-space indent
144
+ // Also support `- module: "AG"` (0-indent) and `- 模块: "AG"`
145
+ const moduleMatch = trimmed.match(/^ - (name|module|模块):\s*(.+)/);
146
+ if (moduleMatch) {
147
+ currentModule = { name: stripYamlQuotes(moduleMatch[2].trim()), description: '', cases: [] };
148
+ result.modules.push(currentModule);
149
+ inSteps = false;
150
+ inPrecondition = false;
151
+ continue;
152
+ }
153
+ if (currentModule && trimmed.startsWith(' description:') && !trimmed.startsWith(' ')) {
154
+ currentModule.description = trimmed.substring(trimmed.indexOf(':') + 1).trim();
155
+ continue;
156
+ }
157
+ // Case: ` - id: "AG-01"` at 6-space indent (inside module.cases)
158
+ const caseMatch = trimmed.match(/^ - (id|用例ID):\s*(.+)/);
159
+ if (caseMatch) {
160
+ currentCase = {
161
+ id: stripYamlQuotes(caseMatch[2].trim()), title: '', priority: '', type: '',
162
+ precondition: [], steps: [], notes: ''
163
+ };
164
+ if (currentModule)
165
+ currentModule.cases.push(currentCase);
166
+ inSteps = false;
167
+ inPrecondition = false;
168
+ continue;
169
+ }
170
+ if (currentCase && !inSteps && !inPrecondition) {
171
+ if (trimmed.startsWith(' title:')) {
172
+ currentCase.title = stripYamlQuotes(trimmed.substring(trimmed.indexOf(':') + 1).trim());
173
+ }
174
+ else if (trimmed.startsWith(' priority:')) {
175
+ currentCase.priority = stripYamlQuotes(trimmed.substring(trimmed.indexOf(':') + 1).trim());
176
+ }
177
+ else if (trimmed.startsWith(' type:')) {
178
+ currentCase.type = stripYamlQuotes(trimmed.substring(trimmed.indexOf(':') + 1).trim());
179
+ }
180
+ else if (trimmed.startsWith(' precondition:')) {
181
+ inPrecondition = true;
182
+ continue;
183
+ }
184
+ else if (trimmed.startsWith(' steps:')) {
185
+ inSteps = true;
186
+ continue;
187
+ }
188
+ else if (trimmed.startsWith(' notes:')) {
189
+ currentCase.notes = stripYamlQuotes(trimmed.substring(trimmed.indexOf(':') + 1).trim());
190
+ }
191
+ }
192
+ if (inPrecondition && currentCase) {
193
+ if (trimmed.startsWith(' - ')) {
194
+ currentCase.precondition.push(stripYamlQuotes(trimmed.substring(trimmed.indexOf('- ') + 2).trim()));
195
+ }
196
+ else if (trimmed.startsWith(' ') && !trimmed.startsWith(' -')) {
197
+ inPrecondition = false;
198
+ if (trimmed.startsWith(' steps:'))
199
+ inSteps = true;
200
+ }
201
+ }
202
+ if (inSteps && currentCase) {
203
+ if (trimmed.startsWith(' - step:')) {
204
+ const stepText = trimmed.substring(trimmed.indexOf(':') + 1).trim();
205
+ currentStep = { step: stripYamlQuotes(stepText), expected: '' };
206
+ currentCase.steps.push(currentStep);
207
+ }
208
+ else if (trimmed.startsWith(' expected:') && currentStep) {
209
+ currentStep.expected = stripYamlQuotes(trimmed.substring(trimmed.indexOf(':') + 1).trim());
210
+ }
211
+ else if (trimmed.match(/^ [^ ]/) && !trimmed.startsWith(' steps:')) {
212
+ // 8-space indent followed by non-space = case-level field, exit steps mode
213
+ inSteps = false;
214
+ currentStep = null;
215
+ }
216
+ }
217
+ }
218
+ return result;
219
+ }
220
+ // ─── Extract specs from test-results.json ─────────────────────────
221
+ function extractAllSpecs(jsonData) {
222
+ const specs = [];
223
+ if (jsonData.suites) {
224
+ for (const suite of jsonData.suites) {
225
+ extractSpecsRecursive(suite, '', specs);
226
+ }
227
+ }
228
+ return specs;
229
+ }
230
+ function extractSpecsRecursive(suite, parentPath, specs) {
231
+ const suitePath = parentPath ? `${parentPath} > ${suite.title}` : suite.title;
232
+ if (suite.specs) {
233
+ for (const spec of suite.specs) {
234
+ specs.push({
235
+ title: spec.title,
236
+ file: spec.file || '',
237
+ suitePath,
238
+ tests: (spec.tests || []).map((t) => {
239
+ const lastResult = t.results?.[t.results.length - 1];
240
+ return {
241
+ status: lastResult?.status || t.status || 'unknown',
242
+ duration: lastResult?.duration?.duration || t.results?.[0]?.duration || 0,
243
+ startTime: lastResult?.startTime || '',
244
+ error: lastResult?.error?.message || '',
245
+ errors: (lastResult?.errors || []).map((e) => e.message || ''),
246
+ stdout: (lastResult?.stdout || []).map((s) => typeof s === 'string' ? s : (s.text || '')).join(''),
247
+ steps: (lastResult?.steps || []).map((s) => ({
248
+ title: s.title, duration: s.duration, error: s.error?.message || null, attachments: s.attachments || []
249
+ })),
250
+ attachments: lastResult?.attachments || [],
251
+ };
252
+ }),
253
+ });
254
+ }
255
+ }
256
+ if (suite.suites) {
257
+ for (const child of suite.suites) {
258
+ extractSpecsRecursive(child, suitePath, specs);
259
+ }
260
+ }
261
+ }
262
+ // ─── Shared attachment parsing ──────────────────────────────────────
263
+ /** Decode Playwright attachment body (may be base64 string, Buffer, or object) */
264
+ function decodeAttachmentBody(body) {
265
+ if (typeof body === 'string') {
266
+ // Playwright JSON reporter serializes as base64
267
+ return Buffer.from(body, 'base64').toString('utf8');
268
+ }
269
+ else if (Buffer.isBuffer(body)) {
270
+ return body.toString('utf8');
271
+ }
272
+ else if (typeof body === 'object' && body !== null) {
273
+ return JSON.stringify(body);
274
+ }
275
+ else {
276
+ return String(body || '');
277
+ }
278
+ }
279
+ /** Parse keveGoals from Playwright test attachments */
280
+ function parseKeveGoals(attachments) {
281
+ const att = (attachments || []).find((a) => a.name === 'keveGoals' && a.contentType === 'application/json');
282
+ if (!att?.body)
283
+ return null;
284
+ try {
285
+ const bodyStr = decodeAttachmentBody(att.body);
286
+ const parsed = JSON.parse(bodyStr);
287
+ return Array.isArray(parsed) ? parsed : null;
288
+ }
289
+ catch {
290
+ return null;
291
+ }
292
+ }
293
+ /** Parse keveAssert results from Playwright test attachments */
294
+ function parseKeveAsserts(attachments) {
295
+ const results = [];
296
+ for (const att of (attachments || [])) {
297
+ if (att.name?.startsWith('keveAssert-') && att.contentType === 'application/json' && att.body) {
298
+ try {
299
+ const bodyStr = decodeAttachmentBody(att.body);
300
+ const body = JSON.parse(bodyStr);
301
+ results.push({ label: att.name.replace('keveAssert-', ''), body });
302
+ }
303
+ catch { /* ignore */ }
304
+ }
305
+ }
306
+ return results;
307
+ }
308
+ /** Extract keveAssert screenshot paths from Playwright test attachments */
309
+ function parseKeveScreenshots(attachments, taskDir) {
310
+ const screenshots = [];
311
+ for (const att of (attachments || [])) {
312
+ if (att.name?.startsWith('keveAssert-') && att.contentType === 'application/json' && att.body) {
313
+ try {
314
+ const bodyStr = decodeAttachmentBody(att.body);
315
+ const body = JSON.parse(bodyStr);
316
+ if (body.screenshotPath) {
317
+ // 保留原始相对路径,前端通过 /api/ai/task/:taskId/screenshot?path=... 按需加载
318
+ screenshots.push({ label: att.name.replace('keveAssert-', ''), path: body.screenshotPath });
319
+ }
320
+ }
321
+ catch { /* ignore */ }
322
+ }
323
+ }
324
+ return screenshots;
325
+ }
326
+ function parseGoalScreenshots(attachments, taskDir) {
327
+ const screenshots = [];
328
+ for (const att of (attachments || [])) {
329
+ if (att.name !== 'keveGoalResult' || att.contentType !== 'application/json' || !att.body)
330
+ continue;
331
+ try {
332
+ const bodyStr = decodeAttachmentBody(att.body);
333
+ const body = JSON.parse(bodyStr);
334
+ const step = body.step || '';
335
+ if (body.goalScreenshotBefore) {
336
+ screenshots.push({ label: step ? `${step} - 执行前` : '执行前', path: body.goalScreenshotBefore });
337
+ }
338
+ if (body.goalScreenshotAfter) {
339
+ screenshots.push({ label: step ? `${step} - 执行后` : '执行后', path: body.goalScreenshotAfter });
340
+ }
341
+ if (body.agentScreenshots && Array.isArray(body.agentScreenshots)) {
342
+ body.agentScreenshots.forEach((sp, idx) => {
343
+ if (sp) {
344
+ screenshots.push({ label: step ? `${step} - 验证截图${idx + 1}` : `验证截图${idx + 1}`, path: sp });
345
+ }
346
+ });
347
+ }
348
+ if (body.actions && Array.isArray(body.actions)) {
349
+ for (const action of body.actions) {
350
+ if (action.screenshotPath) {
351
+ const actionLabel = action.tool ? `${step} - ${action.tool}` : (step || '操作步骤');
352
+ screenshots.push({ label: actionLabel, path: action.screenshotPath });
353
+ }
354
+ }
355
+ }
356
+ }
357
+ catch { /* ignore */ }
358
+ }
359
+ return screenshots;
360
+ }
361
+ // ─── Build case result map ───────────────────────────────────────
362
+ function buildCaseResultMap(specs, taskDir) {
363
+ const map = {};
364
+ for (const spec of specs) {
365
+ const caseId = parseCaseIdFromTitle(spec.title);
366
+ if (!caseId)
367
+ continue;
368
+ const test = spec.tests[spec.tests.length - 1] || spec.tests[0];
369
+ if (!test)
370
+ continue;
371
+ let screenshotPath = '';
372
+ let tracePath = '';
373
+ let videoPath = '';
374
+ const allAttachments = test.attachments || test.steps?.flatMap((s) => s.attachments || []) || [];
375
+ for (const att of allAttachments) {
376
+ if (att.name === 'screenshot')
377
+ screenshotPath = toRelativePath(att.path, taskDir);
378
+ if (att.name === 'trace')
379
+ tracePath = toRelativePath(att.path, taskDir);
380
+ }
381
+ for (const att of test.attachments || []) {
382
+ if (att.name === 'screenshot' && !screenshotPath)
383
+ screenshotPath = toRelativePath(att.path, taskDir);
384
+ if (att.name === 'trace' && !tracePath)
385
+ tracePath = toRelativePath(att.path, taskDir);
386
+ if (att.name === 'video' && !videoPath)
387
+ videoPath = toRelativePath(att.path, taskDir);
388
+ }
389
+ const keveScreenshots = parseKeveScreenshots(test.attachments || [], taskDir);
390
+ const goalScreenshots = parseGoalScreenshots(test.attachments || [], taskDir);
391
+ keveScreenshots.push(...goalScreenshots);
392
+ // keveGoals: prefer @keveGoal decorator data from sceneGoalsMap, fallback to fixture attachment
393
+ const decoratorGoals = keve_registry_1.sceneGoalsMap.get(spec.title);
394
+ const keveGoals = decoratorGoals && decoratorGoals.length > 0
395
+ ? decoratorGoals
396
+ : parseKeveGoals(test.attachments || []);
397
+ // Extract @keveModel info from suitePath: "file.spec.ts > ID: Description"
398
+ const suitePathParts = spec.suitePath.split(' > ');
399
+ const pwModuleTitle = suitePathParts.length >= 2 ? suitePathParts[suitePathParts.length - 1].trim() : suitePathParts[0].trim();
400
+ // @keveModel title format: "${id}: ${description}" — extract module ID and description
401
+ const moduleIdMatch = pwModuleTitle.match(/^([A-Z]+(?:-[A-Z]+)*):\s*(.+)$/);
402
+ const pwModuleId = moduleIdMatch?.[1] || '';
403
+ const pwModuleDescription = moduleIdMatch?.[2] || pwModuleTitle;
404
+ map[caseId] = {
405
+ caseId, title: spec.title, status: test.status, duration: test.duration || 0,
406
+ startTime: test.startTime || '', error: test.error || '', errors: test.errors || [],
407
+ stdout: test.stdout || '', pwSteps: test.steps || [],
408
+ screenshotPath, videoPath, tracePath, attachments: test.attachments || [], keveScreenshots,
409
+ keveGoals, pwModuleTitle, pwModuleId, pwModuleDescription,
410
+ };
411
+ }
412
+ return map;
413
+ }
414
+ // ─── Main: Generate report data JSON ─────────────────────────────
415
+ async function generateReportData(opts) {
416
+ const resolved = resolveOptions(opts);
417
+ const { PROJECT_ROOT, TEST_RESULTS_JSON, CONFIDENCE_DATA_JSONL, TEST_CASES_YAML, SCREENSHOTS_DIR, REPORTS_DIR } = resolved;
418
+ // Derive current round number from test-results.json path (e.g. .../test-artifacts/round-1/test-results.json → 1)
419
+ const resultsDirName = path.basename(path.dirname(TEST_RESULTS_JSON));
420
+ const currentRound = resultsDirName.startsWith('round-')
421
+ ? parseInt(resultsDirName.replace('round-', ''))
422
+ : 1;
423
+ const roundDir = path.dirname(TEST_RESULTS_JSON); // same as the round-N directory
424
+ const latestDir = path.join(REPORTS_DIR, 'latest');
425
+ const testResultsRaw = fs.readFileSync(TEST_RESULTS_JSON, 'utf-8');
426
+ const testResultsJson = JSON.parse(testResultsRaw);
427
+ let testCasesData = { version: '', modules: [], mr_id: '', description: '', docs_url: '', generated_at: '' };
428
+ if (fs.existsSync(TEST_CASES_YAML)) {
429
+ testCasesData = parseYamlSimple(fs.readFileSync(TEST_CASES_YAML, 'utf-8'));
430
+ }
431
+ const specs = extractAllSpecs(testResultsJson);
432
+ // Derive taskDir from resultsPath: .../test-artifacts/round-N/test-results.json → taskDir is 2 levels up from test-artifacts/
433
+ const taskDir = path.resolve(path.dirname(TEST_RESULTS_JSON), '..', '..');
434
+ const caseResultMap = buildCaseResultMap(specs, taskDir);
435
+ let total = 0, passed = 0, failed = 0, skipped = 0;
436
+ const caseResults = [];
437
+ const confidenceDist = { '95-100': 0, '61-90': 0, '41-60': 0, '0-40': 0 };
438
+ // Read confidence-data.jsonl (AI evaluation results from KeveReporter)
439
+ let aiConfidenceMap = {};
440
+ if (fs.existsSync(CONFIDENCE_DATA_JSONL)) {
441
+ try {
442
+ const lines = fs.readFileSync(CONFIDENCE_DATA_JSONL, 'utf-8').trim().split('\n');
443
+ for (const line of lines) {
444
+ const entry = JSON.parse(line);
445
+ const key = entry.title || entry.testId;
446
+ if (entry.confidence !== undefined) {
447
+ // 新版 confidence-data.jsonl 有 steps 字段(合并后)
448
+ // 旧版有 keveGoals + reactSteps,需要合并
449
+ let steps = entry.steps;
450
+ if (!steps) {
451
+ // 旧版兼容:从 keveGoals + reactSteps 合并为 steps
452
+ const goals = entry.keveGoals || [];
453
+ const rs = entry.reactSteps || [];
454
+ if (goals.length > 0 || rs.length > 0) {
455
+ steps = goals.map((g, i) => {
456
+ const matched = rs.find((r) => r.order === g.order || r.step === g.step) || rs[i];
457
+ return {
458
+ step: g.step,
459
+ expected: g.expected,
460
+ precondition: g.precondition,
461
+ order: g.order,
462
+ success: matched?.success,
463
+ actions: matched?.actions || [],
464
+ diagnosticHints: matched?.diagnosticHints || [],
465
+ goalScreenshotBefore: matched?.goalScreenshotBefore,
466
+ goalScreenshotAfter: matched?.goalScreenshotAfter,
467
+ agentScreenshots: matched?.agentScreenshots || [],
468
+ };
469
+ });
470
+ // 补上 reactSteps 中未匹配的步骤
471
+ for (const r of rs) {
472
+ if (!steps.some((s) => s.order === r.order)) {
473
+ steps.push({ ...r, precondition: undefined });
474
+ }
475
+ }
476
+ }
477
+ }
478
+ aiConfidenceMap[key] = {
479
+ data: entry.data || '待确认',
480
+ confidence: entry.confidence,
481
+ thought: entry.thought || '',
482
+ error: entry.error || null,
483
+ errorCategory: entry.errorCategory || undefined,
484
+ diagnosticHint: entry.diagnosticHint || undefined,
485
+ steps,
486
+ };
487
+ }
488
+ }
489
+ if (Object.keys(aiConfidenceMap).length > 0) {
490
+ console.log(` -> Loaded confidence from confidence-data.jsonl (${Object.keys(aiConfidenceMap).length} cases)`);
491
+ }
492
+ }
493
+ catch {
494
+ console.log(` -> Warning: Failed to parse confidence-data.jsonl`);
495
+ }
496
+ }
497
+ // Build case results
498
+ // Primary data source: Playwright test-results.json (from @keveModel/@keveScene decorators)
499
+ // Optional enhancement: test-cases.yaml (priority, precondition, type, notes)
500
+ // This ensures decorator-defined structure drives the report, avoiding ID/title mismatch.
501
+ // Build a YAML lookup by caseId for optional enrichment
502
+ const yamlCaseLookup = {};
503
+ const yamlModuleLookup = {};
504
+ for (const mod of testCasesData.modules) {
505
+ yamlModuleLookup[mod.name] = mod;
506
+ for (const caseDef of mod.cases) {
507
+ yamlCaseLookup[caseDef.id] = caseDef;
508
+ }
509
+ }
510
+ for (const [caseId, result] of Object.entries(caseResultMap)) {
511
+ total++;
512
+ // Playwright returns "timedOut" for test timeout — treat as failed
513
+ const effectiveStatus = result.status === 'timedOut' ? 'failed' : result.status;
514
+ if (effectiveStatus === 'passed')
515
+ passed++;
516
+ else if (effectiveStatus === 'skipped')
517
+ skipped++;
518
+ else
519
+ failed++;
520
+ // AI evaluation: match by Playwright title (scene.title, the canonical key)
521
+ const aiEntry = aiConfidenceMap[result.title] || aiConfidenceMap[caseId];
522
+ const data = effectiveStatus === 'skipped' ? '待确认' : (aiEntry?.data || '待确认');
523
+ // When aiEntry is missing, confidence should be 0 (no AI evaluation performed),
524
+ // not a hardcoded fallback like 70/35 which implies AI evaluated but was uncertain.
525
+ const confidence = effectiveStatus === 'skipped' ? 0 : (aiEntry?.confidence ?? 0);
526
+ const confidenceReason = effectiveStatus === 'skipped' ? '用例被跳过' : (aiEntry?.thought || '无AI评估数据');
527
+ // Distribute by data value + confidence
528
+ if (effectiveStatus === 'skipped') { /* skip */ }
529
+ else if (data === '通过')
530
+ confidenceDist['95-100']++;
531
+ else if (data === '不通过' || confidence <= 40)
532
+ confidenceDist['0-40']++;
533
+ else if (confidence >= 61)
534
+ confidenceDist['61-90']++;
535
+ else
536
+ confidenceDist['41-60']++;
537
+ // 截图仅保留文件路径,前端按需加载(避免 14MB+ base64 内嵌)
538
+ const stepScreenshotPaths = findScreenshotsForCase(caseId, SCREENSHOTS_DIR, taskDir);
539
+ const stepScreenshotsPaths = stepScreenshotPaths.map(p => ({ path: p }));
540
+ const keveScreenshotsPaths = (result.keveScreenshots || []).map((ks) => {
541
+ const { base64, ...rest } = ks; // eslint-disable-line @typescript-eslint/no-unused-vars
542
+ return rest; // 只保留 label, path 等元数据
543
+ });
544
+ // 失败截图不内嵌 base64,仅保留路径
545
+ // screenshotBase64 已移除,前端通过 screenshotPath 按需加载
546
+ // Module ID from @keveModel (extracted from Playwright suitePath)
547
+ const moduleName = result.pwModuleId || 'default';
548
+ const yamlMod = yamlModuleLookup[moduleName];
549
+ // Module description: prefer @keveModel description, fallback to YAML
550
+ const moduleDesc = result.pwModuleDescription || yamlMod?.description || moduleName;
551
+ // YAML enrichment (optional): priority, type, precondition, notes, steps
552
+ const yamlCase = yamlCaseLookup[caseId];
553
+ // Title: use result.title (Playwright spec.title) but strip ID prefix for display
554
+ const displayTitle = result.title.replace(/^[A-Z]+-\d+:\s*/, '').replace(/^\[[A-Z]+-\d+\]\s*/, '');
555
+ const steps = aiEntry?.steps
556
+ ? aiEntry.steps.map((s) => {
557
+ // 截图路径保持原始相对路径(相对于 taskDir),
558
+ // 前端通过 /api/ai/task/:taskId/screenshot?path=... 加载,
559
+ // 后端会以 workspaceDir 为根拼接,拒绝绝对路径
560
+ return {
561
+ step: s.step,
562
+ expected: s.expected,
563
+ precondition: s.precondition,
564
+ order: s.order,
565
+ success: s.success,
566
+ conclusion: s.conclusion,
567
+ goalScreenshotPath: s.goalScreenshotPath || s.goalScreenshotAfter,
568
+ verifyScreenshots: s.verifyScreenshots || s.agentScreenshots,
569
+ actions: (s.actions || []).map((a) => ({
570
+ tool: a.tool,
571
+ role: a.role,
572
+ name: a.name,
573
+ url: a.url,
574
+ text: a.text,
575
+ key: a.key,
576
+ reason: a.reason,
577
+ success: a.success,
578
+ verdict: a.verdict,
579
+ conclusion: a.conclusion,
580
+ result: a.result,
581
+ error: a.error,
582
+ toolOutput: a.toolOutput?.slice(0, 500),
583
+ evaluation: a.evaluation?.slice(0, 300),
584
+ memory: a.memory?.slice(0, 200),
585
+ nextGoal: a.nextGoal?.slice(0, 200),
586
+ screenshotPath: a.screenshotPath,
587
+ })),
588
+ duration: s.duration,
589
+ status: s.status,
590
+ error: s.error,
591
+ };
592
+ })
593
+ : (result.keveGoals && result.keveGoals.length > 0
594
+ ? result.keveGoals.map((g) => ({
595
+ step: g.step,
596
+ expected: g.expected,
597
+ precondition: g.precondition,
598
+ order: g.order,
599
+ }))
600
+ : []);
601
+ // ── Merge executed steps with YAML outline ──
602
+ // For timedOut/failed cases, only partial steps may have been executed.
603
+ // We merge: executed steps (from aiEntry/keveGoals) + unexecuted steps (from YAML outline)
604
+ // so the report shows the full picture (e.g. step 1 pass, step 2/3/4 "未执行").
605
+ const maxExecutedOrder = steps.length > 0
606
+ ? Math.max(...steps.map(s => s.order ?? 0))
607
+ : -1;
608
+ const executedStepSet = new Set(steps.map(s => s.step));
609
+ if (yamlCase?.steps?.length) {
610
+ for (let i = 0; i < yamlCase.steps.length; i++) {
611
+ const ys = yamlCase.steps[i];
612
+ // Skip if already present in executed steps (matched by step text or order)
613
+ if (executedStepSet.has(ys.step))
614
+ continue;
615
+ if (steps.some(s => s.order === i))
616
+ continue;
617
+ // Add unexecuted YAML step
618
+ steps.push({
619
+ step: ys.step,
620
+ expected: ys.expected,
621
+ order: i,
622
+ success: undefined,
623
+ // Mark as unexecuted so frontend can render "未执行" state
624
+ status: 'unexecuted',
625
+ });
626
+ }
627
+ // Sort by order to ensure correct sequencing
628
+ steps.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
629
+ }
630
+ caseResults.push({
631
+ caseId,
632
+ module: moduleName,
633
+ moduleDescription: moduleDesc,
634
+ title: displayTitle,
635
+ priority: yamlCase?.priority || '',
636
+ type: yamlCase?.type || '',
637
+ precondition: yamlCase?.precondition || [],
638
+ steps,
639
+ notes: yamlCase?.notes || '',
640
+ status: effectiveStatus,
641
+ duration: result.duration,
642
+ startTime: result.startTime,
643
+ pwSteps: result.pwSteps,
644
+ error: result.error,
645
+ errors: result.errors,
646
+ stdout: result.stdout,
647
+ data,
648
+ confidence,
649
+ confidenceReason,
650
+ stepScreenshots: stepScreenshotsPaths,
651
+ screenshotPath: result.screenshotPath,
652
+ videoPath: result.videoPath,
653
+ tracePath: result.tracePath,
654
+ keveScreenshots: keveScreenshotsPaths,
655
+ errorCategory: aiEntry?.errorCategory,
656
+ diagnosticHint: aiEntry?.diagnosticHint,
657
+ });
658
+ }
659
+ // Also include YAML cases that have no matching Playwright result (not executed)
660
+ for (const mod of testCasesData.modules) {
661
+ for (const caseDef of mod.cases) {
662
+ if (caseResultMap[caseDef.id])
663
+ continue; // Already included above
664
+ total++;
665
+ failed++;
666
+ // Missing cases: test was not executed by Playwright, so AI evaluation is skipped
667
+ const data = '跳过';
668
+ const confidence = 0;
669
+ confidenceDist['0-40']++;
670
+ caseResults.push({
671
+ caseId: caseDef.id,
672
+ module: mod.name,
673
+ moduleDescription: mod.description || mod.name,
674
+ title: caseDef.title,
675
+ priority: caseDef.priority,
676
+ type: caseDef.type,
677
+ precondition: caseDef.precondition,
678
+ steps: caseDef.steps,
679
+ notes: caseDef.notes,
680
+ status: 'missing',
681
+ duration: 0,
682
+ startTime: '',
683
+ pwSteps: [],
684
+ error: 'YAML中定义了该用例,但Playwright未找到对应的执行结果。可能原因:1) 测试脚本中未实现该场景(@keveScene);2) 用例ID与Playwright测试标题不匹配;3) 测试执行失败导致结果未写入。',
685
+ errors: [],
686
+ stdout: '',
687
+ data,
688
+ confidence,
689
+ confidenceReason: 'YAML中定义但Playwright未执行,无AI评估数据',
690
+ stepScreenshots: [],
691
+ screenshotPath: '',
692
+ videoPath: '',
693
+ tracePath: '',
694
+ keveScreenshots: [],
695
+ errorCategory: undefined,
696
+ });
697
+ }
698
+ }
699
+ const passRate = total > 0 ? ((passed / total) * 100).toFixed(1) : '0';
700
+ const summaryJson = {
701
+ status: failed > 0 ? 'partial' : 'success',
702
+ mr_id: testCasesData.mr_id, description: testCasesData.description,
703
+ docs_url: testCasesData.docs_url,
704
+ generated_at: new Date().toISOString().split('T')[0],
705
+ round: currentRound,
706
+ phases: { phase1: 'completed', phase2: 'completed', phase3: 'completed', phase4: 'completed' },
707
+ summary: { total, passed, failed, skipped, pending: 0, passRate: `${passRate}%` },
708
+ confidenceDist: {
709
+ '95-100%可信': confidenceDist['95-100'],
710
+ '61-90%需分析': confidenceDist['61-90'],
711
+ '41-60%需分析极优先': confidenceDist['41-60'],
712
+ '0-40%不可信': confidenceDist['0-40'],
713
+ },
714
+ artifacts: {
715
+ testCases: '.keve/task_xxx/test-cases.yaml',
716
+ poms: 'playwright_spec/poms/',
717
+ specs: 'playwright_spec/ai-generated/',
718
+ report: `.keve/reports/round-${currentRound}/summary-report.html`,
719
+ playwrightReport: '.keve/playwright-report/index.html',
720
+ },
721
+ failedCases: caseResults.filter(c => c.status === 'failed').map(c => ({
722
+ caseId: c.caseId, reason: (c.error || '').substring(0, 200),
723
+ type: c.data, confidence: c.confidence, phase: 'phase3',
724
+ })),
725
+ nextSteps: [
726
+ `查看完整报告: .keve/reports/round-${currentRound}/summary-report.html`,
727
+ `重跑失败用例: keve run --retry-from=${currentRound}`,
728
+ ],
729
+ };
730
+ const reportData = {
731
+ summary: summaryJson, yamlData: testCasesData, caseResults,
732
+ round: currentRound, history: [], roundDir, latestDir, reportsDir: REPORTS_DIR,
733
+ };
734
+ return reportData;
735
+ }