@autobest-ui/agent 1.0.8 → 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.
- package/bin/sync-assets.test.mjs +2 -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/package.json +1 -1
- package/plugins/autobest-delivery/.codex-plugin/plugin.json +1 -0
- package/plugins/autobest-delivery/README.md +52 -10
- 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 +118 -16
- package/plugins/autobest-delivery/mcp-server/src/runner.mjs +163 -1
- package/plugins/autobest-delivery/mcp-server/src/server.mjs +173 -3
- package/plugins/autobest-delivery/mcp-server/tests/fast-runner.test.mjs +232 -0
- package/plugins/autobest-delivery/mcp-server/tests/fixture-server.mjs +5 -0
- package/plugins/autobest-delivery/mcp-server/tests/mcp-smoke.test.mjs +71 -0
- package/plugins/autobest-delivery/mcp-server/tests/report.test.mjs +107 -1
- package/plugins/autobest-delivery/mcp-server/tests/runner.test.mjs +279 -0
- package/plugins/autobest-delivery/skills/code-audit/SKILL.md +3 -3
- package/plugins/autobest-delivery/skills/delivery-loop/SKILL.md +10 -7
- package/plugins/autobest-delivery/skills/delivery-loop/references/delivery-contract.md +63 -6
- 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/e2e-gen-spec/SKILL.md +7 -6
- package/plugins/autobest-delivery/skills/e2e-ui-checker/SKILL.md +5 -4
- package/plugins/autobest-delivery/skills/export-report/SKILL.md +6 -6
- package/plugins/autobest-delivery/skills/export-report/agents/openai.yaml +1 -2
- package/skills/README.md +1 -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
|
@@ -19,6 +19,11 @@ const html = `<!doctype html>
|
|
|
19
19
|
|
|
20
20
|
export function startFixtureServer() {
|
|
21
21
|
const server = http.createServer((request, response) => {
|
|
22
|
+
if (request.url === '/api/status') {
|
|
23
|
+
response.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
24
|
+
response.end(JSON.stringify({ source: 'real-fixture-server' }));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
22
27
|
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
23
28
|
response.end(html);
|
|
24
29
|
});
|
|
@@ -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';
|
|
@@ -164,6 +164,112 @@ test('同一组件或功能合并为单 Sheet 一行,不输出原子明细', a
|
|
|
164
164
|
);
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
test('REQ 分节规格可从检查映射导出并嵌入报告截图', async () => {
|
|
168
|
+
const { workspaceRoot, featureDir, runDir } = await createFixture();
|
|
169
|
+
await writeFile(
|
|
170
|
+
path.join(featureDir, 'spec.md'),
|
|
171
|
+
'# 订单规格\n\n## REQ-ORDER-001 订单交互\n\n订单页展示标题、支持支付,并显示错误状态。\n'
|
|
172
|
+
);
|
|
173
|
+
const runnerPath = path.join(runDir, 'runner-result.json');
|
|
174
|
+
const runner = JSON.parse(await readFile(runnerPath, 'utf8'));
|
|
175
|
+
for (const check of runner.checks) {
|
|
176
|
+
check.requirementId = 'REQ-ORDER-001';
|
|
177
|
+
}
|
|
178
|
+
runner.metadata.reportCaptures = runner.metadata.visualMappings.map(mapping => ({
|
|
179
|
+
...mapping,
|
|
180
|
+
requirementId: 'REQ-ORDER-001',
|
|
181
|
+
locator: 'main'
|
|
182
|
+
}));
|
|
183
|
+
runner.apiMode = 'mock';
|
|
184
|
+
runner.metadata.apiMode = 'mock';
|
|
185
|
+
runner.metadata.visualMappings = [];
|
|
186
|
+
await writeFile(runnerPath, JSON.stringify(runner));
|
|
187
|
+
|
|
188
|
+
const checkerPath = path.join(featureDir, 'checker', 'checker-result.json');
|
|
189
|
+
const checker = JSON.parse(await readFile(checkerPath, 'utf8'));
|
|
190
|
+
checker.checks[0].requirementId = 'REQ-ORDER-001';
|
|
191
|
+
await writeFile(checkerPath, JSON.stringify(checker));
|
|
192
|
+
|
|
193
|
+
const result = await exportDeliveryReport({ featureDir, workspaceRoot, templatePath });
|
|
194
|
+
assert.equal(result.rows, 3);
|
|
195
|
+
assert.equal(result.groupingSource, 'metadata');
|
|
196
|
+
assert.equal(result.apiMode, 'mock');
|
|
197
|
+
assert.equal(result.embeddedScreenshots, 2);
|
|
198
|
+
assert.equal(result.screenshotPlacements, 2);
|
|
199
|
+
|
|
200
|
+
const workbook = new ExcelJS.Workbook();
|
|
201
|
+
await workbook.xlsx.readFile(result.outputPath);
|
|
202
|
+
assert.match(workbook.getWorksheet('自测报告').getCell('C2').value, /^API 数据:模拟 API。/);
|
|
203
|
+
});
|
|
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
|
+
|
|
167
273
|
test('旧证据必须提供完整语义分组清单,不能按 scene 猜测功能', async () => {
|
|
168
274
|
const { workspaceRoot, featureDir, runDir, desktop, mobile } = await createFixture();
|
|
169
275
|
const runnerPath = path.join(runDir, 'runner-result.json');
|
|
@@ -41,6 +41,138 @@ test('checkEnvironment 能报告浏览器运行时缺失', async () => {
|
|
|
41
41
|
assert.match(result.error, /缺少浏览器可执行文件/);
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
test('real API 模式命中声明端点并记录真实响应证据', async t => {
|
|
45
|
+
const fixture = await startFixtureServer();
|
|
46
|
+
t.after(fixture.close);
|
|
47
|
+
const workspace = await createWorkspace();
|
|
48
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
49
|
+
await fs.writeFile(workspace.scenarioPath, `export const metadata = {
|
|
50
|
+
apiModeSchemaVersion: 1,
|
|
51
|
+
apiMode: 'real',
|
|
52
|
+
apiTargets: [{ id: 'status', method: 'GET', pathname: '/api/status' }],
|
|
53
|
+
scenes: ['real-status']
|
|
54
|
+
};
|
|
55
|
+
export default async function ({ scene }) {
|
|
56
|
+
await scene('real-status', async ({ page, check, baseUrl }) => {
|
|
57
|
+
await page.goto(baseUrl);
|
|
58
|
+
await check({ id: 'real-status', specSnippet: '真实 API 返回状态。', scene: 'real-status',
|
|
59
|
+
errorType: '功能缺陷', expect: '真实 API 响应可用' }, async () => {
|
|
60
|
+
const data = await page.evaluate(async () => (await fetch('/api/status')).json());
|
|
61
|
+
if (data.source !== 'real-fixture-server') throw new Error('响应来源错误');
|
|
62
|
+
return '真实 API 响应可用';
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}\n`, 'utf8');
|
|
66
|
+
|
|
67
|
+
const result = await runFeatureE2E({
|
|
68
|
+
workspaceRoot: workspace.root,
|
|
69
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
70
|
+
baseUrl: fixture.baseUrl,
|
|
71
|
+
outputDir: 'feature/e2e/runs/api-real',
|
|
72
|
+
apiMode: 'real',
|
|
73
|
+
timeoutMs: 10000
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
assert.equal(result.status, 'passed');
|
|
77
|
+
assert.equal(result.apiMode, 'real');
|
|
78
|
+
assert.deepEqual(result.apiCalls.map(call => [call.targetId, call.method, call.pathname, call.status]), [
|
|
79
|
+
['status', 'GET', '/api/status', 200]
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('real API 模式拒绝 page.route 请求拦截', async t => {
|
|
84
|
+
const fixture = await startFixtureServer();
|
|
85
|
+
t.after(fixture.close);
|
|
86
|
+
const workspace = await createWorkspace();
|
|
87
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
88
|
+
await fs.writeFile(workspace.scenarioPath, `export const metadata = {
|
|
89
|
+
apiModeSchemaVersion: 1,
|
|
90
|
+
apiMode: 'real',
|
|
91
|
+
apiTargets: [{ id: 'status', method: 'GET', pathname: '/api/status' }],
|
|
92
|
+
scenes: ['intercept']
|
|
93
|
+
};
|
|
94
|
+
export default async function ({ scene }) {
|
|
95
|
+
await scene('intercept', async ({ page }) => {
|
|
96
|
+
await page.route('**/api/status', route => route.fulfill({ status: 200, body: '{}' }));
|
|
97
|
+
});
|
|
98
|
+
}\n`, 'utf8');
|
|
99
|
+
|
|
100
|
+
const result = await runFeatureE2E({
|
|
101
|
+
workspaceRoot: workspace.root,
|
|
102
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
103
|
+
baseUrl: fixture.baseUrl,
|
|
104
|
+
outputDir: 'feature/e2e/runs/api-real-intercepted',
|
|
105
|
+
apiMode: 'real',
|
|
106
|
+
timeoutMs: 10000
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
assert.equal(result.status, 'blocked');
|
|
110
|
+
assert.match(result.blockers[0].detail, /real API 模式禁止/);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('执行参数与冻结场景 API 模式不一致时阻断', async t => {
|
|
114
|
+
const workspace = await createWorkspace();
|
|
115
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
116
|
+
await fs.writeFile(workspace.scenarioPath, `export const metadata = {
|
|
117
|
+
apiModeSchemaVersion: 1,
|
|
118
|
+
apiMode: 'mock',
|
|
119
|
+
apiTargets: [{ id: 'status', method: 'GET', pathname: '/api/status' }]
|
|
120
|
+
};
|
|
121
|
+
export default async function () {}\n`, 'utf8');
|
|
122
|
+
|
|
123
|
+
const result = await runFeatureE2E({
|
|
124
|
+
workspaceRoot: workspace.root,
|
|
125
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
126
|
+
baseUrl: 'http://127.0.0.1:1',
|
|
127
|
+
outputDir: 'feature/e2e/runs/api-mode-mismatch',
|
|
128
|
+
apiMode: 'real',
|
|
129
|
+
timeoutMs: 10000
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.equal(result.status, 'blocked');
|
|
133
|
+
assert.match(result.blockers[0].detail, /模式 real 与冻结场景模式 mock 不一致/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('mock API 模式允许显式 fixture 并记录模拟响应', async t => {
|
|
137
|
+
const fixture = await startFixtureServer();
|
|
138
|
+
t.after(fixture.close);
|
|
139
|
+
const workspace = await createWorkspace();
|
|
140
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
141
|
+
await fs.writeFile(workspace.scenarioPath, `export const metadata = {
|
|
142
|
+
apiModeSchemaVersion: 1,
|
|
143
|
+
apiMode: 'mock',
|
|
144
|
+
apiTargets: [{ id: 'status', method: 'GET', pathname: '/api/status' }],
|
|
145
|
+
scenes: ['mock-status']
|
|
146
|
+
};
|
|
147
|
+
export default async function ({ scene }) {
|
|
148
|
+
await scene('mock-status', async ({ page, route, check, baseUrl }) => {
|
|
149
|
+
await route('**/api/status', requestRoute => requestRoute.fulfill({
|
|
150
|
+
status: 200, contentType: 'application/json', body: JSON.stringify({ source: 'mock' })
|
|
151
|
+
}));
|
|
152
|
+
await page.goto(baseUrl);
|
|
153
|
+
await check({ id: 'mock-status', specSnippet: '模拟 API 返回状态。', scene: 'mock-status',
|
|
154
|
+
errorType: '功能缺陷', expect: '模拟 API 响应可用' }, async () => {
|
|
155
|
+
const data = await page.evaluate(async () => (await fetch('/api/status')).json());
|
|
156
|
+
if (data.source !== 'mock') throw new Error('响应来源错误');
|
|
157
|
+
return '模拟 API 响应可用';
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}\n`, 'utf8');
|
|
161
|
+
|
|
162
|
+
const result = await runFeatureE2E({
|
|
163
|
+
workspaceRoot: workspace.root,
|
|
164
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
165
|
+
baseUrl: fixture.baseUrl,
|
|
166
|
+
outputDir: 'feature/e2e/runs/api-mock',
|
|
167
|
+
apiMode: 'mock',
|
|
168
|
+
timeoutMs: 10000
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
assert.equal(result.status, 'passed');
|
|
172
|
+
assert.equal(result.apiMode, 'mock');
|
|
173
|
+
assert.equal(result.apiCalls[0].targetId, 'status');
|
|
174
|
+
});
|
|
175
|
+
|
|
44
176
|
test('runFeatureE2E 能执行冻结场景并写入可审查证据', async t => {
|
|
45
177
|
const fixture = await startFixtureServer();
|
|
46
178
|
t.after(fixture.close);
|
|
@@ -123,6 +255,153 @@ export default async function ({ check }) {
|
|
|
123
255
|
assert.match(result.blockers[0].detail, /reportModule/);
|
|
124
256
|
});
|
|
125
257
|
|
|
258
|
+
test('报告截图 schema v1 生成声明截图后通过', async t => {
|
|
259
|
+
const fixture = await startFixtureServer();
|
|
260
|
+
t.after(fixture.close);
|
|
261
|
+
const workspace = await createWorkspace();
|
|
262
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
263
|
+
await fs.writeFile(workspace.scenarioPath, `export const metadata = {
|
|
264
|
+
reportSchemaVersion: 1,
|
|
265
|
+
reportCaptureSchemaVersion: 1,
|
|
266
|
+
traceabilitySchemaVersion: 1,
|
|
267
|
+
requirementIds: ['REQ-PD-001'],
|
|
268
|
+
scenes: ['initial'],
|
|
269
|
+
reportCaptures: [{
|
|
270
|
+
requirementId: 'REQ-PD-001', scene: 'initial', capture: 'detail-desktop.png',
|
|
271
|
+
locator: 'main', reportModule: '详情页', reportGroup: 'detail',
|
|
272
|
+
reportTitle: '详情内容', reportDevice: 'desktop'
|
|
273
|
+
}],
|
|
274
|
+
visualMappings: []
|
|
275
|
+
};
|
|
276
|
+
export default async function ({ scene }) {
|
|
277
|
+
await scene('initial', async ({ page, check, capture, baseUrl }) => {
|
|
278
|
+
await page.goto(baseUrl);
|
|
279
|
+
await check({
|
|
280
|
+
id: 'detail-visible', requirementId: 'REQ-PD-001', specSnippet: '详情可见。',
|
|
281
|
+
scene: 'initial', reportModule: '详情页', reportGroup: 'detail',
|
|
282
|
+
reportTitle: '详情内容', reportMethod: '检查详情内容和运行状态。',
|
|
283
|
+
errorType: '功能缺陷', expect: '详情可见'
|
|
284
|
+
}, async () => '详情可见');
|
|
285
|
+
await capture(page.locator('main'), 'detail-desktop.png');
|
|
286
|
+
});
|
|
287
|
+
}\n`, 'utf8');
|
|
288
|
+
|
|
289
|
+
const result = await runFeatureE2E({
|
|
290
|
+
workspaceRoot: workspace.root,
|
|
291
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
292
|
+
baseUrl: fixture.baseUrl,
|
|
293
|
+
outputDir: 'feature/e2e/runs/report-capture-valid',
|
|
294
|
+
timeoutMs: 10000
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
assert.equal(result.status, 'passed');
|
|
298
|
+
assert.equal(result.captures.length, 1);
|
|
299
|
+
assert.equal(result.metadata.reportCaptures[0].requirementId, 'REQ-PD-001');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test('报告截图 schema v1 拒绝未生成的声明截图', async t => {
|
|
303
|
+
const workspace = await createWorkspace();
|
|
304
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
305
|
+
await fs.writeFile(workspace.scenarioPath, `export const metadata = {
|
|
306
|
+
reportSchemaVersion: 1,
|
|
307
|
+
reportCaptureSchemaVersion: 1,
|
|
308
|
+
scenes: ['initial'],
|
|
309
|
+
reportCaptures: [{
|
|
310
|
+
scene: 'initial', capture: 'missing.png', locator: 'main', reportModule: '详情页',
|
|
311
|
+
reportGroup: 'detail', reportTitle: '详情内容', reportDevice: 'desktop'
|
|
312
|
+
}],
|
|
313
|
+
visualMappings: []
|
|
314
|
+
};
|
|
315
|
+
export default async function ({ scene }) {
|
|
316
|
+
await scene('initial', async ({ check }) => {
|
|
317
|
+
await check({
|
|
318
|
+
id: 'detail-visible', specSnippet: '详情可见。', scene: 'initial',
|
|
319
|
+
reportModule: '详情页', reportGroup: 'detail', reportTitle: '详情内容',
|
|
320
|
+
reportMethod: '检查详情内容。', errorType: '功能缺陷', expect: '详情可见'
|
|
321
|
+
}, async () => '详情可见');
|
|
322
|
+
});
|
|
323
|
+
}\n`, 'utf8');
|
|
324
|
+
|
|
325
|
+
const result = await runFeatureE2E({
|
|
326
|
+
workspaceRoot: workspace.root,
|
|
327
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
328
|
+
baseUrl: 'http://127.0.0.1:1',
|
|
329
|
+
outputDir: 'feature/e2e/runs/report-capture-missing',
|
|
330
|
+
timeoutMs: 10000
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
assert.equal(result.status, 'blocked');
|
|
334
|
+
assert.match(result.blockers[0].detail, /报告截图未生成/);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('报告截图 schema v1 拒绝没有截图的报告功能组', async t => {
|
|
338
|
+
const fixture = await startFixtureServer();
|
|
339
|
+
t.after(fixture.close);
|
|
340
|
+
const workspace = await createWorkspace();
|
|
341
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
342
|
+
await fs.writeFile(workspace.scenarioPath, `export const metadata = {
|
|
343
|
+
reportSchemaVersion: 1,
|
|
344
|
+
reportCaptureSchemaVersion: 1,
|
|
345
|
+
scenes: ['initial'],
|
|
346
|
+
reportCaptures: [{
|
|
347
|
+
scene: 'initial', capture: 'detail.png', locator: 'main', reportModule: '详情页',
|
|
348
|
+
reportGroup: 'detail', reportTitle: '详情内容', reportDevice: 'desktop'
|
|
349
|
+
}],
|
|
350
|
+
visualMappings: []
|
|
351
|
+
};
|
|
352
|
+
export default async function ({ scene }) {
|
|
353
|
+
await scene('initial', async ({ page, check, capture, baseUrl }) => {
|
|
354
|
+
await page.goto(baseUrl);
|
|
355
|
+
for (const [id, group] of [['detail-visible', 'detail'], ['action-visible', 'action']]) {
|
|
356
|
+
await check({ id, specSnippet: '功能可见。', scene: 'initial', reportModule: '详情页',
|
|
357
|
+
reportGroup: group, reportTitle: group, reportMethod: '检查功能。',
|
|
358
|
+
errorType: '功能缺陷', expect: '功能可见' }, async () => '功能可见');
|
|
359
|
+
}
|
|
360
|
+
await capture(page.locator('main'), 'detail.png');
|
|
361
|
+
});
|
|
362
|
+
}\n`, 'utf8');
|
|
363
|
+
|
|
364
|
+
const result = await runFeatureE2E({
|
|
365
|
+
workspaceRoot: workspace.root,
|
|
366
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
367
|
+
baseUrl: fixture.baseUrl,
|
|
368
|
+
outputDir: 'feature/e2e/runs/report-group-missing-capture',
|
|
369
|
+
timeoutMs: 10000
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
assert.equal(result.status, 'blocked');
|
|
373
|
+
assert.match(result.blockers[0].detail, /action/);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('报告截图 schema v1 拒绝未知需求 ID', async t => {
|
|
377
|
+
const workspace = await createWorkspace();
|
|
378
|
+
t.after(() => fs.rm(workspace.root, { recursive: true, force: true }));
|
|
379
|
+
await fs.writeFile(workspace.scenarioPath, `export const metadata = {
|
|
380
|
+
reportCaptureSchemaVersion: 1,
|
|
381
|
+
traceabilitySchemaVersion: 1,
|
|
382
|
+
requirementIds: ['REQ-PD-001'],
|
|
383
|
+
scenes: ['initial'],
|
|
384
|
+
reportCaptures: [{
|
|
385
|
+
requirementId: 'REQ-PD-002', scene: 'initial', capture: 'detail.png',
|
|
386
|
+
locator: 'main', reportModule: '详情页', reportGroup: 'detail',
|
|
387
|
+
reportTitle: '详情内容', reportDevice: 'desktop'
|
|
388
|
+
}],
|
|
389
|
+
visualMappings: []
|
|
390
|
+
};
|
|
391
|
+
export default async function () {}\n`, 'utf8');
|
|
392
|
+
|
|
393
|
+
const result = await runFeatureE2E({
|
|
394
|
+
workspaceRoot: workspace.root,
|
|
395
|
+
scenarioPath: path.relative(workspace.root, workspace.scenarioPath),
|
|
396
|
+
baseUrl: 'http://127.0.0.1:1',
|
|
397
|
+
outputDir: 'feature/e2e/runs/report-capture-unknown-requirement',
|
|
398
|
+
timeoutMs: 10000
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
assert.equal(result.status, 'blocked');
|
|
402
|
+
assert.match(result.blockers[0].detail, /未知需求 ID/);
|
|
403
|
+
});
|
|
404
|
+
|
|
126
405
|
test('runFeatureE2E 将已完成的断言失败报告为 failed', async t => {
|
|
127
406
|
const fixture = await startFixtureServer();
|
|
128
407
|
t.after(fixture.close);
|
|
@@ -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
|
|
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
|
|
19
|
+
追踪模式下,Spec 审查轴必须逐个核对 active REQ ID 是否同时出现在 spec、实现结论、相关代码和 Checker 或快速自测证据中;不得用整体通过代替单项覆盖,也不得接受未知或重编号的需求。快速模式证据只能称为实现者交互式自测,不能表述成独立 Checker 验收。
|
|
20
20
|
|
|
21
21
|
1. 只冻结一次审查范围。审查工作树时纳入与任务相关的暂存、未暂存及新增文件,保留并排除无关的既有改动。
|
|
22
22
|
2. 阅读适用的仓库规则、spec、scope、提交列表、改动文件和完整 diff。
|
|
@@ -12,6 +12,7 @@ description: 围绕功能 spec 调度相互隔离的实现者、E2E 测试编写
|
|
|
12
12
|
- 必需的功能 `spec.md`,并定位同目录的 `scope.md`;存在时必须读取 `spec-traceability.json`。
|
|
13
13
|
- 可选的实现者最大回合数,默认 `3`,包含首次实现。
|
|
14
14
|
- 可选的审计固定点,默认为启动时的 `HEAD`。
|
|
15
|
+
- E2E API 模式:`real` 或 `mock`。用户命令明确包含“真实 API”“执行真实API”等表述时取 `real`;明确包含“模拟 API”“mock”等表述时取 `mock`。
|
|
15
16
|
|
|
16
17
|
只有交付通过且用户明确要求时,才允许提交或推送。
|
|
17
18
|
|
|
@@ -22,13 +23,14 @@ description: 围绕功能 spec 调度相互隔离的实现者、E2E 测试编写
|
|
|
22
23
|
## 流程
|
|
23
24
|
|
|
24
25
|
1. **预检:**阅读 spec、scope、仓库规则、视觉映射、固定审查点和当前工作树。存在 `spec-traceability.json` 时先校验输入哈希、active ID 集合和规格章节;校验失败即阻断,不得启动实现者。在启动实现者前调用 `autobest-delivery` MCP 的 `check_environment` 工具。若环境未就绪,保留工具证据、保持实现者回合数为零并进入 `awaiting-decision`,由用户选择重试、跳过环境验收并记录 waiver,或停止。按契约自行解析可发现的 URL、命令、路由变体、数据准备和定位器,不为这些可发现信息询问用户。
|
|
25
|
-
2.
|
|
26
|
-
3.
|
|
27
|
-
4.
|
|
28
|
-
5.
|
|
29
|
-
6.
|
|
30
|
-
7.
|
|
31
|
-
8.
|
|
26
|
+
2. **API 模式确认:**在启动测试编写者或运行任何 E2E 前锁定 `real` 或 `mock`。用户命令已明确模式时直接采用;否则将状态写为 `awaiting-decision`、阶段写为 `api-mode-confirmation`,询问用户选择真实环境 API 还是模拟 API,在得到答复前不得生成场景或运行 preflight。`real` 模式还要确认唯一可解析的目标环境、认证和测试数据;涉及创建、修改、删除、下单、支付或其他外部副作用时,必须再次说明具体操作并取得明确授权。缺少真实环境条件时保持阻断,不得自行改用 mock。
|
|
27
|
+
3. **状态:**将 `delivery/delivery-state.json` 写为 `running`,记录当前阶段、回合数、固定审查点、既有改动、`apiMode`、选择来源、目标环境和当前产物。每次状态流转都更新该文件。
|
|
28
|
+
4. **实现者:**启动全新的 `$code-craft` 子代理。首回合传入 spec、scope 和已有的 traceability;修复回合只额外传入最新验收失败结果、实现阻断项或代码审计阻断结果。启动时增加实现者回合计数。
|
|
29
|
+
5. **测试编写者:**将已确认的 API 模式和目标环境传给全新的 `$e2e-gen-spec` 子代理。缺少 `e2e/e2e.feature.mjs` 时创建;现有冻结场景未声明相同 API 模式时,不得直接复用,启动新的测试修订回合并记录旧、新 SHA-256。旧 `.spec.ts` 不是标准输入,应保持不变。测试编写者必须使用隔离 scene,为每个报告功能组声明并生成代表性 UI 证据截图,并在冻结前使用相同 API 模式通过同一运行器 preflight;场景代码、定位器、截图或等待条件造成的 preflight Blocked 由测试编写者在草稿阶段自行修正并重跑。`mock` 模式可使用经声明的 fixture;`real` 模式禁止任何请求拦截。报告 `ready` 后才冻结 `.mjs`。环境、spec 或实现阻断进入 `awaiting-decision`,由用户选择补充输入、重试、分类、记录 waiver 或停止。
|
|
30
|
+
6. **验收者:**启动全新的 `$e2e-ui-checker` 子代理,并传入 spec、scope、traceability、冻结场景、已确认 API 模式、目标环境和迭代编号。验收者必须把相同 `apiMode` 传给隔离 MCP 运行器,以 headed 模式执行全部场景,核对 API 响应证据,然后只审查 spec 已映射的视觉基准。`passed` 时继续;`failed` 且仍有回合时返回实现者。
|
|
31
|
+
7. **Blocked 决策:**验收者返回 `awaiting-decision` 时,先确认运行器已在阻断 scene 后继续尝试其余独立 scene,再将交付状态写为同名状态,记录该验收代理,保留可见浏览器与证据,并把契约定义的六个决策及其 effect 交给用户。必须明确 `skip` 是接受阻断 scene 的缺失证据并继续审计,不是从异常语句下一行恢复。收到选择后,把决议回传给同一个验收代理,由它调用 `resolve_blocked_run`;验证 `human-decision.json` 后再按其 `effect` 继续。不得自行修改冻结场景、替用户接受风险或把 Blocked 当作终止;只有 `stop` 是终止决策。
|
|
32
|
+
8. **代码审计:**验收通过,或用户 `accept`/`skip` 形成 waiver 后,从固定审查点启动全新的 `$code-audit` 子代理。存在阻断项且仍有回合时返回实现者,修复后重新执行完整验收。没有阻断项时完成交付。
|
|
33
|
+
9. **完成:**标准场景、最终 Runner、最新验收、最新代码审计、人工决议、状态与产物必须一致且不存在未授权提交。最终 Runner 的 `apiMode` 必须与用户确认值一致,所有声明 API 都有响应证据;同时必须实际执行非空检查,并为每个报告功能组生成已声明的截图。只存在 Trace、preflight、空 API 证据或空截图列表不能算完成。完整验收通过写入 `passed`;存在 `accept`/`skip` waiver 且审计通过写入 `passed-with-waivers`;只有用户明确选择 `stop` 才写入终止态 `blocked`。Maker 回合耗尽时先进入 `awaiting-decision`,不得替用户停止。
|
|
32
34
|
|
|
33
35
|
## 控制规则
|
|
34
36
|
|
|
@@ -39,5 +41,6 @@ description: 围绕功能 spec 调度相互隔离的实现者、E2E 测试编写
|
|
|
39
41
|
- 可见 Blocked 会话过期后按 `retry` 处理,不得推断用户已接受或跳过。
|
|
40
42
|
- `resolve_blocked_run` 必须由创建 `blockedSessionId` 的同一个验收代理调用;返回 `not-found` 时记录会话失效并按 `retry` 处理。
|
|
41
43
|
- 任何业务代码修复都会使之前的验收证据失效。
|
|
44
|
+
- API 模式或目标环境变化会使场景、preflight、Runner 和 Checker 证据失效,必须重新生成或修订场景并完整验收。
|
|
42
45
|
- 实现者和验收者将 spec、scope、视觉基准、冻结场景和既有角色产物视为只读。
|
|
43
46
|
- 只管理和清理由本流程或当前角色启动的进程。
|