miaoda-game-devkit 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -88,8 +88,9 @@ DOM 到 Phaser 的输入、命中测试和游戏状态变化,不验证 Canvas/
88
88
  `host.checkpoint("recovery")` 等稳定检查点,并在清单中通过 `requireCheckpoint` 声明。
89
89
  运行结束会输出稳定的 `GAMEPLAY_AUDIT: ALL CONTRACTS PASSED` 或
90
90
  `GAMEPLAY_AUDIT: ... FAILED` 标识。该标识只表示玩法证据审计;`pnpm test` 的整体结果
91
- 必须以最后的 `TEST_RESULT: PASS` 或 `TEST_RESULT: FAIL` 为准。失败报告会合并重复的问题,
92
- 并给出按类别去重的修复建议,先处理列出的原始测试错误,再重新运行 `pnpm test`。
91
+ 必须以最后的 `TEST_RESULT: PASS` 或 `TEST_RESULT: FAIL` 为准。失败报告按根因合并问题并
92
+ 给出直接修复步骤;测试文件无法加载或配置要求的测试尚未建立时,不再继续报告 Scene
93
+ 注册等派生问题。先处理第一类错误,再重新运行 `pnpm test`。
93
94
 
94
95
  共享 Vitest 配置使用 `minimal` reporter 和 `text-summary` coverage reporter,保留失败
95
96
  定位、Scene 覆盖摘要和最终机器标识,同时避免输出完整测试列表和逐文件噪声。
