miaoda-game-devkit 0.3.0 → 0.5.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.
@@ -103,47 +103,123 @@ var INPUT_EVENTS = [
103
103
  "touchend"
104
104
  ];
105
105
  var MIN_CHECKPOINTS = 2;
106
+ var REACT_PLAYTHROUGH_TRACE_ANNOTATION = "miaoda:react-playthrough-trace";
107
+ var MAX_TRACE_VALUE_LENGTH = 180;
108
+ var MAX_TRACE_LENGTH = 720;
109
+ function truncateTraceValue(value, limit) {
110
+ const compact = value.replace(/\s+/g, " ").trim();
111
+ if (compact.length <= limit) return compact;
112
+ return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
113
+ }
114
+ function formatReactPlaythroughFailureTrace(trace) {
115
+ const stages = [
116
+ ["entered", trace.entered],
117
+ ["after-primary", trace.afterPrimary],
118
+ ["last", trace.last]
119
+ ].filter((stage) => stage[1] !== void 0).map(
120
+ ([stage, value]) => `${stage}=${truncateTraceValue(value, MAX_TRACE_VALUE_LENGTH)}`
121
+ );
122
+ const details = [
123
+ trace.checkpoints.length > 0 ? `checkpoints=${trace.checkpoints.join(",")}` : "checkpoints=none",
124
+ trace.step ? `stepUntil=${trace.step.completed ?? "failed"}/${trace.step.bound}` : void 0
125
+ ].filter((detail) => Boolean(detail));
126
+ const formatted = `${stages.join(" -> ")}${stages.length ? "; " : ""}${details.join("; ")}`;
127
+ return truncateTraceValue(formatted, MAX_TRACE_LENGTH);
128
+ }
129
+ var MAX_FORMATTED_OBSERVATION_LENGTH = 500;
130
+ function formatObservation(fingerprint) {
131
+ if (fingerprint.length <= MAX_FORMATTED_OBSERVATION_LENGTH) return fingerprint;
132
+ return `${fingerprint.slice(0, MAX_FORMATTED_OBSERVATION_LENGTH)}\u2026 (${fingerprint.length} chars)`;
133
+ }
134
+ function sampleObservation(observe, stage) {
135
+ let value;
136
+ try {
137
+ value = observe();
138
+ } catch (error) {
139
+ throw new Error(`observe() threw at ${stage}: ${String(error)}`);
140
+ }
141
+ try {
142
+ const fingerprint = JSON.stringify(value);
143
+ if (fingerprint === void 0) throw new Error("unsupported value");
144
+ return { fingerprint, formatted: formatObservation(fingerprint) };
145
+ } catch {
146
+ throw new Error(
147
+ `observe() must return JSON-serializable read-only state; sampling failed at ${stage}.`
148
+ );
149
+ }
150
+ }
151
+ function formatObservationTimeline(entered, afterPrimary, outcome) {
152
+ return [
153
+ `entered=${entered.formatted}`,
154
+ `after-primary=${afterPrimary?.formatted ?? "<not sampled>"}`,
155
+ `outcome=${outcome.formatted}`
156
+ ].join(", ");
157
+ }
106
158
  function describeMissingEvidence(evidence) {
107
- if (!evidence || evidence.entryInputs === 0) return "entry \u8F93\u5165";
108
- if (evidence.primaryInputs === 0) return "primary \u8F93\u5165";
159
+ if (!evidence || evidence.entryInputs === 0) return "an entry input";
160
+ if (evidence.primaryInputs === 0) return "a primary gameplay input";
109
161
  if (!evidence.checkpoints.includes("entered")) return "entered checkpoint";
110
- if (evidence.boundedRuns === 0) return "\u6709\u754C stepUntil";
111
- if (evidence.assertionsAfterOutcome === 0) return "stepUntil \u540E\u7684\u7ED3\u679C\u65AD\u8A00";
162
+ if (evidence.boundedRuns === 0) return "a bounded stepUntil call";
163
+ if (evidence.assertionsAfterOutcome === 0)
164
+ return "an outcome assertion after stepUntil";
112
165
  if (evidence.checkpoints.length < MIN_CHECKPOINTS)
113
- return `\u81F3\u5C11 ${MIN_CHECKPOINTS} \u4E2A checkpoint`;
166
+ return `at least ${MIN_CHECKPOINTS} checkpoints`;
114
167
  if (!evidence.checkpoints.some(
115
168
  (checkpoint) => checkpoint === "progress" || checkpoint === "terminal"
116
169
  )) {
117
170
  return "progress/terminal checkpoint";
118
171
  }
119
- return "\u5B8C\u6574\u7684 playthrough \u6821\u9A8C\u6807\u8BB0";
172
+ return "a complete playthrough verification marker";
120
173
  }
