@autobest-ui/agent 1.0.8 → 1.0.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autobest-ui/agent",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "private": false,
5
5
  "description": "Autobest Agent skills/plugins/mcp assets + sync cli",
6
6
  "files": [
@@ -65,7 +65,9 @@ npx --yes --package=@autobest-ui/agent@latest autobest-delivery-setup --help
65
65
  - `baseUrl`:可访问的应用 URL。
66
66
  - `outputDir`:工作区内的证据输出目录。
67
67
 
68
- 可选参数包括 `iteration`、`timeoutMs`、`headed`、`keepBrowserOpenOnBlock` 和 `blockedSessionTtlMs`。交付验收使用 headed 模式;发生 Blocked 时,工具保留可见浏览器并返回 session ID、过期时间、结构化检查、组件截图、Trace、控制台错误、网络失败和 SHA-256。
68
+ 新建或修订场景还必须传入用户确认的 `apiMode`(`real` 或 `mock`);Runner 会校验它与冻结场景一致。可选参数包括 `iteration`、`timeoutMs`、`headed`、`keepBrowserOpenOnBlock` 和 `blockedSessionTtlMs`。交付验收使用 headed 模式;发生 Blocked 时,工具保留可见浏览器并返回 session ID、过期时间、结构化检查、API 响应证据、组件截图、Trace、控制台错误、网络失败和 SHA-256。
69
+
70
+ `real` 模式禁止 `route()` 和 `page.route()` 请求拦截,并要求每个声明业务端点都产生响应证据;缺失时不能判为通过。`mock` 模式允许受控 fixture,但 Runner 和报告会明确标记为模拟。Delivery 命令未指定模式时会在 E2E 前询问;例如“执行真实API”会直接锁定 `real`,不会自动创建 mock。
69
71
 
70
72
  新版场景通过 `scene(id, callback)` 隔离页面和执行错误。一个 scene 的定位器、弹窗或准备步骤阻塞时,运行器会记录该 blocker 并继续执行后续独立 scene,全部 scene 尝试结束后再返回 `blocked`。旧版直接使用顶层 `page` 的场景仍可运行,但无法在未捕获异常后继续。
71
73
 
@@ -90,10 +92,29 @@ npx --yes --package=@autobest-ui/agent@latest autobest-delivery-setup --help
90
92
 
91
93
  ```js
92
94
  export const metadata = {
95
+ apiModeSchemaVersion: 1,
96
+ apiMode: 'real',
97
+ apiTargets: [{
98
+ id: 'product-detail',
99
+ method: 'GET',
100
+ pathname: '/api/product/detail'
101
+ }],
102
+ reportSchemaVersion: 1,
103
+ reportCaptureSchemaVersion: 1,
93
104
  traceabilitySchemaVersion: 1,
94
105
  requirementIds: ['REQ-PD-001'],
95
106
  scenes: ['initial'],
96
107
  viewports: [{ name: 'desktop', width: 1400, height: 1000 }],
108
+ reportCaptures: [{
109
+ requirementId: 'REQ-PD-001',
110
+ scene: 'initial',
111
+ capture: 'product-detail-desktop.png',
112
+ locator: '[data-testid="product-detail"]',
113
+ reportModule: '产品详情',
114
+ reportGroup: 'product-detail',
115
+ reportTitle: '产品详情',
116
+ reportDevice: 'desktop'
117
+ }],
97
118
  visualMappings: []
98
119
  };
99
120
 
@@ -113,7 +134,7 @@ export default async function run({
113
134
  }
114
135
  ```
115
136
 
116
- 每个 `metadata.scenes` 项必须由 `scene()` 恰好执行一次,并自行完成导航、API fixture 和状态准备。测试编写者在冻结 SHA-256 前使用同一运行器进行 preflight;定位器、弹窗、fixture 和等待条件等测试机械问题应在草稿阶段修正。preflight 只证明测试代码能够完整执行,不替代独立验收和视觉审查。
137
+ 每个 `metadata.scenes` 项必须由 `scene()` 恰好执行一次,并自行完成导航、数据和状态准备。`real` 模式使用真实环境数据且禁止请求拦截;`mock` 模式才允许 API fixture。每个报告功能组至少声明并生成一张代表性 UI 截图。测试编写者在冻结 SHA-256 前使用相同 API 模式和同一运行器进行 preflight;定位器、弹窗、fixture 和等待条件等测试机械问题应在草稿阶段修正。preflight 只证明测试代码能够完整执行、声明 API 已响应且报告截图已生成,不替代独立验收和视觉审查。
117
138
 
118
139
  视觉审查使用 `1400px` 桌面视口和与 Figma 一致的 `375px` 移动视口。截图必须定位到与基准相同的语义节点;比较组件结构、布局和视觉样式,不比较业务文字的字面值、长度或业务图片主体。文案及数据内容由独立功能检查验证。
119
140
 
@@ -123,16 +144,16 @@ export default async function run({
123
144
 
124
145
  Checker 和代码审计生成的 Markdown 使用简体中文,包括标题、摘要、表头、检查说明、期望、实际结果和 finding。JSON 字段名、机器状态、ID、路径及原始工具错误保持协议格式;英文原始错误旁提供中文解释。
125
146
 
126
- 手动调用 `$export-report` 时,Excel 只生成一个 `自测报告` 工作表。每行代表一个真实 UI 组件或完整业务功能;同一组件或功能的文案、样式、布局、响应式行为和相关检查合并到同一行,桌面端与移动端代表截图也放在该行。报告不展示 story ID、scene 名或原子检查明细。新证据通过 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod` 和 `reportDevice` 提供通用分组;旧证据必须由导出 Skill 分析 `spec.md` 后生成经过哈希和完整覆盖校验的语义分组清单,禁止按 E2E scene 猜测。
147
+ 手动调用 `$export-report` 时,Excel 只生成一个 `自测报告` 工作表。每行代表一个真实 UI 组件或完整业务功能;同一组件或功能的文案、样式、布局、响应式行为和相关检查合并到同一行,桌面端与移动端代表截图也放在该行。报告不展示 REQ/story ID、scene 名或原子检查明细。新证据通过 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod` 和 `reportDevice` 提供通用分组,并通过 `reportCaptures` 留下运行时 UI 截图。`visualMappings` 只负责有 Figma 基准时的视觉一致性验收;普通报告截图不表示视觉比对通过。旧证据必须由导出 Skill 分析 `spec.md` 后生成经过哈希和完整覆盖校验的语义分组清单,禁止按 E2E scene 猜测。
127
148
 
128
149
  ## 交付流程
129
150
 
130
- 功能目录存在 `spec-traceability.json` 时,Delivery 按 active REQ ID 运行。Maker 报告实现编号,E2E 检查和视觉映射携带 `requirementId`,Checker 与 Audit 逐项核对;运行器拒绝格式错误、未知编号和无验收覆盖的需求。没有追踪文件的历史交付仍按旧契约运行。
151
+ 功能目录存在 `spec-traceability.json` 时,Delivery 按 active REQ ID 运行。Maker 报告实现编号,E2E 检查、报告截图和视觉映射携带 `requirementId`,Checker 与 Audit 逐项核对;运行器拒绝格式错误、未知编号和无验收覆盖的需求。没有追踪文件的历史交付仍按旧契约运行。
131
152
 
132
- 1. `$delivery-loop` 校验可用的需求追踪文件,调用 `check_environment` 并记录固定审查点。
153
+ 1. `$delivery-loop` 校验需求追踪文件,确认 `real` 或 `mock` API 模式,调用 `check_environment` 并记录固定审查点。
133
154
  2. `$code-craft` 实现规格。
134
- 3. `$e2e-gen-spec` 创建无依赖场景,运行冻结前 preflight,确认所有 scene 和检查可达后冻结。
135
- 4. `$e2e-ui-checker` 启动声明的应用服务,调用 `run_feature_e2e`,并审查已映射的组件截图。
155
+ 3. `$e2e-gen-spec` 按已确认 API 模式创建无依赖场景,运行冻结前 preflight,确认 API、scene、检查和报告截图可达后冻结。
156
+ 4. `$e2e-ui-checker` 启动声明的应用服务,使用相同 API 模式调用 `run_feature_e2e`,核对 API 和报告截图证据,并审查有本地设计基准的视觉映射。
136
157
  5. Blocked 时进入人工决策;只有用户选择 `stop` 才终止。
137
158
  6. 验收通过或用户接受 waiver 后,`$code-audit` 审查代码规范和规格符合度。
138
159
 
@@ -58,10 +58,10 @@ function resolveEvidencePath(rawPath, { featureDir, workspaceRoot }) {
58
58
  return resolved;
59
59
  }
60
60
 
61
- function parseSpecStories(specText) {
61
+ function parseLegacySpecStories(specText) {
62
62
  const section = specText.match(/(?:^|\n)## 用户故事\s*\n([\s\S]*?)(?=\n## |$)/);
63
63
  if (!section) {
64
- throw new Error('spec.md 缺少“用户故事”章节,无法生成自测点');
64
+ return null;
65
65
  }
66
66
  const stories = [];
67
67
  const lines = section[1].split(/\r?\n/);
@@ -85,6 +85,33 @@ function parseSpecStories(specText) {
85
85
  return stories;
86
86
  }
87
87
 
88
+ function parseTraceableSpecChecks(specText, checks) {
89
+ const requirementIds = new Set(
90
+ [...specText.matchAll(/^## (REQ-[A-Z][A-Z0-9]{1,15}-\d{3})(?:\s+.+)?$/gm)]
91
+ .map(match => match[1])
92
+ );
93
+ if (requirementIds.size === 0) {
94
+ throw new Error('spec.md 既没有“用户故事”章节,也没有 REQ 需求章节');
95
+ }
96
+ if (checks.size === 0) {
97
+ throw new Error('REQ 规格缺少 Runner/Checker 原子检查,无法生成自测点');
98
+ }
99
+ return [...checks.values()].map((check, index) => {
100
+ if (!nonEmptyString(check.requirementId) || !requirementIds.has(check.requirementId)) {
101
+ throw new Error(`检查 ${check.id || index + 1} 缺少有效的 spec REQ 映射`);
102
+ }
103
+ return {
104
+ number: index + 1,
105
+ id: check.id,
106
+ text: check.specSnippet || check.expect || check.id
107
+ };
108
+ });
109
+ }
110
+
111
+ function parseSpecItems(specText, checks) {
112
+ return parseLegacySpecStories(specText) || parseTraceableSpecChecks(specText, checks);
113
+ }
114
+
88
115
  async function findLatestRunnerResult(featureDir) {
89
116
  const runsDir = path.join(featureDir, 'e2e', 'runs');
90
117
  let entries;
@@ -218,8 +245,36 @@ function reportMethodFor(group) {
218
245
  ].join('\n');
219
246
  }
220
247
 
248
+ function resolveApiMode(runner) {
249
+ const resultMode = nonEmptyString(runner.apiMode);
250
+ const metadataMode = nonEmptyString(runner.metadata?.apiMode);
251
+ for (const mode of [resultMode, metadataMode].filter(Boolean)) {
252
+ if (!['real', 'mock'].includes(mode)) {
253
+ throw new Error(`Runner 包含无效 apiMode:${mode}`);
254
+ }
255
+ }
256
+ if (resultMode && metadataMode && resultMode !== metadataMode) {
257
+ throw new Error('Runner 顶层 apiMode 与 metadata.apiMode 不一致');
258
+ }
259
+ return resultMode || metadataMode || null;
260
+ }
261
+
262
+ function reportMethodWithApiMode(group, apiMode) {
263
+ const method = reportMethodFor(group);
264
+ if (apiMode === 'real') {
265
+ return `API 数据:真实环境 API。\n${method}`;
266
+ }
267
+ if (apiMode === 'mock') {
268
+ return `API 数据:模拟 API。\n${method}`;
269
+ }
270
+ return method;
271
+ }
272
+
221
273
  function buildCaptureRecords(runner) {
222
- const mappings = runner.metadata?.visualMappings || [];
274
+ const mappings = [
275
+ ...(runner.metadata?.visualMappings || []),
276
+ ...(runner.metadata?.reportCaptures || [])
277
+ ];
223
278
  const mappingByCapture = new Map(mappings.map(mapping => [mapping.capture, mapping]));
224
279
  return (runner.captures || []).map(rawPath => {
225
280
  const basename = path.basename(rawPath);
@@ -509,14 +564,15 @@ export async function exportDeliveryReport(options) {
509
564
  throw new Error(`缺少 Checker 结果:${checkerPath}`);
510
565
  }
511
566
  const specText = await readFile(specPath, 'utf8');
512
- const stories = parseSpecStories(specText);
513
567
  const checker = await readJson(checkerPath, 'Checker 结果');
514
568
  const runnerPath = await selectRunnerResult(checker, context);
515
569
  if (!runnerPath) {
516
570
  throw new Error(`缺少 Runner 结果:${path.join(featureDir, 'e2e', 'runs')}`);
517
571
  }
518
572
  const runner = await readJson(runnerPath, 'Runner 结果');
573
+ const apiMode = resolveApiMode(runner);
519
574
  const checks = mergeChecks(checker, runner);
575
+ const stories = parseSpecItems(specText, checks);
520
576
  const captures = buildCaptureRecords(runner);
521
577
  const grouping = await resolveReportGroups({
522
578
  stories,
@@ -550,7 +606,7 @@ export async function exportDeliveryReport(options) {
550
606
  const row = worksheet.addRow([
551
607
  group.module,
552
608
  group.title,
553
- reportMethodFor(group),
609
+ reportMethodWithApiMode(group, apiMode),
554
610
  group.status,
555
611
  '',
556
612
  ''
@@ -600,6 +656,7 @@ export async function exportDeliveryReport(options) {
600
656
  embeddedScreenshots,
601
657
  screenshotPlacements,
602
658
  groupingSource: grouping.source,
603
- groupingPath: grouping.groupingPath
659
+ groupingPath: grouping.groupingPath,
660
+ apiMode
604
661
  };
605
662
  }
@@ -155,6 +155,7 @@ export async function runFeatureE2E({
155
155
  scenarioPath,
156
156
  baseUrl,
157
157
  outputDir,
158
+ apiMode,
158
159
  iteration = 1,
159
160
  timeoutMs = 300000,
160
161
  headed = false,
@@ -175,12 +176,14 @@ export async function runFeatureE2E({
175
176
  const scenarioBlockers = [];
176
177
  const consoleErrors = [];
177
178
  const networkFailures = [];
179
+ const apiCalls = [];
178
180
  const invokedScenes = new Set();
179
181
  let browser;
180
182
  let context;
181
183
  let page;
182
184
  let blockedPage;
183
185
  let metadata = {};
186
+ let apiTargets = [];
184
187
  let timeoutId;
185
188
  let traceStarted = false;
186
189
  let traceError;
@@ -197,11 +200,63 @@ export async function runFeatureE2E({
197
200
  throw new TypeError('冻结场景必须导出一个默认异步函数');
198
201
  }
199
202
  metadata = scenarioModule.metadata || {};
203
+ const apiModeSchemaVersion = metadata.apiModeSchemaVersion;
200
204
  const reportSchemaVersion = metadata.reportSchemaVersion;
205
+ const reportCaptureSchemaVersion = metadata.reportCaptureSchemaVersion;
206
+ if (apiModeSchemaVersion !== undefined && apiModeSchemaVersion !== 1) {
207
+ throw new TypeError('metadata.apiModeSchemaVersion 仅支持 1');
208
+ }
209
+ if (apiModeSchemaVersion === 1) {
210
+ if (!['real', 'mock'].includes(metadata.apiMode)) {
211
+ throw new TypeError('metadata.apiMode 必须是 real 或 mock');
212
+ }
213
+ if (!['real', 'mock'].includes(apiMode)) {
214
+ throw new TypeError('run_feature_e2e 必须显式传入 apiMode: real 或 mock');
215
+ }
216
+ if (apiMode !== metadata.apiMode) {
217
+ throw new TypeError(
218
+ `执行 API 模式 ${apiMode} 与冻结场景模式 ${metadata.apiMode} 不一致`
219
+ );
220
+ }
221
+ if (!Array.isArray(metadata.apiTargets) || metadata.apiTargets.length === 0) {
222
+ throw new TypeError('metadata.apiTargets 必须是非空数组');
223
+ }
224
+ const targetIds = new Set();
225
+ apiTargets = metadata.apiTargets.map(target => {
226
+ if (!target || typeof target.id !== 'string' || !/^[a-z0-9][a-z0-9._-]*$/.test(target.id)) {
227
+ throw new TypeError('apiTarget.id 必须是稳定的 ASCII ID');
228
+ }
229
+ if (targetIds.has(target.id)) {
230
+ throw new TypeError(`apiTarget.id 不得重复:${target.id}`);
231
+ }
232
+ targetIds.add(target.id);
233
+ if (typeof target.method !== 'string' || !/^[A-Z]+$/.test(target.method)) {
234
+ throw new TypeError(`apiTarget ${target.id} 的 method 必须是大写 HTTP 方法`);
235
+ }
236
+ if (
237
+ typeof target.pathname !== 'string' ||
238
+ !target.pathname.startsWith('/') ||
239
+ target.pathname.includes('?') ||
240
+ target.pathname.includes('#')
241
+ ) {
242
+ throw new TypeError(`apiTarget ${target.id} 的 pathname 必须是无查询参数的绝对路径`);
243
+ }
244
+ return {
245
+ id: target.id,
246
+ method: target.method,
247
+ pathname: target.pathname
248
+ };
249
+ });
250
+ } else if (apiMode !== undefined && !['real', 'mock'].includes(apiMode)) {
251
+ throw new TypeError('apiMode 仅支持 real 或 mock');
252
+ }
201
253
  const traceabilitySchemaVersion = metadata.traceabilitySchemaVersion;
202
254
  if (reportSchemaVersion !== undefined && reportSchemaVersion !== 1) {
203
255
  throw new TypeError('metadata.reportSchemaVersion 仅支持 1');
204
256
  }
257
+ if (reportCaptureSchemaVersion !== undefined && reportCaptureSchemaVersion !== 1) {
258
+ throw new TypeError('metadata.reportCaptureSchemaVersion 仅支持 1');
259
+ }
205
260
  if (traceabilitySchemaVersion !== undefined && traceabilitySchemaVersion !== 1) {
206
261
  throw new TypeError('metadata.traceabilitySchemaVersion 仅支持 1');
207
262
  }
@@ -227,6 +282,14 @@ export async function runFeatureE2E({
227
282
  throw new TypeError(`visualMapping 引用了未知需求 ID:${mapping.requirementId}`);
228
283
  }
229
284
  }
285
+ for (const mapping of metadata.reportCaptures || []) {
286
+ if (!mapping || typeof mapping.requirementId !== 'string') {
287
+ throw new TypeError('reportCapture 必须包含 requirementId');
288
+ }
289
+ if (!requirementIds.has(mapping.requirementId)) {
290
+ throw new TypeError(`reportCapture 引用了未知需求 ID:${mapping.requirementId}`);
291
+ }
292
+ }
230
293
  }
231
294
  if (reportSchemaVersion === 1) {
232
295
  for (const mapping of metadata.visualMappings || []) {
@@ -243,6 +306,43 @@ export async function runFeatureE2E({
243
306
  }
244
307
  }
245
308
  }
309
+ if (reportCaptureSchemaVersion === 1) {
310
+ if (!Array.isArray(metadata.reportCaptures) || metadata.reportCaptures.length === 0) {
311
+ throw new TypeError('metadata.reportCaptures 必须是非空数组');
312
+ }
313
+ const captureNames = new Set();
314
+ for (const mapping of metadata.reportCaptures) {
315
+ for (const field of [
316
+ 'capture',
317
+ 'scene',
318
+ 'locator',
319
+ 'reportModule',
320
+ 'reportGroup',
321
+ 'reportTitle',
322
+ 'reportDevice'
323
+ ]) {
324
+ if (!mapping || typeof mapping[field] !== 'string' || !mapping[field].trim()) {
325
+ throw new TypeError(`reportCapture 必须包含非空字符串字段 ${field}`);
326
+ }
327
+ }
328
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.png$/.test(mapping.capture)) {
329
+ throw new TypeError('reportCapture.capture 必须是简单的 PNG 文件名');
330
+ }
331
+ if (captureNames.has(mapping.capture)) {
332
+ throw new TypeError(`reportCapture.capture 不得重复:${mapping.capture}`);
333
+ }
334
+ captureNames.add(mapping.capture);
335
+ if (!['desktop', 'mobile'].includes(mapping.reportDevice)) {
336
+ throw new TypeError('reportCapture.reportDevice 必须是 desktop 或 mobile');
337
+ }
338
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(mapping.reportGroup)) {
339
+ throw new TypeError('reportCapture.reportGroup 必须是稳定的 ASCII ID');
340
+ }
341
+ if (Array.isArray(metadata.scenes) && !metadata.scenes.includes(mapping.scene)) {
342
+ throw new TypeError(`reportCapture 引用了未知 scene:${mapping.scene}`);
343
+ }
344
+ }
345
+ }
246
346
 
247
347
  browser = await chromium.launch({ headless: !headed });
248
348
  context = await browser.newContext();
@@ -261,6 +361,28 @@ export async function runFeatureE2E({
261
361
  error: request.failure()?.errorText || '未知请求失败'
262
362
  });
263
363
  });
364
+ currentPage.on('response', response => {
365
+ const request = response.request();
366
+ const requestUrl = new URL(response.url());
367
+ for (const target of apiTargets) {
368
+ if (request.method() === target.method && requestUrl.pathname === target.pathname) {
369
+ apiCalls.push({
370
+ targetId: target.id,
371
+ method: target.method,
372
+ origin: requestUrl.origin,
373
+ pathname: target.pathname,
374
+ status: response.status()
375
+ });
376
+ }
377
+ }
378
+ });
379
+ if (metadata.apiMode === 'real') {
380
+ const rejectInterception = async () => {
381
+ throw new TypeError('real API 模式禁止 route() 或 page.route() 请求拦截');
382
+ };
383
+ Object.defineProperty(currentPage, 'route', { value: rejectInterception });
384
+ Object.defineProperty(currentPage, 'routeFromHAR', { value: rejectInterception });
385
+ }
264
386
  return currentPage;
265
387
  };
266
388
  page = await createPage();
@@ -323,7 +445,12 @@ export async function runFeatureE2E({
323
445
  expect,
324
446
  check,
325
447
  scene,
326
- route: (pattern, handler) => currentPage.route(pattern, handler),
448
+ route: (pattern, handler) => {
449
+ if (metadata.apiMode === 'real') {
450
+ throw new TypeError('real API 模式禁止 route() 请求拦截');
451
+ }
452
+ return currentPage.route(pattern, handler);
453
+ },
327
454
  capture,
328
455
  artifact,
329
456
  baseUrl,
@@ -399,6 +526,37 @@ export async function runFeatureE2E({
399
526
  throw new TypeError(`以下需求 ID 没有运行或视觉覆盖:${uncovered.join(', ')}`);
400
527
  }
401
528
  }
529
+ if (metadata.apiModeSchemaVersion === 1 && scenarioBlockers.length === 0) {
530
+ const observedTargets = new Set(apiCalls.map(call => call.targetId));
531
+ const missingTargets = apiTargets
532
+ .filter(target => !observedTargets.has(target.id))
533
+ .map(target => target.id);
534
+ if (missingTargets.length > 0) {
535
+ throw new TypeError(`以下声明 API 未产生响应证据:${missingTargets.join(', ')}`);
536
+ }
537
+ }
538
+ if (reportCaptureSchemaVersion === 1 && scenarioBlockers.length === 0) {
539
+ const declaredCaptures = new Set(metadata.reportCaptures.map(item => item.capture));
540
+ const actualCaptures = new Set(captures.map(item => path.basename(item)));
541
+ const missingCaptures = [...declaredCaptures].filter(name => !actualCaptures.has(name));
542
+ if (missingCaptures.length > 0) {
543
+ throw new TypeError(`以下报告截图未生成:${missingCaptures.join(', ')}`);
544
+ }
545
+ const captureGroups = new Set(metadata.reportCaptures.map(item => item.reportGroup));
546
+ const missingGroups = [...new Set(checks.map(item => item.reportGroup))]
547
+ .filter(group => group && !captureGroups.has(group));
548
+ if (missingGroups.length > 0) {
549
+ throw new TypeError(`以下报告功能组没有截图:${missingGroups.join(', ')}`);
550
+ }
551
+ const declaredEvidence = new Set([
552
+ ...declaredCaptures,
553
+ ...(metadata.visualMappings || []).map(item => item.capture)
554
+ ]);
555
+ const undeclaredCaptures = [...actualCaptures].filter(name => !declaredEvidence.has(name));
556
+ if (undeclaredCaptures.length > 0) {
557
+ throw new TypeError(`以下截图未在 metadata 中声明:${undeclaredCaptures.join(', ')}`);
558
+ }
559
+ }
402
560
  if (blockedPage && !blockedPage.isClosed()) {
403
561
  await blockedPage.bringToFront().catch(() => undefined);
404
562
  page = blockedPage;
@@ -412,6 +570,8 @@ export async function runFeatureE2E({
412
570
  scenarioPath: toEvidencePath(root, scenario),
413
571
  scenarioSha256: beforeHash,
414
572
  metadata,
573
+ apiMode: metadata.apiMode || null,
574
+ apiCalls,
415
575
  checks,
416
576
  captures,
417
577
  tracePath: toEvidencePath(root, tracePath),
@@ -432,6 +592,8 @@ export async function runFeatureE2E({
432
592
  scenarioPath: toEvidencePath(root, scenario),
433
593
  scenarioSha256: beforeHash,
434
594
  metadata,
595
+ apiMode: metadata.apiMode || null,
596
+ apiCalls,
435
597
  checks,
436
598
  captures,
437
599
  tracePath: traceStarted ? toEvidencePath(root, tracePath) : null,
@@ -61,12 +61,13 @@ server.registerTool(
61
61
  {
62
62
  title: '执行冻结的功能 E2E 场景',
63
63
  description:
64
- '使用插件自带的 Playwright 运行器执行无依赖冻结场景;scene 隔离可在单个场景阻塞后继续执行其余场景,并在功能目录下写入结构化证据。',
64
+ '使用插件自带的 Playwright 运行器执行无依赖冻结场景;显式校验 real/mock API 模式,scene 隔离可在单个场景阻塞后继续执行其余场景,并在功能目录下写入结构化证据。',
65
65
  inputSchema: {
66
66
  workspaceRoot: z.string().min(1),
67
67
  scenarioPath: z.string().min(1),
68
68
  baseUrl: z.string().url(),
69
69
  outputDir: z.string().min(1),
70
+ apiMode: z.enum(['real', 'mock']).optional(),
70
71
  iteration: z.number().int().positive().default(1),
71
72
  timeoutMs: z.number().int().min(1000).max(900000).default(300000),
72
73
  headed: z.boolean().default(false),
@@ -79,6 +80,8 @@ server.registerTool(
79
80
  scenarioPath: z.string(),
80
81
  scenarioSha256: z.string().nullable(),
81
82
  metadata: z.record(z.string(), z.unknown()),
83
+ apiMode: z.enum(['real', 'mock']).nullable(),
84
+ apiCalls: z.array(z.record(z.string(), z.unknown())),
82
85
  checks: z.array(z.record(z.string(), z.unknown())),
83
86
  captures: z.array(z.string()),
84
87
  tracePath: z.string().nullable(),
@@ -119,6 +122,8 @@ server.registerTool(
119
122
  scenarioPath: input.scenarioPath,
120
123
  scenarioSha256: null,
121
124
  metadata: {},
125
+ apiMode: input.apiMode || null,
126
+ apiCalls: [],
122
127
  checks: [],
123
128
  captures: [],
124
129
  tracePath: null,
@@ -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
  });
@@ -164,6 +164,44 @@ 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
+
167
205
  test('旧证据必须提供完整语义分组清单,不能按 scene 猜测功能', async () => {
168
206
  const { workspaceRoot, featureDir, runDir, desktop, mobile } = await createFixture();
169
207
  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);
@@ -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. **状态:**将 `delivery/delivery-state.json` 写为 `running`,记录当前阶段、回合数、固定审查点、既有改动和当前产物。每次状态流转都更新该文件。
26
- 3. **实现者:**启动全新的 `$code-craft` 子代理。首回合传入 spec、scope 和已有的 traceability;修复回合只额外传入最新验收失败结果、实现阻断项或代码审计阻断结果。启动时增加实现者回合计数。
27
- 4. **测试编写者:**缺少 `e2e/e2e.feature.mjs` 时,启动全新的 `$e2e-gen-spec` 子代理。旧 `.spec.ts` 不是标准输入,应保持不变。测试编写者必须使用隔离 scene,并在冻结前通过同一运行器 preflight;场景代码、定位器、弹窗、fixture 或等待条件造成的 preflight Blocked 由测试编写者在草稿阶段自行修正并重跑。报告 `ready` 后才冻结 `.mjs`。环境、spec 或实现阻断进入 `awaiting-decision`,由用户选择补充输入、重试、分类、记录 waiver 或停止。
28
- 5. **验收者:**启动全新的 `$e2e-ui-checker` 子代理,并传入 spec、scope、traceability、冻结场景和迭代编号。验收者必须通过隔离 MCP 运行器以 headed 模式执行全部场景,然后只审查 spec 已映射的视觉基准。`passed` 时继续;`failed` 且仍有回合时返回实现者。
29
- 6. **Blocked 决策:**验收者返回 `awaiting-decision` 时,先确认运行器已在阻断 scene 后继续尝试其余独立 scene,再将交付状态写为同名状态,记录该验收代理,保留可见浏览器与证据,并把契约定义的六个决策及其 effect 交给用户。必须明确 `skip` 是接受阻断 scene 的缺失证据并继续审计,不是从异常语句下一行恢复。收到选择后,把决议回传给同一个验收代理,由它调用 `resolve_blocked_run`;验证 `human-decision.json` 后再按其 `effect` 继续。不得自行修改冻结场景、替用户接受风险或把 Blocked 当作终止;只有 `stop` 是终止决策。
30
- 7. **代码审计:**验收通过,或用户 `accept`/`skip` 形成 waiver 后,从固定审查点启动全新的 `$code-audit` 子代理。存在阻断项且仍有回合时返回实现者,修复后重新执行完整验收。没有阻断项时完成交付。
31
- 8. **完成:**标准场景、最新验收、最新代码审计、人工决议、状态与产物必须一致且不存在未授权提交。完整验收通过写入 `passed`;存在 `accept`/`skip` waiver 且审计通过写入 `passed-with-waivers`;只有用户明确选择 `stop` 才写入终止态 `blocked`。Maker 回合耗尽时先进入 `awaiting-decision`,不得替用户停止。
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
  - 只管理和清理由本流程或当前角色启动的进程。
@@ -34,6 +34,15 @@
34
34
 
35
35
  JSON 字段名、状态枚举、检查 ID、scene、`reportGroup`、文件路径、命令、代码标识符和协议值保持原始机器格式。浏览器、控制台、网络和工具返回的英文原始错误必须原样保留,但角色应在相邻字段用简体中文解释其含义,不得篡改证据。
36
36
 
37
+ ## API 数据模式
38
+
39
+ 任何 E2E 场景生成或执行前必须由用户确认 `real` 或 `mock`。用户命令已明确“真实 API”时视为选择 `real`,明确“模拟 API”或 `mock` 时视为选择 `mock`;命令未明确时,编排器进入 `awaiting-decision`,在用户答复前不得让测试编写者自行选择。选择结果、来源和目标环境写入 `delivery-state.json`,并传给测试编写者、Runner 和 Checker。
40
+
41
+ - `real`:浏览器调用已确认环境的真实 API。场景不得使用 `route()`、`page.route()`、HAR 或 fetch/XHR 替换来伪造响应。无法获得认证、测试数据或端点响应时阻断,不得自动降级到 `mock`。涉及外部写入或不可逆副作用时,需要额外明确授权。
42
+ - `mock`:场景可以通过 `route()` 提供受控 fixture,但必须在 metadata、Runner、Checker 和人读报告中明确标记为模拟证据,不得描述成真实环境联调结果。
43
+
44
+ 新建或修订场景声明 `apiModeSchemaVersion: 1`、`apiMode` 和非空 `apiTargets`。每个 target 包含稳定 `id`、大写 HTTP `method` 和不带 query/hash 的绝对 `pathname`。Runner 调用必须显式传入相同 `apiMode`,记录每个声明端点的响应状态;模式不一致、真实模式发生拦截或任一声明端点没有响应证据时均为 `blocked`。旧场景仅为历史证据兼容;进入新的 Delivery 验收时必须先按已确认模式修订并重新冻结。
45
+
37
46
  ## 运行器接口
38
47
 
39
48
  任何实现者回合开始前,都必须调用插件 MCP 工具 `check_environment`。就绪结果证明插件自带的 Node、Playwright 和 Chromium 可以启动。不得检查或依赖目标仓库中的 Playwright 包。
@@ -46,6 +55,7 @@ JSON 字段名、状态枚举、检查 ID、scene、`reportGroup`、文件路径
46
55
  "scenarioPath": ".scratch/feature/e2e/e2e.feature.mjs",
47
56
  "baseUrl": "http://localhost:6060",
48
57
  "outputDir": ".scratch/feature/e2e/runs/iteration-01",
58
+ "apiMode": "real",
49
59
  "iteration": 1,
50
60
  "timeoutMs": 300000,
51
61
  "headed": true,
@@ -87,11 +97,29 @@ JSON 字段名、状态枚举、检查 ID、scene、`reportGroup`、文件路径
87
97
 
88
98
  ```js
89
99
  export const metadata = {
100
+ apiModeSchemaVersion: 1,
101
+ apiMode: 'real',
102
+ apiTargets: [{
103
+ id: 'product-detail',
104
+ method: 'GET',
105
+ pathname: '/api/product/detail'
106
+ }],
90
107
  reportSchemaVersion: 1,
108
+ reportCaptureSchemaVersion: 1,
91
109
  traceabilitySchemaVersion: 1,
92
110
  requirementIds: ['REQ-PD-001'],
93
111
  scenes: ['initial'],
94
112
  viewports: [{ name: 'desktop', width: 1400, height: 1000 }],
113
+ reportCaptures: [{
114
+ requirementId: 'REQ-PD-001',
115
+ scene: 'initial',
116
+ capture: 'product-detail-desktop.png',
117
+ locator: '[data-testid="product-detail"]',
118
+ reportModule: '产品详情',
119
+ reportGroup: 'product-detail',
120
+ reportTitle: '产品详情',
121
+ reportDevice: 'desktop'
122
+ }],
95
123
  visualMappings: []
96
124
  };
97
125
 
@@ -111,19 +139,23 @@ export default async function run({
111
139
  }
112
140
  ```
113
141
 
114
- 场景不得使用 `require`、`process`、`__dirname`、包导入或文件系统写入,也不得额外创建 browser、page 或 context。所有能力都由运行器注入。每个 `metadata.scenes` 项必须通过 `scene(id, callback)` 恰好执行一次;运行器为每个 scene 创建独立 page,scene 内未捕获的定位器、弹窗、fixture 或准备步骤错误只阻断该 scene,记录后继续执行后续 scene,并在全部独立 scene 尝试完毕后返回 `blocked`。scene 必须自包含其页面导航、路由 fixture 和必要状态,不能依赖前一个 scene 的 page 或临时状态。
142
+ 场景不得使用 `require`、`process`、`__dirname`、包导入或文件系统写入,也不得额外创建 browser、page 或 context。所有能力都由运行器注入。每个 `metadata.scenes` 项必须通过 `scene(id, callback)` 恰好执行一次;运行器为每个 scene 创建独立 page,scene 内未捕获的定位器、弹窗、fixture 或准备步骤错误只阻断该 scene,记录后继续执行后续 scene,并在全部独立 scene 尝试完毕后返回 `blocked`。scene 必须自包含其页面导航、数据准备和必要状态,不能依赖前一个 scene 的 page 或临时状态。只有 `mock` 模式可安装路由 fixture;`real` 模式调用注入的 `route` 或 `page.route` 会被运行器拒绝。
115
143
 
116
144
  新建或修订场景必须声明 `metadata.reportSchemaVersion: 1`;运行器据此校验通用报告分组字段,未声明版本的历史冻结场景继续兼容。每条原子运行预期通过 `check(definition, assertion)` 记录。新建或修订的检查定义必须同时包含 `reportModule`、`reportGroup`、`reportTitle` 和 `reportMethod`:`reportModule` 是人读的页面或模块名称,`reportGroup` 是当前功能目录内稳定且唯一的功能组 ID,`reportTitle` 是该组在报告中的简短中文标题,`reportMethod` 是该组件或完整功能合并后的中文检查方式。属于同一 UI 组件、同一页面位置或同一完整用户操作流的文案、样式、布局、响应式行为和相关功能检查共用同一组及同一套模块、标题和检查方式;不同组件、独立业务能力、状态转换或风险边界不得为了减少行数而合并。分组只表达产品功能语义,不表达 E2E scene 或执行顺序。可能失败的用户操作和等待应放在所属的 `check` 或 `scene` 内,不能作为未归属的顶层 await。
117
145
 
118
- 存在追踪输入的新场景必须声明 `metadata.traceabilitySchemaVersion: 1` 和完整、唯一的 `metadata.requirementIds`。每个 `check` 和 `visualMappings` 项必须包含一个已声明的 `requirementId`;运行器拒绝格式错误、未知编号和完全没有运行或视觉覆盖的声明编号。`reportGroup` 用于报告聚合,不能替代 REQ ID。
146
+ 存在追踪输入的新场景必须声明 `metadata.traceabilitySchemaVersion: 1` 和完整、唯一的 `metadata.requirementIds`。每个 `check`、`reportCaptures` 和 `visualMappings` 项必须包含一个已声明的 `requirementId`;运行器拒绝格式错误、未知编号和完全没有运行或视觉覆盖的声明编号。`reportGroup` 用于报告聚合,不能替代 REQ ID。
147
+
148
+ 新建或修订场景必须声明 `metadata.reportCaptureSchemaVersion: 1` 和非空 `metadata.reportCaptures`。每项报告截图包含 `requirementId`、`scene`、简单 PNG 文件名 `capture`、稳定组件 `locator`、`reportModule`、`reportGroup`、`reportTitle` 和 `reportDevice`。每个运行检查的报告功能组至少有一张代表性截图;scope 明确支持且可稳定复现桌面端和移动端时分别保留。场景必须在对应 UI 达到已验证状态后调用 `capture`,最终 Runner 必须记录所有声明截图。缺少声明截图、功能组无截图或出现未声明截图均属于证据链阻断。未声明该 schema 的历史场景继续兼容。
149
+
150
+ `reportCaptures` 是报告中的运行时 UI 证据,不要求 Figma 基准,也不产生视觉一致性结论。`visualMappings` 是设计基准到实际组件截图的映射,只在 spec 要求视觉比对且本地基准存在时使用。一个截图可以同时承担两种用途,但必须分别满足两份 metadata 契约。
119
151
 
120
- spec 声明的每个视觉对比都必须在 `metadata.visualMappings` 中出现且只出现一次,并包含检查定义、场景、截图文件名、定位器说明、仓库相对路径基准、`reportDevice`(`desktop` 或 `mobile`)以及同样的 `reportModule`、`reportGroup`、`reportTitle`。视觉映射与它证明的运行检查必须复用同一功能组。运行检查与视觉映射必须完整且不重复地划分所有原子 spec。
152
+ spec 声明的每个视觉对比都必须在 `metadata.visualMappings` 中出现且只出现一次,并包含检查定义、场景、截图文件名、定位器说明、仓库相对路径基准、`reportDevice`(`desktop` 或 `mobile`)以及同样的 `reportModule`、`reportGroup`、`reportTitle`。视觉映射与它证明的运行检查必须复用同一功能组。运行检查与视觉映射必须完整且不重复地划分所有原子 spec;报告截图不替代任一原子检查或视觉映射。
121
153
 
122
154
  旧版直接使用顶层 `page` 的默认函数仍可执行,但它不具备 scene 级故障隔离;测试编写者创建或修订场景时必须使用 `scene()`。
123
155
 
124
156
  ## 冻结前 Preflight
125
157
 
126
- 测试编写者拥有场景草稿,必须在写入 `ready` 和冻结 SHA-256 前通过同一 `run_feature_e2e` 运行器执行 preflight。preflight 使用独立输出目录和 `keepBrowserOpenOnBlock: false`,只验证场景机械可执行性、scene 覆盖、运行检查可达性、fixture、弹窗和定位器,不给出最终视觉结论。
158
+ 测试编写者拥有场景草稿,必须在写入 `ready` 和冻结 SHA-256 前通过同一 `run_feature_e2e` 运行器执行 preflight。preflight 使用独立输出目录和 `keepBrowserOpenOnBlock: false`,只验证场景机械可执行性、scene 覆盖、运行检查可达性、fixture、弹窗、定位器以及声明的报告截图全部生成,不给出最终视觉结论。
127
159
 
128
160
  - `passed` 或证据完整的 `failed` 证明测试代码可以完整执行;业务断言失败留给独立验收者判定。
129
161
  - `blocked` 且证据指向场景代码、定位器、弹窗处理、fixture 或等待条件时,测试编写者在冻结前自行修正并重跑,不需要人工 `test_defect` 决议。
@@ -163,6 +195,16 @@ fixture 应提供足以触发目标布局和状态的代表性数据,不需要
163
195
  "scenarioPath": ".scratch/feature/e2e/e2e.feature.mjs",
164
196
  "scenarioSha256": "64 个小写十六进制字符",
165
197
  "metadata": {},
198
+ "apiMode": "real",
199
+ "apiCalls": [
200
+ {
201
+ "targetId": "product-detail",
202
+ "method": "GET",
203
+ "origin": "https://test.example.com",
204
+ "pathname": "/api/product/detail",
205
+ "status": 200
206
+ }
207
+ ],
166
208
  "checks": [],
167
209
  "captures": [],
168
210
  "tracePath": ".scratch/feature/e2e/runs/iteration-01/trace.zip",
@@ -176,7 +218,7 @@ fixture 应提供足以触发目标布局和状态的代表性数据,不需要
176
218
  }
177
219
  ```
178
220
 
179
- `passed` 表示所有运行检查均通过。`failed` 表示执行完整结束,但至少一个业务断言失败。`blocked` 表示至少一个 scene、运行器、浏览器、超时、路径策略或证据链阻止了完整证据;使用 `scene()` 时其余独立 scene 会继续执行,全部尝试后再触发人工决议。
221
+ `passed` 表示 API 模式一致、所有声明 API 均有响应证据且全部运行检查通过。`failed` 表示证据完整,但至少一个业务断言失败。`blocked` 表示 API 模式或响应证据、scene、运行器、浏览器、超时、路径策略或其他证据链阻止了完整证据;使用 `scene()` 时其余独立 scene 会继续执行,全部尝试后再触发人工决议。
180
222
 
181
223
  ## 角色结果
182
224
 
@@ -203,6 +245,8 @@ fixture 应提供足以触发目标布局和状态的代表性数据,不需要
203
245
  "scenes": [],
204
246
  "scenarioSha256": "64 个小写十六进制字符",
205
247
  "preflightRunnerResult": ".scratch/feature/e2e/preflight/attempt-01/runner-result.json",
248
+ "apiMode": "real",
249
+ "apiCalls": [],
206
250
  "preflightChecks": [],
207
251
  "blockers": []
208
252
  }
@@ -215,6 +259,8 @@ fixture 应提供足以触发目标布局和状态的代表性数据,不需要
215
259
  "status": "passed",
216
260
  "iteration": 1,
217
261
  "runnerResult": ".scratch/feature/e2e/runs/iteration-01/runner-result.json",
262
+ "apiMode": "real",
263
+ "apiCalls": [],
218
264
  "checks": [],
219
265
  "blockers": []
220
266
  }
@@ -247,4 +293,4 @@ Excel 只包含一个名为 `自测报告` 的工作表,每行代表一个真
247
293
 
248
294
  ## 编排状态
249
295
 
250
- `delivery/delivery-state.json` 记录 `status`、`phase`、`makerTurns`、`maxMakerTurns`、`fixedPoint`、`preExistingChanges`、最新产物路径,以及等待决议时的验收代理、`blockedSessionId`、过期时间和 waiver。`status` 可为 `running`、`awaiting-decision`、`passed`、`passed-with-waivers` 或 `blocked`。该文件只记录结论,不得覆盖角色自行拥有的结果。
296
+ `delivery/delivery-state.json` 记录 `status`、`phase`、`makerTurns`、`maxMakerTurns`、`fixedPoint`、`preExistingChanges`、`apiMode`、`apiModeSource`、目标环境、最新产物路径,以及等待决议时的验收代理、`blockedSessionId`、过期时间和 waiver。`status` 可为 `running`、`awaiting-decision`、`passed`、`passed-with-waivers` 或 `blocked`。API 模式尚未确认时使用 `status: "awaiting-decision"` 和 `phase: "api-mode-confirmation"`。该文件只记录结论,不得覆盖角色自行拥有的结果。
@@ -12,6 +12,7 @@ description: 在首版实现可运行后,为 Autobest Delivery 运行器编写
12
12
  - `featureDir/spec.md` 及同目录的 `scope.md`。
13
13
  - 仓库规则、路由、fixture、运行事实,以及 spec 已映射的本地视觉基准。
14
14
  - 可运行的首版实现。
15
+ - 编排器已确认的 `apiMode`(`real` 或 `mock`)及目标环境;不得由测试编写者自行选择或切换。
15
16
  - 仅在修订回合读取 `test_defect` 人工决议及其指向的 Blocked runner/checker 证据。
16
17
 
17
18
  存在 `featureDir/spec-traceability.json` 时必须读取并校验;它定义本场景允许使用的完整 active REQ ID 集合。
@@ -20,15 +21,15 @@ description: 在首版实现可运行后,为 Autobest Delivery 运行器编写
20
21
 
21
22
  ## 流程
22
23
 
23
- 追踪模式下,每项运行检查和视觉映射都写入所属 `requirementId`,不得创建或重编号。`metadata` 必须声明 `traceabilitySchemaVersion: 1` 和完整、唯一的 `requirementIds`;冻结前确认每个 active ID 至少有一项运行或视觉覆盖。
24
+ 追踪模式下,每项运行检查、报告截图和视觉映射都写入所属 `requirementId`,不得创建或重编号。`metadata` 必须声明 `traceabilitySchemaVersion: 1` 和完整、唯一的 `requirementIds`;冻结前确认每个 active ID 至少有一项运行或视觉覆盖。
24
25
 
25
26
  1. 将每条原子 spec 预期准确映射到一个运行检查或一个视觉映射。为每项分配通用的 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod`,让同一 UI 组件、页面位置或完整用户操作流的文案、样式、布局、响应式行为和相关功能检查在报告中合并;同组的模块、标题和检查方式必须完全一致。不同组件、独立业务能力、状态转换或风险边界保持不同组。分组只表达当前 spec 的功能语义,不得按 scene 或执行顺序分组,不得写死特定页面族词汇或依赖导出器猜测业务语义。覆盖所有必需页面变体、状态转换、空态或错误态、导航结果和响应式变体。
26
- 2. 解析并探测具体 URL、启动行为、受控 API 数据、持久化状态和面向用户的稳定定位器。通过解码后的状态或受控 fixture 确认通配路由变体。
27
- 3. 首次编写只写入 `featureDir/e2e/e2e.feature.mjs`,保持旧 `.spec.ts` 不变。该文件不得包含 import,只导出 `metadata` 和一个默认异步函数,并且只能使用运行器注入的 `scene`、`page`、`expect`、`check`、`route`、`capture`、`artifact`、`baseUrl` 和 `parseUrl`。新建或修订场景在 metadata 中声明 `reportSchemaVersion: 1`,使运行器校验所有检查和视觉映射的通用报告分组字段。每个 `metadata.scenes` 项通过 `scene(id, callback)` 恰好执行一次;每个 scene 自包含页面导航、API fixture 和必要状态。冻结后只有用户通过 `resolve_blocked_run` 明确选择 `test_defect` 才能修订;修订结果必须记录旧、新 SHA-256 和决议证据路径。
27
+ 2. 解析并探测具体 URL、启动行为、API 端点、持久化状态和面向用户的稳定定位器。`real` 模式使用已确认环境的真实账号和测试数据,不得调用 `route()`、`page.route()`、HAR 回放或在浏览器内替换 fetch/XHR;无法取得所需真实数据时返回 `environment` 阻断,不得改用 fixture。`mock` 模式才允许使用受控 fixture 确认数据变体。涉及外部写入副作用但未获得明确授权时停止并交回编排器。
28
+ 3. 首次编写只写入 `featureDir/e2e/e2e.feature.mjs`,保持旧 `.spec.ts` 不变。该文件不得包含 import,只导出 `metadata` 和一个默认异步函数,并且只能使用运行器注入的 `scene`、`page`、`expect`、`check`、`route`、`capture`、`artifact`、`baseUrl` 和 `parseUrl`。新建或修订场景在 metadata 中声明 `apiModeSchemaVersion: 1`、已确认的 `apiMode`、非空 `apiTargets`、`reportSchemaVersion: 1` 和 `reportCaptureSchemaVersion: 1`。`apiTargets` 列举本次必须真实命中的 HTTP method 和 pathname;不把遥测、配置等无关请求当作业务 API 证据。每个 `metadata.scenes` 项通过 `scene(id, callback)` 恰好执行一次;每个 scene 自包含页面导航、数据准备和必要状态。冻结后只有用户通过 `resolve_blocked_run` 明确选择 `test_defect` 或明确改变 API 模式/环境时才能修订;修订结果必须记录旧、新 SHA-256 和决议证据路径。
28
29
  4. 在 metadata 中冻结桌面宽度 `1400px`、移动端宽度 `375px` 和明确稳定的高度。spec 声明的每个组件视觉对比都要映射场景、截图文件名、定位器说明、仓库相对路径基准、`reportDevice`(`desktop` 或 `mobile`)、`reportModule`、`reportGroup`、`reportTitle`,以及完整的验收检查定义。视觉映射与它证明的运行检查复用同一功能组。截图定位器必须与 Figma 基准指向同一语义节点;fixture 只需提供能触发目标布局和状态的代表性数据,不复刻文字长度、业务文案或图片主体,视觉比较边界遵循共享契约。
29
- 5. 通过 `check(definition, assertion)` 记录功能和 DOM 预期;定义中的 `reportModule`、`reportTitle` 和 `reportMethod` 使用简体中文,`reportGroup` 使用稳定的 ASCII ID。把可能失败的点击、选择、弹窗、等待和状态准备放在所属的 `check` 或 `scene` 内,不在两个检查之间留下未归属的可失败 await。`capture` 仅用于已映射的组件根节点,`artifact` 用于结构化 DOM 测量。不得创建其他浏览器、context 或 page,不得直接写文件、使用 Node 全局变量、导入包、截取未映射的整页截图或自行给出视觉结论。
30
- 6. 使用 `node --check` 检查草稿语法后,启动必需服务并调用同一隔离运行器执行冻结前 preflight,输出到 `e2e/preflight/attempt-NN`,使用 `headed: false`、`keepBrowserOpenOnBlock: false`。`blocked` 若源于场景代码、定位器、弹窗处理、fixture 或等待条件,在冻结前自行修正并重跑;不得放宽 spec 断言。环境、spec 或实现阻断按真实类型报告。
31
- 7. 只有 preflight 返回 `passed`,或返回证据完整且所有声明 scene 和预期检查均已到达的 `failed`,才能冻结当前 SHA-256。核对 metadata、检查项覆盖和路径所有权,将最终 preflight 路径、SHA-256、scene 和检查覆盖写入 `e2e/author-result.json` 并标记 `ready`。preflight 只证明测试代码可执行,不替代独立最终验收或视觉审查。
30
+ 5. 通过 `check(definition, assertion)` 记录功能和 DOM 预期;定义中的 `reportModule`、`reportTitle` 和 `reportMethod` 使用简体中文,`reportGroup` 使用稳定的 ASCII ID。每个报告功能组在 `metadata.reportCaptures` 中至少声明一张代表性组件截图,并在对应 scene 达到已验证状态后调用 `capture`;scope 明确支持且能稳定复现两种设备时分别保留 desktop/mobile。报告截图用于证明运行状态,不因缺少 Figma 基准而省略,也不参与视觉一致性判定。把可能失败的点击、选择、弹窗、等待和状态准备放在所属的 `check` 或 `scene` 内,不在两个检查之间留下未归属的可失败 await。`artifact` 用于结构化 DOM 测量。不得创建其他浏览器、context 或 page,不得直接写文件、使用 Node 全局变量、导入包、截取与报告功能无关的整页截图或自行给出视觉结论。
31
+ 6. 使用 `node --check` 检查草稿语法后,启动必需服务并调用同一隔离运行器执行冻结前 preflight,显式传入编排器确认的 `apiMode`,输出到 `e2e/preflight/attempt-NN`,使用 `headed: false`、`keepBrowserOpenOnBlock: false`。`blocked` 若源于场景代码、定位器、弹窗处理、mock fixture 或等待条件,在冻结前自行修正并重跑;不得放宽 spec 断言。真实环境 API 不可用时按 `environment` 报告,不得通过补 mock 使 preflight 通过。
32
+ 7. 只有 preflight 返回 `passed`,或返回证据完整且所有声明 API、scene、预期检查和报告截图均已到达的 `failed`,才能冻结当前 SHA-256。核对 API 模式、`apiCalls`、metadata、检查项覆盖、截图覆盖和路径所有权,将最终 preflight 路径、SHA-256、API 模式、API 响应证据、scene、检查及截图覆盖写入 `e2e/author-result.json` 并标记 `ready`。`reportMethod` 必须如实注明“真实环境 API”或“模拟 API”,不得让两类结果在报告中无法区分。preflight 只证明测试代码可执行,不替代独立最终验收或视觉审查。
32
33
 
33
34
  ## 阻断条件
34
35
 
@@ -13,17 +13,18 @@ description: 使用 Autobest Delivery MCP 运行器在可见浏览器中独立
13
13
  - spec 已映射的本地视觉基准。
14
14
  - 迭代编号;单独使用时默认为 `1`。
15
15
  - 可发现的开发启动命令和基础 URL。
16
+ - 编排器已确认的 `apiMode` 和目标环境。
16
17
 
17
18
  ## 流程
18
19
 
19
20
  存在 `spec-traceability.json` 时,先确认冻结场景声明的 REQ ID 与 active 集合完全一致。合并 Runner 和视觉结果时原样保留每项 `requirementId`,逐个汇总 passed、failed 或 blocked;未知编号、缺失编号或 active 需求无证据都属于证据链阻断,不能判为通过。
20
21
 
21
- 1. 建立原子 spec 检查清单,确认冻结运行检查和 metadata 视觉映射完整且不重复地划分了全部检查项。`reportSchemaVersion: 1` 的场景须核对每个运行检查包含 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod`,同组字段一致且表达真实组件或完整功能,而不是 E2E scene;同时核对视觉映射的同组字段和 `reportDevice` 正确区分桌面端与移动端。历史场景没有报告 schema 时不据此阻断。
22
+ 1. 建立原子 spec 检查清单,确认冻结场景的 `apiMode` 与编排器确认值一致,`apiTargets` 覆盖本次业务 API;不一致或缺失时不得运行。确认冻结运行检查和 metadata 视觉映射完整且不重复地划分了全部检查项。`reportSchemaVersion: 1` 的场景须核对每个运行检查包含 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod`,同组字段一致且表达真实组件或完整功能,而不是 E2E scene;同时核对视觉映射的同组字段和 `reportDevice` 正确区分桌面端与移动端。`reportCaptureSchemaVersion: 1` 的场景还须核对 `metadata.reportCaptures`:每个报告功能组至少有一张已声明的代表性 UI 截图,scene、locator、设备及 REQ 映射有效。历史场景没有对应 schema 时不据此阻断。
22
23
  2. 调用 `autobest-delivery` MCP 的 `check_environment` 工具并传入 `headed: true`。环境未就绪时,以返回的版本和错误证据形成 `tool` 阻断;不得降级使用通用 Playwright MCP 或目标仓库依赖。
23
24
  3. 只解析并启动仓库已声明且本次必需的开发服务,记录其 PID,并确认基础 URL。复用冻结的 `1400px` 和 `375px` 视口及受控路由或 API 状态。
24
- 4. 调用 `run_feature_e2e`,传入工作区绝对路径、标准场景路径、基础 URL、`featureDir/e2e/runs/iteration-NN`、迭代编号、有界超时、`headed: true` 和 `keepBrowserOpenOnBlock: true`。将运行器返回结果视为不可修改的证据。
25
- 5. 运行器返回 `blocked` 时,确认 `blockedSessionId`、过期时间、当前页面和 Trace,并核对每个独立 scene 的已执行检查、缺失检查和 blocker;scene 级阻断不应阻止其余独立 scene 留下证据。验收结果写为 `awaiting-decision` 并交回编排器;保留本验收代理供决议 follow-up 使用。编排器回传用户选择后,由同一个验收代理调用 `resolve_blocked_run` 并复读决议产物。明确说明 `skip` 只接受阻断 scene 的缺失证据,不会从异常语句恢复执行。不得自行修改测试、选择决议或给出验收结论。返回 `failed` 时取得功能失败项。运行器返回 `passed` 或 `failed` 时,检查每个已映射组件的实际截图、基准截图和已记录的叶子 DOM 测量。只比较映射指定的组件、状态和设备,并按共享契约执行语义视觉比较:忽略业务文字的内容和长度及业务图片主体,检查结构、几何、间距、对齐、响应式排列和视觉样式。文案正确性由运行检查判定。默认 `1px` 几何容差仅用于视口、语义节点和组件状态一致的可比元素;捕获边界不一致应判为视觉映射缺陷,不得判为实现缺陷。
26
- 6. 合并运行检查和视觉检查,确保每条原子 spec 只出现一次。证据完整但存在失败项时为 `failed`;全部通过时为 `passed`;缺失任一必要证据时为 `awaiting-decision`。
25
+ 4. 调用 `run_feature_e2e`,传入工作区绝对路径、标准场景路径、基础 URL、`featureDir/e2e/runs/iteration-NN`、已确认的 `apiMode`、迭代编号、有界超时、`headed: true` 和 `keepBrowserOpenOnBlock: true`。将运行器返回结果视为不可修改的证据。
26
+ 5. 运行器返回 `blocked` 时,确认 `blockedSessionId`、过期时间、当前页面和 Trace,并核对每个独立 scene 的已执行检查、缺失检查和 blocker;scene 级阻断不应阻止其余独立 scene 留下证据。验收结果写为 `awaiting-decision` 并交回编排器;保留本验收代理供决议 follow-up 使用。编排器回传用户选择后,由同一个验收代理调用 `resolve_blocked_run` 并复读决议产物。明确说明 `skip` 只接受阻断 scene 的缺失证据,不会从异常语句恢复执行。不得自行修改测试、选择决议或给出验收结论。返回 `failed` 时取得功能失败项。运行器返回 `passed` 或 `failed` 时,先确认每个 `metadata.reportCaptures` 声明的截图都真实存在于本轮 Runner 结果中;缺少任一报告截图均为证据链阻断并进入 `awaiting-decision`。报告截图只证明对应运行状态,不作为视觉一致性结论。随后检查每个已映射组件的实际截图、基准截图和已记录的叶子 DOM 测量;只有 `visualMappings` 才执行视觉比较。只比较映射指定的组件、状态和设备,并按共享契约执行语义视觉比较:忽略业务文字的内容和长度及业务图片主体,检查结构、几何、间距、对齐、响应式排列和视觉样式。文案正确性由运行检查判定。默认 `1px` 几何容差仅用于视口、语义节点和组件状态一致的可比元素;捕获边界不一致应判为视觉映射缺陷,不得判为实现缺陷。
27
+ 6. 核对 Runner 的 `apiMode` 和每个 `apiTargets` 对应的 `apiCalls`。`real` 模式出现请求拦截、缺少真实响应证据或环境不可用时为 `awaiting-decision`,不得改用 mock 重跑;`mock` 模式必须在报告中明确标为模拟。合并运行检查和视觉检查,确保每条原子 spec 只出现一次。证据完整但存在失败项时为 `failed`;全部通过时为 `passed`;缺失任一必要证据时为 `awaiting-decision`。
27
28
  7. 写入并复读 `checker/checker-result.json` 和 `.md`,确保两份报告结论一致,且所有证据路径均为仓库相对路径。合并 Runner 检查时原样保留 `reportModule`、`reportGroup`、`reportTitle` 和 `reportMethod`。角色撰写的检查说明、期望、实际结果和全部 Markdown 内容必须使用简体中文;Markdown 使用共享契约规定的中文标题、中文表头和中文展示状态。JSON 的字段名、机器状态、ID、scene 和路径保持协议格式,英文原始错误原样引用并附中文解释。
28
29
  8. 普通完成时关闭浏览器资源;存在 `blockedSessionId` 时保留浏览器和当前验收者启动的开发服务,直到编排器完成决议。决议后只清理由本轮启动的资源;会话过期也必须清理。
29
30
 
@@ -9,7 +9,7 @@ description: 从已完成或已阻断的 Autobest Delivery Maker、Runner 和 Ch
9
9
 
10
10
  ## 输入
11
11
 
12
- - 用户指定的功能目录,其中包含 `spec.md`、`checker/checker-result.json` 和对应 Runner 结果。
12
+ - 用户指定的功能目录,其中包含 `spec.md`、`checker/checker-result.json` 和对应 Runner 结果。`spec.md` 可以是旧版 `## 用户故事` 结构,也可以是按 `## REQ-<SCOPE>-<序号>` 分节的新追踪结构。
13
13
  - 可选的报告输出路径;必须位于功能目录内。默认写入 `featureDir/delivery-report.xlsx`。
14
14
 
15
15
  ## 执行
@@ -27,13 +27,13 @@ node <plugin-root>/scripts/export-delivery-report.mjs <feature-dir> \
27
27
  [--grouping <feature-dir>/report/report-groups.json]
28
28
  ```
29
29
 
30
- 4. 复读脚本的 JSON 输出,向用户报告文件路径、Runner iteration、Checker/Runner 状态、功能行数、状态计数、唯一嵌入截图数、截图放置数和分组来源。
30
+ 4. 复读脚本的 JSON 输出,向用户报告文件路径、Runner iteration、Checker/Runner 状态、API 模式、功能行数、状态计数、唯一嵌入截图数、截图放置数和分组来源。
31
31
 
32
- 报告固定使用插件资产 `assets/delivery-report-template.xlsx`,且只能生成一个名为 `自测报告` 的工作表。列固定为:页面/模块、自测点、自测方式、通过、截图1(桌面端)、截图2(移动端)。不得生成原子检查明细 Sheet,不得在可见单元格中输出 story ID、scene 名、历史场景或其他执行结构。
32
+ 报告固定使用插件资产 `assets/delivery-report-template.xlsx`,且只能生成一个名为 `自测报告` 的工作表。列固定为:页面/模块、自测点、自测方式、通过、截图1(桌面端)、截图2(移动端)。导出器根据 Runner 的 `apiMode` 自动在“自测方式”中标注“真实环境 API”或“模拟 API”;历史 Runner 没有 API 模式时不推断。不得生成原子检查明细 Sheet,不得在可见单元格中输出 story ID、scene 名、历史场景或其他执行结构。
33
33
 
34
34
  每一行代表一个用户能识别的真实 UI 组件或完整业务功能。属于同一组件、同一页面位置或同一完整操作流的文案、样式、布局、响应式行为和相关功能检查必须合并到一行;仅仅修改了文字、颜色、间距或设备变体,不得拆成多行。不同组件、独立业务能力、状态转换或风险边界不得为了减少行数而强行合并。分组依据是 `spec.md` 描述的产品语义,不是 E2E scene、检查数量、story 顺序或截图文件名。
35
35
 
36
- 新证据由测试编写者在每个运行检查中提供 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod`,并在视觉映射中提供同组字段和 `reportDevice`。同一 `reportGroup` 的模块、标题和自测方式必须一致。组内状态按 `Blocked > 不通过 > 通过` 取最严重值;桌面端和移动端代表截图放在同一行,每种设备最多一张。相同截图二进制只嵌入一次。
36
+ 新证据由测试编写者在每个运行检查中提供 `reportModule`、`reportGroup`、`reportTitle`、`reportMethod`,其中 `reportMethod` 必须明确说明使用“真实环境 API”或“模拟 API”,并与 Runner 的 `apiMode` 一致;不得把 mock 报告描述为真实联调。通过 `metadata.reportCaptures` 提供同组的运行时代表截图和 `reportDevice`。`reportCaptures` 不要求 Figma 基准,只用于报告展示已验证的 UI 状态;`visualMappings` 只用于存在本地设计基准的视觉一致性验收,两者不得混为一种结论。同一 `reportGroup` 的模块、标题和自测方式必须一致。组内状态按 `Blocked > 不通过 > 通过` 取最严重值;桌面端和移动端代表截图放在同一行,每种设备最多一张。相同截图二进制只嵌入一次。
37
37
 
38
38
  ## 历史证据
39
39
 
@@ -62,7 +62,7 @@ node <plugin-root>/scripts/export-delivery-report.mjs <feature-dir> \
62
62
  }
63
63
  ```
64
64
 
65
- 清单必须与当前 spec、iteration 和冻结场景哈希完全一致,并让每个 story 恰好出现一次。截图字段只能指向 Runner 已记录且能唯一匹配的截图。脚本负责验证清单和生成工作簿,不对缺失的业务语义进行自动猜测。
65
+ 清单必须与当前 spec、iteration 和冻结场景哈希完全一致,并让每个旧版 story 或新版运行检查恰好出现一次。截图字段只能指向 Runner 已记录且能唯一匹配的截图。脚本负责验证清单和生成工作簿,不对缺失的业务语义进行自动猜测。历史 Runner 没有截图时仍允许导出无截图报告;新建或修订的 Delivery 场景应由运行器确保报告截图完整。
66
66
 
67
67
  ## 完成条件
68
68