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.
- package/README.md +13 -4
- package/dist/react/index.js +1 -1
- package/dist/react/index.mjs +1 -1
- package/dist/react/testing.d.mts +10 -11
- package/dist/react/testing.d.ts +10 -11
- package/dist/react/testing.js +174 -33
- package/dist/react/testing.mjs +174 -33
- package/dist/react/vitest-config.js +236 -52
- package/dist/react/vitest-config.mjs +236 -52
- package/dist/rules/react-test-boundary-plugin.js +97 -0
- package/oxlint-config.json +4 -2
- package/package.json +1 -1
|
@@ -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
|
|
108
|
-
if (evidence.primaryInputs === 0) return "primary
|
|
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 "
|
|
111
|
-
if (evidence.assertionsAfterOutcome === 0)
|
|
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
|
|
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 "
|
|
172
|
+
return "a complete playthrough verification marker";
|
|
120
173
|
}
|
|
121
|
-
function
|
|
174
|
+
function createEvidence() {
|
|
122
175
|
return {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
221
|
-
const
|
|
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 (
|
|
228
|
-
|
|
229
|
-
|
|
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,
|
|
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
|
-
'
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
...
|
|
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?.
|
|
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
|
|
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:
|
|
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: "
|
|
412
|
-
next: '
|
|
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:
|
|
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] ?? "
|
|
435
|
-
next: "
|
|
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] ?? "
|
|
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:
|
|
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("
|
|
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 =
|
|
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 } : {},
|