121
- function createMetadata(waiverReason) {
174
+ function createEvidence() {
122
175
  return {
123
- version: 3,
124
- waiverReason,
125
- evidence: {
126
- domInputEvents: 0,
127
- entryInputs: 0,
128
- primaryInputs: 0,
129
- boundedRuns: 0,
130
- assertionsAfterOutcome: 0,
131
- checkpoints: [],
132
- verified: false
133
- }
176
+ domInputEvents: 0,
177
+ entryInputs: 0,
178
+ primaryInputs: 0,
179
+ boundedRuns: 0,
180
+ assertionsAfterOutcome: 0,
181
+ checkpoints: [],
182
+ verified: false
134
183
  };
135
184
  }
136
- function definePlaythrough(element, run, waiverReason) {
185
+ function createMetadata(waiverReason) {
186
+ return { version: 3, waiverReason, evidence: createEvidence() };
187
+ }
188
+ function definePlaythrough(element, run, playthroughOptions, waiverReason) {
137
189
  const reason = normalizePlaythroughWaiverReason(waiverReason);
138
190
  const metadata = createMetadata(reason);
139
191
  (0, import_vitest.test)("production game completes a bounded playthrough", {
140
192
  skip: Boolean(reason),
141
193
  meta: { reactPlaythrough: metadata }
142
- }, async ({ expect }) => {
194
+ }, async ({ annotate, expect }) => {
195
+ metadata.evidence = createEvidence();
196
+ metadata.trace = void 0;
143
197
  const evidence = metadata.evidence;
144
198
  let assertionsAtOutcome;
145
199
  let enteredRecorded = false;
146
200
  let domTextAtEntered;
201
+ let enteredObservation;
202
+ let afterPrimaryObservation;
203
+ let enteredTrace;
204
+ let afterPrimaryTrace;
205
+ let outcomeTrace;
206
+ let stepTrace;
207
+ const sampleDomTrace = () => JSON.stringify((document.body.textContent ?? "").replace(/\s+/g, " ").trim());
208
+ const sampleLastTrace = () => {
209
+ if (!playthroughOptions?.observe) return sampleDomTrace();
210
+ try {
211
+ return sampleObservation(playthroughOptions.observe, "outcome").formatted;
212
+ } catch (error) {
213
+ return `<observe unavailable: ${String(error)}>`;
214
+ }
215
+ };
216
+ const createFailureTrace = () => formatReactPlaythroughFailureTrace({
217
+ entered: enteredTrace,
218
+ afterPrimary: afterPrimaryTrace,
219
+ last: outcomeTrace ?? sampleLastTrace(),
220
+ checkpoints: [...evidence.checkpoints],
221
+ step: stepTrace
222
+ });
147
223
  const recordInput = () => {
148
224
  evidence.domInputEvents += 1;
149
225
  };
@@ -181,7 +257,18 @@ function definePlaythrough(element, run, waiverReason) {
181
257
  );
182
258
  }
183
259
  if (kind === "entry") evidence.entryInputs += 1;
184
- else evidence.primaryInputs += 1;
260
+ else {
261
+ evidence.primaryInputs += 1;
262
+ if (playthroughOptions?.observe) {
263
+ afterPrimaryObservation = sampleObservation(
264
+ playthroughOptions.observe,
265
+ "after-primary"
266
+ );
267
+ afterPrimaryTrace = afterPrimaryObservation.formatted;
268
+ } else {
269
+ afterPrimaryTrace = sampleDomTrace();
270
+ }
271
+ }
185
272
  },
186
273
  checkpoint(kind) {
187
274
  if (kind === "entered") {
@@ -196,7 +283,16 @@ function definePlaythrough(element, run, waiverReason) {
196
283
  );
197
284
  }
198
285
  enteredRecorded = true;
199
- domTextAtEntered = document.body.textContent ?? "";
286
+ if (playthroughOptions?.observe) {
287
+ enteredObservation = sampleObservation(
288
+ playthroughOptions.observe,
289
+ "entered"
290
+ );
291
+ enteredTrace = enteredObservation.formatted;
292
+ } else {
293
+ domTextAtEntered = document.body.textContent ?? "";
294
+ enteredTrace = sampleDomTrace();
295
+ }
200
296
  evidence.checkpoints.push(kind);
201
297
  return;
202
298
  }
@@ -217,17 +313,43 @@ function definePlaythrough(element, run, waiverReason) {
217
313
  }
218
314
  evidence.checkpoints.push(kind);
219
315
  },
220
- async stepUntil(condition, options = {}) {
221
- const steps = await runBoundedUntil(condition, options);
316
+ async stepUntil(condition, stepOptions = {}) {
317
+ const stepBound = stepOptions.maxSteps ?? 120;
318
+ stepTrace = { bound: stepBound };
319
+ const boundedOptions = stepOptions.diagnostics || !playthroughOptions?.observe ? stepOptions : {
320
+ ...stepOptions,
321
+ diagnostics: playthroughOptions.observe
322
+ };
323
+ const steps = await runBoundedUntil(condition, boundedOptions);
324
+ stepTrace = { bound: stepBound, completed: steps };
222
325
  if (evidence.primaryInputs === 0) {
223
326
  throw new Error(
224
327
  'stepUntil must follow performInput("primary", ...). A menu/help click is not gameplay evidence.'
225
328
  );
226
329
  }
227
- if (steps === 0 && options.allowStaticDom !== true && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
228
- throw new Error(
229
- 'stepUntil outcome was already true at step 0 and the DOM has not changed since checkpoint("entered"), so the flow cannot prove that gameplay changed anything. \u610F\u601D\uFF1A\u6E38\u620F\u4E00\u6B65\u90FD\u6CA1\u73A9\uFF0C\u7B49\u5F85\u7684"\u7ED3\u679C"\u5C31\u5DF2\u7ECF\u6210\u7ACB\uFF0C\u9875\u9762\u4E5F\u4E00\u4E2A\u5B57\u6CA1\u53D8\u2014\u2014\u8FD9\u4E2A\u7ED3\u679C\u8BC1\u660E\u4E0D\u4E86\u4EFB\u4F55\u4E8B\u3002\u5E38\u89C1\u539F\u56E0\uFF1A\u2460 \u8F93\u5165\u6CA1\u6709\u63A5\u5230\u6E38\u620F\u4E0A\uFF1B\u2461 \u754C\u9762\u5361\u6B7B\uFF08\u72B6\u6001\u6539\u4E86\u4F46 snapshot \u5F15\u7528\u6CA1\u6362\uFF0CReact \u6CA1\u6709\u5237\u65B0\uFF09\uFF1B\u2462 \u65AD\u8A00\u4E86\u5F00\u5C40\u524D\u5C31\u5B58\u5728\u7684\u9759\u6001\u6587\u672C\u3002\u4FEE\u590D\uFF1A\u7B49\u5F85\u5E76\u65AD\u8A00\u53EA\u6709\u73A9\u8D77\u6765\u4E4B\u540E\u624D\u4F1A\u51FA\u73B0\u7684\u4E1C\u897F\uFF08\u5F00\u59CB\u906E\u7F69\u6D88\u5931\u3001\u6BD4\u5206\u53D8\u5316\u3001\u7ED3\u7B97\u753B\u9762\u51FA\u73B0\uFF09\u3002\u4F8B\u5916\uFF1A\u7ED3\u679C\u753B\u5728 Canvas \u4E0A\u3001\u7ECF Telemetry \u7B49\u9875\u9762\u5916\u72B6\u6001\u89C2\u5BDF\u7684\u6E38\u620F\uFF0C\u663E\u5F0F\u4F20 { allowStaticDom: true }\u3002'
330
+ if (playthroughOptions?.observe) {
331
+ if (!enteredObservation) {
332
+ throw new Error(
333
+ 'observe requires checkpoint("entered") before primary gameplay input.'
334
+ );
335
+ }
336
+ const outcomeObservation = sampleObservation(
337
+ playthroughOptions.observe,
338
+ "outcome"
230
339
  );
340
+ outcomeTrace = outcomeObservation.formatted;
341
+ if (outcomeObservation.fingerprint === enteredObservation.fingerprint) {
342
+ throw new Error(
343
+ `The authoritative observation did not change from checkpoint("entered") to the outcome. Timeline: ${formatObservationTimeline(enteredObservation, afterPrimaryObservation, outcomeObservation)}`
344
+ );
345
+ }
346
+ } else {
347
+ outcomeTrace = sampleDomTrace();
348
+ if (steps === 0 && domTextAtEntered !== void 0 && (document.body.textContent ?? "") === domTextAtEntered) {
349
+ throw new Error(
350
+ '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.'
351
+ );
352
+ }
231
353
  }
232
354
  evidence.boundedRuns += 1;
233
355
  assertionsAtOutcome = expect.getState().assertionCalls;
@@ -262,7 +384,15 @@ function definePlaythrough(element, run, waiverReason) {
262
384
  );
263
385
  }
264
386
  evidence.verified = true;
387
+ } catch (error) {
388
+ metadata.trace = createFailureTrace();
389
+ try {
390
+ await annotate(metadata.trace, REACT_PLAYTHROUGH_TRACE_ANNOTATION);
391
+ } catch {
392
+ }
393
+ throw error;
265
394
  } finally {
395
+ metadata.trace ??= createFailureTrace();
266
396
  for (const event of INPUT_EVENTS) {
267
397
  document.removeEventListener(event, recordInput, true);
268
398
  }
@@ -270,9 +400,16 @@ function definePlaythrough(element, run, waiverReason) {
270
400
  });
271
401
  }
272
402
  var playthroughTest = Object.assign(
273
- (element, run) => definePlaythrough(element, run),
403
+ (element, optionsOrRun, maybeRun) => {
404
+ if (typeof optionsOrRun === "function") {
405
+ definePlaythrough(element, optionsOrRun);
406
+ return;
407
+ }
408
+ if (!maybeRun) throw new TypeError("playthroughTest requires a run callback.");
409
+ definePlaythrough(element, maybeRun, optionsOrRun);
410
+ },
274
411
  {
275
- skip: (reason, element, run) => definePlaythrough(element, run, reason)
412
+ skip: (reason, element, run) => definePlaythrough(element, run, void 0, reason)
276
413
  }
277
414
  );
278
415
  function auditReactPlaythroughRun(tests) {
@@ -289,7 +426,7 @@ function auditReactPlaythroughRun(tests) {
289
426
  const issues = [];
290
427
  if (declared.length === 0) {
291
428
  issues.push(
292
- '\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'
429
+ '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").'
293
430
  );
294
431
  } else {
295
432
  for (const candidate of declared) {
@@ -298,13 +435,17 @@ function auditReactPlaythroughRun(tests) {
298
435
  if (isValid || isWaived) continue;
299
436
  if (candidate.state === "skipped") {
300
437
  issues.push(
301
- `\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`
438
+ `Playthrough ${JSON.stringify(candidate.name)} was skipped without an explicit reason of at least 20 characters.`
302
439
  );
303
440
  } else if (candidate.state !== "passed") {
304
- issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7684\u72B6\u6001\u4E3A ${candidate.state}\u3002`);
441
+ issues.push(
442
+ `Playthrough ${JSON.stringify(candidate.name)} finished with state ${candidate.state}.`
443
+ );
305
444
  } else {
306
445
  const missing = describeMissingEvidence(candidate.metadata?.evidence);
307
- issues.push(`\u73A9\u6CD5\u9A8C\u8BC1\u201C${candidate.name}\u201D\u7F3A\u5C11${missing}\u3002`);
446
+ issues.push(
447
+ `Playthrough ${JSON.stringify(candidate.name)} is missing ${missing}.`
448
+ );
308
449
  }
309
450
  }
310
451
  }
@@ -344,21 +485,41 @@ function errorLocation(value) {
344
485
  const match = value.match(/(?:^|\n)\s*at\s+(.*?\.(?:test|spec)\.[^\n]+:\d+:\d+)/);
345
486
  return match?.[1];
346
487
  }
488
+ function failureTrace(test2) {
489
+ const annotations = test2.annotations();
490
+ let annotationTrace;
491
+ for (let index = annotations.length - 1; index >= 0; index -= 1) {
492
+ if (annotations[index].type === REACT_PLAYTHROUGH_TRACE_ANNOTATION) {
493
+ annotationTrace = annotations[index].message;
494
+ break;
495
+ }
496
+ }
497
+ const metadata = test2.meta().reactPlaythrough;
498
+ const trace = annotationTrace ?? (isMetadata(metadata) ? metadata.trace : void 0);
499
+ return trace ? truncateReporterLine(trace, 720) : void 0;
500
+ }
501
+ function truncateReporterLine(value, limit) {
502
+ const compact = (0, import_node_util.stripVTControlCharacters)(value).replace(/\s+/g, " ").trim();
503
+ if (compact.length <= limit) return compact;
504
+ return `${compact.slice(0, Math.max(0, limit - 1))}\u2026`;
505
+ }
347
506
  function toModuleResult(module2, projectRoot) {
348
507
  const tests = [...module2.children.allTests()];
508
+ const moduleErrors = module2.errors().map((error) => firstLine(error.message)).filter((message) => Boolean(message));
349
509
  const errors = [
350
- ...module2.errors().map((error) => firstLine(error.message)),
510
+ ...moduleErrors,
351
511
  ...tests.flatMap(
352
512
  (test2) => (test2.result().errors ?? []).map((error) => firstLine(error.message))
353
513
  )
354
514
  ].filter((message) => Boolean(message));
355
515
  const failures = tests.filter((test2) => test2.result().state === "failed").map((test2) => {
356
- const raw = test2.result().errors?.[0]?.message ?? module2.errors()[0]?.message ?? "Unknown failure";
516
+ const raw = test2.result().errors?.at(-1)?.message ?? module2.errors()[0]?.message ?? "Unknown failure";
357
517
  return {
358
518
  test: test2.fullName,
359
519
  cause: firstLine(raw) ?? "Unknown failure",
360
520
  location: test2.location ? `${(0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/")}:${test2.location.line}:${test2.location.column}` : errorLocation(raw),
361
- hint: failureHint(raw)
521
+ hint: failureHint(raw),
522
+ trace: failureTrace(test2)
362
523
  };
363
524
  });
364
525
  if (failures.length === 0 && tests.length === 0 && module2.errors().length > 0) {
@@ -374,11 +535,12 @@ function toModuleResult(module2, projectRoot) {
374
535
  file: (0, import_node_path.relative)(projectRoot, module2.moduleId).replaceAll("\\", "/"),
375
536
  state: module2.state(),
376
537
  errors,
538
+ primaryError: moduleErrors[0] ?? failures[0]?.cause,
377
539
  tests: tests.map(toAuditInput),
378
540
  failures
379
541
  };
380
542
  }
381
- function formatFailureSummary(modules) {
543
+ function formatReactFailureSummary(modules) {
382
544
  const failures = modules.flatMap(
383
545
  (module2) => (module2.failures ?? []).map((failure) => ({ ...failure, file: module2.file }))
384
546
  );
@@ -388,12 +550,36 @@ function formatFailureSummary(modules) {
388
550
  lines.push(`FAILURE_${index + 1}: ${failure.file}`);
389
551
  lines.push(`TEST: ${failure.test}`);
390
552
  lines.push(`CAUSE: ${failure.cause}`);
553
+ if (failure.trace) lines.push(`TRACE: ${failure.trace}`);
391
554
  if (failure.location) lines.push(`AT: ${failure.location}`);
392
555
  if (failure.hint) lines.push(`HINT: ${failure.hint}`);
393
556
  }
394
557
  lines.push("TEST_RESULT: FAIL");
395
558
  return lines;
396
559
  }
560
+ function repairGuidance(cause) {
561
+ if (/snapshot\(\) returned the same reference/i.test(cause)) {
562
+ 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.";
563
+ }
564
+ if (/(?:already true|found the outcome) at step 0.*DOM has not changed/i.test(
565
+ cause
566
+ )) {
567
+ 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.';
568
+ }
569
+ if (/authoritative observation did not change/i.test(cause)) {
570
+ 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.";
571
+ }
572
+ if (/No step callback was provided/i.test(cause)) {
573
+ 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.";
574
+ }
575
+ if (/outcome was not reached within \d+ steps/i.test(cause)) {
576
+ 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.";
577
+ }
578
+ if (/performInput|checkpoint/.test(cause)) {
579
+ 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").';
580
+ }
581
+ 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.";
582
+ }
397
583
  function assessReactPlaythroughReport(input) {
398
584
  const base = { file: input.expectedFile };
399
585
  if (!input.expectedFileScheduled) {
@@ -401,15 +587,15 @@ function assessReactPlaythroughReport(input) {
401
587
  return {
402
588
  ...base,
403
589
  status: "NOT_CHECKED",
404
- 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`,
590
+ next: `This focused run did not execute the minimum production playthrough. Run pnpm test before submitting and make sure ${input.expectedFile} passes.`,
405
591
  failsRun: false
406
592
  };
407
593
  }
408
594
  return {
409
595
  ...base,
410
596
  status: "FAILED",
411
- cause: "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u6587\u4EF6\u4E0D\u5B58\u5728\u3002",
412
- 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',
597
+ cause: "The required production playthrough test file does not exist.",
598
+ 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").',
413
599
  failsRun: true
414
600
  };
415
601
  }
@@ -423,7 +609,7 @@ function assessReactPlaythroughReport(input) {
423
609
  return {
424
610
  ...base,
425
611
  status: "NOT_CHECKED",
426
- 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`,
612
+ next: `The name or line filter did not execute the minimum production playthrough. Run pnpm test before submitting and make sure ${input.expectedFile} passes.`,
427
613
  failsRun: false
428
614
  };
429
615
  }
@@ -431,25 +617,20 @@ function assessReactPlaythroughReport(input) {
431
617
  return {
432
618
  ...base,
433
619
  status: "NOT_RUN",
434
- cause: productionModule?.errors[0] ?? input.unhandledErrors?.[0] ?? "\u751F\u4EA7\u53EF\u73A9\u6027\u6D4B\u8BD5\u672A\u5B8C\u6210\u6536\u96C6\u6216\u6267\u884C\u3002",
435
- 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",
620
+ cause: productionModule?.errors[0] ?? input.unhandledErrors?.[0] ?? "The production playthrough could not be collected or executed.",
621
+ 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.",
436
622
  failsRun: true
437
623
  };
438
624
  }
439
625
  const tests = input.modules.flatMap((module2) => module2.tests);
440
626
  const audit = auditReactPlaythroughRun(tests);
441
627
  if (!audit.passed) {
442
- 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";
443
- const timedOut = /outcome was not reached within \d+ steps/i.test(cause);
444
- const missingStep = /No step callback was provided/i.test(cause);
445
- const staticOutcome = /already true at step 0 and the DOM has not changed/i.test(cause);
446
- const staleSnapshot = /snapshot\(\) returned the same reference/i.test(cause);
447
- const missingStructuredEvidence = /performInput|checkpoint/.test(cause);
628
+ const cause = productionModule.primaryError ?? productionModule.errors[0] ?? input.unhandledErrors?.[0] ?? audit.issues[0] ?? "The production playthrough did not leave complete gameplay evidence.";
448
629
  return {
449
630
  ...base,
450
631
  status: "FAILED",
451
632
  cause,
452
- next: staleSnapshot ? "\u6E38\u620F\u539F\u5730\u4FEE\u6539\u72B6\u6001\u540E\u6CA1\u6709\u53D1\u5E03\u65B0\u7684\u5FEB\u7167\u5F15\u7528\uFF0CReact \u56E0 Object.is \u6BD4\u8F83\u76F8\u540C\u800C\u8DF3\u8FC7\u91CD\u6E32\u3002\u5728 controller \u7684 notify \u8DEF\u5F84\u4E0A\u53D1\u5E03\u65B0\u9876\u5C42\u5BF9\u8C61\uFF08cachedSnapshot = { ...state }\uFF09\uFF0C\u4E0D\u8981\u628A\u53EF\u53D8\u7684\u5185\u90E8\u5BF9\u8C61\u76F4\u63A5\u4F5C\u4E3A\u5FEB\u7167\u66B4\u9732\u3002" : staticOutcome ? "\u7ED3\u679C\u5728\u63A8\u8FDB\u524D\u5DF2\u6210\u7ACB\u4E14 DOM \u81EA entered \u4EE5\u6765\u65E0\u53D8\u5316\uFF1A\u8F93\u5165\u53EF\u80FD\u672A\u63A5\u5230\u751F\u4EA7\u63A7\u5236\uFF0CUI \u53EF\u80FD\u51BB\u7ED3\uFF0C\u4E5F\u53EF\u80FD\u65AD\u8A00\u4E86\u6E38\u620F\u5F00\u59CB\u524D\u5C31\u5B58\u5728\u7684\u9759\u6001\u6587\u672C\u3002\u6539\u4E3A\u7B49\u5F85\u5E76\u65AD\u8A00\u53EA\u6709\u73A9\u6CD5\u63A8\u8FDB\u540E\u624D\u51FA\u73B0\u7684\u72B6\u6001\uFF08\u5F00\u59CB\u906E\u7F69\u6D88\u5931\u3001\u6BD4\u5206\u53D8\u5316\u3001\u7ED3\u7B97\u51FA\u73B0\uFF09\uFF1B\u7ECF Canvas/Telemetry \u7B49 DOM \u5916\u6743\u5A01\u72B6\u6001\u89C2\u5BDF\u7684\u6D41\u7A0B\u4F20 { allowStaticDom: true }\u3002" : 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",
633
+ next: repairGuidance(cause),
453
634
  failsRun: true
454
635
  };
455
636
  }
@@ -467,7 +648,7 @@ function formatReactPlaythroughReport(report) {
467
648
  const lines = [`REACT_PLAYTHROUGH: ${report.status}`, `FILE: ${report.file}`];
468
649
  if (report.cause) lines.push(`CAUSE: ${report.cause}`);
469
650
  if (report.waiverReasons?.length) {
470
- lines.push(`REASON: ${report.waiverReasons.join("\uFF1B")}`);
651
+ lines.push(`REASON: ${report.waiverReasons.join("; ")}`);
471
652
  }
472
653
  if (report.next) lines.push(`NEXT: ${report.next}`);
473
654
  return `
@@ -514,7 +695,7 @@ var ReactPlaythroughReporter = class {
514
695
  } else {
515
696
  console.log(output);
516
697
  }
517
- const summary = formatFailureSummary(
698
+ const summary = formatReactFailureSummary(
518
699
  testModules.map((module2) => toModuleResult(module2, this.projectRoot))
519
700
  );
520
701
  if (report.failsRun && summary[0] === "TEST_RESULT: PASS") {
@@ -550,6 +731,9 @@ function resolvePhaser3BrowserEntry(projectRoot) {
550
731
  function defineReactGameVitestConfig(options) {
551
732
  const phaser3BrowserEntry = resolvePhaser3BrowserEntry(options.projectRoot);
552
733
  return (0, import_config.defineConfig)({
734
+ // Keep discovery and dependency resolution anchored to the generated app even
735
+ // when an external runner invokes Vitest from a parent workspace directory.
736
+ root: options.projectRoot,
553
737
  resolve: {
554
738
  alias: {
555
739
  ...phaser3BrowserEntry ? { phaser: phaser3BrowserEntry } : {},