miaoda-game-devkit 0.2.21 → 0.4.0

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.
@@ -40,6 +40,7 @@ var import_config = require("vitest/config");
40
40
  // src/react/react-playthrough-reporter.ts
41
41
  var import_node_fs = require("fs");
42
42
  var import_node_path = require("path");
43
+ var import_node_util = require("util");
43
44
 
44
45
  // src/react/react-playthrough.ts
45
46
  var import_react2 = require("@testing-library/react");
@@ -102,20 +103,50 @@ var INPUT_EVENTS = [
102
103
  "touchend"
103
104
  ];
104
105
  var MIN_CHECKPOINTS = 2;
106
+ var MAX_FORMATTED_OBSERVATION_LENGTH = 500;
107
+ function formatObservation(fingerprint) {
108
+ if (fingerprint.length <= MAX_FORMATTED_OBSERVATION_LENGTH) return fingerprint;
109
+ return `${fingerprint.slice(0, MAX_FORMATTED_OBSERVATION_LENGTH)}\u2026 (${fingerprint.length} chars)`;
110
+ }
111
+ function sampleObservation(observe, stage) {
112
+ let value;
113
+ try {
114
+ value = observe();
115
+ } catch (error) {
116
+ throw new Error(`observe() threw at ${stage}: ${String(error)}`);
117
+ }
118
+ try {
119
+ const fingerprint = JSON.stringify(value);
120
+ if (fingerprint === void 0) throw new Error("unsupported value");
121
+ return { fingerprint, formatted: formatObservation(fingerprint) };
122
+ } catch {
123
+ throw new Error(
124
+ `observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
125
+ );
126
+ }
127
+ }
128
+ function formatObservationTimeline(entered, afterPrimary, outcome) {
129
+ return [
130
+ `entered=${entered.formatted}`,
131
+ `after-primary=${afterPrimary?.formatted ?? "<not sampled>"}`,
132
+ `outcome=${outcome.formatted}`
133
+ ].join(", ");
134
+ }
105
135
  function describeMissingEvidence(evidence) {
106
- if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
107
- if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
136
+ if (!evidence || evidence.entryInputs === 0) return "an entry input";
137
+ if (evidence.primaryInputs === 0) return "a primary gameplay input";
108
138
  if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
109
- if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
110
- if (evidence.assertionsAfterOutcome === 0) return "stepUntil \u540E\u7684\u7ED3\u679C\u65AD\u8A00";
139
+ if (evidence.boundedRuns === 0) return "a bounded stepUntil call";
140
+ if (evidence.assertionsAfterOutcome === 0)
141
+ return "an outcome assertion after stepUntil";
111
142
  if (evidence.checkpoints.length < MIN_CHECKPOINTS)
112
- return `\u81F3\u5C11 ${MIN_CHECKPOINTS} \u4E2A checkpoint`;
143
+ return `at least ${MIN_CHECKPOINTS} checkpoints`;
113
144
  if (!evidence.checkpoints.some(
114
145
  (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
115
146
  )) {
116
147
  return "progress/terminal checkpoint";
117
148
  }
118
- return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
149
+ return "a complete playthrough verification marker";
119
150
  }
120
151
  function createMetadata(waiverReason) {
121
152
  return {
@@ -132,7 +163,7 @@ function createMetadata(waiverReason) {
132
163
  }
133
164
  };
134
165
  }
135
- function definePlaythrough(element, run, waiverReason) {
166
+ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
136
167
  const reason = normalizePlaythroughWaiverReason(waiverReason);
137
168
  const metadata = createMetadata(reason);
138
169
  (0, import_vitest.test)("production game completes a bounded playthrough", {
@@ -142,6 +173,9 @@ function definePlaythrough(element, run, waiverReason) {
142
173
  const evidence = metadata.evidence;
143
174
  let assertionsAtOutcome;
144
175
  let enteredRecorded = false;
176
+ let domTextAtEntered;
177
+ let enteredObservation;
178
+ let afterPrimaryObservation;
145
179
  const recordInput = () => {
146
180
  evidence.domInputEvents += 1;
147
181
  };
@@ -179,7 +213,15 @@ function definePlaythrough(element, run, waiverReason) {
179
213
  );
180
214
  }
181
215
  if (kind === "entry") evidence.entryInputs += 1;
182
- else evidence.primaryInputs += 1;
216
+ else {
217
+ evidence.primaryInputs += 1;
218
+ if (playthroughOptions?.observe) {
219
+ afterPrimaryObservation = sampleObservation(
220
+ playthroughOptions.observe,
221
+ "after-primary"
222
+ );
223
+ }
224
+ }
183
225
  },
184
226
  checkpoint(kind) {
185
227
  if (kind === "entered") {
@@ -194,6 +236,14 @@ function definePlaythrough(element, run, waiverReason) {
194
236
  );
195
237
  }
196
238
  enteredRecorded = true;
239
+ if (playthroughOptions?.observe) {
240
+ enteredObservation = sampleObservation(
241
+ playthroughOptions.observe,
242
+ "entered"
243
+ );
244
+ } else {
245
+ domTextAtEntered = document.body.textContent ?? "";
246
+ }
197
247
  evidence.checkpoints.push(kind);
198
248
  return;
199
249
  }
@@ -214,13 +264,37 @@ function definePlaythrough(element, run, waiverReason) {
214
264
  }
215
265
  evidence.checkpoints.push(kind);
216
266
  },
217
- async stepUntil(condition, options = {}) {
218
- const steps = await runBoundedUntil(condition, options);
267
+ async stepUntil(condition, stepOptions = {}) {
268
+ const boundedOptions = stepOptions.diagnostics || !playthroughOptions?.observe ? stepOptions : {
269
+ ...stepOptions,
270
+ diagnostics: playthroughOptions.observe
271
+ };
272
+ const steps = await runBoundedUntil(condition, boundedOptions);
219
273
  if (evidence.primaryInputs === 0) {
220
274
  throw new Error(
221
275
  'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
222
276
  );
223
277
  }
278
+ if (playthroughOptions?.observe) {
279
+ if (!enteredObservation) {
280
+ throw new Error(
281
+ 'observe requires checkpoint("entered") before primary gameplay input.'
282
+ );
283
+ }
284
+ const outcomeObservation = sampleObservation(
285
+ playthroughOptions.observe,
286
+ "outcome"
287
+ );
288
+ if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
289
+ throw new Error(
290
+ `The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
291
+ );
292
+ }
293
+ } else if (steps === 0 && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
294
+ throw new Error(
295
+ 'stepUntil found the outcome at step 0, and the DOM has not changed since checkpoint("entered"). The flow therefore provides no evidence that the primary gameplay input produced a result. For Canvas or Controller state outside the DOM, declare one playthrough observe callback.'
296
+ );
297
+ }
224
298
  evidence.boundedRuns += 1;