@@ -217,7 +217,7 @@ function auditGameplayCollection(options2, file, tests) {
217
217
  const matches = actual.filter((test) => test.metadata?.id === contract.id);
218
218
  if (matches.length === 0) {
219
219
  issues.push(
220
- `\u6D4B\u8BD5\u6267\u884C\u524D\u672A\u6536\u96C6\u5230 contract "${contract.id}"\uFF08${normalizedFile}\uFF09\u3002`
220
+ `\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "${contract.id}" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08${normalizedFile}\uFF09\u3002`
221
221
  );
222
222
  } else if (matches.length > 1) {
223
223
  issues.push(
@@ -334,18 +334,21 @@ function auditGameplayRun(options2, tests) {
334
334
  observedEvidence.flatMap((evidence) => evidence.registeredScenes)
335
335
  );
336
336
  const expectedScenes = normalizeSet(options2.scenes);
337
- for (const scene of expectedScenes) {
338
- if (!registeredScenes.includes(scene)) {
339
- issues.push(
340
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
341
- );
337
+ if (observedEvidence.length > 0) {
338
+ for (const scene of expectedScenes) {
339
+ if (!registeredScenes.includes(scene)) {
340
+ issues.push(
341
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
342
+ );
343
+ }
344
+ }
345
+ for (const scene of registeredScenes) {
346
+ if (!expectedScenes.includes(scene)) {
347
+ issues.push(
348
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
349
+ );
350
+ }
342
351
  }
343
- }
344
- for (const scene of registeredScenes) {
345
- if (!expectedScenes.includes(scene))
346
- issues.push(
347
- `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
348
- );
349
352
  }
350
353
  return { passed: issues.length === 0, issues: [...new Set(issues)] };
351
354
  }
@@ -356,18 +359,31 @@ function formatGameplayIssues(issues) {
356
359
  const groups = /* @__PURE__ */ new Map();
357
360
  const repairKeys = /* @__PURE__ */ new Set();
358
361
  const repairs = [];
362
+ const missingByFile = /* @__PURE__ */ new Map();
363
+ const missingIds = /* @__PURE__ */ new Set();
364
+ for (const issue of issues) {
365
+ const match = issue.match(
366
+ /没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
367
+ );
368
+ if (!match) continue;
369
+ const [, id, file] = match;
370
+ missingIds.add(id);
371
+ const ids = missingByFile.get(file) ?? [];
372
+ if (!ids.includes(id)) ids.push(id);
373
+ missingByFile.set(file, ids);
374
+ }
359
375
  const repairText = {
360
376
  input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
361
377
  frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
362
378
  physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
363
- transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
379
+ transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
364
380
  visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
365
381
  restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
366
- checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
382
+ checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
367
383
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
368
- registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
369
- missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
370
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
384
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
385
+ missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
386
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
371
387
  };
372
388
  const getCategory = (issue) => {
373
389
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -395,9 +411,26 @@ function formatGameplayIssues(issues) {
395
411
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
396
412
  return { category: "other", key: `other:${issue}` };
397
413
  };
414
+ for (const [file, ids] of missingByFile) {
415
+ groups.set(`missing:${file}`, {
416
+ text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
417
+ ids: [],
418
+ key: `missing:${file}`
419
+ });
420
+ }
421
+ if (missingByFile.size > 0) {
422
+ repairKeys.add("missing");
423
+ repairs.push(repairText.missing);
424
+ }
398
425
  for (const rawIssue of [...new Set(issues)]) {
399
426
  const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
400
- const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
427
+ if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
428
+ const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
429
+ if (missingContract && missingIds.has(missingContract)) continue;
430
+ if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
431
+ continue;
432
+ }
433
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
401
434
  const id = compact.match(/contract "([^"]+)"/)?.[1];
402
435
  const { category, key } = getCategory(compact);
403
436
  const group = groups.get(key);
@@ -414,7 +447,7 @@ function formatGameplayIssues(issues) {
414
447
  const grouped = [...groups.values()];
415
448
  const visible = grouped.slice(0, 8).map((group) => {
416
449
  if (group.ids.length <= 1) return group.text;
417
- return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
450
+ return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
418
451
  });
419
452
  return {
420
453
  issues: visible.map(
@@ -650,7 +683,7 @@ describe("gameplay audit", () => {
650
683
  expect(result).toEqual({
651
684
  passed: false,
652
685
  issues: [
653
- '\u6D4B\u8BD5\u6267\u884C\u524D\u672A\u6536\u96C6\u5230 contract "gameplay.main-flow"\uFF08tests/gameplay.test.ts\uFF09\u3002'
686
+ '\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "gameplay.main-flow" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08tests/gameplay.test.ts\uFF09\u3002'
654
687
  ]
655
688
  });
656
689
  });
@@ -758,7 +791,7 @@ describe("gameplay audit", () => {
758
791
  expect(result.issues).toHaveLength(4);
759
792
  expect(result.repairs).toHaveLength(3);
760
793
  expect(result.repairs[0]).toContain("\u8F93\u5165");
761
- expect(result.repairs[1]).toContain("\u8F6C\u573A");
794
+ expect(result.repairs[1]).toContain("\u573A\u666F\u5207\u6362");
762
795
  });
763
796
  it("\u5C06\u591A\u68C0\u67E5\u9879\u7684\u7F3A\u53E3\u9650\u5236\u4E3A\u95EE\u9898\u7EC4\u800C\u4E0D\u662F\u539F\u59CB\u65E5\u5FD7\u884C", () => {
764
797
  const issues = Array.from(
@@ -770,4 +803,18 @@ describe("gameplay audit", () => {
770
803
  expect(result.repairs).toHaveLength(1);
771
804
  expect(result.omitted).toBe(0);
772
805
  });
806
+ it("\u5C06\u7F3A\u5931\u6D4B\u8BD5\u5408\u5E76\u4E3A\u4E00\u4E2A\u53EF\u6267\u884C\u4FEE\u590D\u5E76\u9690\u85CF Scene \u7EA7\u8054\u9519\u8BEF", () => {
807
+ const result = formatGameplayIssues([
808
+ '\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "IndexScene.boot" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08tests/production-game.test.ts\uFF09\u3002',
809
+ '\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "BattleScene.playthrough" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08tests/production-game.test.ts\uFF09\u3002',
810
+ '\u7F3A\u5C11 gameplay contract "IndexScene.boot"\u3002',
811
+ '\u7F3A\u5C11 gameplay contract "BattleScene.playthrough"\u3002',
812
+ '\u751F\u4EA7 Scene "BattleScene" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002'
813
+ ]);
814
+ expect(result.issues).toEqual([
815
+ "tests/production-game.test.ts \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1AIndexScene.boot\u3001BattleScene.playthrough\u3002"
816
+ ]);
817
+ expect(result.repairs).toHaveLength(1);
818
+ expect(result.repairs[0]).toContain("vitest.config.ts");
819
+ });
773
820
  });
@@ -222,7 +222,7 @@ function auditGameplayCollection(options, file, tests) {
222
222
  const matches = actual.filter((test) => test.metadata?.id === contract.id);
223
223
  if (matches.length === 0) {
224
224
  issues.push(
225
- `\u6D4B\u8BD5\u6267\u884C\u524D\u672A\u6536\u96C6\u5230 contract "${contract.id}"\uFF08${normalizedFile}\uFF09\u3002`
225
+ `\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "${contract.id}" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08${normalizedFile}\uFF09\u3002`
226
226
  );
227
227
  } else if (matches.length > 1) {
228
228
  issues.push(
@@ -339,18 +339,21 @@ function auditGameplayRun(options, tests) {
339
339
  observedEvidence.flatMap((evidence) => evidence.registeredScenes)
340
340
  );
341
341
  const expectedScenes = normalizeSet(options.scenes);
342
- for (const scene of expectedScenes) {
343
- if (!registeredScenes.includes(scene)) {
344
- issues.push(
345
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
346
- );
342
+ if (observedEvidence.length > 0) {
343
+ for (const scene of expectedScenes) {
344
+ if (!registeredScenes.includes(scene)) {
345
+ issues.push(
346
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
347
+ );
348
+ }
349
+ }
350
+ for (const scene of registeredScenes) {
351
+ if (!expectedScenes.includes(scene)) {
352
+ issues.push(
353
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
354
+ );
355
+ }
347
356
  }
348
- }
349
- for (const scene of registeredScenes) {
350
- if (!expectedScenes.includes(scene))
351
- issues.push(
352
- `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
353
- );
354
357
  }
355
358
  return { passed: issues.length === 0, issues: [...new Set(issues)] };
356
359
  }
@@ -378,18 +381,31 @@ function formatGameplayIssues(issues) {
378
381
  const groups = /* @__PURE__ */ new Map();
379
382
  const repairKeys = /* @__PURE__ */ new Set();
380
383
  const repairs = [];
384
+ const missingByFile = /* @__PURE__ */ new Map();
385
+ const missingIds = /* @__PURE__ */ new Set();
386
+ for (const issue of issues) {
387
+ const match = issue.match(
388
+ /没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
389
+ );
390
+ if (!match) continue;
391
+ const [, id, file] = match;
392
+ missingIds.add(id);
393
+ const ids = missingByFile.get(file) ?? [];
394
+ if (!ids.includes(id)) ids.push(id);
395
+ missingByFile.set(file, ids);
396
+ }
381
397
  const repairText = {
382
398
  input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
383
399
  frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
384
400
  physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
385
- transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
401
+ transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
386
402
  visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
387
403
  restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
388
- checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
404
+ checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
389
405
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
390
- registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
391
- missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
392
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
406
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
407
+ missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
408
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
393
409
  };
394
410
  const getCategory = (issue) => {
395
411
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -417,9 +433,26 @@ function formatGameplayIssues(issues) {
417
433
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
418
434
  return { category: "other", key: `other:${issue}` };
419
435
  };
436
+ for (const [file, ids] of missingByFile) {
437
+ groups.set(`missing:${file}`, {
438
+ text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
439
+ ids: [],
440
+ key: `missing:${file}`
441
+ });
442
+ }
443
+ if (missingByFile.size > 0) {
444
+ repairKeys.add("missing");
445
+ repairs.push(repairText.missing);
446
+ }
420
447
  for (const rawIssue of [...new Set(issues)]) {
421
448
  const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
422
- const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
449
+ if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
450
+ const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
451
+ if (missingContract && missingIds.has(missingContract)) continue;
452
+ if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
453
+ continue;
454
+ }
455
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
423
456
  const id = compact.match(/contract "([^"]+)"/)?.[1];
424
457
  const { category, key } = getCategory(compact);
425
458
  const group = groups.get(key);
@@ -436,7 +469,7 @@ function formatGameplayIssues(issues) {
436
469
  const grouped = [...groups.values()];
437
470
  const visible = grouped.slice(0, 8).map((group) => {
438
471
  if (group.ids.length <= 1) return group.text;
439
- return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
472
+ return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
440
473
  });
441
474
  return {
442
475
  issues: visible.map(
@@ -450,9 +483,7 @@ function printIssues(stage, issues) {
450
483
  if (issues.length === 0) return;
451
484
  console.error(`
452
485
  GAMEPLAY_AUDIT: ${stage} FAILED`);
453
- console.error(
454
- "\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
455
- );
486
+ console.error("\u73A9\u6CD5\u6D4B\u8BD5\u672A\u901A\u8FC7\u3002\u8BF7\u5148\u5904\u7406\u7B2C\u4E00\u7C7B\u95EE\u9898\uFF0C\u91CD\u65B0\u8FD0\u884C\u540E\u518D\u5904\u7406\u540E\u7EED\u95EE\u9898\u3002");
456
487
  const formatted = formatGameplayIssues(issues);
457
488
  for (const issue of formatted.issues) console.error(`- ${issue}`);
458
489
  if (formatted.repairs.length > 0) {
@@ -499,7 +530,17 @@ var GameplayAuditReporter = class {
499
530
  for (const issue of result.issues) this.reportPreflightIssue(issue);
500
531
  }
501
532
  /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
502
- onTestRunEnd(testModules) {
533
+ onTestRunEnd(testModules, _unhandledErrors = [], reason = "passed") {
534
+ const collectionFailed = reason === "failed" && testModules.some(
535
+ (testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
536
+ );
537
+ if (collectionFailed) {
538
+ console.error(
539
+ "\nGAMEPLAY_AUDIT: NOT RUN\n\u73A9\u6CD5\u6D4B\u8BD5\u6587\u4EF6\u65E0\u6CD5\u52A0\u8F7D\uFF0C\u56E0\u6B64\u65E0\u6CD5\u68C0\u67E5\u6E38\u620F\u884C\u4E3A\u3002\u8BF7\u5148\u4FEE\u590D Vitest \u4E0A\u65B9\u663E\u793A\u7684\u8BED\u6CD5\u6216\u5BFC\u5165\u9519\u8BEF\uFF0C\u518D\u8FD0\u884C pnpm test\u3002"
540
+ );
541
+ process.exitCode = 1;
542
+ return;
543
+ }
503
544
  const tests = testModules.flatMap(
504
545
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
505
546
  );
@@ -509,9 +550,6 @@ var GameplayAuditReporter = class {
509
550
  ...result.issues
510
551
  ];
511
552
  if (combinedIssues.length > 0) {
512
- if (this.preflightIssues.size > 0) {
513
- console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
514
- }
515
553
  printIssues("FINAL", [...new Set(combinedIssues)]);
516
554
  process.exitCode = 1;
517
555
  } else {
@@ -242,7 +242,7 @@ function auditGameplayCollection(options, file, tests) {
242
242
  const matches = actual.filter((test) => test.metadata?.id === contract.id);
243
243
  if (matches.length === 0) {
244
244
  issues.push(
245
- `\u6D4B\u8BD5\u6267\u884C\u524D\u672A\u6536\u96C6\u5230 contract "${contract.id}"\uFF08${normalizedFile}\uFF09\u3002`
245
+ `\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "${contract.id}" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08${normalizedFile}\uFF09\u3002`
246
246
  );
247
247
  } else if (matches.length > 1) {
248
248
  issues.push(
@@ -359,18 +359,21 @@ function auditGameplayRun(options, tests) {
359
359
  observedEvidence.flatMap((evidence) => evidence.registeredScenes)
360
360
  );
361
361
  const expectedScenes = normalizeSet(options.scenes);
362
- for (const scene of expectedScenes) {
363
- if (!registeredScenes.includes(scene)) {
364
- issues.push(
365
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
366
- );
362
+ if (observedEvidence.length > 0) {
363
+ for (const scene of expectedScenes) {
364
+ if (!registeredScenes.includes(scene)) {
365
+ issues.push(
366
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
367
+ );
368
+ }
369
+ }
370
+ for (const scene of registeredScenes) {
371
+ if (!expectedScenes.includes(scene)) {
372
+ issues.push(
373
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
374
+ );
375
+ }
367
376
  }
368
- }
369
- for (const scene of registeredScenes) {
370
- if (!expectedScenes.includes(scene))
371
- issues.push(
372
- `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
373
- );
374
377
  }
375
378
  return { passed: issues.length === 0, issues: [...new Set(issues)] };
376
379
  }
@@ -398,18 +401,31 @@ function formatGameplayIssues(issues) {
398
401
  const groups = /* @__PURE__ */ new Map();
399
402
  const repairKeys = /* @__PURE__ */ new Set();
400
403
  const repairs = [];
404
+ const missingByFile = /* @__PURE__ */ new Map();
405
+ const missingIds = /* @__PURE__ */ new Set();
406
+ for (const issue of issues) {
407
+ const match = issue.match(
408
+ /没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
409
+ );
410
+ if (!match) continue;
411
+ const [, id, file] = match;
412
+ missingIds.add(id);
413
+ const ids = missingByFile.get(file) ?? [];
414
+ if (!ids.includes(id)) ids.push(id);
415
+ missingByFile.set(file, ids);
416
+ }
401
417
  const repairText = {
402
418
  input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
403
419
  frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
404
420
  physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
405
- transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
421
+ transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
406
422
  visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
407
423
  restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
408
- checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
424
+ checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
409
425
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
410
- registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
411
- missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
412
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
426
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
427
+ missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
428
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
413
429
  };
414
430
  const getCategory = (issue) => {
415
431
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -437,9 +453,26 @@ function formatGameplayIssues(issues) {
437
453
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
438
454
  return { category: "other", key: `other:${issue}` };
439
455
  };
456
+ for (const [file, ids] of missingByFile) {
457
+ groups.set(`missing:${file}`, {
458
+ text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
459
+ ids: [],
460
+ key: `missing:${file}`
461
+ });
462
+ }
463
+ if (missingByFile.size > 0) {
464
+ repairKeys.add("missing");
465
+ repairs.push(repairText.missing);
466
+ }
440
467
  for (const rawIssue of [...new Set(issues)]) {
441
468
  const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
442
- const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
469
+ if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
470
+ const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
471
+ if (missingContract && missingIds.has(missingContract)) continue;
472
+ if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
473
+ continue;
474
+ }
475
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
443
476
  const id = compact.match(/contract "([^"]+)"/)?.[1];
444
477
  const { category, key } = getCategory(compact);
445
478
  const group = groups.get(key);
@@ -456,7 +489,7 @@ function formatGameplayIssues(issues) {
456
489
  const grouped = [...groups.values()];
457
490
  const visible = grouped.slice(0, 8).map((group) => {
458
491
  if (group.ids.length <= 1) return group.text;
459
- return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
492
+ return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
460
493
  });
461
494
  return {
462
495
  issues: visible.map(
@@ -470,9 +503,7 @@ function printIssues(stage, issues) {
470
503
  if (issues.length === 0) return;
471
504
  console.error(`
472
505
  GAMEPLAY_AUDIT: ${stage} FAILED`);
473
- console.error(
474
- "\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
475
- );
506
+ console.error("\u73A9\u6CD5\u6D4B\u8BD5\u672A\u901A\u8FC7\u3002\u8BF7\u5148\u5904\u7406\u7B2C\u4E00\u7C7B\u95EE\u9898\uFF0C\u91CD\u65B0\u8FD0\u884C\u540E\u518D\u5904\u7406\u540E\u7EED\u95EE\u9898\u3002");
476
507
  const formatted = formatGameplayIssues(issues);
477
508
  for (const issue of formatted.issues) console.error(`- ${issue}`);
478
509
  if (formatted.repairs.length > 0) {
@@ -519,7 +550,17 @@ var GameplayAuditReporter = class {
519
550
  for (const issue of result.issues) this.reportPreflightIssue(issue);
520
551
  }
521
552
  /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
522
- onTestRunEnd(testModules) {
553
+ onTestRunEnd(testModules, _unhandledErrors = [], reason = "passed") {
554
+ const collectionFailed = reason === "failed" && testModules.some(
555
+ (testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
556
+ );
557
+ if (collectionFailed) {
558
+ console.error(
559
+ "\nGAMEPLAY_AUDIT: NOT RUN\n\u73A9\u6CD5\u6D4B\u8BD5\u6587\u4EF6\u65E0\u6CD5\u52A0\u8F7D\uFF0C\u56E0\u6B64\u65E0\u6CD5\u68C0\u67E5\u6E38\u620F\u884C\u4E3A\u3002\u8BF7\u5148\u4FEE\u590D Vitest \u4E0A\u65B9\u663E\u793A\u7684\u8BED\u6CD5\u6216\u5BFC\u5165\u9519\u8BEF\uFF0C\u518D\u8FD0\u884C pnpm test\u3002"
560
+ );
561
+ process.exitCode = 1;
562
+ return;
563
+ }
523
564
  const tests = testModules.flatMap(
524
565
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
525
566
  );
@@ -529,9 +570,6 @@ var GameplayAuditReporter = class {
529
570
  ...result.issues
530
571
  ];
531
572
  if (combinedIssues.length > 0) {
532
- if (this.preflightIssues.size > 0) {
533
- console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
534
- }
535
573
  printIssues("FINAL", [...new Set(combinedIssues)]);
536
574
  process.exitCode = 1;
537
575
  } else {
@@ -218,7 +218,7 @@ function auditGameplayCollection(options, file, tests) {
218
218
  const matches = actual.filter((test) => test.metadata?.id === contract.id);
219
219
  if (matches.length === 0) {
220
220
  issues.push(
221
- `\u6D4B\u8BD5\u6267\u884C\u524D\u672A\u6536\u96C6\u5230 contract "${contract.id}"\uFF08${normalizedFile}\uFF09\u3002`
221
+ `\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 "${contract.id}" \u7684\u53EF\u6267\u884C\u6D4B\u8BD5\uFF08${normalizedFile}\uFF09\u3002`
222
222
  );
223
223
  } else if (matches.length > 1) {
224
224
  issues.push(
@@ -335,18 +335,21 @@ function auditGameplayRun(options, tests) {
335
335
  observedEvidence.flatMap((evidence) => evidence.registeredScenes)
336
336
  );
337
337
  const expectedScenes = normalizeSet(options.scenes);
338
- for (const scene of expectedScenes) {
339
- if (!registeredScenes.includes(scene)) {
340
- issues.push(
341
- `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
342
- );
338
+ if (observedEvidence.length > 0) {
339
+ for (const scene of expectedScenes) {
340
+ if (!registeredScenes.includes(scene)) {
341
+ issues.push(
342
+ `\u751F\u4EA7 Scene "${scene}" \u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry \u4E2D\u3002`
343
+ );
344
+ }
345
+ }
346
+ for (const scene of registeredScenes) {
347
+ if (!expectedScenes.includes(scene)) {
348
+ issues.push(
349
+ `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
350
+ );
351
+ }
343
352
  }
344
- }
345
- for (const scene of registeredScenes) {
346
- if (!expectedScenes.includes(scene))
347
- issues.push(
348
- `host registry \u51FA\u73B0\u672A\u5217\u5165 gameplayAudit.scenes \u7684\u751F\u4EA7 Scene "${scene}"\u3002`
349
- );
350
353
  }
351
354
  return { passed: issues.length === 0, issues: [...new Set(issues)] };
352
355
  }
@@ -374,18 +377,31 @@ function formatGameplayIssues(issues) {
374
377
  const groups = /* @__PURE__ */ new Map();
375
378
  const repairKeys = /* @__PURE__ */ new Set();
376
379
  const repairs = [];
380
+ const missingByFile = /* @__PURE__ */ new Map();
381
+ const missingIds = /* @__PURE__ */ new Set();
382
+ for (const issue of issues) {
383
+ const match = issue.match(
384
+ /没有找到检查项 "([^"]+)" 的可执行测试((.+))。/
385
+ );
386
+ if (!match) continue;
387
+ const [, id, file] = match;
388
+ missingIds.add(id);
389
+ const ids = missingByFile.get(file) ?? [];
390
+ if (!ids.includes(id)) ids.push(id);
391
+ missingByFile.set(file, ids);
392
+ }
377
393
  const repairText = {
378
394
  input: "\u8F93\u5165\uFF1A\u5148\u63A8\u8FDB\u4E00\u5E27\uFF0C\u518D\u901A\u8FC7 host.input.mouse\u3001host.input.keyboard \u6216 host.input.touch \u9A71\u52A8\u771F\u5B9E\u751F\u4EA7\u8F93\u5165\u3002",
379
395
  frames: "\u5E27\u63A8\u8FDB\uFF1A\u5728\u8F93\u5165\u6216\u751F\u4EA7\u547D\u4EE4\u540E\u8C03\u7528 host.stepFrames()\uFF0C\u5F02\u6B65 update \u4F7F\u7528 host.stepFramesAsync()\u3002",
380
396
  physics: "\u7269\u7406\uFF1A\u4EC5\u5728\u751F\u4EA7\u4F7F\u7528 Arcade Physics \u65F6\u8C03\u7528 host.stepPhysics()\uFF1B\u5176\u4ED6\u7269\u7406\u7CFB\u7EDF\u4F7F\u7528\u5B8C\u6574\u5E27\u3002",
381
- transition: "\u8F6C\u573A\uFF1Aboot \u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u751F\u4EA7\u8F6C\u573A\u547D\u4EE4\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u65AD\u8A00\u6E90/\u76EE\u6807\u72B6\u6001\u3002",
397
+ transition: "\u573A\u666F\u5207\u6362\uFF1A\u542F\u52A8\u6E38\u620F\u524D\u6CE8\u518C\u76EE\u6807 Scene\uFF0C\u8C03\u7528\u6E38\u620F\u4E2D\u7684\u5207\u6362\u903B\u8F91\uFF0C\u518D\u7528 host.stepUntil() \u7B49\u5F85\u76EE\u6807\u6FC0\u6D3B\u5E76\u68C0\u67E5\u5207\u6362\u7ED3\u679C\u3002",
382
398
  visit: "Scene\uFF1A\u786E\u8BA4\u6D4B\u8BD5\u901A\u8FC7\u771F\u5B9E\u5165\u53E3\u6216\u5408\u6CD5\u8F6C\u573A\u8BBF\u95EE\u8BE5 Scene\uFF0C\u5E76\u4F7F\u7528\u771F\u5B9E\u914D\u7F6E\u542F\u52A8\u3002",
383
399
  restart: "\u6062\u590D\uFF1A\u901A\u8FC7\u771F\u5B9E\u8F93\u5165\u6216\u751F\u4EA7 controls \u89E6\u53D1 restart\uFF0C\u7B49\u5F85\u5B8C\u6574\u5E27\u5E76\u65AD\u8A00\u72B6\u6001\u5DF2\u6062\u590D\u3002",
384
- checkpoint: "\u68C0\u67E5\u70B9\uFF1A\u5148\u65AD\u8A00 authoritative state\uFF0C\u518D\u8C03\u7528 host.checkpoint()\uFF1B\u4E0D\u8981\u8C03\u7528 contract.checkpoint()\u3002",
400
+ checkpoint: "\u8FDB\u5EA6\u6807\u8BB0\uFF1A\u5148\u68C0\u67E5\u6E38\u620F\u81EA\u8EAB\u7684\u72B6\u6001\uFF08\u4F8B\u5982\u5206\u6570\u3001\u751F\u547D\u503C\u6216\u5F53\u524D\u5173\u5361\uFF09\uFF0C\u518D\u8C03\u7528 host.checkpoint()\u3002",
385
401
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
386
- registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4 Scene \u5728\u751F\u4EA7 registry \u4E2D\uFF0C\u5E76\u5728 boot \u524D\u901A\u8FC7 additionalScenes \u6CE8\u518C\u8F6C\u573A\u76EE\u6807\u3002",
387
- missing: "\u6D4B\u8BD5\u7F3A\u5931\uFF1A\u5728\u6307\u5B9A\u6587\u4EF6\u4F7F\u7528 gameplayTest \u5B9A\u4E49\u68C0\u67E5\u9879\uFF0C\u5E76\u901A\u8FC7 contract.attachHost(host) \u7ED1\u5B9A host\u3002",
388
- test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF1B\u68C0\u67E5\u9879\u53EA\u6C47\u603B\u7ED3\u679C\uFF0C\u4E0D\u662F\u8FD0\u884C\u65F6\u5BF9\u8C61\u3002"
402
+ registry: "Scene \u6CE8\u518C\uFF1A\u786E\u8BA4\u6E38\u620F\u6E90\u7801\u6CE8\u518C\u4E86\u8BE5 Scene\uFF1B\u6D4B\u8BD5\u9700\u8981\u5207\u6362\u5230\u5B83\u65F6\uFF0C\u5728\u521B\u5EFA host \u65F6\u901A\u8FC7 additionalScenes \u63D0\u4F9B Scene \u7C7B\u3002",
403
+ missing: "\u6253\u5F00\u4E0A\u8FF0\u6587\u4EF6\uFF0C\u7528 gameplayTest \u6DFB\u52A0\u8FD9\u4E9B\u73A9\u6CD5\u6D4B\u8BD5\uFF1Bid \u5FC5\u987B\u4E0E vitest.config.ts \u4E2D\u7684\u914D\u7F6E\u5B8C\u5168\u4E00\u81F4\uFF0C\u521B\u5EFA host \u540E\u8C03\u7528 contract.attachHost(host)\u3002",
404
+ test: "\u5148\u4FEE\u590D\u5BF9\u5E94\u6D4B\u8BD5\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF\uFF0C\u7136\u540E\u91CD\u65B0\u8FD0\u884C pnpm test\u3002"
389
405
  };
390
406
  const getCategory = (issue) => {
391
407
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -413,9 +429,26 @@ function formatGameplayIssues(issues) {
413
429
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
414
430
  return { category: "other", key: `other:${issue}` };
415
431
  };
432
+ for (const [file, ids] of missingByFile) {
433
+ groups.set(`missing:${file}`, {
434
+ text: `${file} \u4E2D\u6CA1\u6709\u627E\u5230\u914D\u7F6E\u8981\u6C42\u7684\u73A9\u6CD5\u6D4B\u8BD5\uFF1A${ids.join("\u3001")}\u3002`,
435
+ ids: [],
436
+ key: `missing:${file}`
437
+ });
438
+ }
439
+ if (missingByFile.size > 0) {
440
+ repairKeys.add("missing");
441
+ repairs.push(repairText.missing);
442
+ }
416
443
  for (const rawIssue of [...new Set(issues)]) {
417
444
  const compact = rawIssue.replaceAll(/\s+/g, " ").trim();
418
- const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u68C0\u67E5\u9879 "$1"').replaceAll("evidence requirements", "\u6240\u9700\u8FD0\u884C\u8BC1\u636E").replaceAll("metadata", "\u6D4B\u8BD5\u58F0\u660E").replaceAll("host registry", "\u5DF2\u6CE8\u518C Scene \u6E05\u5355");
445
+ if (compact.startsWith("\u6CA1\u6709\u627E\u5230\u68C0\u67E5\u9879 ")) continue;
446
+ const missingContract = compact.match(/缺少 (?:gameplay )?contract "([^"]+)"/)?.[1];
447
+ if (missingContract && missingIds.has(missingContract)) continue;
448
+ if (missingIds.size > 0 && compact.includes("\u672A\u51FA\u73B0\u5728\u6D4B\u8BD5\u5B9E\u9645\u8BB0\u5F55\u7684 host registry")) {
449
+ continue;
450
+ }
451
+ const readable = compact.replaceAll(/(?:gameplay )?contract "([^"]+)"/g, '\u73A9\u6CD5\u6D4B\u8BD5 "$1"').replaceAll("evidence requirements", "\u8981\u6C42\u9A8C\u8BC1\u7684\u6E38\u620F\u884C\u4E3A").replaceAll("metadata", "\u6D4B\u8BD5\u914D\u7F6E").replaceAll("host registry", "\u6D4B\u8BD5\u8BB0\u5F55\u7684 Scene \u5217\u8868");
419
452
  const id = compact.match(/contract "([^"]+)"/)?.[1];
420
453
  const { category, key } = getCategory(compact);
421
454
  const group = groups.get(key);
@@ -432,7 +465,7 @@ function formatGameplayIssues(issues) {
432
465
  const grouped = [...groups.values()];
433
466
  const visible = grouped.slice(0, 8).map((group) => {
434
467
  if (group.ids.length <= 1) return group.text;
435
- return `${group.text}\uFF08\u53D7\u5F71\u54CD\u68C0\u67E5\u9879\uFF1A${group.ids.join("\u3001")}\uFF09`;
468
+ return `${group.text}\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1A${group.ids.slice(1).join("\u3001")}\uFF09`;
436
469
  });
437
470
  return {
438
471
  issues: visible.map(
@@ -446,9 +479,7 @@ function printIssues(stage, issues) {
446
479
  if (issues.length === 0) return;
447
480
  console.error(`
448
481
  GAMEPLAY_AUDIT: ${stage} FAILED`);
449
- console.error(
450
- "\u73A9\u6CD5\u68C0\u67E5\u5931\u8D25\uFF1A\u4EE5\u4E0B\u6BCF\u4E2A\u68C0\u67E5\u9879\u5BF9\u5E94\u4E00\u4E2A\u9700\u8981\u6D4B\u8BD5\u8BC1\u660E\u7684\u6E38\u620F\u884C\u4E3A\u3002"
451
- );
482
+ console.error("\u73A9\u6CD5\u6D4B\u8BD5\u672A\u901A\u8FC7\u3002\u8BF7\u5148\u5904\u7406\u7B2C\u4E00\u7C7B\u95EE\u9898\uFF0C\u91CD\u65B0\u8FD0\u884C\u540E\u518D\u5904\u7406\u540E\u7EED\u95EE\u9898\u3002");
452
483
  const formatted = formatGameplayIssues(issues);
453
484
  for (const issue of formatted.issues) console.error(`- ${issue}`);
454
485
  if (formatted.repairs.length > 0) {
@@ -495,7 +526,17 @@ var GameplayAuditReporter = class {
495
526
  for (const issue of result.issues) this.reportPreflightIssue(issue);
496
527
  }
497
528
  /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
498
- onTestRunEnd(testModules) {
529
+ onTestRunEnd(testModules, _unhandledErrors = [], reason = "passed") {
530
+ const collectionFailed = reason === "failed" && testModules.some(
531
+ (testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
532
+ );
533
+ if (collectionFailed) {
534
+ console.error(
535
+ "\nGAMEPLAY_AUDIT: NOT RUN\n\u73A9\u6CD5\u6D4B\u8BD5\u6587\u4EF6\u65E0\u6CD5\u52A0\u8F7D\uFF0C\u56E0\u6B64\u65E0\u6CD5\u68C0\u67E5\u6E38\u620F\u884C\u4E3A\u3002\u8BF7\u5148\u4FEE\u590D Vitest \u4E0A\u65B9\u663E\u793A\u7684\u8BED\u6CD5\u6216\u5BFC\u5165\u9519\u8BEF\uFF0C\u518D\u8FD0\u884C pnpm test\u3002"
536
+ );
537
+ process.exitCode = 1;
538
+ return;
539
+ }
499
540
  const tests = testModules.flatMap(
500
541
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
501
542
  );
@@ -505,9 +546,6 @@ var GameplayAuditReporter = class {
505
546
  ...result.issues
506
547
  ];
507
548
  if (combinedIssues.length > 0) {
508
- if (this.preflightIssues.size > 0) {
509
- console.error("\nGAMEPLAY_AUDIT: PREFLIGHT FAILED (included in final summary)");
510
- }
511
549
  printIssues("FINAL", [...new Set(combinedIssues)]);
512
550
  process.exitCode = 1;
513
551
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",