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.
- package/README.md +18 -12
- package/dist/{gameplay-audit-CusQfLYQ.d.mts → gameplay-audit-CCJo4Qhk.d.mts} +2 -2
- package/dist/{gameplay-audit-CusQfLYQ.d.ts → gameplay-audit-CCJo4Qhk.d.ts} +2 -2
- package/dist/index.d.mts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +52 -0
- package/dist/index.mjs +49 -0
- package/dist/lint/game-telemetry.contract.mjs +1241 -0
- package/dist/lint/gameplay-audit.contract.mjs +138 -49
- package/dist/lint/gameplay-contract.contract.mjs +5 -2
- package/dist/lint/vitest-config.contract.mjs +72 -35
- package/dist/vitest-config.d.mts +2 -2
- package/dist/vitest-config.d.ts +2 -2
- package/dist/vitest-config.js +72 -35
- package/dist/vitest-config.mjs +72 -35
- package/package.json +1 -1
package/dist/vitest-config.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const hasInput =
|
|
160
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 (${
|
|
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"
|
package/dist/vitest-config.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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 (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const hasInput =
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 (${
|
|
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"
|