@epoint-testtech/ep-stage-skill 0.0.5 → 0.0.7
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/codex-skill/ep-stage/glue-run-test/SKILL.md +7 -3
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +26 -2
- package/dist/src/cli/run-report.d.ts +43 -0
- package/dist/src/cli/run-report.d.ts.map +1 -0
- package/dist/src/cli/run-report.js +292 -0
- package/dist/src/cli/run.d.ts +2 -0
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +51 -6
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: glue-run-test
|
|
3
3
|
description: 使用时机:当用户对某需求要「跑测试 / 验证 / 端到端执行 / 跑胶水测试 / 看报告」或显式触发 `/glue-run-test <testcase.json5路径>` 时使用。
|
|
4
|
-
version: 2026-08-12+
|
|
4
|
+
version: 2026-08-12+2f7c618
|
|
5
5
|
argument-hint: "<testcase.json5绝对/相对路径>"
|
|
6
6
|
compatibility: 需要由 stage-create 生成的 glue 模式 Playwright 项目 + 已审过的 `testcase.json5` + 同目录 `credentials.json5`。
|
|
7
7
|
metadata:
|
|
@@ -215,7 +215,9 @@ CLI 非 0 退出 → 复述 stderr,不进 Step 7。
|
|
|
215
215
|
| `<projectDir>/src/tests/<需求名>/.ep-stage/ai-runtime-trace.json5` | prepare-tasks 先写 tasks;run 回写 results / repairRounds |
|
|
216
216
|
| `<projectDir>/src/tests/<需求名>/.ep-stage/elementmap.json` | Agent O-D-A-V 落的页面控件定位清单(含 grid columns);run 不触碰 |
|
|
217
217
|
| `<projectDir>/src/tests/<需求名>/.ep-stage/repair-runs/<round>/repair-report.md` | 失败时才产;含失败分类与摘要,待人审确认 |
|
|
218
|
-
|
|
|
218
|
+
| `<projectDir>/e2e/glue-code-mvp/glue-report/<run-timestamp>/` | run 子命令产;执行账本(`index.html` glue 风格综合报告(全部用例)+ `playwright-results.json` + `run-output.log` 完整执行输出 + `run-info.json`) |
|
|
219
|
+
| `<projectDir>/playwright-report/index.html` | run 子命令随执行产;Playwright 综合报告(用例状态/错误/截图/trace 回放),`npx playwright show-report` 打开 |
|
|
220
|
+
| `<projectDir>/stage-report/` | run 子命令随执行产;stage-core 流程视角报告(index.html + report.pdf) |
|
|
219
221
|
|
|
220
222
|
## 4. 错误处理
|
|
221
223
|
|
|
@@ -238,7 +240,9 @@ Step 7 全部通过后,按以下中文格式呈现:
|
|
|
238
240
|
> 需求:`<需求名>`/项目:`<projectDir>`
|
|
239
241
|
> Spec:`<projectDir>/src/tests/<需求名>/crud.spec.ts`(glue 骨架 + Agent snippet 合并)
|
|
240
242
|
> AI Runtime Trace:`<projectDir>/src/tests/<需求名>/.ep-stage/ai-runtime-trace.json5`
|
|
241
|
-
>
|
|
243
|
+
> 报告目录:`<projectDir>/e2e/glue-code-mvp/glue-report/<run-timestamp>/`(run-output.log + run-info.json)
|
|
244
|
+
> 综合报告:`<projectDir>/e2e/glue-code-mvp/glue-report/<run-timestamp>/index.html`(glue 风格,覆盖全部用例)
|
|
245
|
+
> 技术细节:`<projectDir>/playwright-report/index.html`(trace/截图/视频)+ `<projectDir>/stage-report/`(流程 PDF)
|
|
242
246
|
> 运行版本:`version = v1`
|
|
243
247
|
>
|
|
244
248
|
> 下一步:① 调整 `testcase.json5` / `contract.json5` 后重跑 `/glue-run-test <testcase.json5路径>`;② 失败时查看 repair-report.md 确认修补方向后再重跑。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AAOA;;;;;;;;;;;GAWG;AACH,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/cli/index.ts"],"names":[],"mappings":";AAOA;;;;;;;;;;;GAWG;AACH,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,OAAO,CAAC,CAuD1E;AAED;;GAEG;AACH,wBAAgB,SAAS,IAAI,IAAI,CAchC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAS3F"}
|
package/dist/src/cli/index.js
CHANGED
|
@@ -34,8 +34,32 @@ export async function main(argv = process.argv) {
|
|
|
34
34
|
].join('\n'));
|
|
35
35
|
return result;
|
|
36
36
|
}
|
|
37
|
-
case 'run':
|
|
38
|
-
|
|
37
|
+
case 'run': {
|
|
38
|
+
const result = await runRun(parseRunArgs(args));
|
|
39
|
+
// 结果摘要回显(此前 run 静默退出,自动化无法感知成败)。
|
|
40
|
+
const lines = [
|
|
41
|
+
`spec: ${result.specPath}`,
|
|
42
|
+
`report: ${result.reportDir}`,
|
|
43
|
+
`trace: ${result.aiRuntimeTracePath}`,
|
|
44
|
+
];
|
|
45
|
+
if (result.comprehensiveReportPath) {
|
|
46
|
+
lines.push(`综合报告: ${result.comprehensiveReportPath}`);
|
|
47
|
+
}
|
|
48
|
+
if (result.repairReportPath) {
|
|
49
|
+
lines.push(`repair: ${result.repairReportPath}`);
|
|
50
|
+
}
|
|
51
|
+
if (result.credentialsNotice) {
|
|
52
|
+
lines.push(result.credentialsNotice);
|
|
53
|
+
}
|
|
54
|
+
console.log(lines.join('\n'));
|
|
55
|
+
// 执行失败(已落修补报告)以非 0 退出,对齐 skill 文档「CLI 非 0 退出 → 复述 stderr」契约;
|
|
56
|
+
// 修补流程本身不受影响,人审确认方向后重跑即可。
|
|
57
|
+
if (result.repairReportPath) {
|
|
58
|
+
console.error(`执行失败:已落修补报告 ${result.repairReportPath},待人审确认修补方向`);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
39
63
|
case undefined:
|
|
40
64
|
case '--help':
|
|
41
65
|
case '-h':
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-report.ts — run 子命令的综合报告渲染(glue 风格单文件 HTML)。
|
|
3
|
+
*
|
|
4
|
+
* 输入:Playwright JSON reporter 产物(PLAYWRIGHT_JSON_OUTPUT_NAME 指定落盘)。
|
|
5
|
+
* 输出:<reportDir>/index.html —— 与脚手架 stage-glue 报告同款样式
|
|
6
|
+
* (概览 + 统计卡片 + 状态色卡片 + 页脚原生报告入口),
|
|
7
|
+
* 但数据来自 Playwright 真实执行结果(覆盖全部用例,含 AI snippet)。
|
|
8
|
+
*/
|
|
9
|
+
export type RunReportTestItem = {
|
|
10
|
+
title: string;
|
|
11
|
+
suitePath: string;
|
|
12
|
+
projectName: string;
|
|
13
|
+
status: 'passed' | 'failed' | 'flaky' | 'skipped';
|
|
14
|
+
durationMs: number;
|
|
15
|
+
location?: string;
|
|
16
|
+
errorMessage?: string;
|
|
17
|
+
};
|
|
18
|
+
export type RunReportData = {
|
|
19
|
+
suiteTitle: string;
|
|
20
|
+
startedAt: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
specFile?: string;
|
|
23
|
+
items: RunReportTestItem[];
|
|
24
|
+
/** AI 任务标题集合(命中者标记为 Agent 推理执行)。 */
|
|
25
|
+
aiTaskTitles: string[];
|
|
26
|
+
/** 相对 reportDir 的链接。 */
|
|
27
|
+
playwrightReportHref?: string;
|
|
28
|
+
stageReportHref?: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* 把 Playwright JSON reporter 输出摊平为报告用例条目。
|
|
32
|
+
*/
|
|
33
|
+
export declare function parsePlaywrightJsonResults(jsonText: string): {
|
|
34
|
+
suiteTitle: string;
|
|
35
|
+
durationMs: number;
|
|
36
|
+
startedAt: string;
|
|
37
|
+
items: RunReportTestItem[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* 渲染 glue 风格综合报告(单文件、内联样式、无外部依赖)。
|
|
41
|
+
*/
|
|
42
|
+
export declare function renderRunReportHtml(data: RunReportData): string;
|
|
43
|
+
//# sourceMappingURL=run-report.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-report.d.ts","sourceRoot":"","sources":["../../../src/cli/run-report.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAClD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,iBAAiB,EAAE,CAAC;IAC3B,oCAAoC;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,wBAAwB;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AA4CF;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B,CAwCA;AA4DD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAkL/D"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-report.ts — run 子命令的综合报告渲染(glue 风格单文件 HTML)。
|
|
3
|
+
*
|
|
4
|
+
* 输入:Playwright JSON reporter 产物(PLAYWRIGHT_JSON_OUTPUT_NAME 指定落盘)。
|
|
5
|
+
* 输出:<reportDir>/index.html —— 与脚手架 stage-glue 报告同款样式
|
|
6
|
+
* (概览 + 统计卡片 + 状态色卡片 + 页脚原生报告入口),
|
|
7
|
+
* 但数据来自 Playwright 真实执行结果(覆盖全部用例,含 AI snippet)。
|
|
8
|
+
*/
|
|
9
|
+
function escapeHtml(value) {
|
|
10
|
+
return value
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"');
|
|
15
|
+
}
|
|
16
|
+
function mapTestStatus(status) {
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'expected':
|
|
19
|
+
return 'passed';
|
|
20
|
+
case 'unexpected':
|
|
21
|
+
return 'failed';
|
|
22
|
+
case 'flaky':
|
|
23
|
+
return 'flaky';
|
|
24
|
+
default:
|
|
25
|
+
return 'skipped';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 把 Playwright JSON reporter 输出摊平为报告用例条目。
|
|
30
|
+
*/
|
|
31
|
+
export function parsePlaywrightJsonResults(jsonText) {
|
|
32
|
+
const parsed = JSON.parse(jsonText);
|
|
33
|
+
const items = [];
|
|
34
|
+
const walk = (suite, trail) => {
|
|
35
|
+
const nextTrail = suite.title ? [...trail, suite.title] : trail;
|
|
36
|
+
for (const spec of suite.specs ?? []) {
|
|
37
|
+
for (const test of spec.tests ?? []) {
|
|
38
|
+
const lastResult = test.results?.[test.results.length - 1];
|
|
39
|
+
const errorMessage = lastResult?.error?.message ?? lastResult?.errors?.[0]?.message;
|
|
40
|
+
items.push({
|
|
41
|
+
title: spec.title,
|
|
42
|
+
suitePath: nextTrail.join(' › '),
|
|
43
|
+
projectName: test.projectName ?? '',
|
|
44
|
+
status: mapTestStatus(test.status),
|
|
45
|
+
durationMs: lastResult?.duration ?? 0,
|
|
46
|
+
location: spec.file !== undefined && spec.line !== undefined
|
|
47
|
+
? `${spec.file}:${spec.line}`
|
|
48
|
+
: spec.file,
|
|
49
|
+
errorMessage: errorMessage?.split('\n').slice(0, 12).join('\n'),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
for (const child of suite.suites ?? []) {
|
|
54
|
+
walk(child, nextTrail);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
for (const suite of parsed.suites ?? []) {
|
|
58
|
+
walk(suite, []);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
suiteTitle: parsed.suites?.[0]?.title ?? 'Playwright 测试套件',
|
|
62
|
+
durationMs: Math.round(parsed.stats?.duration ?? 0),
|
|
63
|
+
startedAt: parsed.stats?.startTime ?? '',
|
|
64
|
+
items,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const STATUS_LABEL = {
|
|
68
|
+
passed: '通过',
|
|
69
|
+
failed: '失败',
|
|
70
|
+
flaky: '不稳定',
|
|
71
|
+
skipped: '跳过',
|
|
72
|
+
};
|
|
73
|
+
const STATUS_BADGE_CLASS = {
|
|
74
|
+
passed: 'passed',
|
|
75
|
+
failed: 'failed',
|
|
76
|
+
flaky: 'needs_review',
|
|
77
|
+
skipped: 'unresolved',
|
|
78
|
+
};
|
|
79
|
+
function formatDuration(ms) {
|
|
80
|
+
if (ms >= 60_000) {
|
|
81
|
+
return `${Math.floor(ms / 60_000)}m ${Math.round((ms % 60_000) / 1000)}s`;
|
|
82
|
+
}
|
|
83
|
+
if (ms >= 1000) {
|
|
84
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
85
|
+
}
|
|
86
|
+
return `${ms}ms`;
|
|
87
|
+
}
|
|
88
|
+
function formatDisplayTimestamp(iso) {
|
|
89
|
+
if (!iso)
|
|
90
|
+
return '-';
|
|
91
|
+
const d = new Date(iso);
|
|
92
|
+
if (Number.isNaN(d.getTime()))
|
|
93
|
+
return iso;
|
|
94
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
95
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
96
|
+
}
|
|
97
|
+
function renderTestCard(item, strategy) {
|
|
98
|
+
const errorHtml = item.errorMessage
|
|
99
|
+
? `<div class="error"><strong>错误信息:</strong><pre>${escapeHtml(item.errorMessage)}</pre></div>`
|
|
100
|
+
: '';
|
|
101
|
+
return `
|
|
102
|
+
<article class="workflow workflow--${escapeHtml(STATUS_BADGE_CLASS[item.status])}" data-strategy="${escapeHtml(strategy)}" data-status="${escapeHtml(item.status)}">
|
|
103
|
+
<header class="workflow__header">
|
|
104
|
+
<div class="workflow__title">
|
|
105
|
+
<h2>${escapeHtml(item.title)}</h2>
|
|
106
|
+
<code class="workflow__id">${escapeHtml(item.location ?? '')}</code>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="workflow__meta">
|
|
109
|
+
<span class="badge badge--kind">${escapeHtml(item.projectName)}</span>
|
|
110
|
+
<span class="badge badge--source">${escapeHtml(strategy === 'agent_inferred' ? 'Agent推理执行' : '胶水编码执行')}</span>
|
|
111
|
+
<span class="badge badge--status badge--${escapeHtml(STATUS_BADGE_CLASS[item.status])}">${escapeHtml(STATUS_LABEL[item.status])}</span>
|
|
112
|
+
</div>
|
|
113
|
+
</header>
|
|
114
|
+
<dl class="workflow__body">
|
|
115
|
+
<dt>套件路径</dt><dd>${escapeHtml(item.suitePath)}</dd>
|
|
116
|
+
<dt>耗时</dt><dd>${escapeHtml(formatDuration(item.durationMs))}</dd>
|
|
117
|
+
</dl>
|
|
118
|
+
${errorHtml}
|
|
119
|
+
</article>
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 渲染 glue 风格综合报告(单文件、内联样式、无外部依赖)。
|
|
124
|
+
*/
|
|
125
|
+
export function renderRunReportHtml(data) {
|
|
126
|
+
const count = (s) => data.items.filter((i) => i.status === s).length;
|
|
127
|
+
const agentCount = data.items.filter((i) => data.aiTaskTitles.includes(i.title)).length;
|
|
128
|
+
const overviewRows = [
|
|
129
|
+
['测试套件', data.suiteTitle],
|
|
130
|
+
['执行时间', formatDisplayTimestamp(data.startedAt)],
|
|
131
|
+
['执行耗时', formatDuration(data.durationMs)],
|
|
132
|
+
['Spec 文件', data.specFile ?? '-'],
|
|
133
|
+
];
|
|
134
|
+
const overviewHtml = overviewRows
|
|
135
|
+
.map(([label, value]) => `<tr><th>${escapeHtml(label)}</th><td>${escapeHtml(value)}</td></tr>`)
|
|
136
|
+
.join('');
|
|
137
|
+
const strategyCards = [
|
|
138
|
+
['总测试流程', data.items.length, 'total', ''],
|
|
139
|
+
['胶水编码执行', data.items.length - agentCount, 'glue', 'glue_code'],
|
|
140
|
+
['Agent推理执行', agentCount, 'agent', 'agent_inferred'],
|
|
141
|
+
];
|
|
142
|
+
const resultCards = [
|
|
143
|
+
['通过', count('passed'), 'passed', 'passed'],
|
|
144
|
+
['失败', count('failed'), 'failed', 'failed'],
|
|
145
|
+
['不稳定', count('flaky'), 'review', 'flaky'],
|
|
146
|
+
['跳过', count('skipped'), 'unresolved', 'skipped'],
|
|
147
|
+
];
|
|
148
|
+
const renderCards = (cards, filterGroup) => cards
|
|
149
|
+
.map(([label, value, kind, filterValue]) => `
|
|
150
|
+
<div class="summary-card summary-card--${kind}" data-filter-group="${filterGroup}" data-filter-value="${filterValue}" role="button" tabindex="0">
|
|
151
|
+
<div class="summary-card__value">${value}</div>
|
|
152
|
+
<div class="summary-card__label">${escapeHtml(label)}</div>
|
|
153
|
+
</div>`)
|
|
154
|
+
.join('');
|
|
155
|
+
const workflowsHtml = data.items.length
|
|
156
|
+
? data.items
|
|
157
|
+
.map((item) => renderTestCard(item, data.aiTaskTitles.includes(item.title) ? 'agent_inferred' : 'glue_code'))
|
|
158
|
+
.join('\n')
|
|
159
|
+
: '<p class="muted">(本次运行未发现任何用例结果)</p>';
|
|
160
|
+
const footerLinks = [
|
|
161
|
+
data.playwrightReportHref
|
|
162
|
+
? `<a class="pw-link" href="${escapeHtml(data.playwrightReportHref)}" target="_blank" rel="noopener">Playwright 原生报告(trace / 截图 / 视频)</a>`
|
|
163
|
+
: '',
|
|
164
|
+
data.stageReportHref
|
|
165
|
+
? `<a class="pw-link pw-link--secondary" href="${escapeHtml(data.stageReportHref)}" target="_blank" rel="noopener">stage-core 流程报告(PDF)</a>`
|
|
166
|
+
: '',
|
|
167
|
+
]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join(' ');
|
|
170
|
+
const style = `
|
|
171
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; background: #f7f8fa; color: #1f2933; margin: 0; padding: 24px; }
|
|
172
|
+
h1 { margin: 0 0 16px; font-size: 22px; }
|
|
173
|
+
h2 { font-size: 16px; margin: 16px 0 8px; }
|
|
174
|
+
section.overview { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); margin-bottom: 24px; }
|
|
175
|
+
.overview-table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
|
|
176
|
+
.overview-table th, .overview-table td { text-align: left; padding: 6px 12px; border-bottom: 1px solid #eee; font-size: 13px; }
|
|
177
|
+
.overview-table th { width: 140px; color: #52606d; font-weight: 600; }
|
|
178
|
+
.summary-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
|
|
179
|
+
.summary-card { background: #f0f4f8; border-radius: 6px; padding: 12px; text-align: center; cursor: pointer; transition: box-shadow 0.15s, transform 0.1s; }
|
|
180
|
+
.summary-card:hover { box-shadow: 0 2px 6px rgba(0,0,0,0.1); transform: translateY(-1px); }
|
|
181
|
+
.summary-card--active { box-shadow: 0 0 0 2px #2563eb; }
|
|
182
|
+
.summary-card__value { font-size: 22px; font-weight: 600; }
|
|
183
|
+
.summary-card__label { font-size: 12px; color: #52606d; margin-top: 4px; }
|
|
184
|
+
.summary-card--passed { background: #e3fcec; }
|
|
185
|
+
.summary-card--failed { background: #ffe3e3; }
|
|
186
|
+
.summary-card--review { background: #fff4d6; }
|
|
187
|
+
.summary-card--unresolved { background: #f0e8ff; }
|
|
188
|
+
article.workflow { background: #fff; border-radius: 8px; padding: 16px 20px; margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border-left: 4px solid #cbd2d9; }
|
|
189
|
+
article.workflow--passed { border-left-color: #2f9e44; }
|
|
190
|
+
article.workflow--failed { border-left-color: #e03131; }
|
|
191
|
+
article.workflow--needs_review { border-left-color: #f59f00; }
|
|
192
|
+
article.workflow--unresolved { border-left-color: #7048e8; }
|
|
193
|
+
.workflow__header { display: flex; justify-content: space-between; align-items: flex-start; flex-wrap: wrap; gap: 12px; }
|
|
194
|
+
.workflow__title h2 { margin: 0; font-size: 16px; }
|
|
195
|
+
.workflow__id { font-size: 12px; color: #52606d; }
|
|
196
|
+
.workflow__meta { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
197
|
+
.badge { display: inline-block; padding: 2px 8px; font-size: 12px; border-radius: 12px; background: #e4e7eb; color: #1f2933; }
|
|
198
|
+
.badge--passed { background: #2f9e44; color: #fff; }
|
|
199
|
+
.badge--failed { background: #e03131; color: #fff; }
|
|
200
|
+
.badge--needs_review { background: #f59f00; color: #fff; }
|
|
201
|
+
.badge--unresolved { background: #7048e8; color: #fff; }
|
|
202
|
+
dl.workflow__body { display: grid; grid-template-columns: 100px 1fr; gap: 6px 12px; margin: 12px 0 4px; font-size: 13px; }
|
|
203
|
+
dl.workflow__body dt { color: #52606d; font-weight: 600; }
|
|
204
|
+
code { background: #f0f4f8; padding: 1px 4px; border-radius: 3px; font-size: 12px; }
|
|
205
|
+
.error pre { background: #fff5f5; color: #c92a2a; padding: 8px; border-radius: 4px; overflow-x: auto; font-size: 12px; white-space: pre-wrap; }
|
|
206
|
+
.muted { color: #829ab1; font-size: 13px; }
|
|
207
|
+
footer.footer { background: #fff; border-radius: 8px; padding: 16px 20px; margin-top: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); }
|
|
208
|
+
.pw-link { display: inline-block; padding: 8px 16px; background: #2563eb; color: #fff; border-radius: 4px; text-decoration: none; font-size: 14px; margin-right: 8px; }
|
|
209
|
+
.pw-link:hover { background: #1d4ed8; }
|
|
210
|
+
.pw-link--secondary { background: #334e68; }
|
|
211
|
+
.workflow-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin: 16px 0 8px; }
|
|
212
|
+
.workflow-toolbar h2 { margin: 0; }
|
|
213
|
+
.workflow-filters { display: flex; gap: 8px; }
|
|
214
|
+
.workflow-filters select { padding: 4px 8px; font-size: 13px; border: 1px solid #cbd2d9; border-radius: 4px; background: #fff; color: #1f2933; }
|
|
215
|
+
.summary-section-title { font-size: 13px; color: #52606d; margin: 12px 0 4px; font-weight: 600; }
|
|
216
|
+
`;
|
|
217
|
+
return `<!DOCTYPE html>
|
|
218
|
+
<html lang="zh-CN">
|
|
219
|
+
<head>
|
|
220
|
+
<meta charset="UTF-8" />
|
|
221
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
|
222
|
+
<title>胶水编码-测试执行</title>
|
|
223
|
+
<style>${style}</style>
|
|
224
|
+
</head>
|
|
225
|
+
<body>
|
|
226
|
+
<section class="overview">
|
|
227
|
+
<h1>胶水编码-测试执行(综合报告)</h1>
|
|
228
|
+
<table class="overview-table">${overviewHtml}</table>
|
|
229
|
+
<h3 class="summary-section-title">执行策略</h3>
|
|
230
|
+
<div class="summary-grid summary-grid--strategy">${renderCards(strategyCards, 'strategy')}</div>
|
|
231
|
+
<h3 class="summary-section-title">执行结果</h3>
|
|
232
|
+
<div class="summary-grid summary-grid--result">${renderCards(resultCards, 'status')}</div>
|
|
233
|
+
</section>
|
|
234
|
+
<section class="workflows">
|
|
235
|
+
<div class="workflow-toolbar">
|
|
236
|
+
<h2>用例执行明细</h2>
|
|
237
|
+
<div class="workflow-filters">
|
|
238
|
+
<select id="filter-strategy">
|
|
239
|
+
<option value="">全部执行策略</option>
|
|
240
|
+
<option value="glue_code">胶水编码执行</option>
|
|
241
|
+
<option value="agent_inferred">Agent推理执行</option>
|
|
242
|
+
</select>
|
|
243
|
+
<select id="filter-status">
|
|
244
|
+
<option value="">全部执行结果</option>
|
|
245
|
+
<option value="passed">通过</option>
|
|
246
|
+
<option value="failed">失败</option>
|
|
247
|
+
<option value="flaky">不稳定</option>
|
|
248
|
+
<option value="skipped">跳过</option>
|
|
249
|
+
</select>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
${workflowsHtml}
|
|
253
|
+
</section>
|
|
254
|
+
<footer class="footer">
|
|
255
|
+
${footerLinks || '<p class="muted">本次未发现关联报告产物</p>'}
|
|
256
|
+
</footer>
|
|
257
|
+
<script>
|
|
258
|
+
(function() {
|
|
259
|
+
var fs = document.getElementById('filter-strategy');
|
|
260
|
+
var fst = document.getElementById('filter-status');
|
|
261
|
+
var cards = document.querySelectorAll('article.workflow');
|
|
262
|
+
var summaryCards = document.querySelectorAll('.summary-card');
|
|
263
|
+
function apply() {
|
|
264
|
+
var s = fs.value, st = fst.value;
|
|
265
|
+
for (var i = 0; i < cards.length; i++) {
|
|
266
|
+
var c = cards[i];
|
|
267
|
+
var show = (!s || c.dataset.strategy === s) && (!st || c.dataset.status === st);
|
|
268
|
+
c.style.display = show ? '' : 'none';
|
|
269
|
+
}
|
|
270
|
+
for (var j = 0; j < summaryCards.length; j++) {
|
|
271
|
+
var card = summaryCards[j];
|
|
272
|
+
var select = card.dataset.filterGroup === 'strategy' ? fs : fst;
|
|
273
|
+
card.classList.toggle('summary-card--active', select.value !== '' && select.value === card.dataset.filterValue);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
fs.addEventListener('change', apply);
|
|
277
|
+
fst.addEventListener('change', apply);
|
|
278
|
+
for (var j = 0; j < summaryCards.length; j++) {
|
|
279
|
+
(function(card) {
|
|
280
|
+
card.addEventListener('click', function() {
|
|
281
|
+
var select = card.dataset.filterGroup === 'strategy' ? fs : fst;
|
|
282
|
+
select.value = select.value === card.dataset.filterValue ? '' : card.dataset.filterValue;
|
|
283
|
+
apply();
|
|
284
|
+
});
|
|
285
|
+
})(summaryCards[j]);
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
</script>
|
|
289
|
+
</body>
|
|
290
|
+
</html>
|
|
291
|
+
`;
|
|
292
|
+
}
|
package/dist/src/cli/run.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export type RunArgs = {
|
|
|
21
21
|
* @property version - 'v1':已接入 Agent snippets 合并、spec 执行和 repair 回路。
|
|
22
22
|
* @property aiRuntimeTracePath - 落盘的 .ep-stage/ai-runtime-trace.json5(含 tasks 与 results)。
|
|
23
23
|
* @property repairReportPath - 执行失败时落盘的 repair-report.md;成功时为 undefined。
|
|
24
|
+
* @property comprehensiveReportPath - glue 风格综合报告 <reportDir>/index.html;渲染失败时为 undefined。
|
|
24
25
|
*/
|
|
25
26
|
export type RunResult = {
|
|
26
27
|
specPath: string;
|
|
@@ -29,6 +30,7 @@ export type RunResult = {
|
|
|
29
30
|
version: 'v1';
|
|
30
31
|
aiRuntimeTracePath?: string;
|
|
31
32
|
repairReportPath?: string;
|
|
33
|
+
comprehensiveReportPath?: string;
|
|
32
34
|
};
|
|
33
35
|
/**
|
|
34
36
|
* runRun 的可注入依赖(测试即契约:允许单测替换推理与执行,避免触发真机)。
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/run.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAG/E,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,aAAa,EACnB,MAAM,gCAAgC,CAAC;
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/cli/run.ts"],"names":[],"mappings":"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"}
|
package/dist/src/cli/run.js
CHANGED
|
@@ -5,6 +5,7 @@ import JSON5 from 'json5';
|
|
|
5
5
|
import { validateTestcase } from '../validation/testcase.js';
|
|
6
6
|
import { classifyRunFailure, createInitialAiRuntimeTrace, nextRepairRound, writeAiRuntimeTrace, writeRepairReport, } from '../runtime/ai-runtime-trace.js';
|
|
7
7
|
import { runSpec } from './spec.js';
|
|
8
|
+
import { parsePlaywrightJsonResults, renderRunReportHtml } from './run-report.js';
|
|
8
9
|
/**
|
|
9
10
|
* AI 追加段分隔标记。重跑时 detect 已有标记则覆盖(保证幂等)。
|
|
10
11
|
*
|
|
@@ -120,9 +121,12 @@ export function buildPlaywrightCommand(input) {
|
|
|
120
121
|
const basename = path.basename(input.specPath);
|
|
121
122
|
const escapedBasename = basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
122
123
|
const specArg = `.*${escapedBasename}`;
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
124
|
+
// 枚举多个 reporter:list 保证控制台有可被 classifyRunFailure 消费的原生输出;
|
|
125
|
+
// json 落机器可读结果(经 PLAYWRIGHT_JSON_OUTPUT_NAME 指定文件)供 glue 风格综合报告渲染;
|
|
126
|
+
// html 与 stage-core reporter 让综合报告(playwright-report / stage-report)随 run 一并生成,
|
|
127
|
+
// 避免执行完还要手工补跑才能拿到汇总报告。CLI --reporter 会整体覆盖项目 config,
|
|
128
|
+
// 所以必须在同一条参数里显式枚举,不能只加 list。
|
|
129
|
+
const args = ['playwright', 'test', specArg, '--reporter=list,json,html,@epoint-testtech/stage-core/reporter'];
|
|
126
130
|
if (!input.headless) {
|
|
127
131
|
args.push('--headed');
|
|
128
132
|
}
|
|
@@ -146,6 +150,9 @@ async function defaultExecuteFullSpec(input) {
|
|
|
146
150
|
cwd: cmd.cwd,
|
|
147
151
|
encoding: 'utf8',
|
|
148
152
|
shell: true,
|
|
153
|
+
env: input.resultsJsonPath
|
|
154
|
+
? { ...process.env, PLAYWRIGHT_JSON_OUTPUT_NAME: input.resultsJsonPath }
|
|
155
|
+
: process.env,
|
|
149
156
|
});
|
|
150
157
|
const stdout = spawned.stdout ?? '';
|
|
151
158
|
const stderr = spawned.stderr ?? '';
|
|
@@ -502,17 +509,24 @@ export async function runRun(args, deps) {
|
|
|
502
509
|
}
|
|
503
510
|
appendAiSnippets(paths.specPath, aiRuntimeTasks, resolvedResults);
|
|
504
511
|
writeAiRuntimeTrace(tracePath, trace);
|
|
512
|
+
// 报告目录先于执行创建:json reporter 结果文件落这里,供 glue 风格综合报告渲染。
|
|
513
|
+
const reportDir = path.join(paths.projectDir, 'e2e/glue-code-mvp/glue-report', new Date().toISOString().replace(/[:.]/g, '-'));
|
|
514
|
+
mkdirSync(reportDir, { recursive: true });
|
|
515
|
+
const resultsJsonPath = path.join(reportDir, 'playwright-results.json');
|
|
505
516
|
// 执行完整 spec。
|
|
506
517
|
const executor = deps?.executeFullSpec ?? defaultExecuteFullSpec;
|
|
507
518
|
const execution = await executor({
|
|
508
519
|
projectDir: paths.projectDir,
|
|
509
520
|
specPath: paths.specPath,
|
|
510
521
|
headless: args.headless,
|
|
522
|
+
resultsJsonPath,
|
|
511
523
|
});
|
|
512
524
|
// 失败时分类并写 repair report,停止等待人审;成功时 repairReportPath 留空。
|
|
513
525
|
let repairReportPath;
|
|
526
|
+
let failureType;
|
|
514
527
|
if (!execution.passed) {
|
|
515
528
|
const classification = classifyRunFailure(execution.output);
|
|
529
|
+
failureType = classification.failureType;
|
|
516
530
|
const round = nextRepairRound(paths.caseDir);
|
|
517
531
|
repairReportPath = writeRepairReport({
|
|
518
532
|
caseDir: paths.caseDir,
|
|
@@ -529,9 +543,39 @@ export async function runRun(args, deps) {
|
|
|
529
543
|
});
|
|
530
544
|
writeAiRuntimeTrace(tracePath, trace);
|
|
531
545
|
}
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
|
|
546
|
+
// 报告目录:落执行账本(完整执行输出 + 机器可读 run 信息),
|
|
547
|
+
// 修补报告只截失败输出片段,完整证据以这里为准。
|
|
548
|
+
writeFileSync(path.join(reportDir, 'run-output.log'), execution.output);
|
|
549
|
+
writeFileSync(path.join(reportDir, 'run-info.json'), `${JSON.stringify({
|
|
550
|
+
specPath: paths.specPath,
|
|
551
|
+
passed: execution.passed,
|
|
552
|
+
failureType,
|
|
553
|
+
repairReportPath,
|
|
554
|
+
executedAt: new Date().toISOString(),
|
|
555
|
+
}, null, 2)}\n`);
|
|
556
|
+
// glue 风格综合报告(index.html):数据来自 Playwright JSON 结果,
|
|
557
|
+
// 覆盖全部用例(含 AI snippet);渲染失败只降级缺省,不阻断主流程。
|
|
558
|
+
let comprehensiveReportPath;
|
|
559
|
+
try {
|
|
560
|
+
if (existsSync(resultsJsonPath)) {
|
|
561
|
+
const parsedResults = parsePlaywrightJsonResults(readFileSync(resultsJsonPath, 'utf8'));
|
|
562
|
+
const html = renderRunReportHtml({
|
|
563
|
+
suiteTitle: parsedResults.suiteTitle,
|
|
564
|
+
startedAt: parsedResults.startedAt,
|
|
565
|
+
durationMs: parsedResults.durationMs,
|
|
566
|
+
specFile: paths.specPath,
|
|
567
|
+
items: parsedResults.items,
|
|
568
|
+
aiTaskTitles: aiRuntimeTasks.map((task) => task.title),
|
|
569
|
+
playwrightReportHref: '../../../../playwright-report/index.html',
|
|
570
|
+
stageReportHref: '../../../../stage-report/report.pdf',
|
|
571
|
+
});
|
|
572
|
+
comprehensiveReportPath = path.join(reportDir, 'index.html');
|
|
573
|
+
writeFileSync(comprehensiveReportPath, html);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
comprehensiveReportPath = undefined;
|
|
578
|
+
}
|
|
535
579
|
// credentials.json5 消费边界声明。
|
|
536
580
|
const credentialsNotice = loadCredentialsNotice(testcase, paths.testcasePath);
|
|
537
581
|
return {
|
|
@@ -541,5 +585,6 @@ export async function runRun(args, deps) {
|
|
|
541
585
|
version: 'v1',
|
|
542
586
|
aiRuntimeTracePath: tracePath,
|
|
543
587
|
repairReportPath,
|
|
588
|
+
comprehensiveReportPath,
|
|
544
589
|
};
|
|
545
590
|
}
|