@autobest-ui/agent 1.0.10 → 1.0.12
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 +20 -6
- package/bin/sync-assets.test.mjs +4 -0
- package/mcp/code-mcp-pr/README.md +3 -2
- package/mcp/code-mcp-pr/azure-devops.js +81 -2
- package/mcp/code-mcp-pr/index.js +34 -1
- package/mcp/code-mcp-pr/index.test.js +161 -11
- package/mcp/trace-mcp-recorder/.env.example +6 -0
- package/mcp/trace-mcp-recorder/README.md +58 -0
- package/mcp/trace-mcp-recorder/config.toml.example +11 -0
- package/mcp/trace-mcp-recorder/index.js +469 -0
- package/mcp/trace-mcp-recorder/index.test.js +179 -0
- package/mcp/trace-mcp-recorder/package.json +25 -0
- package/package.json +9 -2
- package/plugins/autobest-delivery/.codex-plugin/plugin.json +1 -0
- package/plugins/autobest-delivery/README.md +25 -4
- package/plugins/autobest-delivery/mcp-server/package.json +1 -1
- package/plugins/autobest-delivery/mcp-server/src/fast-runner.mjs +470 -0
- package/plugins/autobest-delivery/mcp-server/src/paths.mjs +12 -7
- package/plugins/autobest-delivery/mcp-server/src/report.mjs +57 -12
- package/plugins/autobest-delivery/mcp-server/src/server.mjs +167 -2
- package/plugins/autobest-delivery/mcp-server/tests/fast-runner.test.mjs +232 -0
- package/plugins/autobest-delivery/mcp-server/tests/mcp-smoke.test.mjs +71 -0
- package/plugins/autobest-delivery/mcp-server/tests/report.test.mjs +69 -1
- package/plugins/autobest-delivery/skills/code-audit/SKILL.md +3 -3
- package/plugins/autobest-delivery/skills/delivery-loop/references/delivery-contract.md +11 -0
- package/plugins/autobest-delivery/skills/delivery-loop-fast/SKILL.md +62 -0
- package/plugins/autobest-delivery/skills/delivery-loop-fast/agents/openai.yaml +7 -0
- package/plugins/autobest-delivery/skills/export-report/SKILL.md +3 -3
- package/plugins/autobest-delivery/skills/export-report/agents/openai.yaml +1 -2
- package/skills/README.md +2 -0
- package/skills/common/code-pr-submit/SKILL.md +25 -4
- package/skills/common/repo-setup/SKILL.md +91 -0
- package/skills/common/repo-setup/agents/openai.yaml +4 -0
- package/skills/common/trace-recorder/SKILL.md +25 -0
- package/skills/common/trace-recorder/agents/openai.yaml +11 -0
|
@@ -135,6 +135,27 @@ async function findLatestRunnerResult(featureDir) {
|
|
|
135
135
|
return null;
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
async function findLatestFastCheckResult(featureDir) {
|
|
139
|
+
const runsDir = path.join(featureDir, 'fast-check', 'runs');
|
|
140
|
+
let entries;
|
|
141
|
+
try {
|
|
142
|
+
entries = await readdir(runsDir, { withFileTypes: true });
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const candidates = entries
|
|
147
|
+
.filter(entry => entry.isDirectory() && /^iteration-\d+$/.test(entry.name))
|
|
148
|
+
.map(entry => ({
|
|
149
|
+
iteration: Number(entry.name.slice('iteration-'.length)),
|
|
150
|
+
filePath: path.join(runsDir, entry.name, 'fast-check-result.json')
|
|
151
|
+
}))
|
|
152
|
+
.sort((left, right) => right.iteration - left.iteration);
|
|
153
|
+
for (const candidate of candidates) {
|
|
154
|
+
if (await fileExists(candidate.filePath)) return candidate.filePath;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
async function selectRunnerResult(checker, context) {
|
|
139
160
|
if (checker.runnerResult) {
|
|
140
161
|
const declared = resolveEvidencePath(checker.runnerResult, context);
|
|
@@ -249,7 +270,7 @@ function resolveApiMode(runner) {
|
|
|
249
270
|
const resultMode = nonEmptyString(runner.apiMode);
|
|
250
271
|
const metadataMode = nonEmptyString(runner.metadata?.apiMode);
|
|
251
272
|
for (const mode of [resultMode, metadataMode].filter(Boolean)) {
|
|
252
|
-
if (!['real', 'mock'].includes(mode)) {
|
|
273
|
+
if (!['real', 'mock', 'none'].includes(mode)) {
|
|
253
274
|
throw new Error(`Runner 包含无效 apiMode:${mode}`);
|
|
254
275
|
}
|
|
255
276
|
}
|
|
@@ -267,6 +288,9 @@ function reportMethodWithApiMode(group, apiMode) {
|
|
|
267
288
|
if (apiMode === 'mock') {
|
|
268
289
|
return `API 数据:模拟 API。\n${method}`;
|
|
269
290
|
}
|
|
291
|
+
if (apiMode === 'none') {
|
|
292
|
+
return `API 数据:本次未指定业务 API。\n${method}`;
|
|
293
|
+
}
|
|
270
294
|
return method;
|
|
271
295
|
}
|
|
272
296
|
|
|
@@ -560,16 +584,35 @@ export async function exportDeliveryReport(options) {
|
|
|
560
584
|
if (!(await fileExists(specPath))) {
|
|
561
585
|
throw new Error(`缺少 spec.md:${specPath}`);
|
|
562
586
|
}
|
|
563
|
-
if (!(await fileExists(checkerPath))) {
|
|
564
|
-
throw new Error(`缺少 Checker 结果:${checkerPath}`);
|
|
565
|
-
}
|
|
566
587
|
const specText = await readFile(specPath, 'utf8');
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
588
|
+
let checker;
|
|
589
|
+
let runnerPath;
|
|
590
|
+
let evidenceSource;
|
|
591
|
+
if (await fileExists(checkerPath)) {
|
|
592
|
+
checker = await readJson(checkerPath, 'Checker 结果');
|
|
593
|
+
runnerPath = await selectRunnerResult(checker, context);
|
|
594
|
+
evidenceSource = 'independent-checker';
|
|
595
|
+
if (!runnerPath) {
|
|
596
|
+
throw new Error(`缺少 Runner 结果:${path.join(featureDir, 'e2e', 'runs')}`);
|
|
597
|
+
}
|
|
598
|
+
} else {
|
|
599
|
+
runnerPath = await findLatestFastCheckResult(featureDir);
|
|
600
|
+
if (!runnerPath) {
|
|
601
|
+
throw new Error(`缺少 Checker 或快速自测结果:${checkerPath}`);
|
|
602
|
+
}
|
|
603
|
+
const fastResult = await readJson(runnerPath, '快速自测结果');
|
|
604
|
+
if (fastResult.evidenceSource !== 'interactive-playwright') {
|
|
605
|
+
throw new Error(`快速自测结果包含无效 evidenceSource:${fastResult.evidenceSource}`);
|
|
606
|
+
}
|
|
607
|
+
checker = {
|
|
608
|
+
status: fastResult.status,
|
|
609
|
+
iteration: fastResult.iteration,
|
|
610
|
+
scenarioSha256: null,
|
|
611
|
+
checks: []
|
|
612
|
+
};
|
|
613
|
+
evidenceSource = 'interactive-playwright';
|
|
571
614
|
}
|
|
572
|
-
const runner = await readJson(runnerPath, 'Runner 结果');
|
|
615
|
+
const runner = await readJson(runnerPath, evidenceSource === 'interactive-playwright' ? '快速自测结果' : 'Runner 结果');
|
|
573
616
|
const apiMode = resolveApiMode(runner);
|
|
574
617
|
const checks = mergeChecks(checker, runner);
|
|
575
618
|
const stories = parseSpecItems(specText, checks);
|
|
@@ -649,14 +692,16 @@ export async function exportDeliveryReport(options) {
|
|
|
649
692
|
return {
|
|
650
693
|
outputPath,
|
|
651
694
|
iteration: runner.iteration ?? checker.iteration ?? null,
|
|
652
|
-
checkerStatus: checker.status,
|
|
653
|
-
runnerStatus: runner.status,
|
|
695
|
+
checkerStatus: evidenceSource === 'independent-checker' ? checker.status : null,
|
|
696
|
+
runnerStatus: evidenceSource === 'independent-checker' ? runner.status : null,
|
|
697
|
+
fastCheckStatus: evidenceSource === 'interactive-playwright' ? runner.status : null,
|
|
654
698
|
rows: groups.length,
|
|
655
699
|
statuses: counts,
|
|
656
700
|
embeddedScreenshots,
|
|
657
701
|
screenshotPlacements,
|
|
658
702
|
groupingSource: grouping.source,
|
|
659
703
|
groupingPath: grouping.groupingPath,
|
|
660
|
-
apiMode
|
|
704
|
+
apiMode,
|
|
705
|
+
evidenceSource
|
|
661
706
|
};
|
|
662
707
|
}
|
|
@@ -9,6 +9,17 @@ import {
|
|
|
9
9
|
resolveBlockedRun,
|
|
10
10
|
runFeatureE2E
|
|
11
11
|
} from './runner.mjs';
|
|
12
|
+
import {
|
|
13
|
+
abortFastCheck,
|
|
14
|
+
captureFastCheck,
|
|
15
|
+
closeAllFastChecks,
|
|
16
|
+
fastBrowserAction,
|
|
17
|
+
fastBrowserCheck,
|
|
18
|
+
fastBrowserSnapshot,
|
|
19
|
+
finishFastCheck,
|
|
20
|
+
mockFastCheckRoute,
|
|
21
|
+
startFastCheck
|
|
22
|
+
} from './fast-runner.mjs';
|
|
12
23
|
|
|
13
24
|
const server = new McpServer(
|
|
14
25
|
{
|
|
@@ -17,10 +28,164 @@ const server = new McpServer(
|
|
|
17
28
|
},
|
|
18
29
|
{
|
|
19
30
|
instructions:
|
|
20
|
-
'调用
|
|
31
|
+
'调用 E2E 或快速浏览器工具前先调用 check_environment。run_feature_e2e 执行冻结场景;start_fast_check 提供不生成场景文件的交互式自测。'
|
|
21
32
|
}
|
|
22
33
|
);
|
|
23
34
|
|
|
35
|
+
function jsonResult(result) {
|
|
36
|
+
return {
|
|
37
|
+
structuredContent: result,
|
|
38
|
+
content: [{ type: 'text', text: JSON.stringify(result) }]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
server.registerTool(
|
|
43
|
+
'start_fast_check',
|
|
44
|
+
{
|
|
45
|
+
title: '启动快速 Playwright 自测',
|
|
46
|
+
description: '启动无需 E2E 场景文件的交互式浏览器会话,并在工作区内准备快速证据目录。',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
workspaceRoot: z.string().min(1),
|
|
49
|
+
baseUrl: z.string().url(),
|
|
50
|
+
outputDir: z.string().min(1),
|
|
51
|
+
apiMode: z.enum(['real', 'mock', 'none']).default('none'),
|
|
52
|
+
iteration: z.number().int().positive().default(1),
|
|
53
|
+
headed: z.boolean().default(false),
|
|
54
|
+
viewport: z.object({
|
|
55
|
+
width: z.number().int().min(320).max(3840),
|
|
56
|
+
height: z.number().int().min(480).max(2160)
|
|
57
|
+
}).default({ width: 1400, height: 1000 }),
|
|
58
|
+
requirementIds: z.array(z.string().regex(/^REQ-[A-Z][A-Z0-9]{1,15}-\d{3}$/)).default([]),
|
|
59
|
+
sessionTtlMs: z.number().int().min(60000).max(3600000).default(3600000)
|
|
60
|
+
},
|
|
61
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
62
|
+
},
|
|
63
|
+
async input => jsonResult(await startFastCheck(input))
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
server.registerTool(
|
|
67
|
+
'fast_browser_snapshot',
|
|
68
|
+
{
|
|
69
|
+
title: '读取快速自测页面',
|
|
70
|
+
description: '返回当前页面的 URL、标题和精简可访问性快照,供后续交互定位。',
|
|
71
|
+
inputSchema: {
|
|
72
|
+
sessionId: z.string().uuid(),
|
|
73
|
+
timeoutMs: z.number().int().min(1000).max(120000).default(10000)
|
|
74
|
+
},
|
|
75
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
76
|
+
},
|
|
77
|
+
async input => jsonResult(await fastBrowserSnapshot(input))
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
server.registerTool(
|
|
81
|
+
'fast_browser_action',
|
|
82
|
+
{
|
|
83
|
+
title: '执行快速自测页面操作',
|
|
84
|
+
description: '在快速自测会话中执行导航、点击、填写、选择、按键、等待或视口切换。',
|
|
85
|
+
inputSchema: {
|
|
86
|
+
sessionId: z.string().uuid(),
|
|
87
|
+
action: z.enum(['navigate', 'click', 'fill', 'select', 'press', 'wait', 'viewport']),
|
|
88
|
+
locator: z.string().optional(),
|
|
89
|
+
value: z.string().optional(),
|
|
90
|
+
url: z.string().optional(),
|
|
91
|
+
width: z.number().int().min(320).max(3840).optional(),
|
|
92
|
+
height: z.number().int().min(480).max(2160).optional(),
|
|
93
|
+
timeoutMs: z.number().int().min(1000).max(120000).default(10000)
|
|
94
|
+
},
|
|
95
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
96
|
+
},
|
|
97
|
+
async input => jsonResult(await fastBrowserAction(input))
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
server.registerTool(
|
|
101
|
+
'mock_fast_check_route',
|
|
102
|
+
{
|
|
103
|
+
title: '设置快速自测 Mock',
|
|
104
|
+
description: '仅在 mock 模式为指定 HTTP method 和 pathname 设置 JSON 响应。',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
sessionId: z.string().uuid(),
|
|
107
|
+
method: z.string().regex(/^[A-Z]+$/).default('GET'),
|
|
108
|
+
pathname: z.string().startsWith('/'),
|
|
109
|
+
status: z.number().int().min(100).max(599).default(200),
|
|
110
|
+
body: z.unknown().default({})
|
|
111
|
+
},
|
|
112
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
113
|
+
},
|
|
114
|
+
async input => jsonResult(await mockFastCheckRoute(input))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const reportFields = {
|
|
118
|
+
requirementId: z.string().optional(),
|
|
119
|
+
reportModule: z.string().min(1),
|
|
120
|
+
reportGroup: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
|
|
121
|
+
reportTitle: z.string().min(1),
|
|
122
|
+
reportMethod: z.string().min(1)
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
server.registerTool(
|
|
126
|
+
'fast_browser_check',
|
|
127
|
+
{
|
|
128
|
+
title: '记录快速自测检查',
|
|
129
|
+
description: '执行一个结构化页面检查并记录可供报告导出的检查证据。',
|
|
130
|
+
inputSchema: {
|
|
131
|
+
sessionId: z.string().uuid(),
|
|
132
|
+
id: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
|
|
133
|
+
...reportFields,
|
|
134
|
+
specSnippet: z.string().min(1),
|
|
135
|
+
expect: z.string().min(1),
|
|
136
|
+
checkType: z.enum(['visible', 'hidden', 'text', 'value', 'count', 'url', 'attribute', 'css']),
|
|
137
|
+
locator: z.string().optional(),
|
|
138
|
+
expected: z.string().optional(),
|
|
139
|
+
expectedNumber: z.number().int().nonnegative().optional(),
|
|
140
|
+
property: z.string().optional(),
|
|
141
|
+
exact: z.boolean().default(false),
|
|
142
|
+
timeoutMs: z.number().int().min(1000).max(120000).default(10000)
|
|
143
|
+
},
|
|
144
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
145
|
+
},
|
|
146
|
+
async input => jsonResult(await fastBrowserCheck(input))
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
server.registerTool(
|
|
150
|
+
'capture_fast_check',
|
|
151
|
+
{
|
|
152
|
+
title: '保存快速自测报告截图',
|
|
153
|
+
description: '保存页面或组件 PNG,并记录功能分组和桌面端/移动端报告映射。',
|
|
154
|
+
inputSchema: {
|
|
155
|
+
sessionId: z.string().uuid(),
|
|
156
|
+
...reportFields,
|
|
157
|
+
capture: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.png$/),
|
|
158
|
+
locator: z.string().optional(),
|
|
159
|
+
reportDevice: z.enum(['desktop', 'mobile']),
|
|
160
|
+
fullPage: z.boolean().default(true)
|
|
161
|
+
},
|
|
162
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
163
|
+
},
|
|
164
|
+
async input => jsonResult(await captureFastCheck(input))
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
server.registerTool(
|
|
168
|
+
'finish_fast_check',
|
|
169
|
+
{
|
|
170
|
+
title: '完成快速 Playwright 自测',
|
|
171
|
+
description: '关闭会话并写入 fast-check-result.json、Markdown、Trace 和截图索引。',
|
|
172
|
+
inputSchema: { sessionId: z.string().uuid() },
|
|
173
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
174
|
+
},
|
|
175
|
+
async input => jsonResult(await finishFastCheck(input))
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
server.registerTool(
|
|
179
|
+
'abort_fast_check',
|
|
180
|
+
{
|
|
181
|
+
title: '放弃快速 Playwright 自测',
|
|
182
|
+
description: '关闭快速自测浏览器且不生成完成证据。',
|
|
183
|
+
inputSchema: { sessionId: z.string().uuid() },
|
|
184
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
185
|
+
},
|
|
186
|
+
async input => jsonResult(await abortFastCheck(input))
|
|
187
|
+
);
|
|
188
|
+
|
|
24
189
|
server.registerTool(
|
|
25
190
|
'check_environment',
|
|
26
191
|
{
|
|
@@ -198,7 +363,7 @@ await server.connect(transport);
|
|
|
198
363
|
|
|
199
364
|
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
200
365
|
process.once(signal, async () => {
|
|
201
|
-
await closeAllBlockedRuns();
|
|
366
|
+
await Promise.all([closeAllBlockedRuns(), closeAllFastChecks()]);
|
|
202
367
|
process.exit(0);
|
|
203
368
|
});
|
|
204
369
|
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
abortFastCheck,
|
|
9
|
+
activeFastCheckCount,
|
|
10
|
+
captureFastCheck,
|
|
11
|
+
fastBrowserAction,
|
|
12
|
+
fastBrowserCheck,
|
|
13
|
+
fastBrowserSnapshot,
|
|
14
|
+
finishFastCheck,
|
|
15
|
+
mockFastCheckRoute,
|
|
16
|
+
startFastCheck
|
|
17
|
+
} from '../src/fast-runner.mjs';
|
|
18
|
+
import { startFixtureServer } from './fixture-server.mjs';
|
|
19
|
+
|
|
20
|
+
const report = {
|
|
21
|
+
reportModule: '交付测试页',
|
|
22
|
+
reportGroup: 'reveal-result',
|
|
23
|
+
reportTitle: '结果展示',
|
|
24
|
+
reportMethod: '通过交互式 Playwright 点击按钮并检查结果文案。'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
test('交互式快速自测生成检查、截图、Trace 和结果文件,不生成场景代码', async t => {
|
|
28
|
+
const fixture = await startFixtureServer();
|
|
29
|
+
t.after(fixture.close);
|
|
30
|
+
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'autobest-fast-check-'));
|
|
31
|
+
t.after(() => fs.rm(workspaceRoot, { recursive: true, force: true }));
|
|
32
|
+
|
|
33
|
+
const started = await startFastCheck({
|
|
34
|
+
workspaceRoot,
|
|
35
|
+
baseUrl: fixture.baseUrl,
|
|
36
|
+
outputDir: 'feature/fast-check/runs/iteration-01',
|
|
37
|
+
iteration: 1
|
|
38
|
+
});
|
|
39
|
+
assert.equal(activeFastCheckCount(), 1);
|
|
40
|
+
|
|
41
|
+
const snapshot = await fastBrowserSnapshot({ sessionId: started.sessionId });
|
|
42
|
+
assert.match(snapshot.snapshot, /显示结果/);
|
|
43
|
+
|
|
44
|
+
const click = await fastBrowserAction({
|
|
45
|
+
sessionId: started.sessionId,
|
|
46
|
+
action: 'click',
|
|
47
|
+
locator: '#reveal'
|
|
48
|
+
});
|
|
49
|
+
assert.equal(click.success, true);
|
|
50
|
+
|
|
51
|
+
const checked = await fastBrowserCheck({
|
|
52
|
+
sessionId: started.sessionId,
|
|
53
|
+
id: 'story-01',
|
|
54
|
+
...report,
|
|
55
|
+
specSnippet: '点击按钮后显示运行结果。',
|
|
56
|
+
expect: '结果区域显示“运行器运行正常”',
|
|
57
|
+
checkType: 'text',
|
|
58
|
+
locator: '#result',
|
|
59
|
+
expected: '运行器运行正常',
|
|
60
|
+
timeoutMs: 10000
|
|
61
|
+
});
|
|
62
|
+
assert.equal(checked.check.isPass, true);
|
|
63
|
+
|
|
64
|
+
const captured = await captureFastCheck({
|
|
65
|
+
sessionId: started.sessionId,
|
|
66
|
+
...report,
|
|
67
|
+
capture: 'result-desktop.png',
|
|
68
|
+
locator: 'main',
|
|
69
|
+
reportDevice: 'desktop'
|
|
70
|
+
});
|
|
71
|
+
assert.match(captured.capture.path, /result-desktop\.png$/);
|
|
72
|
+
|
|
73
|
+
const result = await finishFastCheck({ sessionId: started.sessionId });
|
|
74
|
+
assert.equal(result.status, 'passed');
|
|
75
|
+
assert.equal(result.evidenceSource, 'interactive-playwright');
|
|
76
|
+
assert.equal(result.scenarioPath, null);
|
|
77
|
+
assert.equal(result.scenarioSha256, null);
|
|
78
|
+
assert.equal(result.checks.length, 1);
|
|
79
|
+
assert.equal(result.captureRecords.length, 1);
|
|
80
|
+
assert.equal(activeFastCheckCount(), 0);
|
|
81
|
+
await assert.rejects(
|
|
82
|
+
fastBrowserSnapshot({ sessionId: started.sessionId }),
|
|
83
|
+
/会话不存在或已结束/
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const resultPath = path.join(workspaceRoot, result.resultPath);
|
|
87
|
+
const persisted = JSON.parse(await fs.readFile(resultPath, 'utf8'));
|
|
88
|
+
assert.equal(persisted.status, 'passed');
|
|
89
|
+
await fs.access(path.join(workspaceRoot, result.captureRecords[0].path));
|
|
90
|
+
await fs.access(path.join(workspaceRoot, result.tracePath));
|
|
91
|
+
await fs.access(path.join(workspaceRoot, result.markdownPath));
|
|
92
|
+
await assert.rejects(fs.access(path.join(workspaceRoot, 'feature', 'e2e', 'e2e.feature.mjs')));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('快速自测失败会记录结果,会话也可以显式放弃', async t => {
|
|
96
|
+
const fixture = await startFixtureServer();
|
|
97
|
+
t.after(fixture.close);
|
|
98
|
+
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'autobest-fast-check-failed-'));
|
|
99
|
+
t.after(() => fs.rm(workspaceRoot, { recursive: true, force: true }));
|
|
100
|
+
|
|
101
|
+
const failedSession = await startFastCheck({
|
|
102
|
+
workspaceRoot,
|
|
103
|
+
baseUrl: fixture.baseUrl,
|
|
104
|
+
outputDir: 'feature/fast-check/runs/iteration-01'
|
|
105
|
+
});
|
|
106
|
+
await fastBrowserCheck({
|
|
107
|
+
sessionId: failedSession.sessionId,
|
|
108
|
+
id: 'story-01',
|
|
109
|
+
...report,
|
|
110
|
+
specSnippet: '结果应立即显示。',
|
|
111
|
+
expect: '结果文案可见',
|
|
112
|
+
checkType: 'text',
|
|
113
|
+
locator: '#result',
|
|
114
|
+
expected: '不存在的结果',
|
|
115
|
+
timeoutMs: 1000
|
|
116
|
+
});
|
|
117
|
+
await captureFastCheck({
|
|
118
|
+
sessionId: failedSession.sessionId,
|
|
119
|
+
...report,
|
|
120
|
+
capture: 'failed-desktop.png',
|
|
121
|
+
reportDevice: 'desktop'
|
|
122
|
+
});
|
|
123
|
+
assert.equal((await finishFastCheck({ sessionId: failedSession.sessionId })).status, 'failed');
|
|
124
|
+
|
|
125
|
+
const abortedSession = await startFastCheck({
|
|
126
|
+
workspaceRoot,
|
|
127
|
+
baseUrl: fixture.baseUrl,
|
|
128
|
+
outputDir: 'feature/fast-check/runs/iteration-02'
|
|
129
|
+
});
|
|
130
|
+
assert.deepEqual(await abortFastCheck({ sessionId: abortedSession.sessionId }), {
|
|
131
|
+
status: 'aborted',
|
|
132
|
+
sessionId: abortedSession.sessionId
|
|
133
|
+
});
|
|
134
|
+
assert.equal(activeFastCheckCount(), 0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('快速自测拒绝越界输出,且只有 mock 模式允许安装响应', async t => {
|
|
138
|
+
const fixture = await startFixtureServer();
|
|
139
|
+
t.after(fixture.close);
|
|
140
|
+
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'autobest-fast-check-policy-'));
|
|
141
|
+
t.after(() => fs.rm(workspaceRoot, { recursive: true, force: true }));
|
|
142
|
+
|
|
143
|
+
await assert.rejects(
|
|
144
|
+
startFastCheck({
|
|
145
|
+
workspaceRoot,
|
|
146
|
+
baseUrl: fixture.baseUrl,
|
|
147
|
+
outputDir: '../outside'
|
|
148
|
+
}),
|
|
149
|
+
/outputDir 必须位于 workspaceRoot 内/
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const realSession = await startFastCheck({
|
|
153
|
+
workspaceRoot,
|
|
154
|
+
baseUrl: fixture.baseUrl,
|
|
155
|
+
outputDir: 'feature/fast-check/runs/iteration-01',
|
|
156
|
+
apiMode: 'real'
|
|
157
|
+
});
|
|
158
|
+
await assert.rejects(
|
|
159
|
+
mockFastCheckRoute({
|
|
160
|
+
sessionId: realSession.sessionId,
|
|
161
|
+
pathname: '/api/status'
|
|
162
|
+
}),
|
|
163
|
+
/只有 mock 模式可以安装请求响应/
|
|
164
|
+
);
|
|
165
|
+
await abortFastCheck({ sessionId: realSession.sessionId });
|
|
166
|
+
|
|
167
|
+
const mockSession = await startFastCheck({
|
|
168
|
+
workspaceRoot,
|
|
169
|
+
baseUrl: fixture.baseUrl,
|
|
170
|
+
outputDir: 'feature/fast-check/runs/iteration-02',
|
|
171
|
+
apiMode: 'mock'
|
|
172
|
+
});
|
|
173
|
+
assert.deepEqual(
|
|
174
|
+
await mockFastCheckRoute({
|
|
175
|
+
sessionId: mockSession.sessionId,
|
|
176
|
+
pathname: '/api/status',
|
|
177
|
+
body: { source: 'mock' }
|
|
178
|
+
}),
|
|
179
|
+
{
|
|
180
|
+
sessionId: mockSession.sessionId,
|
|
181
|
+
method: 'GET',
|
|
182
|
+
pathname: '/api/status',
|
|
183
|
+
status: 200,
|
|
184
|
+
success: true
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
await abortFastCheck({ sessionId: mockSession.sessionId });
|
|
188
|
+
assert.equal(activeFastCheckCount(), 0);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('追踪需求或报告功能组缺少证据时快速自测不能通过', async t => {
|
|
192
|
+
const fixture = await startFixtureServer();
|
|
193
|
+
t.after(fixture.close);
|
|
194
|
+
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'autobest-fast-check-coverage-'));
|
|
195
|
+
t.after(() => fs.rm(workspaceRoot, { recursive: true, force: true }));
|
|
196
|
+
|
|
197
|
+
const started = await startFastCheck({
|
|
198
|
+
workspaceRoot,
|
|
199
|
+
baseUrl: fixture.baseUrl,
|
|
200
|
+
outputDir: 'feature/fast-check/runs/iteration-01',
|
|
201
|
+
requirementIds: ['REQ-PAGE-001', 'REQ-PAGE-002']
|
|
202
|
+
});
|
|
203
|
+
await fastBrowserCheck({
|
|
204
|
+
sessionId: started.sessionId,
|
|
205
|
+
id: 'req-page-001-heading',
|
|
206
|
+
requirementId: 'REQ-PAGE-001',
|
|
207
|
+
...report,
|
|
208
|
+
reportGroup: 'page-heading',
|
|
209
|
+
reportTitle: '页面标题',
|
|
210
|
+
specSnippet: '页面显示标题。',
|
|
211
|
+
expect: '标题可见',
|
|
212
|
+
checkType: 'visible',
|
|
213
|
+
locator: 'h1'
|
|
214
|
+
});
|
|
215
|
+
await captureFastCheck({
|
|
216
|
+
sessionId: started.sessionId,
|
|
217
|
+
requirementId: 'REQ-PAGE-001',
|
|
218
|
+
...report,
|
|
219
|
+
reportGroup: 'other-group',
|
|
220
|
+
capture: 'other-group-desktop.png',
|
|
221
|
+
reportDevice: 'desktop'
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await finishFastCheck({ sessionId: started.sessionId });
|
|
225
|
+
assert.equal(result.status, 'blocked');
|
|
226
|
+
assert.ok(result.blockers.some(blocker =>
|
|
227
|
+
blocker.kind === 'traceability' && blocker.detail.includes('REQ-PAGE-002')
|
|
228
|
+
));
|
|
229
|
+
assert.ok(result.blockers.some(blocker =>
|
|
230
|
+
blocker.kind === 'report-capture' && blocker.detail.includes('page-heading')
|
|
231
|
+
));
|
|
232
|
+
});
|
|
@@ -35,6 +35,21 @@ test('stdio MCP 能暴露环境信息并执行功能场景', async t => {
|
|
|
35
35
|
await client.connect(transport);
|
|
36
36
|
t.after(() => client.close());
|
|
37
37
|
|
|
38
|
+
const { tools } = await client.listTools();
|
|
39
|
+
const toolNames = tools.map(tool => tool.name);
|
|
40
|
+
for (const toolName of [
|
|
41
|
+
'start_fast_check',
|
|
42
|
+
'fast_browser_snapshot',
|
|
43
|
+
'fast_browser_action',
|
|
44
|
+
'mock_fast_check_route',
|
|
45
|
+
'fast_browser_check',
|
|
46
|
+
'capture_fast_check',
|
|
47
|
+
'finish_fast_check',
|
|
48
|
+
'abort_fast_check'
|
|
49
|
+
]) {
|
|
50
|
+
assert.ok(toolNames.includes(toolName), `缺少 MCP 工具 ${toolName}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
const environment = await client.callTool({
|
|
39
54
|
name: 'check_environment',
|
|
40
55
|
arguments: {}
|
|
@@ -57,6 +72,62 @@ test('stdio MCP 能暴露环境信息并执行功能场景', async t => {
|
|
|
57
72
|
assert.equal(execution.structuredContent.status, 'passed');
|
|
58
73
|
assert.equal(execution.structuredContent.checks.length, 2);
|
|
59
74
|
|
|
75
|
+
const fastStarted = await client.callTool({
|
|
76
|
+
name: 'start_fast_check',
|
|
77
|
+
arguments: {
|
|
78
|
+
workspaceRoot,
|
|
79
|
+
baseUrl: fixture.baseUrl,
|
|
80
|
+
outputDir: 'feature/fast-check/runs/iteration-01'
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const fastSessionId = fastStarted.structuredContent.sessionId;
|
|
84
|
+
const fastSnapshot = await client.callTool({
|
|
85
|
+
name: 'fast_browser_snapshot',
|
|
86
|
+
arguments: { sessionId: fastSessionId }
|
|
87
|
+
});
|
|
88
|
+
assert.match(fastSnapshot.structuredContent.snapshot, /显示结果/);
|
|
89
|
+
|
|
90
|
+
await client.callTool({
|
|
91
|
+
name: 'fast_browser_action',
|
|
92
|
+
arguments: { sessionId: fastSessionId, action: 'click', locator: '#reveal' }
|
|
93
|
+
});
|
|
94
|
+
const fastCheck = await client.callTool({
|
|
95
|
+
name: 'fast_browser_check',
|
|
96
|
+
arguments: {
|
|
97
|
+
sessionId: fastSessionId,
|
|
98
|
+
id: 'story-01',
|
|
99
|
+
reportModule: '交付测试页',
|
|
100
|
+
reportGroup: 'reveal-result',
|
|
101
|
+
reportTitle: '结果展示',
|
|
102
|
+
reportMethod: '通过交互式 Playwright 点击按钮并检查结果文案。',
|
|
103
|
+
specSnippet: '点击按钮后显示结果。',
|
|
104
|
+
expect: '结果文案可见',
|
|
105
|
+
checkType: 'text',
|
|
106
|
+
locator: '#result',
|
|
107
|
+
expected: '运行器运行正常'
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
assert.equal(fastCheck.structuredContent.check.isPass, true);
|
|
111
|
+
await client.callTool({
|
|
112
|
+
name: 'capture_fast_check',
|
|
113
|
+
arguments: {
|
|
114
|
+
sessionId: fastSessionId,
|
|
115
|
+
reportModule: '交付测试页',
|
|
116
|
+
reportGroup: 'reveal-result',
|
|
117
|
+
reportTitle: '结果展示',
|
|
118
|
+
reportMethod: '通过交互式 Playwright 点击按钮并检查结果文案。',
|
|
119
|
+
capture: 'result-desktop.png',
|
|
120
|
+
locator: 'main',
|
|
121
|
+
reportDevice: 'desktop'
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const fastFinished = await client.callTool({
|
|
125
|
+
name: 'finish_fast_check',
|
|
126
|
+
arguments: { sessionId: fastSessionId }
|
|
127
|
+
});
|
|
128
|
+
assert.equal(fastFinished.structuredContent.status, 'passed');
|
|
129
|
+
assert.equal(fastFinished.structuredContent.scenarioPath, null);
|
|
130
|
+
|
|
60
131
|
const rejectedPath = await client.callTool({
|
|
61
132
|
name: 'run_feature_e2e',
|
|
62
133
|
arguments: {
|