225
299
  assertionsAtOutcome = expect.getState().assertionCalls;
226
300
  return steps;
@@ -262,9 +336,16 @@ function definePlaythrough(element, run, waiverReason) {
262
336
  });
263
337
  }
264
338
  var playthroughTest = Object.assign(
265
- (element, run) => definePlaythrough(element, run),
339
+ (element, optionsOrRun, maybeRun) => {
340
+ if (typeof optionsOrRun === "function") {
341
+ definePlaythrough(element, optionsOrRun);
342
+ return;
343
+ }
344
+ if (!maybeRun) throw new TypeError("playthroughTest requires a run callback.");
345
+ definePlaythrough(element, maybeRun, optionsOrRun);
346
+ },
266
347
  {
267
- skip: (reason, element, run) => definePlaythrough(element, run, reason)
348
+ skip: (reason, element, run) => definePlaythrough(element, run, void 0, reason)
268
349
  }
269
350
  );
270
351
  function auditReactPlaythroughRun(tests) {
@@ -281,7 +362,7 @@ function auditReactPlaythroughRun(tests) {
281
362
  const issues = [];
282
363
  if (declared.length === 0) {
283
364
  issues.push(
284
- '\u7F3A\u5C11\u751F\u4EA7\u6E38\u620F\u53EF\u73A9\u6027\u9A8C\u8BC1\uFF1A\u4F7F\u7528 playthroughTest \u6E32\u67D3 <App />\uFF0C\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u6743\u5A01\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 checkpoint("progress") \u6216 checkpoint("terminal")\uFF1B\u81F3\u5C11\u5B8C\u6210\u8FD9\u4E24\u6B21 checkpoint \u7B7E\u5230\u3002'
365
+ 'No production gameplay verification was declared. Use playthroughTest to render <App />, then run performInput("entry"), checkpoint("entered"), performInput("primary"), and a bounded stepUntil. Assert the authoritative outcome with the expect provided by playthroughTest, then record checkpoint("progress") or checkpoint("terminal").'
285
366
  );
286
367
  } else {
287
368
  for (const candidate of declared) {
@@ -290,13 +371,17 @@ function auditReactPlaythroughRun(tests) {
290
371
  if (isValid || isWaived) continue;
291
372
  if (candidate.state === "skipped") {
292
373
  issues.push(
293
- `\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u88AB\u8DF3\u8FC7\uFF0C\u4F46\u6CA1\u6709\u81F3\u5C11 20 \u4E2A\u5B57\u7B26\u7684\u660E\u786E\u7406\u7531\u3002`
374
+ `Playthrough ${JSON.stringify(candidate.name)} was skipped without an explicit reason of at least 20 characters.`
294
375
  );
295
376
  } else if (candidate.state !== "passed") {
296
- issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
377
+ issues.push(
378
+ `Playthrough ${JSON.stringify(candidate.name)} finished with state ${candidate.state}.`
379
+ );
297
380
  } else {
298
381
  const missing = describeMissingEvidence(candidate.metadata?.evidence);
299
- issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
382
+ issues.push(
383
+ `Playthrough ${JSON.stringify(candidate.name)} is missing ${missing}.`
384
+ );
300
385
  }
301
386
  }
302
387
  }
@@ -323,7 +408,18 @@ function toAuditInput(test2) {
323
408
  }
324
409
  function firstLine(value) {
325
410
  if (typeof value !== "string") return void 0;
326
- return value.split("\n").map((line) => line.trim()).find(Boolean);
411
+ return (0, import_node_util.stripVTControlCharacters)(value).split("\n").map((line) => line.trim()).find(Boolean);
412
+ }
413
+ function failureHint(value) {
414
+ const names = [...value.matchAll(/button\s*\n\s*Name "([^"]+)"/g)].map(
415
+ (match) => match[1]
416
+ );
417
+ if (names.length === 0) return void 0;
418
+ return `Available button names: ${names.map((name) => JSON.stringify(name)).join(", ")}`;
419
+ }
420
+ function errorLocation(value) {
421
+ const match = value.match(/(?:^|\n)\s*at\s+(.*?\.(?:test|spec)\.[^\n]+:\d+:\d+)/);
422
+ return match?.[1];
327
423
  }
328
424
  function toModuleResult(module2, projectRoot) {
329
425
  const tests = [...module2.children.allTests()];
@@ -333,13 +429,71 @@ function toModuleResult(module2, projectRoot) {
333
429
  (test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
334
430
  )
335
431
  ].filter((message) => Boolean(message));
432
+ const failures = tests.filter((test2) => test2.result().state === "failed").map((test2) => {
433
+ const raw = test2.result().errors?.[0]?.message ?? module2.errors()[0]?.message ?? "Unknown failure";
434
+ return {
435
+ test: test2.fullName,
436
+ cause: firstLine(raw) ?? "Unknown failure",
437
+ location: test2.location ? `${(0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/")}:${test2.location.line}:${test2.location.column}` : errorLocation(raw),
438
+ hint: failureHint(raw)
439
+ };
440
+ });
441
+ if (failures.length === 0 && tests.length === 0 && module2.errors().length > 0) {
442
+ const raw = module2.errors()[0]?.message ?? "Module failed to load";
443
+ failures.push({
444
+ test: "<collection>",
445
+ cause: firstLine(raw) ?? "Module failed to load",
446
+ location: errorLocation(raw),
447
+ hint: failureHint(raw)
448
+ });
449
+ }
336
450
  return {
337
451
  file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
338
452
  state: module2.state(),
339
453
  errors,
340
- tests: tests.map(toAuditInput)
454
+ tests: tests.map(toAuditInput),
455
+ failures
341
456
  };
342
457
  }
458
+ function formatFailureSummary(modules) {
459
+ const failures = modules.flatMap(
460
+ (module2) => (module2.failures ?? []).map((failure) => ({ ...failure, file: module2.file }))
461
+ );
462
+ if (failures.length === 0) return ["TEST_RESULT: PASS"];
463
+ const lines = [`FAILED_TESTS: ${failures.length}`];
464
+ for (const [index, failure] of failures.entries()) {
465
+ lines.push(`FAILURE_${index + 1}: ${failure.file}`);
466
+ lines.push(`TEST: ${failure.test}`);
467
+ lines.push(`CAUSE: ${failure.cause}`);
468
+ if (failure.location) lines.push(`AT: ${failure.location}`);
469
+ if (failure.hint) lines.push(`HINT: ${failure.hint}`);
470
+ }
471
+ lines.push("TEST_RESULT: FAIL");
472
+ return lines;
473
+ }
474
+ function repairGuidance(cause) {
475
+ if (/snapshot\(\) returned the same reference/i.test(cause)) {
476
+ return "The game mutated state without publishing a new snapshot reference, so React skipped the render after an Object.is comparison. Publish a new top-level object before notifying subscribers, for example cachedSnapshot = { ...state }, and never expose a mutable internal object as the snapshot.";
477
+ }
478
+ if (/(?:already true|found the outcome) at step 0.*DOM has not changed/i.test(
479
+ cause
480
+ )) {
481
+ return 'Make the stepUntil condition false at checkpoint("entered"). Verify that the primary input reaches the production control, then wait for a post-input outcome such as a changed score, a removed entry overlay, a completed turn, or a result screen. For Canvas or Controller state outside the DOM, declare observe once on playthroughTest and return the read-only production Telemetry snapshot.';
482
+ }
483
+ if (/authoritative observation did not change/i.test(cause)) {
484
+ return "The flow reached its condition while observe still returned the same authoritative state. Make observe read the same production Controller that React renders, verify the primary input changes that Controller, and wait for a post-input result. Inspect the Timeline values to locate the disconnected stage.";
485
+ }
486
+ if (/No step callback was provided/i.test(cause)) {
487
+ return "This flow is driven by time or frames, but stepUntil did not advance the game clock. Inject the devkit GameClock into the production game and pass step: () => clock.stepFrame(). Do not replace deterministic advancement with a real setTimeout.";
488
+ }
489
+ if (/outcome was not reached within \d+ steps/i.test(cause)) {
490
+ return "The real input was dispatched, but gameplay did not reach the outcome within the bound. Confirm that the production control received the input, then inspect Last diagnostics to determine whether the game loop, rule state, or UI synchronization failed to advance.";
491
+ }
492
+ if (/performInput|checkpoint/.test(cause)) {
493
+ return 'Complete the evidence sequence in order: performInput("entry"), checkpoint("entered"), performInput("primary"), bounded stepUntil, an authoritative result assertion using the provided expect, then checkpoint("progress") or checkpoint("terminal").';
494
+ }
495
+ return "Start from the production entry, dispatch real DOM input, and use stepUntil to reach a bounded player-visible or authoritative game outcome before asserting it. Do not jump to an internal level or mutate gameplay state.";
496
+ }
343
497
  function assessReactPlaythroughReport(input) {
344
498
  const base = { file: input.expectedFile };
345
499
  if (!input.expectedFileScheduled) {
@@ -347,15 +501,15 @@ function assessReactPlaythroughReport(input) {
347
501
  return {
348
502
  ...base,
349
503
  status: "NOT_CHECKED",
350
- next: `\u672C\u6B21\u662F\u805A\u7126\u8FD0\u884C\uFF0C\u672A\u68C0\u67E5\u6700\u4F4E\u53EF\u73A9\u6D41\u7A0B\uFF1B\u63D0\u4EA4\u524D\u8FD0\u884C pnpm test\uFF0C\u786E\u4FDD ${input.expectedFile} \u901A\u8FC7\u3002`,
504
+ next: `This focused run did not execute the minimum production playthrough. Run pnpm test before submitting and make sure ${input.expectedFile} passes.`,
351
505
  failsRun: false
352
506
  };
353
507
  }
354
508
  return {
355
509
  ...base,
356
510
  status: "FAILED",
357
- cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
358
- next: '\u521B\u5EFA\u8BE5\u6587\u4EF6\uFF1A\u4ECE <App /> \u901A\u8FC7\u771F\u5B9E DOM \u8F93\u5165\u4F9D\u6B21\u6267\u884C performInput("entry")\u3001checkpoint("entered")\u3001performInput("primary")\u3001stepUntil\u3001\u7528 playthroughTest \u63D0\u4F9B\u7684 expect \u65AD\u8A00\u6743\u5A01\u7ED3\u679C\uFF0C\u5E76\u8BB0\u5F55 checkpoint("progress") \u6216 checkpoint("terminal")\u3002\u6BCF\u6761\u4E3B\u6D41\u7A0B\u81F3\u5C11\u9700\u8981\u8FD9\u4E24\u6B21 checkpoint \u7B7E\u5230\u3002',
511
+ cause: "The required production playthrough test file does not exist.",
512
+ next: 'Create the file and render <App />. Drive a legal entry with performInput("entry"), record checkpoint("entered"), perform a core game action with performInput("primary"), and use a bounded stepUntil. Assert the authoritative result with the provided expect, then record checkpoint("progress") or checkpoint("terminal").',
359
513
  failsRun: true
360
514
  };
361
515
  }
@@ -369,7 +523,7 @@ function assessReactPlaythroughReport(input) {
369
523
  return {
370
524
  ...base,
371
525
  status: "NOT_CHECKED",
372
- next: `\u672C\u6B21\u540D\u79F0\u6216\u884C\u53F7\u8FC7\u6EE4\u6CA1\u6709\u6267\u884C\u6700\u4F4E\u53EF\u73A9\u6D41\u7A0B\uFF1B\u63D0\u4EA4\u524D\u8FD0\u884C pnpm test\uFF0C\u786E\u4FDD ${input.expectedFile} \u901A\u8FC7\u3002`,
526
+ next: `The name or line filter did not execute the minimum production playthrough. Run pnpm test before submitting and make sure ${input.expectedFile} passes.`,
373
527
  failsRun: false
374
528
  };
375
529
  }
@@ -377,23 +531,20 @@ function assessReactPlaythroughReport(input) {
377
531
  return {
378
532
  ...base,
379
533
  status: "NOT_RUN",
380
- cause: productionModule?.errors[0] ?? input.unhandledErrors?.[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u672A\u5B8C\u6210\u6536\u96C6\u6216\u6267\u884C\u3002",
381
- next: "\u5148\u4FEE\u590D Vitest \u4E0A\u65B9\u9996\u4E2A\u8BED\u6CD5\u3001\u5BFC\u5165\u6216\u6536\u96C6\u9519\u8BEF\uFF0C\u518D\u8FD0\u884C pnpm test\uFF1B\u4E0D\u8981\u7528 skip \u63A9\u76D6\u52A0\u8F7D\u5931\u8D25\u3002",
534
+ cause: productionModule?.errors[0] ?? input.unhandledErrors?.[0] ?? "The production playthrough could not be collected or executed.",
535
+ next: "Fix the first Vitest syntax, import, environment, or collection error shown above, then run pnpm test again. Do not use skip to hide a load failure.",
382
536
  failsRun: true
383
537
  };
384
538
  }
385
539
  const tests = input.modules.flatMap((module2) => module2.tests);
386
540
  const audit = auditReactPlaythroughRun(tests);
387
541
  if (!audit.passed) {
388
- const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6D41\u7A0B\u6CA1\u6709\u7559\u4E0B\u5B8C\u6574\u8BC1\u636E\u3002";
389
- const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
390
- const missingStep = /No step callback was provided/i.test(cause);
391
- const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
542
+ const cause = productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "The production playthrough did not leave complete gameplay evidence.";
392
543
  return {
393
544
  ...base,
394
545
  status: "FAILED",
395
546
  cause,
396
- next: missingStep ? "\u8BE5\u6D41\u7A0B\u662F\u65F6\u95F4\u6216\u5E27\u9A71\u52A8\u7684\uFF0C\u4F46 stepUntil \u6CA1\u6709\u63A8\u8FDB\u6E38\u620F\u65F6\u95F4\uFF1B\u4E3A\u751F\u4EA7\u6E38\u620F\u6CE8\u5165 devkit \u7684 GameClock\uFF0C\u5E76\u4F20\u5165 step: () => clock.stepFrame()\u3002\u4E0D\u8981\u7528\u771F\u5B9E setTimeout\u3002" : timedOut ? "\u771F\u5B9E\u8F93\u5165\u5DF2\u6267\u884C\uFF0C\u4F46\u73A9\u6CD5\u6CA1\u6709\u5728\u4E0A\u9650\u5185\u4EA7\u751F\u7ED3\u679C\uFF1B\u68C0\u67E5\u751F\u4EA7\u63A7\u5236\u662F\u5426\u6536\u5230\u8F93\u5165\uFF0C\u518D\u67E5\u770B\u8D85\u65F6\u9519\u8BEF\u4E2D\u7684 Last diagnostics \u5224\u65AD\u662F\u6E38\u620F\u5FAA\u73AF\u3001\u89C4\u5219\u72B6\u6001\u8FD8\u662F UI \u540C\u6B65\u672A\u63A8\u8FDB\u3002" : missingStructuredEvidence ? '\u6309\u987A\u5E8F\u8865\u9F50\u81F3\u5C11\u4E24\u6B21\u7B7E\u5230\uFF1AperformInput("entry") \u540E\u8C03\u7528 checkpoint("entered")\uFF1B\u518D\u7528 performInput("primary") \u6267\u884C\u6838\u5FC3\u64CD\u4F5C\uFF0CstepUntil \u7B49\u5F85\u7ED3\u679C\uFF0C\u7528\u56DE\u8C03\u63D0\u4F9B\u7684 expect \u65AD\u8A00\u540E\u8C03\u7528 checkpoint("progress") \u6216 checkpoint("terminal")\u3002' : "\u4ECE\u751F\u4EA7\u5165\u53E3\u6267\u884C\u771F\u5B9E DOM \u8F93\u5165\uFF0C\u7528 stepUntil \u6709\u754C\u63A8\u8FDB\u5230\u73A9\u5BB6\u53EF\u89C1\u7ED3\u679C\u6216\u6E38\u620F\u6743\u5A01\u72B6\u6001\uFF0C\u5E76\u5728\u5176\u540E\u65AD\u8A00\uFF1B\u4E0D\u8981\u76F4\u8FBE\u5185\u90E8\u5173\u5361\u6216\u4FEE\u6539\u73A9\u6CD5\u72B6\u6001\u3002",
547
+ next: repairGuidance(cause),
397
548
  failsRun: true
398
549
  };
399
550
  }
@@ -411,7 +562,7 @@ function formatReactPlaythroughReport(report) {
411
562
  const lines = [`REACT_PLAYTHROUGH: ${report.status}`, `FILE: ${report.file}`];
412
563
  if (report.cause) lines.push(`CAUSE: ${report.cause}`);
413
564
  if (report.waiverReasons?.length) {
414
- lines.push(`REASON: ${report.waiverReasons.join("\uFF1B")}`);
565
+ lines.push(`REASON: ${report.waiverReasons.join("; ")}`);
415
566
  }
416
567
  if (report.next) lines.push(`NEXT: ${report.next}`);
417
568
  return `
@@ -458,6 +609,14 @@ var ReactPlaythroughReporter = class {
458
609
  } else {
459
610
  console.log(output);
460
611
  }
612
+ const summary = formatFailureSummary(
613
+ testModules.map((module2) => toModuleResult(module2, this.projectRoot))
614
+ );
615
+ if (report.failsRun && summary[0] === "TEST_RESULT: PASS") {
616
+ summary[0] = "TEST_RESULT: FAIL";
617
+ }
618
+ console.log(`
619
+ ${summary.join("\n")}`);
461
620
  }
462
621
  };
463
622
 
@@ -486,6 +645,9 @@ function resolvePhaser3BrowserEntry(projectRoot) {
486
645
  function defineReactGameVitestConfig(options) {
487
646
  const phaser3BrowserEntry = resolvePhaser3BrowserEntry(options.projectRoot);
488
647
  return (0, import_config.defineConfig)({
648
+ // Keep discovery and dependency resolution anchored to the generated app even
649
+ // when an external runner invokes Vitest from a parent workspace directory.
650
+ root: options.projectRoot,
489
651
  resolve: {
490
652
  alias: {
491
653
  ...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},
@@ -506,6 +668,7 @@ function defineReactGameVitestConfig(options) {
506
668
  environmentOptions: {
507
669
  jsdom: { url: "http://localhost/", pretendToBeVisual: true }
508
670
  },
671
+ includeTaskLocation: true,
509
672
  setupFiles: [
510
673
  "miaoda-game-devkit/react/vitest-setup",
511
674
  ...options.additionalSetupFiles ?? []
@@ -513,8 +676,8 @@ function defineReactGameVitestConfig(options) {
513
676
  sequence: {
514
677
  setupFiles: "list"
515
678
  },
516
- // minimal 保留业务失败;附加 reporter 负责项目级最低可玩性门禁。
517
- reporters: ["minimal", new ReactPlaythroughReporter(options.projectRoot)],
679
+ // 单一 reporter 输出无 ANSI 的紧凑失败摘要和项目级可玩性门禁。
680
+ reporters: [new ReactPlaythroughReporter(options.projectRoot)],
518
681
  restoreMocks: true,
519
682
  clearMocks: true,
520
683
  testTimeout: options.testTimeout,