@autobest-ui/agent 1.0.10 → 1.0.11

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 (26) hide show
  1. package/bin/sync-assets.test.mjs +2 -0
  2. package/mcp/code-mcp-pr/README.md +3 -2
  3. package/mcp/code-mcp-pr/azure-devops.js +81 -2
  4. package/mcp/code-mcp-pr/index.js +34 -1
  5. package/mcp/code-mcp-pr/index.test.js +161 -11
  6. package/package.json +1 -1
  7. package/plugins/autobest-delivery/.codex-plugin/plugin.json +1 -0
  8. package/plugins/autobest-delivery/README.md +25 -4
  9. package/plugins/autobest-delivery/mcp-server/package.json +1 -1
  10. package/plugins/autobest-delivery/mcp-server/src/fast-runner.mjs +470 -0
  11. package/plugins/autobest-delivery/mcp-server/src/paths.mjs +12 -7
  12. package/plugins/autobest-delivery/mcp-server/src/report.mjs +57 -12
  13. package/plugins/autobest-delivery/mcp-server/src/server.mjs +167 -2
  14. package/plugins/autobest-delivery/mcp-server/tests/fast-runner.test.mjs +232 -0
  15. package/plugins/autobest-delivery/mcp-server/tests/mcp-smoke.test.mjs +71 -0
  16. package/plugins/autobest-delivery/mcp-server/tests/report.test.mjs +69 -1
  17. package/plugins/autobest-delivery/skills/code-audit/SKILL.md +3 -3
  18. package/plugins/autobest-delivery/skills/delivery-loop/references/delivery-contract.md +11 -0
  19. package/plugins/autobest-delivery/skills/delivery-loop-fast/SKILL.md +62 -0
  20. package/plugins/autobest-delivery/skills/delivery-loop-fast/agents/openai.yaml +7 -0
  21. package/plugins/autobest-delivery/skills/export-report/SKILL.md +3 -3
  22. package/plugins/autobest-delivery/skills/export-report/agents/openai.yaml +1 -2
  23. package/skills/README.md +1 -0
  24. package/skills/common/code-pr-submit/SKILL.md +25 -4
  25. package/skills/common/repo-setup/SKILL.md +91 -0
  26. package/skills/common/repo-setup/agents/openai.yaml +4 -0
