@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.
- package/README.md +53 -0
- package/dist/bin/keve.d.ts +13 -0
- package/dist/bin/keve.js +164 -0
- package/dist/commands/auth-update.d.ts +16 -0
- package/dist/commands/auth-update.js +88 -0
- package/dist/commands/clean.d.ts +10 -0
- package/dist/commands/clean.js +78 -0
- package/dist/commands/config.d.ts +17 -0
- package/dist/commands/config.js +99 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +292 -0
- package/dist/commands/report.d.ts +12 -0
- package/dist/commands/report.js +159 -0
- package/dist/commands/run.d.ts +29 -0
- package/dist/commands/run.js +417 -0
- package/dist/commands/test.d.ts +12 -0
- package/dist/commands/test.js +102 -0
- package/dist/commands/vitest.d.ts +14 -0
- package/dist/commands/vitest.js +166 -0
- package/dist/config.d.ts +109 -0
- package/dist/config.js +291 -0
- package/dist/core/action-log-writer.d.ts +47 -0
- package/dist/{tools/write-file.js → core/action-log-writer.js} +42 -26
- package/dist/core/auth.d.ts +17 -0
- package/dist/core/auth.js +224 -0
- package/dist/core/cdp-resolver.d.ts +5 -0
- package/dist/core/cdp-resolver.js +93 -0
- package/dist/core/generateReport/report.d.ts +19 -0
- package/dist/core/generateReport/report.js +91 -0
- package/dist/core/generateReport/reportData.d.ts +215 -0
- package/dist/core/generateReport/reportData.js +735 -0
- package/dist/core/global-setup.d.ts +6 -0
- package/dist/core/global-setup.js +113 -0
- package/dist/core/keve-aspect.d.ts +54 -0
- package/dist/core/keve-aspect.js +52 -0
- package/dist/core/keve-decorators.d.ts +51 -0
- package/dist/core/keve-decorators.js +95 -0
- package/dist/core/keve-registry.d.ts +38 -0
- package/dist/core/keve-registry.js +25 -0
- package/dist/core/keve-report.d.ts +27 -0
- package/dist/core/keve-report.js +357 -0
- package/dist/core/keve-test.d.ts +36 -0
- package/dist/core/keve-test.js +364 -0
- package/dist/core/learned-actions.d.ts +24 -0
- package/dist/core/learned-actions.js +65 -0
- package/dist/core/pw-config.d.ts +19 -0
- package/dist/core/pw-config.js +180 -0
- package/dist/index.d.ts +8 -23
- package/dist/index.js +36 -43
- package/dist/page-agent/agent.d.ts +129 -0
- package/dist/page-agent/agent.js +585 -0
- package/dist/page-agent/hooks.d.ts +13 -0
- package/dist/page-agent/hooks.js +218 -0
- package/dist/page-agent/tools.d.ts +29 -0
- package/dist/page-agent/tools.js +297 -0
- package/doc/README.md +124 -0
- package/doc/agents/keve-dev-test-agent.md +140 -0
- package/doc/agents/keve-test-agent.md +178 -0
- 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
- package/doc/keve-agent-demo.html +370 -0
- package/doc/skills/keve-bddcase-gen/SKILL.md +332 -0
- package/doc/skills/keve-bddcase-gen/references/test-case-rules.md +91 -0
- package/doc/skills/keve-bddcase-gen/references/test-cases-example.md +103 -0
- package/doc/skills/keve-component-gen/SKILL.md +255 -0
- package/doc/skills/keve-component-gen/references/component-mock-strategy.md +74 -0
- package/doc/skills/keve-component-gen/references/component-model-template.md +98 -0
- package/doc/skills/keve-component-gen/references/component-test-template.md +100 -0
- package/doc/skills/keve-component-gen/references/vue-test-utils-guide.md +91 -0
- package/doc/skills/keve-e2e-gen/SKILL.md +179 -0
- package/doc/skills/keve-e2e-gen/references/code-analysis-guide.md +208 -0
- package/doc/skills/keve-e2e-gen/references/decisions.md +36 -0
- package/doc/skills/keve-e2e-gen/references/pom-generation-guide.md +140 -0
- package/doc/skills/keve-e2e-gen/references/script-generation-guide.md +236 -0
- package/doc/skills/keve-e2e-gen/references/test-auditor-guide.md +189 -0
- package/doc/skills/keve-e2e-gen/scripts/extract-components.sh +85 -0
- package/doc/skills/keve-e2e-gen/scripts/inject-testids.sh +95 -0
- package/doc/skills/keve-static-analysis/SKILL.md +362 -0
- package/doc/skills/keve-static-analysis/references/codegraph-analysis-guide.md +108 -0
- package/doc/skills/keve-static-analysis/references/git-impact-analysis.md +81 -0
- package/doc/skills/keve-static-analysis/references/lint-fix-guide.md +69 -0
- package/doc/skills/keve-static-analysis/references/runtime-risk-patterns.md +92 -0
- package/doc/skills/keve-static-analysis/references/security-scan-guide.md +32 -0
- package/doc/skills/keve-static-analysis/references/ts-error-patterns.md +98 -0
- package/doc/skills/keve-static-analysis/references/vue-security-guide.md +72 -0
- package/doc/skills/keve-test-eval/SKILL.md +78 -0
- package/doc/skills/keve-test-eval/references/common-fix-guide.md +166 -0
- package/doc/skills/keve-test-eval/references/confidence-data-guide.md +78 -0
- package/doc/skills/keve-test-eval/references/fix-loop-guide.md +67 -0
- package/doc/skills/keve-unit-gen/SKILL.md +203 -0
- package/doc/skills/keve-unit-gen/references/boundary-conditions.md +78 -0
- package/doc/skills/keve-unit-gen/references/unit-mock-strategy.md +71 -0
- package/doc/skills/keve-unit-gen/references/unit-test-template.md +105 -0
- package/package.json +47 -25
- package/dist/agent/factory.d.ts +0 -25
- package/dist/agent/factory.js +0 -99
- package/dist/agent/index.d.ts +0 -3
- package/dist/agent/index.js +0 -7
- package/dist/agent/loader.d.ts +0 -22
- package/dist/agent/loader.js +0 -117
- package/dist/agent/types.d.ts +0 -9
- package/dist/agent/types.js +0 -2
- package/dist/agent-loop/complete-task.d.ts +0 -28
- package/dist/agent-loop/complete-task.js +0 -70
- package/dist/agent-loop/index.d.ts +0 -16
- package/dist/agent-loop/index.js +0 -26
- package/dist/agent-loop/loop.d.ts +0 -42
- package/dist/agent-loop/loop.js +0 -282
- package/dist/agent-loop/runtime.d.ts +0 -40
- package/dist/agent-loop/runtime.js +0 -163
- package/dist/agent-loop/types.d.ts +0 -161
- package/dist/agent-loop/types.js +0 -105
- package/dist/context/index.d.ts +0 -8
- package/dist/context/index.js +0 -29
- package/dist/context/tokenCalculation.d.ts +0 -55
- package/dist/context/tokenCalculation.js +0 -95
- package/dist/context/truncation.d.ts +0 -67
- package/dist/context/truncation.js +0 -142
- package/dist/context/types.d.ts +0 -7
- package/dist/context/types.js +0 -27
- package/dist/llm/OpenAIClient.d.ts +0 -31
- package/dist/llm/OpenAIClient.js +0 -455
- package/dist/llm/constants.d.ts +0 -2
- package/dist/llm/constants.js +0 -6
- package/dist/llm/errors.d.ts +0 -25
- package/dist/llm/errors.js +0 -51
- package/dist/llm/index.d.ts +0 -49
- package/dist/llm/index.js +0 -158
- package/dist/llm/types.d.ts +0 -124
- package/dist/llm/types.js +0 -2
- package/dist/llm/utils.d.ts +0 -29
- package/dist/llm/utils.js +0 -284
- package/dist/skill/frontmatter-parser.d.ts +0 -50
- package/dist/skill/frontmatter-parser.js +0 -152
- package/dist/skill/index.d.ts +0 -12
- package/dist/skill/index.js +0 -17
- package/dist/skill/loader.d.ts +0 -96
- package/dist/skill/loader.js +0 -256
- package/dist/tools/ask-user.d.ts +0 -8
- package/dist/tools/ask-user.js +0 -47
- package/dist/tools/bash.d.ts +0 -47
- package/dist/tools/bash.js +0 -213
- package/dist/tools/edit-file.d.ts +0 -9
- package/dist/tools/edit-file.js +0 -76
- package/dist/tools/glob.d.ts +0 -8
- package/dist/tools/glob.js +0 -101
- package/dist/tools/grep.d.ts +0 -10
- package/dist/tools/grep.js +0 -76
- package/dist/tools/index.d.ts +0 -24
- package/dist/tools/index.js +0 -47
- package/dist/tools/read-files.d.ts +0 -8
- package/dist/tools/read-files.js +0 -81
- 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
|
+
}
|