miaoda-game-devkit 0.2.6 → 0.2.8

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.
@@ -18,7 +18,9 @@ interface HeadlessGameHost<TScene extends Phaser.Scene> {
18
18
  /** 推进完整 Phaser 帧并等待异步 update 或生命周期完成。 */
19
19
  stepFramesAsync(count?: number): Promise<void>;
20
20
  /** 在限定帧数内推进游戏,直到条件成立。 */
21
- stepUntil(condition: () => boolean, maxFrames?: number): number;
21
+ stepUntil(condition: () => boolean, maxFrames?: number | {
22
+ maxFrames?: number;
23
+ }): number;
22
24
  /** 精确推进指定次数的 Arcade Physics 单步。 */
23
25
  stepPhysics(steps?: number): void;
24
26
  /** 记录由测试定义的可序列化玩法检查点。 */
@@ -18,7 +18,9 @@ interface HeadlessGameHost<TScene extends Phaser.Scene> {
18
18
  /** 推进完整 Phaser 帧并等待异步 update 或生命周期完成。 */
19
19
  stepFramesAsync(count?: number): Promise<void>;
20
20
  /** 在限定帧数内推进游戏,直到条件成立。 */
21
- stepUntil(condition: () => boolean, maxFrames?: number): number;
21
+ stepUntil(condition: () => boolean, maxFrames?: number | {
22
+ maxFrames?: number;
23
+ }): number;
22
24
  /** 精确推进指定次数的 Arcade Physics 单步。 */
23
25
  stepPhysics(steps?: number): void;
24
26
  /** 记录由测试定义的可序列化玩法检查点。 */
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
- import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-XBGq_jIV.mjs';
2
- export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-XBGq_jIV.mjs';
1
+ import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-CusQfLYQ.mjs';
2
+ export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-CusQfLYQ.mjs';
3
3
  import * as Phaser from 'phaser';
4
4
  import { TestContext, TestOptions } from 'vitest';
5
5
 
@@ -23,6 +23,8 @@ interface GameplayTestOptions extends GameplayContractOptions {
23
23
  interface GameplayContractHandle<TScene extends Phaser.Scene> {
24
24
  /** 将当前 contract 绑定到实际创建的 HEADLESS host。 */
25
25
  attachHost(host: HeadlessGameHost<TScene>): void;
26
+ /** 记录一个由测试先断言、再声明的玩法检查点。 */
27
+ checkpoint(id: string): void;
26
28
  /** 立即验证证据;通常由 onTestFinished 自动执行。 */
27
29
  verify(): void;
28
30
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-XBGq_jIV.js';
2
- export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-XBGq_jIV.js';
1
+ import { G as GameplayContractMetadata, H as HeadlessGameHost, a as HeadlessGameplayEvidenceRequirements } from './gameplay-audit-CusQfLYQ.js';
2
+ export { b as GameplayAuditContract, c as GameplayAuditOptions, d as HeadlessCanvasBounds, e as HeadlessGameOptions, f as HeadlessGameplayEvidence, g as HeadlessInputDriver, h as HeadlessInputPoint, i as HeadlessKeyboardEventOptions, j as HeadlessKeyboardInput, k as HeadlessMouseInput, l as HeadlessTouchInput, m as createHeadlessGame } from './gameplay-audit-CusQfLYQ.js';
3
3
  import * as Phaser from 'phaser';
4
4
  import { TestContext, TestOptions } from 'vitest';
5
5
 
@@ -23,6 +23,8 @@ interface GameplayTestOptions extends GameplayContractOptions {
23
23
  interface GameplayContractHandle<TScene extends Phaser.Scene> {
24
24
  /** 将当前 contract 绑定到实际创建的 HEADLESS host。 */
25
25
  attachHost(host: HeadlessGameHost<TScene>): void;
26
+ /** 记录一个由测试先断言、再声明的玩法检查点。 */
27
+ checkpoint(id: string): void;
26
28
  /** 立即验证证据;通常由 onTestFinished 自动执行。 */
27
29
  verify(): void;
28
30
  }
package/dist/index.js CHANGED
@@ -1021,7 +1021,8 @@ async function createHeadlessGame(scene, options = {}) {
1021
1021
  stepFrames(count);
1022
1022
  await settleRuntime();
1023
1023
  },
1024
- stepUntil(condition, maxFrames = 120) {
1024
+ stepUntil(condition, maxFramesOrOptions = 120) {
1025
+ const maxFrames = typeof maxFramesOrOptions === "number" ? maxFramesOrOptions : maxFramesOrOptions.maxFrames ?? 120;
1025
1026
  assertStepCount(maxFrames, "stepUntil maxFrames");
1026
1027
  throwRuntimeError();
1027
1028
  for (let frame = 0; frame <= maxFrames; frame += 1) {
@@ -1193,6 +1194,14 @@ function registerGameplayContract(context, options) {
1193
1194
  }
1194
1195
  host = currentHost;
1195
1196
  },
1197
+ checkpoint(id) {
1198
+ if (!host) {
1199
+ throw new Error(
1200
+ `Gameplay contract "${options.id}" must attach a HEADLESS host before recording a checkpoint.`
1201
+ );
1202
+ }
1203
+ host.checkpoint(id);
1204
+ },
1196
1205
  verify
1197
1206
  };
1198
1207
  }
package/dist/index.mjs CHANGED
@@ -979,7 +979,8 @@ async function createHeadlessGame(scene, options = {}) {
979
979
  stepFrames(count);
980
980
  await settleRuntime();
981
981
  },
982
- stepUntil(condition, maxFrames = 120) {
982
+ stepUntil(condition, maxFramesOrOptions = 120) {
983
+ const maxFrames = typeof maxFramesOrOptions === "number" ? maxFramesOrOptions : maxFramesOrOptions.maxFrames ?? 120;
983
984
  assertStepCount(maxFrames, "stepUntil maxFrames");
984
985
  throwRuntimeError();
985
986
  for (let frame = 0; frame <= maxFrames; frame += 1) {
@@ -1151,6 +1152,14 @@ function registerGameplayContract(context, options) {
1151
1152
  }
1152
1153
  host = currentHost;
1153
1154
  },
1155
+ checkpoint(id) {
1156
+ if (!host) {
1157
+ throw new Error(
1158
+ `Gameplay contract "${options.id}" must attach a HEADLESS host before recording a checkpoint.`
1159
+ );
1160
+ }
1161
+ host.checkpoint(id);
1162
+ },
1154
1163
  verify
1155
1164
  };
1156
1165
  }
@@ -308,7 +308,11 @@ function auditGameplayRun(options2, tests) {
308
308
  }
309
309
  issues.push(...getDefinitionIssues(expected, metadata));
310
310
  if (test.state !== "passed") {
311
- issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
311
+ if (test.error) {
312
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
313
+ } else {
314
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
315
+ }
312
316
  continue;
313
317
  }
314
318
  if (!metadata.verified || !metadata.evidence) {
@@ -383,7 +387,8 @@ function formatGameplayIssues(issues) {
383
387
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
384
388
  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
389
  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"
390
+ 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",
391
+ unhandled: "\u5F02\u6B65\u9519\u8BEF\uFF1A\u5148\u4FEE\u590D\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u9519\u8BEF\uFF1B\u8F93\u5165\u3001\u5E27\u63A8\u8FDB\u548C\u751F\u547D\u5468\u671F\u64CD\u4F5C\u90FD\u8981\u5728\u6B63\u786E\u7684 host \u751F\u547D\u5468\u671F\u5185\u5E76\u4F7F\u7528 await\u3002"
387
392
  };
388
393
  const getCategory = (issue) => {
389
394
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -408,6 +413,14 @@ function formatGameplayIssues(issues) {
408
413
  return { category: "registry", key: `registry:${scene}` };
409
414
  }
410
415
  if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
416
+ if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
417
+ const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
418
+ return { category: "test", key: `test:${message}` };
419
+ }
420
+ if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
421
+ const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
422
+ return { category: "unhandled", key: `test:${message}` };
423
+ }
411
424
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
412
425
  return { category: "other", key: `other:${issue}` };
413
426
  };
@@ -431,7 +444,7 @@ function formatGameplayIssues(issues) {
431
444
  continue;
432
445
  }
433
446
  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");
434
- const id = compact.match(/contract "([^"]+)"/)?.[1];
447
+ const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
435
448
  const { category, key } = getCategory(compact);
436
449
  const group = groups.get(key);
437
450
  if (group) {
@@ -817,4 +830,15 @@ describe("gameplay audit", () => {
817
830
  expect(result.repairs).toHaveLength(1);
818
831
  expect(result.repairs[0]).toContain("vitest.config.ts");
819
832
  });
833
+ it("\u4FDD\u7559\u5E76\u5408\u5E76 Vitest \u8FD0\u884C\u65F6\u5F02\u5E38\u6D88\u606F", () => {
834
+ const result = formatGameplayIssues([
835
+ '\u73A9\u6CD5\u6D4B\u8BD5 "Map.transition" \u6D4B\u8BD5\u5931\u8D25\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events',
836
+ '\u73A9\u6CD5\u6D4B\u8BD5 "Editor.transition" \u6D4B\u8BD5\u5931\u8D25\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events',
837
+ "\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events"
838
+ ]);
839
+ expect(result.issues).toEqual([
840
+ '\u73A9\u6CD5\u6D4B\u8BD5 "Map.transition" \u6D4B\u8BD5\u5931\u8D25\uFF1APhaser HEADLESS mouse input target cannot dispatch DOM events\uFF08\u540C\u7C7B\u95EE\u9898\u8FD8\u51FA\u73B0\u5728\uFF1AEditor.transition\uFF09'
841
+ ]);
842
+ expect(result.repairs[0]).toContain("\u7B2C\u4E00\u6761\u539F\u59CB\u9519\u8BEF");
843
+ });
820
844
  });
@@ -983,7 +983,8 @@ async function createHeadlessGame(scene, options = {}) {
983
983
  stepFrames(count);
984
984
  await settleRuntime();
985
985
  },
986
- stepUntil(condition, maxFrames = 120) {
986
+ stepUntil(condition, maxFramesOrOptions = 120) {
987
+ const maxFrames = typeof maxFramesOrOptions === "number" ? maxFramesOrOptions : maxFramesOrOptions.maxFrames ?? 120;
987
988
  assertStepCount(maxFrames, "stepUntil maxFrames");
988
989
  throwRuntimeError();
989
990
  for (let frame = 0; frame <= maxFrames; frame += 1) {
@@ -1155,6 +1156,14 @@ function registerGameplayContract(context, options) {
1155
1156
  }
1156
1157
  host = currentHost;
1157
1158
  },
1159
+ checkpoint(id) {
1160
+ if (!host) {
1161
+ throw new Error(
1162
+ `Gameplay contract "${options.id}" must attach a HEADLESS host before recording a checkpoint.`
1163
+ );
1164
+ }
1165
+ host.checkpoint(id);
1166
+ },
1158
1167
  verify
1159
1168
  };
1160
1169
  }
@@ -1190,7 +1199,8 @@ describe("gameplay contract lifecycle integration", () => {
1190
1199
  id: "InputScene.hit",
1191
1200
  kind: "playthrough",
1192
1201
  requireInput: true,
1193
- requireFrameAdvance: true
1202
+ requireFrameAdvance: true,
1203
+ requireCheckpoint: "hit-recorded"
1194
1204
  });
1195
1205
  let host;
1196
1206
  try {
@@ -1199,12 +1209,14 @@ describe("gameplay contract lifecycle integration", () => {
1199
1209
  host.stepFrames();
1200
1210
  await host.input.mouse.click(40, 40);
1201
1211
  expect(host.scene.hits).toBe(1);
1212
+ contract.checkpoint("hit-recorded");
1202
1213
  host.destroy();
1203
1214
  await callbacks[0]?.();
1204
1215
  expect(context.task.meta.gameplayContract).toMatchObject({
1205
1216
  id: "InputScene.hit",
1206
1217
  evidence: {
1207
1218
  clicks: 1,
1219
+ checkpoints: ["hit-recorded"],
1208
1220
  frames: 1,
1209
1221
  mouseEvents: 3
1210
1222
  }
@@ -983,7 +983,8 @@ async function createHeadlessGame(scene, options = {}) {
983
983
  stepFrames(count);
984
984
  await settleRuntime();
985
985
  },
986
- stepUntil(condition, maxFrames = 120) {
986
+ stepUntil(condition, maxFramesOrOptions = 120) {
987
+ const maxFrames = typeof maxFramesOrOptions === "number" ? maxFramesOrOptions : maxFramesOrOptions.maxFrames ?? 120;
987
988
  assertStepCount(maxFrames, "stepUntil maxFrames");
988
989
  throwRuntimeError();
989
990
  for (let frame = 0; frame <= maxFrames; frame += 1) {
@@ -1149,6 +1150,9 @@ describe("Phaser HEADLESS host", () => {
1149
1150
  expect(() => boundedHost.stepUntil(() => false, 1.5)).toThrow(
1150
1151
  "stepUntil maxFrames must be a non-negative safe integer"
1151
1152
  );
1153
+ expect(() => boundedHost.stepUntil(() => false, { maxFrames: 0 })).toThrow(
1154
+ "Condition was not met within 0 complete Phaser frames"
1155
+ );
1152
1156
  expect(() => boundedHost.stepPhysics(-1)).toThrow(
1153
1157
  "stepPhysics steps must be a non-negative safe integer"
1154
1158
  );
@@ -313,7 +313,11 @@ function auditGameplayRun(options, tests) {
313
313
  }
314
314
  issues.push(...getDefinitionIssues(expected, metadata));
315
315
  if (test.state !== "passed") {
316
- issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
316
+ if (test.error) {
317
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
318
+ } else {
319
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
320
+ }
317
321
  continue;
318
322
  }
319
323
  if (!metadata.verified || !metadata.evidence) {
@@ -370,11 +374,13 @@ function isGameplayContractMetadata(value) {
370
374
  }
371
375
  function toAuditTest(test) {
372
376
  const metadata = test.meta().gameplayContract;
377
+ const firstError = test.result().errors?.[0];
373
378
  return {
374
379
  file: normalizePath(test.module.relativeModuleId),
375
380
  name: test.fullName,
376
381
  state: test.result().state,
377
- metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
382
+ metadata: isGameplayContractMetadata(metadata) ? metadata : void 0,
383
+ error: firstError?.message?.replaceAll(/\s+/g, " ").trim().slice(0, 360) || void 0
378
384
  };
379
385
  }
380
386
  function formatGameplayIssues(issues) {
@@ -405,7 +411,8 @@ function formatGameplayIssues(issues) {
405
411
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
406
412
  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
413
  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"
414
+ 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",
415
+ unhandled: "\u5F02\u6B65\u9519\u8BEF\uFF1A\u5148\u4FEE\u590D\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u9519\u8BEF\uFF1B\u8F93\u5165\u3001\u5E27\u63A8\u8FDB\u548C\u751F\u547D\u5468\u671F\u64CD\u4F5C\u90FD\u8981\u5728\u6B63\u786E\u7684 host \u751F\u547D\u5468\u671F\u5185\u5E76\u4F7F\u7528 await\u3002"
409
416
  };
410
417
  const getCategory = (issue) => {
411
418
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -430,6 +437,14 @@ function formatGameplayIssues(issues) {
430
437
  return { category: "registry", key: `registry:${scene}` };
431
438
  }
432
439
  if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
440
+ if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
441
+ const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
442
+ return { category: "test", key: `test:${message}` };
443
+ }
444
+ if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
445
+ const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
446
+ return { category: "unhandled", key: `test:${message}` };
447
+ }
433
448
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
434
449
  return { category: "other", key: `other:${issue}` };
435
450
  };
@@ -453,7 +468,7 @@ function formatGameplayIssues(issues) {
453
468
  continue;
454
469
  }
455
470
  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");
456
- const id = compact.match(/contract "([^"]+)"/)?.[1];
471
+ const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
457
472
  const { category, key } = getCategory(compact);
458
473
  const group = groups.get(key);
459
474
  if (group) {
@@ -530,7 +545,7 @@ var GameplayAuditReporter = class {
530
545
  for (const issue of result.issues) this.reportPreflightIssue(issue);
531
546
  }
532
547
  /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
533
- onTestRunEnd(testModules, _unhandledErrors = [], reason = "passed") {
548
+ onTestRunEnd(testModules, unhandledErrors = [], reason = "passed") {
534
549
  const collectionFailed = reason === "failed" && testModules.some(
535
550
  (testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
536
551
  );
@@ -545,9 +560,11 @@ var GameplayAuditReporter = class {
545
560
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
546
561
  );
547
562
  const result = auditGameplayRun(this.options, tests);
563
+ const runtimeIssues = unhandledErrors.map((error) => error.message?.replaceAll(/\s+/g, " ").trim()).filter(Boolean).map((message) => `\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A${message}`);
548
564
  const combinedIssues = [
549
565
  ...this.preflightIssues,
550
- ...result.issues
566
+ ...result.issues,
567
+ ...runtimeIssues
551
568
  ];
552
569
  if (combinedIssues.length > 0) {
553
570
  printIssues("FINAL", [...new Set(combinedIssues)]);
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-XBGq_jIV.mjs';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-CusQfLYQ.mjs';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
@@ -1,5 +1,5 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
- import { c as GameplayAuditOptions } from './gameplay-audit-XBGq_jIV.js';
2
+ import { c as GameplayAuditOptions } from './gameplay-audit-CusQfLYQ.js';
3
3
  import 'phaser';
4
4
 
5
5
  interface GameVitestConfigOptions {
@@ -333,7 +333,11 @@ function auditGameplayRun(options, tests) {
333
333
  }
334
334
  issues.push(...getDefinitionIssues(expected, metadata));
335
335
  if (test.state !== "passed") {
336
- issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
336
+ if (test.error) {
337
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
338
+ } else {
339
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
340
+ }
337
341
  continue;
338
342
  }
339
343
  if (!metadata.verified || !metadata.evidence) {
@@ -390,11 +394,13 @@ function isGameplayContractMetadata(value) {
390
394
  }
391
395
  function toAuditTest(test) {
392
396
  const metadata = test.meta().gameplayContract;
397
+ const firstError = test.result().errors?.[0];
393
398
  return {
394
399
  file: normalizePath(test.module.relativeModuleId),
395
400
  name: test.fullName,
396
401
  state: test.result().state,
397
- metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
402
+ metadata: isGameplayContractMetadata(metadata) ? metadata : void 0,
403
+ error: firstError?.message?.replaceAll(/\s+/g, " ").trim().slice(0, 360) || void 0
398
404
  };
399
405
  }
400
406
  function formatGameplayIssues(issues) {
@@ -425,7 +431,8 @@ function formatGameplayIssues(issues) {
425
431
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
426
432
  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
433
  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"
434
+ 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",
435
+ unhandled: "\u5F02\u6B65\u9519\u8BEF\uFF1A\u5148\u4FEE\u590D\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u9519\u8BEF\uFF1B\u8F93\u5165\u3001\u5E27\u63A8\u8FDB\u548C\u751F\u547D\u5468\u671F\u64CD\u4F5C\u90FD\u8981\u5728\u6B63\u786E\u7684 host \u751F\u547D\u5468\u671F\u5185\u5E76\u4F7F\u7528 await\u3002"
429
436
  };
430
437
  const getCategory = (issue) => {
431
438
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -450,6 +457,14 @@ function formatGameplayIssues(issues) {
450
457
  return { category: "registry", key: `registry:${scene}` };
451
458
  }
452
459
  if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
460
+ if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
461
+ const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
462
+ return { category: "test", key: `test:${message}` };
463
+ }
464
+ if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
465
+ const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
466
+ return { category: "unhandled", key: `test:${message}` };
467
+ }
453
468
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
454
469
  return { category: "other", key: `other:${issue}` };
455
470
  };
@@ -473,7 +488,7 @@ function formatGameplayIssues(issues) {
473
488
  continue;
474
489
  }
475
490
  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");
476
- const id = compact.match(/contract "([^"]+)"/)?.[1];
491
+ const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
477
492
  const { category, key } = getCategory(compact);
478
493
  const group = groups.get(key);
479
494
  if (group) {
@@ -550,7 +565,7 @@ var GameplayAuditReporter = class {
550
565
  for (const issue of result.issues) this.reportPreflightIssue(issue);
551
566
  }
552
567
  /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
553
- onTestRunEnd(testModules, _unhandledErrors = [], reason = "passed") {
568
+ onTestRunEnd(testModules, unhandledErrors = [], reason = "passed") {
554
569
  const collectionFailed = reason === "failed" && testModules.some(
555
570
  (testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
556
571
  );
@@ -565,9 +580,11 @@ var GameplayAuditReporter = class {
565
580
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
566
581
  );
567
582
  const result = auditGameplayRun(this.options, tests);
583
+ const runtimeIssues = unhandledErrors.map((error) => error.message?.replaceAll(/\s+/g, " ").trim()).filter(Boolean).map((message) => `\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A${message}`);
568
584
  const combinedIssues = [
569
585
  ...this.preflightIssues,
570
- ...result.issues
586
+ ...result.issues,
587
+ ...runtimeIssues
571
588
  ];
572
589
  if (combinedIssues.length > 0) {
573
590
  printIssues("FINAL", [...new Set(combinedIssues)]);
@@ -309,7 +309,11 @@ function auditGameplayRun(options, tests) {
309
309
  }
310
310
  issues.push(...getDefinitionIssues(expected, metadata));
311
311
  if (test.state !== "passed") {
312
- issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
312
+ if (test.error) {
313
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
314
+ } else {
315
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
316
+ }
313
317
  continue;
314
318
  }
315
319
  if (!metadata.verified || !metadata.evidence) {
@@ -366,11 +370,13 @@ function isGameplayContractMetadata(value) {
366
370
  }
367
371
  function toAuditTest(test) {
368
372
  const metadata = test.meta().gameplayContract;
373
+ const firstError = test.result().errors?.[0];
369
374
  return {
370
375
  file: normalizePath(test.module.relativeModuleId),
371
376
  name: test.fullName,
372
377
  state: test.result().state,
373
- metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
378
+ metadata: isGameplayContractMetadata(metadata) ? metadata : void 0,
379
+ error: firstError?.message?.replaceAll(/\s+/g, " ").trim().slice(0, 360) || void 0
374
380
  };
375
381
  }
376
382
  function formatGameplayIssues(issues) {
@@ -401,7 +407,8 @@ function formatGameplayIssues(issues) {
401
407
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
402
408
  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
409
  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"
410
+ 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",
411
+ unhandled: "\u5F02\u6B65\u9519\u8BEF\uFF1A\u5148\u4FEE\u590D\u4E0A\u65B9\u663E\u793A\u7684\u7B2C\u4E00\u6761\u9519\u8BEF\uFF1B\u8F93\u5165\u3001\u5E27\u63A8\u8FDB\u548C\u751F\u547D\u5468\u671F\u64CD\u4F5C\u90FD\u8981\u5728\u6B63\u786E\u7684 host \u751F\u547D\u5468\u671F\u5185\u5E76\u4F7F\u7528 await\u3002"
405
412
  };
406
413
  const getCategory = (issue) => {
407
414
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -426,6 +433,14 @@ function formatGameplayIssues(issues) {
426
433
  return { category: "registry", key: `registry:${scene}` };
427
434
  }
428
435
  if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
436
+ if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
437
+ const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
438
+ return { category: "test", key: `test:${message}` };
439
+ }
440
+ if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
441
+ const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
442
+ return { category: "unhandled", key: `test:${message}` };
443
+ }
429
444
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
430
445
  return { category: "other", key: `other:${issue}` };
431
446
  };
@@ -449,7 +464,7 @@ function formatGameplayIssues(issues) {
449
464
  continue;
450
465
  }
451
466
  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");
452
- const id = compact.match(/contract "([^"]+)"/)?.[1];
467
+ const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
453
468
  const { category, key } = getCategory(compact);
454
469
  const group = groups.get(key);
455
470
  if (group) {
@@ -526,7 +541,7 @@ var GameplayAuditReporter = class {
526
541
  for (const issue of result.issues) this.reportPreflightIssue(issue);
527
542
  }
528
543
  /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
529
- onTestRunEnd(testModules, _unhandledErrors = [], reason = "passed") {
544
+ onTestRunEnd(testModules, unhandledErrors = [], reason = "passed") {
530
545
  const collectionFailed = reason === "failed" && testModules.some(
531
546
  (testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
532
547
  );
@@ -541,9 +556,11 @@ var GameplayAuditReporter = class {
541
556
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
542
557
  );
543
558
  const result = auditGameplayRun(this.options, tests);
559
+ const runtimeIssues = unhandledErrors.map((error) => error.message?.replaceAll(/\s+/g, " ").trim()).filter(Boolean).map((message) => `\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A${message}`);
544
560
  const combinedIssues = [
545
561
  ...this.preflightIssues,
546
- ...result.issues
562
+ ...result.issues,
563
+ ...runtimeIssues
547
564
  ];
548
565
  if (combinedIssues.length > 0) {
549
566
  printIssues("FINAL", [...new Set(combinedIssues)]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",