@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.
Files changed (35) hide show
  1. package/README.md +20 -6
  2. package/bin/sync-assets.test.mjs +4 -0
  3. package/mcp/code-mcp-pr/README.md +3 -2
  4. package/mcp/code-mcp-pr/azure-devops.js +81 -2
  5. package/mcp/code-mcp-pr/index.js +34 -1
  6. package/mcp/code-mcp-pr/index.test.js +161 -11
  7. package/mcp/trace-mcp-recorder/.env.example +6 -0
  8. package/mcp/trace-mcp-recorder/README.md +58 -0
  9. package/mcp/trace-mcp-recorder/config.toml.example +11 -0
  10. package/mcp/trace-mcp-recorder/index.js +469 -0
  11. package/mcp/trace-mcp-recorder/index.test.js +179 -0
  12. package/mcp/trace-mcp-recorder/package.json +25 -0
  13. package/package.json +9 -2
  14. package/plugins/autobest-delivery/.codex-plugin/plugin.json +1 -0
  15. package/plugins/autobest-delivery/README.md +25 -4
  16. package/plugins/autobest-delivery/mcp-server/package.json +1 -1
  17. package/plugins/autobest-delivery/mcp-server/src/fast-runner.mjs +470 -0
  18. package/plugins/autobest-delivery/mcp-server/src/paths.mjs +12 -7
  19. package/plugins/autobest-delivery/mcp-server/src/report.mjs +57 -12
  20. package/plugins/autobest-delivery/mcp-server/src/server.mjs +167 -2
  21. package/plugins/autobest-delivery/mcp-server/tests/fast-runner.test.mjs +232 -0
  22. package/plugins/autobest-delivery/mcp-server/tests/mcp-smoke.test.mjs +71 -0
  23. package/plugins/autobest-delivery/mcp-server/tests/report.test.mjs +69 -1
  24. package/plugins/autobest-delivery/skills/code-audit/SKILL.md +3 -3
  25. package/plugins/autobest-delivery/skills/delivery-loop/references/delivery-contract.md +11 -0
  26. package/plugins/autobest-delivery/skills/delivery-loop-fast/SKILL.md +62 -0
  27. package/plugins/autobest-delivery/skills/delivery-loop-fast/agents/openai.yaml +7 -0
  28. package/plugins/autobest-delivery/skills/export-report/SKILL.md +3 -3
  29. package/plugins/autobest-delivery/skills/export-report/agents/openai.yaml +1 -2
  30. package/skills/README.md +2 -0
  31. package/skills/common/code-pr-submit/SKILL.md +25 -4
  32. package/skills/common/repo-setup/SKILL.md +91 -0
  33. package/skills/common/repo-setup/agents/openai.yaml +4 -0
  34. package/skills/common/trace-recorder/SKILL.md +25 -0
  35. 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
- const checker = await readJson(checkerPath, 'Checker 结果');
568
- const runnerPath = await selectRunnerResult(checker, context);
569
- if (!runnerPath) {
570
- throw new Error(`缺少 Runner 结果:${path.join(featureDir, 'e2e', 'runs')}`);
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
- '调用 run_feature_e2e 前先调用 check_environment。headed 运行在 Blocked 时可保留浏览器,随后通过 resolve_blocked_run 记录人工决议并关闭会话。'
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: {