miaoda-game-devkit 0.2.7 → 0.2.9

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.
@@ -70,6 +70,7 @@ function createGameplayContractMetadata(contract) {
70
70
  }
71
71
  function validateGameplayAuditOptions(options, projectRoot) {
72
72
  const issues = [];
73
+ const contracts = options.contracts;
73
74
  const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
74
75
  const mode = options.mode ?? "interactive";
75
76
  if (mode === "non-interactive") {
@@ -91,7 +92,7 @@ function validateGameplayAuditOptions(options, projectRoot) {
91
92
  const coveredScenes = /* @__PURE__ */ new Set();
92
93
  const kinds = /* @__PURE__ */ new Map();
93
94
  const invalidKindContracts = [];
94
- for (const contract of options.contracts) {
95
+ for (const contract of contracts ?? []) {
95
96
  const id = contract.id.trim();
96
97
  if (!id) {
97
98
  issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
@@ -117,7 +118,6 @@ function validateGameplayAuditOptions(options, projectRoot) {
117
118
  kinds.set(contract.kind, list);
118
119
  }
119
120
  if (contract.kind === "playthrough") {
120
- if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
121
121
  if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
122
122
  if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
123
123
  }
@@ -125,7 +125,6 @@ function validateGameplayAuditOptions(options, projectRoot) {
125
125
  issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
126
126
  }
127
127
  if (contract.kind === "recovery") {
128
- if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
129
128
  if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
130
129
  if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
131
130
  if (normalizeList(contract.requireCheckpoint).length < 2) issues.push(`recovery contract "${id}" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002`);
@@ -147,31 +146,24 @@ function validateGameplayAuditOptions(options, projectRoot) {
147
146
  `\u4EE5\u4E0B contract \u7F3A\u5C11\u6709\u6548 kind\uFF1A${invalidKindContracts.map((id) => `"${id}"`).join("\u3001")}\u3002\u8BF7\u4E3A\u6BCF\u4E2A contract \u8BBE\u7F6E kind: "boot"\u3001"playthrough"\u3001"transition" \u6216 "recovery"\u3002`
148
147
  );
149
148
  }
150
- for (const scene of scenes) {
151
- if (!coveredScenes.has(scene)) {
152
- issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
149
+ if (contracts) {
150
+ for (const scene of scenes) {
151
+ if (!coveredScenes.has(scene)) {
152
+ issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
153
+ }
153
154
  }
155
+ if (contracts.length === 0)
156
+ issues.push("gameplayAudit.contracts \u5B58\u5728\u65F6\u4E0D\u80FD\u4E3A\u7A7A\uFF1B\u4E0D\u4F7F\u7528\u663E\u5F0F\u6E05\u5355\u8BF7\u7701\u7565\u8BE5\u5B57\u6BB5\u3002");
154
157
  }
155
- if (options.contracts.length === 0)
156
- issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
157
- if (mode === "interactive") {
158
- const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
159
- const hasInput = options.contracts.some((contract) => contract.requireInput);
160
- const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
161
- if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
158
+ if (mode === "interactive" && contracts) {
159
+ const hasPlaythrough = (kinds.get("playthrough") ?? []).some(
160
+ (contract) => Boolean(contract.requireFrameAdvance) && normalizeList(contract.requireCheckpoint).length > 0
161
+ );
162
+ const hasInput = contracts.some((contract) => contract.requireInput);
163
+ if (!hasPlaythrough || !hasInput) {
162
164
  issues.push(
163
- '\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A kind: "playthrough" contract \u5FC5\u987B\u540C\u65F6\u58F0\u660E requireInput: true\u3001requireFrameAdvance: true \u548C requireCheckpoint: ["progress"]\u3002'
164
- );
165
- }
166
- const entryScene = scenes[0];
167
- for (const scene of scenes.slice(1)) {
168
- const transitioned = options.contracts.some(
169
- (contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
165
+ "\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A playthrough contract \u5FC5\u987B\u58F0\u660E requireFrameAdvance \u548C checkpoint\uFF0C\u4E14\u81F3\u5C11\u4E00\u4E2A contract \u5FC5\u987B\u58F0\u660E requireInput: true\u3002"
170
166
  );
171
- if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
172
- }
173
- if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
174
- issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
175
167
  }
176
168
  const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
177
169
  if (restartSignal && options.flows?.restart !== true) {
@@ -233,11 +225,21 @@ function getDefinitionIssues(expected, actual) {
233
225
  }
234
226
  function auditGameplayCollection(options, file, tests) {
235
227
  const normalizedFile = file.replaceAll("\\", "/");
236
- const expected = options.contracts.filter(
228
+ const expected = (options.contracts ?? []).filter(
237
229
  (contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
238
230
  );
239
231
  const actual = tests.filter((test) => test.metadata);
240
232
  const issues = [];
233
+ if (!options.contracts) {
234
+ const actualIds = /* @__PURE__ */ new Set();
235
+ for (const test of actual) {
236
+ const id = test.metadata?.id;
237
+ if (!id) continue;
238
+ if (actualIds.has(id)) issues.push(`contract "${id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`);
239
+ actualIds.add(id);
240
+ }
241
+ return { passed: issues.length === 0, issues };
242
+ }
241
243
  for (const contract of expected) {
242
244
  const matches = actual.filter((test) => test.metadata?.id === contract.id);
243
245
  if (matches.length === 0) {
@@ -310,7 +312,25 @@ function auditGameplayRun(options, tests) {
310
312
  const issues = [];
311
313
  const actualContracts = tests.filter((test) => test.metadata);
312
314
  const observedEvidence = [];
313
- for (const expected of options.contracts) {
315
+ const discoveredContracts = actualContracts.flatMap((test) => {
316
+ const metadata = test.metadata;
317
+ if (!metadata) return [];
318
+ return [{
319
+ id: metadata.id,
320
+ kind: metadata.kind,
321
+ testFile: test.file,
322
+ scenes: metadata.scenes,
323
+ ...metadata.requirements
324
+ }];
325
+ });
326
+ const expectedContracts = options.contracts ?? discoveredContracts;
327
+ if (!options.contracts) {
328
+ const discoveredIssues = validateGameplayAuditOptions(
329
+ { ...options, contracts: discoveredContracts }
330
+ );
331
+ issues.push(...discoveredIssues);
332
+ }
333
+ for (const expected of expectedContracts) {
314
334
  const matches = actualContracts.filter(
315
335
  (test2) => test2.metadata?.id === expected.id
316
336
  );
@@ -333,7 +353,11 @@ function auditGameplayRun(options, tests) {
333
353
  }
334
354
  issues.push(...getDefinitionIssues(expected, metadata));
335
355
  if (test.state !== "passed") {
336
- issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
356
+ if (test.error) {
357
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
358
+ } else {
359
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
360
+ }
337
361
  continue;
338
362
  }
339
363
  if (!metadata.verified || !metadata.evidence) {
@@ -349,7 +373,7 @@ function auditGameplayRun(options, tests) {
349
373
  }
350
374
  for (const test of actualContracts) {
351
375
  const id = test.metadata?.id;
352
- if (id && !options.contracts.some((contract) => contract.id === id)) {
376
+ if (id && options.contracts && !options.contracts.some((contract) => contract.id === id)) {
353
377
  issues.push(
354
378
  `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
355
379
  );
@@ -390,11 +414,13 @@ function isGameplayContractMetadata(value) {
390
414
  }
391
415
  function toAuditTest(test) {
392
416
  const metadata = test.meta().gameplayContract;
417
+ const firstError = test.result().errors?.[0];
393
418
  return {
394
419
  file: normalizePath(test.module.relativeModuleId),
395
420
  name: test.fullName,
396
421
  state: test.result().state,
397
- metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
422
+ metadata: isGameplayContractMetadata(metadata) ? metadata : void 0,
423
+ error: firstError?.message?.replaceAll(/\s+/g, " ").trim().slice(0, 360) || void 0
398
424
  };
399
425
  }
400
426
  function formatGameplayIssues(issues) {
@@ -425,7 +451,8 @@ function formatGameplayIssues(issues) {
425
451
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
426
452
  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
453
  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"
454
+ 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",
455
+ 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
456
  };
430
457
  const getCategory = (issue) => {
431
458
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -450,6 +477,14 @@ function formatGameplayIssues(issues) {
450
477
  return { category: "registry", key: `registry:${scene}` };
451
478
  }
452
479
  if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
480
+ if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
481
+ const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
482
+ return { category: "test", key: `test:${message}` };
483
+ }
484
+ if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
485
+ const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
486
+ return { category: "unhandled", key: `test:${message}` };
487
+ }
453
488
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
454
489
  return { category: "other", key: `other:${issue}` };
455
490
  };
@@ -473,7 +508,7 @@ function formatGameplayIssues(issues) {
473
508
  continue;
474
509
  }
475
510
  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];
511
+ const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
477
512
  const { category, key } = getCategory(compact);
478
513
  const group = groups.get(key);
479
514
  if (group) {
@@ -533,7 +568,7 @@ var GameplayAuditReporter = class {
533
568
  (specification) => normalizePath((0, import_node_path2.relative)(this.projectRoot, specification.moduleId))
534
569
  )
535
570
  );
536
- for (const contract of this.options.contracts) {
571
+ for (const contract of this.options.contracts ?? []) {
537
572
  const testFile = normalizePath(contract.testFile);
538
573
  if (!scheduledFiles.has(testFile)) {
539
574
  this.reportPreflightIssue(
@@ -550,7 +585,7 @@ var GameplayAuditReporter = class {
550
585
  for (const issue of result.issues) this.reportPreflightIssue(issue);
551
586
  }
552
587
  /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
553
- onTestRunEnd(testModules, _unhandledErrors = [], reason = "passed") {
588
+ onTestRunEnd(testModules, unhandledErrors = [], reason = "passed") {
554
589
  const collectionFailed = reason === "failed" && testModules.some(
555
590
  (testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
556
591
  );
@@ -565,9 +600,11 @@ var GameplayAuditReporter = class {
565
600
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
566
601
  );
567
602
  const result = auditGameplayRun(this.options, tests);
603
+ 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
604
  const combinedIssues = [
569
605
  ...this.preflightIssues,
570
- ...result.issues
606
+ ...result.issues,
607
+ ...runtimeIssues
571
608
  ];
572
609
  if (combinedIssues.length > 0) {
573
610
  printIssues("FINAL", [...new Set(combinedIssues)]);
@@ -575,7 +612,7 @@ var GameplayAuditReporter = class {
575
612
  } else {
576
613
  console.log(
577
614
  `
578
- GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
615
+ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${tests.filter((test) => test.metadata).length} contracts, ${this.options.scenes.length} scenes)`
579
616
  );
580
617
  console.log(
581
618
  "GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
@@ -46,6 +46,7 @@ function createGameplayContractMetadata(contract) {
46
46
  }
47
47
  function validateGameplayAuditOptions(options, projectRoot) {
48
48
  const issues = [];
49
+ const contracts = options.contracts;
49
50
  const scenes = [...new Set(options.scenes.map((scene) => scene.trim()).filter(Boolean))];
50
51
  const mode = options.mode ?? "interactive";
51
52
  if (mode === "non-interactive") {
@@ -67,7 +68,7 @@ function validateGameplayAuditOptions(options, projectRoot) {
67
68
  const coveredScenes = /* @__PURE__ */ new Set();
68
69
  const kinds = /* @__PURE__ */ new Map();
69
70
  const invalidKindContracts = [];
70
- for (const contract of options.contracts) {
71
+ for (const contract of contracts ?? []) {
71
72
  const id = contract.id.trim();
72
73
  if (!id) {
73
74
  issues.push("gameplayAudit.contracts \u4E2D\u5B58\u5728\u7A7A contract id\u3002");
@@ -93,7 +94,6 @@ function validateGameplayAuditOptions(options, projectRoot) {
93
94
  kinds.set(contract.kind, list);
94
95
  }
95
96
  if (contract.kind === "playthrough") {
96
- if (!contract.requireInput) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
97
97
  if (!contract.requireFrameAdvance) issues.push(`playthrough contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
98
98
  if (normalizeList(contract.requireCheckpoint).length === 0) issues.push(`playthrough contract "${id}" \u5FC5\u987B\u58F0\u660E requireCheckpoint\u3002`);
99
99
  }
@@ -101,7 +101,6 @@ function validateGameplayAuditOptions(options, projectRoot) {
101
101
  issues.push(`transition contract "${id}" \u5FC5\u987B\u58F0\u660E requireTransition\u3002`);
102
102
  }
103
103
  if (contract.kind === "recovery") {
104
- if (!contract.requireInput) issues.push(`recovery contract "${id}" \u5FC5\u987B requireInput: true\u3002`);
105
104
  if (!contract.requireFrameAdvance) issues.push(`recovery contract "${id}" \u5FC5\u987B requireFrameAdvance: true\u3002`);
106
105
  if (normalizeList(contract.requireRestart).length === 0) issues.push(`recovery contract "${id}" \u5FC5\u987B\u58F0\u660E requireRestart\u3002`);
107
106
  if (normalizeList(contract.requireCheckpoint).length < 2) issues.push(`recovery contract "${id}" \u81F3\u5C11\u9700\u8981\u4E24\u4E2A checkpoint\uFF08\u91CD\u7F6E\u524D\u540E\uFF09\u3002`);
@@ -123,31 +122,24 @@ function validateGameplayAuditOptions(options, projectRoot) {
123
122
  `\u4EE5\u4E0B contract \u7F3A\u5C11\u6709\u6548 kind\uFF1A${invalidKindContracts.map((id) => `"${id}"`).join("\u3001")}\u3002\u8BF7\u4E3A\u6BCF\u4E2A contract \u8BBE\u7F6E kind: "boot"\u3001"playthrough"\u3001"transition" \u6216 "recovery"\u3002`
124
123
  );
125
124
  }
126
- for (const scene of scenes) {
127
- if (!coveredScenes.has(scene)) {
128
- issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
125
+ if (contracts) {
126
+ for (const scene of scenes) {
127
+ if (!coveredScenes.has(scene)) {
128
+ issues.push(`Scene "${scene}" \u6CA1\u6709\u4EFB\u4F55 gameplay contract \u8D1F\u8D23\u9A8C\u8BC1\u3002`);
129
+ }
129
130
  }
131
+ if (contracts.length === 0)
132
+ issues.push("gameplayAudit.contracts \u5B58\u5728\u65F6\u4E0D\u80FD\u4E3A\u7A7A\uFF1B\u4E0D\u4F7F\u7528\u663E\u5F0F\u6E05\u5355\u8BF7\u7701\u7565\u8BE5\u5B57\u6BB5\u3002");
130
133
  }
131
- if (options.contracts.length === 0)
132
- issues.push("gameplayAudit.contracts \u4E0D\u80FD\u4E3A\u7A7A\u3002");
133
- if (mode === "interactive") {
134
- const hasPlaythrough = (kinds.get("playthrough") ?? []).length > 0;
135
- const hasInput = options.contracts.some((contract) => contract.requireInput);
136
- const hasCheckpoint = options.contracts.some((contract) => normalizeList(contract.requireCheckpoint).length > 0);
137
- if (!hasPlaythrough || !hasInput || !hasCheckpoint) {
134
+ if (mode === "interactive" && contracts) {
135
+ const hasPlaythrough = (kinds.get("playthrough") ?? []).some(
136
+ (contract) => Boolean(contract.requireFrameAdvance) && normalizeList(contract.requireCheckpoint).length > 0
137
+ );
138
+ const hasInput = contracts.some((contract) => contract.requireInput);
139
+ if (!hasPlaythrough || !hasInput) {
138
140
  issues.push(
139
- '\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A kind: "playthrough" contract \u5FC5\u987B\u540C\u65F6\u58F0\u660E requireInput: true\u3001requireFrameAdvance: true \u548C requireCheckpoint: ["progress"]\u3002'
140
- );
141
- }
142
- const entryScene = scenes[0];
143
- for (const scene of scenes.slice(1)) {
144
- const transitioned = options.contracts.some(
145
- (contract) => (contract.kind === "transition" || contract.kind === "playthrough") && normalizeList(contract.requireTransition).includes(scene)
141
+ "\u4EA4\u4E92\u9879\u76EE\u7F3A\u5C11\u6700\u4F4E\u73A9\u6CD5\u57FA\u7EBF\uFF1A\u81F3\u5C11\u4E00\u4E2A playthrough contract \u5FC5\u987B\u58F0\u660E requireFrameAdvance \u548C checkpoint\uFF0C\u4E14\u81F3\u5C11\u4E00\u4E2A contract \u5FC5\u987B\u58F0\u660E requireInput: true\u3002"
146
142
  );
147
- if (!transitioned) issues.push(`\u975E\u5165\u53E3 Scene "${scene}" \u5FC5\u987B\u7531 transition/playthrough contract \u901A\u8FC7\u751F\u4EA7\u8F6C\u573A\u8986\u76D6\u3002`);
148
- }
149
- if (entryScene && scenes.length > 1 && (kinds.get("transition") ?? []).length === 0 && (kinds.get("playthrough") ?? []).length === 0 && invalidKindContracts.length === 0) {
150
- issues.push(`\u4EA4\u4E92\u9879\u76EE\u5165\u53E3 Scene "${entryScene}" \u7F3A\u5C11 transition/playthrough flow\u3002`);
151
143
  }
152
144
  const restartSignal = projectRoot ? containsProductionRestart(projectRoot) : false;
153
145
  if (restartSignal && options.flows?.restart !== true) {
@@ -209,11 +201,21 @@ function getDefinitionIssues(expected, actual) {
209
201
  }
210
202
  function auditGameplayCollection(options, file, tests) {
211
203
  const normalizedFile = file.replaceAll("\\", "/");
212
- const expected = options.contracts.filter(
204
+ const expected = (options.contracts ?? []).filter(
213
205
  (contract) => contract.testFile.replaceAll("\\", "/") === normalizedFile
214
206
  );
215
207
  const actual = tests.filter((test) => test.metadata);
216
208
  const issues = [];
209
+ if (!options.contracts) {
210
+ const actualIds = /* @__PURE__ */ new Set();
211
+ for (const test of actual) {
212
+ const id = test.metadata?.id;
213
+ if (!id) continue;
214
+ if (actualIds.has(id)) issues.push(`contract "${id}" \u5728 ${normalizedFile} \u4E2D\u91CD\u590D\u5B9A\u4E49\u3002`);
215
+ actualIds.add(id);
216
+ }
217
+ return { passed: issues.length === 0, issues };
218
+ }
217
219
  for (const contract of expected) {
218
220
  const matches = actual.filter((test) => test.metadata?.id === contract.id);
219
221
  if (matches.length === 0) {
@@ -286,7 +288,25 @@ function auditGameplayRun(options, tests) {
286
288
  const issues = [];
287
289
  const actualContracts = tests.filter((test) => test.metadata);
288
290
  const observedEvidence = [];
289
- for (const expected of options.contracts) {
291
+ const discoveredContracts = actualContracts.flatMap((test) => {
292
+ const metadata = test.metadata;
293
+ if (!metadata) return [];
294
+ return [{
295
+ id: metadata.id,
296
+ kind: metadata.kind,
297
+ testFile: test.file,
298
+ scenes: metadata.scenes,
299
+ ...metadata.requirements
300
+ }];
301
+ });
302
+ const expectedContracts = options.contracts ?? discoveredContracts;
303
+ if (!options.contracts) {
304
+ const discoveredIssues = validateGameplayAuditOptions(
305
+ { ...options, contracts: discoveredContracts }
306
+ );
307
+ issues.push(...discoveredIssues);
308
+ }
309
+ for (const expected of expectedContracts) {
290
310
  const matches = actualContracts.filter(
291
311
  (test2) => test2.metadata?.id === expected.id
292
312
  );
@@ -309,7 +329,11 @@ function auditGameplayRun(options, tests) {
309
329
  }
310
330
  issues.push(...getDefinitionIssues(expected, metadata));
311
331
  if (test.state !== "passed") {
312
- issues.push(`contract "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
332
+ if (test.error) {
333
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u6D4B\u8BD5\u5931\u8D25\uFF1A${test.error}`);
334
+ } else {
335
+ issues.push(`\u73A9\u6CD5\u6D4B\u8BD5 "${expected.id}" \u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A ${test.state}\u3002`);
336
+ }
313
337
  continue;
314
338
  }
315
339
  if (!metadata.verified || !metadata.evidence) {
@@ -325,7 +349,7 @@ function auditGameplayRun(options, tests) {
325
349
  }
326
350
  for (const test of actualContracts) {
327
351
  const id = test.metadata?.id;
328
- if (id && !options.contracts.some((contract) => contract.id === id)) {
352
+ if (id && options.contracts && !options.contracts.some((contract) => contract.id === id)) {
329
353
  issues.push(
330
354
  `\u53D1\u73B0\u672A\u5217\u5165 gameplayAudit \u6E05\u5355\u7684 contract "${id}"\uFF08${test.file}\uFF09\u3002`
331
355
  );
@@ -366,11 +390,13 @@ function isGameplayContractMetadata(value) {
366
390
  }
367
391
  function toAuditTest(test) {
368
392
  const metadata = test.meta().gameplayContract;
393
+ const firstError = test.result().errors?.[0];
369
394
  return {
370
395
  file: normalizePath(test.module.relativeModuleId),
371
396
  name: test.fullName,
372
397
  state: test.result().state,
373
- metadata: isGameplayContractMetadata(metadata) ? metadata : void 0
398
+ metadata: isGameplayContractMetadata(metadata) ? metadata : void 0,
399
+ error: firstError?.message?.replaceAll(/\s+/g, " ").trim().slice(0, 360) || void 0
374
400
  };
375
401
  }
376
402
  function formatGameplayIssues(issues) {
@@ -401,7 +427,8 @@ function formatGameplayIssues(issues) {
401
427
  destroy: "\u6E05\u7406\uFF1A\u5728 afterEach \u4E2D\u8C03\u7528 host.destroy()\uFF0C\u5E76\u4FDD\u6301\u9500\u6BC1\u5E42\u7B49\u3002",
402
428
  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
429
  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"
430
+ 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",
431
+ 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
432
  };
406
433
  const getCategory = (issue) => {
407
434
  if (issue.includes("\u672A\u8BB0\u5F55\u771F\u5B9E DOM \u8F93\u5165")) return { category: "input", key: "input" };
@@ -426,6 +453,14 @@ function formatGameplayIssues(issues) {
426
453
  return { category: "registry", key: `registry:${scene}` };
427
454
  }
428
455
  if (issue.includes("\u7F3A\u5C11 gameplay contract")) return { category: "missing", key: "missing" };
456
+ if (issue.includes("\u6D4B\u8BD5\u5931\u8D25\uFF1A")) {
457
+ const message = issue.split("\u6D4B\u8BD5\u5931\u8D25\uFF1A")[1] ?? issue;
458
+ return { category: "test", key: `test:${message}` };
459
+ }
460
+ if (issue.includes("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")) {
461
+ const message = issue.split("\u6D4B\u8BD5\u8FD0\u884C\u671F\u95F4\u53D1\u751F\u5F02\u6B65\u9519\u8BEF\uFF1A")[1] ?? issue;
462
+ return { category: "unhandled", key: `test:${message}` };
463
+ }
429
464
  if (issue.includes("\u7684\u6D4B\u8BD5\u72B6\u6001\u4E3A")) return { category: "test", key: "test" };
430
465
  return { category: "other", key: `other:${issue}` };
431
466
  };
@@ -449,7 +484,7 @@ function formatGameplayIssues(issues) {
449
484
  continue;
450
485
  }
451
486
  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];
487
+ const id = compact.match(/(?:contract|玩法测试) "([^"]+)"/)?.[1];
453
488
  const { category, key } = getCategory(compact);
454
489
  const group = groups.get(key);
455
490
  if (group) {
@@ -509,7 +544,7 @@ var GameplayAuditReporter = class {
509
544
  (specification) => normalizePath(relative(this.projectRoot, specification.moduleId))
510
545
  )
511
546
  );
512
- for (const contract of this.options.contracts) {
547
+ for (const contract of this.options.contracts ?? []) {
513
548
  const testFile = normalizePath(contract.testFile);
514
549
  if (!scheduledFiles.has(testFile)) {
515
550
  this.reportPreflightIssue(
@@ -526,7 +561,7 @@ var GameplayAuditReporter = class {
526
561
  for (const issue of result.issues) this.reportPreflightIssue(issue);
527
562
  }
528
563
  /** 在测试运行结束后汇总全部 contract、Ledger 和 Scene registry,并决定命令退出码。 */
529
- onTestRunEnd(testModules, _unhandledErrors = [], reason = "passed") {
564
+ onTestRunEnd(testModules, unhandledErrors = [], reason = "passed") {
530
565
  const collectionFailed = reason === "failed" && testModules.some(
531
566
  (testModule) => testModule.state() === "failed" && [...testModule.children.allTests()].length === 0
532
567
  );
@@ -541,9 +576,11 @@ var GameplayAuditReporter = class {
541
576
  (testModule) => [...testModule.children.allTests()].map(toAuditTest)
542
577
  );
543
578
  const result = auditGameplayRun(this.options, tests);
579
+ 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
580
  const combinedIssues = [
545
581
  ...this.preflightIssues,
546
- ...result.issues
582
+ ...result.issues,
583
+ ...runtimeIssues
547
584
  ];
548
585
  if (combinedIssues.length > 0) {
549
586
  printIssues("FINAL", [...new Set(combinedIssues)]);
@@ -551,7 +588,7 @@ var GameplayAuditReporter = class {
551
588
  } else {
552
589
  console.log(
553
590
  `
554
- GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${this.options.contracts.length} contracts, ${this.options.scenes.length} scenes)`
591
+ GAMEPLAY_AUDIT: ALL CONTRACTS PASSED (${tests.filter((test) => test.metadata).length} contracts, ${this.options.scenes.length} scenes)`
555
592
  );
556
593
  console.log(
557
594
  "GAMEPLAY_AUDIT_NOTE: \u73A9\u6CD5\u68C0\u67E5\u5DF2\u901A\u8FC7\uFF1B\u4ECD\u9700\u4EE5\u6700\u540E\u7684 TEST_RESULT \u5224\u65AD\u6574\u4F53\u6210\u529F\u3002"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miaoda-game-devkit",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Shared lint and deterministic Phaser HEADLESS testing tools for Miaoda games",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",