@@ -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: {
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { createHash } from 'node:crypto';
3
- import { mkdtemp, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
4
  import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import test from 'node:test';
@@ -202,6 +202,74 @@ test('REQ 分节规格可从检查映射导出并嵌入报告截图', async () =
202
202
  assert.match(workbook.getWorksheet('自测报告').getCell('C2').value, /^API 数据:模拟 API。/);
203
203
  });
204
204
 
205
+ test('交互式快速自测结果无需 E2E 场景和 Checker 即可导出报告', async t => {
206
+ const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), 'autobest-fast-report-'));
207
+ t.after(() => rm(workspaceRoot, { recursive: true, force: true }));
208
+ await mkdir(path.join(workspaceRoot, '.git'));
209
+ const featureDir = path.join(workspaceRoot, '.scratch', 'fast-feature');
210
+ const runDir = path.join(featureDir, 'fast-check', 'runs', 'iteration-01');
211
+ const capturePath = path.join(runDir, 'captures', 'heading-desktop.png');
212
+ await mkdir(path.dirname(capturePath), { recursive: true });
213
+ await writeFile(capturePath, png);
214
+ await writeFile(
215
+ path.join(featureDir, 'spec.md'),
216
+ '# 页面规格\n\n## 用户故事\n\n1. 页面显示标题。\n'
217
+ );
218
+ const relativeCapture = path.relative(workspaceRoot, capturePath).split(path.sep).join('/');
219
+ await writeFile(
220
+ path.join(runDir, 'fast-check-result.json'),
221
+ JSON.stringify({
222
+ evidenceSchemaVersion: 1,
223
+ evidenceSource: 'interactive-playwright',
224
+ status: 'passed',
225
+ iteration: 1,
226
+ scenarioPath: null,
227
+ scenarioSha256: null,
228
+ apiMode: 'none',
229
+ metadata: {
230
+ apiMode: 'none',
231
+ reportCaptures: [{
232
+ capture: 'heading-desktop.png',
233
+ locator: 'h1',
234
+ reportModule: '示例页面',
235
+ reportGroup: 'page-heading',
236
+ reportTitle: '页面标题',
237
+ reportMethod: '通过交互式 Playwright 检查标题并截图。',
238
+ reportDevice: 'desktop'
239
+ }],
240
+ visualMappings: []
241
+ },
242
+ checks: [{
243
+ id: 'story-01',
244
+ specSnippet: '页面显示标题。',
245
+ reportModule: '示例页面',
246
+ reportGroup: 'page-heading',
247
+ reportTitle: '页面标题',
248
+ reportMethod: '通过交互式 Playwright 检查标题并截图。',
249
+ expect: '标题可见',
250
+ actual: '标题可见',
251
+ isPass: true
252
+ }],
253
+ captures: [relativeCapture]
254
+ })
255
+ );
256
+
257
+ const result = await exportDeliveryReport({ featureDir, workspaceRoot, templatePath });
258
+ assert.equal(result.evidenceSource, 'interactive-playwright');
259
+ assert.equal(result.checkerStatus, null);
260
+ assert.equal(result.runnerStatus, null);
261
+ assert.equal(result.fastCheckStatus, 'passed');
262
+ assert.equal(result.apiMode, 'none');
263
+ assert.equal(result.rows, 1);
264
+ assert.equal(result.embeddedScreenshots, 1);
265
+
266
+ const workbook = new ExcelJS.Workbook();
267
+ await workbook.xlsx.readFile(result.outputPath);
268
+ const worksheet = workbook.getWorksheet('自测报告');
269
+ assert.equal(worksheet.getCell('B2').value, '页面标题');
270
+ assert.match(worksheet.getCell('C2').value, /^API 数据:本次未指定业务 API。/);
271
+ });
272
+
205
273
  test('旧证据必须提供完整语义分组清单,不能按 scene 猜测功能', async () => {
206
274
  const { workspaceRoot, featureDir, runDir, desktop, mobile } = await createFixture();
207
275
  const runnerPath = path.join(runDir, 'runner-result.json');
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: code-audit
3
- description: 在独立验收者通过后,从固定审查点检查交付改动是否符合仓库规范和功能规格。
3
+ description: 在标准独立验收或快速交互式自测通过后,从固定审查点检查交付改动是否符合仓库规范和功能规格。
4
4
  ---
5
5
 
6
6
  # 代码审计
@@ -11,12 +11,12 @@ description: 在独立验收者通过后,从固定审查点检查交付改动
11
11
 
12
12
  - 可解析的固定审查点。
13
13
  - 功能规格及同目录的 scope。
14
- - 同目录存在时的 `spec-traceability.json`,以及 Maker、Checker 的逐项需求结果。
14
+ - 同目录存在时的 `spec-traceability.json`。标准模式读取 Maker、Checker 的逐项需求结果;快速模式读取 `delivery-state.json` 和最新 `fast-check-result.json`。
15
15
  - 默认审查 `worktree`,也可审查用户明确指定的 `committed` 改动。
16
16
 
17
17
  ## 流程
18
18
 
19
- 追踪模式下,Spec 审查轴必须逐个核对 active REQ ID 是否同时出现在 spec、Maker 结论、相关代码和 Checker 证据中;不得用整体通过代替单项覆盖,也不得接受未知或重编号的需求。
19
+ 追踪模式下,Spec 审查轴必须逐个核对 active REQ ID 是否同时出现在 spec、实现结论、相关代码和 Checker 或快速自测证据中;不得用整体通过代替单项覆盖,也不得接受未知或重编号的需求。快速模式证据只能称为实现者交互式自测,不能表述成独立 Checker 验收。
20
20
 
21
21
  1. 只冻结一次审查范围。审查工作树时纳入与任务相关的暂存、未暂存及新增文件,保留并排除无关的既有改动。
22
22
  2. 阅读适用的仓库规则、spec、scope、提交列表、改动文件和完整 diff。
@@ -19,9 +19,20 @@
19
19
  | `review/code-audit-result.json` 和 `.md` | 代码审计者 | 只读 |
20
20
  | `delivery/delivery-state.json` | 编排者 | 只读 |
21
21
  | `delivery/code-craft-result.json` | 实现者 | 只读 |
22
+ | `fast-check/runs/iteration-NN/fast-check-result.json`、`.md`、Trace 和截图 | 快速模式 MCP | 只读 |
22
23
 
23
24
  旧 `e2e.feature.spec.ts` 文件不是本契约的可执行输入。保持这些文件不变;缺少标准 `.mjs` 场景时生成新场景。
24
25
 
26
+ ## 快速交付证据
27
+
28
+ `delivery-loop-fast` 是用户显式选择的快速模式。当前 Agent 同时实现业务代码并使用 MCP 交互式 Playwright 工具自测,不启动独立测试编写者或 UI Checker,不生成 `e2e/e2e.feature.mjs`、`e2e/author-result.json` 和 `checker/checker-result.*`。这条路径不进行风险分类,也不因改动类型自动切换到完整流程。
29
+
30
+ 每轮快速自测写入 `fast-check/runs/iteration-NN/fast-check-result.json` 和 `.md`、`trace.zip` 及 `captures/`。结果必须使用 `evidenceSchemaVersion: 1`、`evidenceSource: interactive-playwright`,包含实际检查、报告分组、截图映射、API 模式、控制台错误和网络失败。`real` 表示真实 API,`mock` 表示受控响应,`none` 表示本次没有指定需要证明的业务 API;只有 mock 模式能安装请求响应。
31
+
32
+ 每项检查使用稳定 ID,并包含 `specSnippet`、`expect`、`actual`、`isPass`、`reportModule`、`reportGroup`、`reportTitle` 和 `reportMethod`;追踪模式同时包含 manifest 中的 `requirementId`。每个报告功能组至少有一个 `captureRecords` 条目,包含相同的报告字段、`reportDevice`、工作区相对路径和 SHA-256。截图是运行时状态证据,不是独立验收或 Figma 一致性结论。
33
+
34
+ 业务代码或交互步骤修复后必须开始新的 iteration;旧结果保留但失效。最终 `passed` 需要最新快速自测通过和全新独立 `$code-audit` 通过。代码审计读取快速证据,但不能把它升级描述为独立 Checker 证据。`export-report` 可以直接读取最新快速结果,并在摘要中标记 `evidenceSource: interactive-playwright`。
35
+
25
36
  ## 需求追踪
26
37
 
27
38
  当功能目录包含 `spec-traceability.json` 时,交付必须按其中的 active REQ ID 运行,编号格式为 `REQ-<页面或Scope简称>-<三位序号>`。编号由上游 `ui-prd-scope` 所有;本插件的编排者、Maker、测试编写者、Checker 和 Audit 只能引用,不能创建、修改或重排。开始 Maker 前必须确认 scope、review、spec 及其 SHA-256 一致。