miaoda-game-devkit 0.6.5 → 0.7.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.
@@ -122,47 +122,145 @@ function safeJson(value) {
122
122
  return void 0;
123
123
  }
124
124
  }
125
- function collectEntries(value, fallbackCode, seen) {
125
+ function diagnosticValue(value) {
126
+ if (value === void 0) return void 0;
126
127
  if (typeof value === "string") {
127
- return value.trim() ? [{ code: fallbackCode, message: truncate(value) }] : [];
128
+ return truncate(value.replace(/\s+/g, " ").trim());
129
+ }
130
+ if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
131
+ return String(value);
132
+ }
133
+ if (typeof value === "symbol") return value.toString();
134
+ if (typeof value === "function") {
135
+ return `Function<${value.name || "anonymous"}>`;
136
+ }
137
+ const json = safeJson(value);
138
+ return truncate(json ?? String(value));
139
+ }
140
+ function normalizeFile(value) {
141
+ return value.replaceAll("\\", "/").replace(/^file:\/\//, "");
142
+ }
143
+ function failureLayer(file) {
144
+ const normalized = normalizeFile(file);
145
+ if (normalized.includes("/miaoda-game-devkit/") || normalized.includes("/packages/game-devkit/")) {
146
+ return "harness";
147
+ }
148
+ if (normalized.includes("/node_modules/")) return "dependency";
149
+ if (/(?:^|\/)tests?\//.test(normalized) || /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(normalized)) {
150
+ return "test";
151
+ }
152
+ if (/(?:^|\/)src\//.test(normalized)) return "product";
153
+ return "unknown";
154
+ }
155
+ function parsedOrigin(value) {
156
+ if (!value || typeof value !== "object") return void 0;
157
+ const frame = value;
158
+ if (typeof frame.file !== "string" || typeof frame.line !== "number" || typeof frame.column !== "number") {
159
+ return void 0;
160
+ }
161
+ return {
162
+ file: normalizeFile(frame.file),
163
+ line: frame.line,
164
+ column: frame.column
165
+ };
166
+ }
167
+ function stackOrigins(record) {
168
+ if (Array.isArray(record.stacks)) {
169
+ const parsed = record.stacks.map(parsedOrigin).filter((origin) => Boolean(origin));
170
+ if (parsed.length > 0) return parsed;
171
+ }
172
+ if (typeof record.stack !== "string") return [];
173
+ const origins = [];
174
+ const pattern = /(?:at\s+.*?\()?((?:file:\/\/)?[^()\s]+):(\d+):(\d+)\)?/g;
175
+ for (const match of record.stack.matchAll(pattern)) {
176
+ origins.push({
177
+ file: normalizeFile(match[1]),
178
+ line: Number(match[2]),
179
+ column: Number(match[3])
180
+ });
181
+ }
182
+ return origins;
183
+ }
184
+ function selectOrigin(record) {
185
+ const origins = stackOrigins(record);
186
+ return origins.find((origin) => {
187
+ const layer = failureLayer(origin.file);
188
+ return layer === "product" || layer === "test";
189
+ }) ?? origins.find((origin) => failureLayer(origin.file) !== "dependency");
190
+ }
191
+ function fallbackCode(record, origin, defaultCode) {
192
+ if (typeof record.code === "string" && record.code.trim()) {
193
+ return record.code.trim();
194
+ }
195
+ if (record.name === "AssertionError" && record.actual !== void 0 && record.expected !== void 0) {
196
+ return "EXPECTATION_MISMATCH";
197
+ }
198
+ if (record.name === "TypeError" && origin) {
199
+ const layer = failureLayer(origin.file);
200
+ if (layer === "product") return "PRODUCT_RUNTIME_TYPE_ERROR";
201
+ if (layer === "test") return "TEST_API_MISMATCH";
202
+ }
203
+ return defaultCode;
204
+ }
205
+ function structuredEntry(record, message, defaultCode) {
206
+ const origin = selectOrigin(record);
207
+ return {
208
+ code: fallbackCode(record, origin, defaultCode),
209
+ message: truncate(message),
210
+ errorName: typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0,
211
+ actual: diagnosticValue(record.actual),
212
+ expected: diagnosticValue(record.expected),
213
+ origin,
214
+ layer: origin ? failureLayer(origin.file) : void 0
215
+ };
216
+ }
217
+ function collectEntries(value, fallbackCode2, seen) {
218
+ if (typeof value === "string") {
219
+ return value.trim() ? [{ code: fallbackCode2, message: truncate(value) }] : [];
128
220
  }
129
221
  if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
130
- return [{ code: fallbackCode, message: String(value) }];
222
+ return [{ code: fallbackCode2, message: String(value) }];
131
223
  }
132
224
  if (typeof value === "function") {
133
225
  return [
134
- { code: fallbackCode, message: `Function<${value.name || "anonymous"}>` }
226
+ { code: fallbackCode2, message: `Function<${value.name || "anonymous"}>` }
135
227
  ];
136
228
  }
137
229
  if (seen.has(value)) return [];
138
230
  seen.add(value);
139
231
  if (Array.isArray(value)) {
140
- return value.flatMap((item) => collectEntries(item, fallbackCode, seen));
232
+ return value.flatMap((item) => collectEntries(item, fallbackCode2, seen));
141
233
  }
142
234
  const record = value;
143
- const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : fallbackCode;
144
235
  const entries = [];
145
236
  if (typeof record.message === "string" && record.message.trim()) {
146
- entries.push({ code, message: truncate(record.message) });
237
+ entries.push(structuredEntry(record, record.message, fallbackCode2));
147
238
  }
148
239
  if (record.cause !== void 0) {
149
- entries.push(...collectEntries(record.cause, fallbackCode, seen));
240
+ entries.push(...collectEntries(record.cause, fallbackCode2, seen));
150
241
  }
151
242
  if (Array.isArray(record.errors)) {
152
- entries.push(...collectEntries(record.errors, fallbackCode, seen));
243
+ entries.push(...collectEntries(record.errors, fallbackCode2, seen));
153
244
  }
154
245
  if (entries.length > 0) return entries;
155
246
  if (typeof record.stack === "string" && record.stack.trim()) {
156
- return [{ code, message: truncate(record.stack) }];
247
+ return [structuredEntry(record, record.stack, fallbackCode2)];
157
248
  }
158
249
  const json = safeJson(value);
159
- return json && json !== "{}" ? [{ code, message: truncate(json) }] : [];
250
+ return json && json !== "{}" ? [structuredEntry(record, json, fallbackCode2)] : [];
160
251
  }
161
- function extractFailureEntries(value, fallbackCode = "TEST_FAILURE") {
162
- const entries = collectEntries(value, fallbackCode, /* @__PURE__ */ new WeakSet());
252
+ function extractFailureEntries(value, fallbackCode2 = "TEST_FAILURE") {
253
+ const entries = collectEntries(value, fallbackCode2, /* @__PURE__ */ new WeakSet());
163
254
  const keys = /* @__PURE__ */ new Set();
164
255
  return entries.filter((entry) => {
165
- const key = `${entry.code}\0${entry.message}`;
256
+ const key = [
257
+ entry.code,
258
+ entry.message,
259
+ entry.errorName,
260
+ entry.actual,
261
+ entry.expected,
262
+ entry.origin ? `${entry.origin.file}:${entry.origin.line}:${entry.origin.column}` : void 0
263
+ ].join("\0");
166
264
  if (keys.has(key)) return false;
167
265
  keys.add(key);
168
266
  return true;
@@ -243,10 +341,10 @@ async function runBoundedUntil(condition, options = {}) {
243
341
  }
244
342
  }
245
343
  const diagnostics = formatDiagnostics(options.diagnostics);
246
- const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, so time-driven gameplay was not advanced. Inject a ManualGameClock for this test and pass step: () => clock.stepFrame().";
344
+ const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
247
345
  const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
248
346
  throw codedError(
249
- options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_CLOCK_NOT_ADVANCED",
347
+ options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
250
348
  `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
251
349
  );
252
350
  }
@@ -84,47 +84,145 @@ function safeJson(value) {
84
84
  return void 0;
85
85
  }
86
86
  }
87
- function collectEntries(value, fallbackCode, seen) {
87
+ function diagnosticValue(value) {
88
+ if (value === void 0) return void 0;
88
89
  if (typeof value === "string") {
89
- return value.trim() ? [{ code: fallbackCode, message: truncate(value) }] : [];
90
+ return truncate(value.replace(/\s+/g, " ").trim());
91
+ }
92
+ if (value === null || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
93
+ return String(value);
94
+ }
95
+ if (typeof value === "symbol") return value.toString();
96
+ if (typeof value === "function") {
97
+ return `Function<${value.name || "anonymous"}>`;
98
+ }
99
+ const json = safeJson(value);
100
+ return truncate(json ?? String(value));
101
+ }
102
+ function normalizeFile(value) {
103
+ return value.replaceAll("\\", "/").replace(/^file:\/\//, "");
104
+ }
105
+ function failureLayer(file) {
106
+ const normalized = normalizeFile(file);
107
+ if (normalized.includes("/miaoda-game-devkit/") || normalized.includes("/packages/game-devkit/")) {
108
+ return "harness";
109
+ }
110
+ if (normalized.includes("/node_modules/")) return "dependency";
111
+ if (/(?:^|\/)tests?\//.test(normalized) || /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(normalized)) {
112
+ return "test";
113
+ }
114
+ if (/(?:^|\/)src\//.test(normalized)) return "product";
115
+ return "unknown";
116
+ }
117
+ function parsedOrigin(value) {
118
+ if (!value || typeof value !== "object") return void 0;
119
+ const frame = value;
120
+ if (typeof frame.file !== "string" || typeof frame.line !== "number" || typeof frame.column !== "number") {
121
+ return void 0;
122
+ }
123
+ return {
124
+ file: normalizeFile(frame.file),
125
+ line: frame.line,
126
+ column: frame.column
127
+ };
128
+ }
129
+ function stackOrigins(record) {
130
+ if (Array.isArray(record.stacks)) {
131
+ const parsed = record.stacks.map(parsedOrigin).filter((origin) => Boolean(origin));
132
+ if (parsed.length > 0) return parsed;
133
+ }
134
+ if (typeof record.stack !== "string") return [];
135
+ const origins = [];
136
+ const pattern = /(?:at\s+.*?\()?((?:file:\/\/)?[^()\s]+):(\d+):(\d+)\)?/g;
137
+ for (const match of record.stack.matchAll(pattern)) {
138
+ origins.push({
139
+ file: normalizeFile(match[1]),
140
+ line: Number(match[2]),
141
+ column: Number(match[3])
142
+ });
143
+ }
144
+ return origins;
145
+ }
146
+ function selectOrigin(record) {
147
+ const origins = stackOrigins(record);
148
+ return origins.find((origin) => {
149
+ const layer = failureLayer(origin.file);
150
+ return layer === "product" || layer === "test";
151
+ }) ?? origins.find((origin) => failureLayer(origin.file) !== "dependency");
152
+ }
153
+ function fallbackCode(record, origin, defaultCode) {
154
+ if (typeof record.code === "string" && record.code.trim()) {
155
+ return record.code.trim();
156
+ }
157
+ if (record.name === "AssertionError" && record.actual !== void 0 && record.expected !== void 0) {
158
+ return "EXPECTATION_MISMATCH";
159
+ }
160
+ if (record.name === "TypeError" && origin) {
161
+ const layer = failureLayer(origin.file);
162
+ if (layer === "product") return "PRODUCT_RUNTIME_TYPE_ERROR";
163
+ if (layer === "test") return "TEST_API_MISMATCH";
164
+ }
165
+ return defaultCode;
166
+ }
167
+ function structuredEntry(record, message, defaultCode) {
168
+ const origin = selectOrigin(record);
169
+ return {
170
+ code: fallbackCode(record, origin, defaultCode),
171
+ message: truncate(message),
172
+ errorName: typeof record.name === "string" && record.name.trim() ? record.name.trim() : void 0,
173
+ actual: diagnosticValue(record.actual),
174
+ expected: diagnosticValue(record.expected),
175
+ origin,
176
+ layer: origin ? failureLayer(origin.file) : void 0
177
+ };
178
+ }
179
+ function collectEntries(value, fallbackCode2, seen) {
180
+ if (typeof value === "string") {
181
+ return value.trim() ? [{ code: fallbackCode2, message: truncate(value) }] : [];
90
182
  }
91
183
  if (value === null || value === void 0 || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" || typeof value === "symbol") {
92
- return [{ code: fallbackCode, message: String(value) }];
184
+ return [{ code: fallbackCode2, message: String(value) }];
93
185
  }
94
186
  if (typeof value === "function") {
95
187
  return [
96
- { code: fallbackCode, message: `Function<${value.name || "anonymous"}>` }
188
+ { code: fallbackCode2, message: `Function<${value.name || "anonymous"}>` }
97
189
  ];
98
190
  }
99
191
  if (seen.has(value)) return [];
100
192
  seen.add(value);
101
193
  if (Array.isArray(value)) {
102
- return value.flatMap((item) => collectEntries(item, fallbackCode, seen));
194
+ return value.flatMap((item) => collectEntries(item, fallbackCode2, seen));
103
195
  }
104
196
  const record = value;
105
- const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : fallbackCode;
106
197
  const entries = [];
107
198
  if (typeof record.message === "string" && record.message.trim()) {
108
- entries.push({ code, message: truncate(record.message) });
199
+ entries.push(structuredEntry(record, record.message, fallbackCode2));
109
200
  }
110
201
  if (record.cause !== void 0) {
111
- entries.push(...collectEntries(record.cause, fallbackCode, seen));
202
+ entries.push(...collectEntries(record.cause, fallbackCode2, seen));
112
203
  }
113
204
  if (Array.isArray(record.errors)) {
114
- entries.push(...collectEntries(record.errors, fallbackCode, seen));
205
+ entries.push(...collectEntries(record.errors, fallbackCode2, seen));
115
206
  }
116
207
  if (entries.length > 0) return entries;
117
208
  if (typeof record.stack === "string" && record.stack.trim()) {
118
- return [{ code, message: truncate(record.stack) }];
209
+ return [structuredEntry(record, record.stack, fallbackCode2)];
119
210
  }
120
211
  const json = safeJson(value);
121
- return json && json !== "{}" ? [{ code, message: truncate(json) }] : [];
212
+ return json && json !== "{}" ? [structuredEntry(record, json, fallbackCode2)] : [];
122
213
  }
123
- function extractFailureEntries(value, fallbackCode = "TEST_FAILURE") {
124
- const entries = collectEntries(value, fallbackCode, /* @__PURE__ */ new WeakSet());
214
+ function extractFailureEntries(value, fallbackCode2 = "TEST_FAILURE") {
215
+ const entries = collectEntries(value, fallbackCode2, /* @__PURE__ */ new WeakSet());
125
216
  const keys = /* @__PURE__ */ new Set();
126
217
  return entries.filter((entry) => {
127
- const key = `${entry.code}\0${entry.message}`;
218
+ const key = [
219
+ entry.code,
220
+ entry.message,
221
+ entry.errorName,
222
+ entry.actual,
223
+ entry.expected,
224
+ entry.origin ? `${entry.origin.file}:${entry.origin.line}:${entry.origin.column}` : void 0
225
+ ].join("\0");
128
226
  if (keys.has(key)) return false;
129
227
  keys.add(key);
130
228
  return true;
@@ -205,10 +303,10 @@ async function runBoundedUntil(condition, options = {}) {
205
303
  }
206
304
  }
207
305
  const diagnostics = formatDiagnostics(options.diagnostics);
208
- const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, so time-driven gameplay was not advanced. Inject a ManualGameClock for this test and pass step: () => clock.stepFrame().";
306
+ const guidance = options.step ? "The step callback ran, but the authoritative outcome did not change." : "No step callback was provided, and the condition did not become true after the stage action. This does not identify a clock problem; inspect the condition, production input wiring, and observed state boundary.";
209
307
  const suffix = diagnostics ? ` Last diagnostics: ${diagnostics}` : "";
210
308
  throw codedError(
211
- options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_CLOCK_NOT_ADVANCED",
309
+ options.step ? "PLAYTHROUGH_BOUND_EXHAUSTED" : "PLAYTHROUGH_OUTCOME_NOT_REACHED",
212
310
  `Playthrough outcome was not reached within ${maxSteps} steps. ${guidance}${suffix}`
213
311
  );
214
312
  }
@@ -3,6 +3,11 @@ import { ViteUserConfig } from 'vitest/config';
3
3
  interface ReactGameVitestConfigOptions {
4
4
  /** React 模板的绝对根目录,通常传入 `import.meta.dirname`。 */
5
5
  projectRoot: string;
6
+ /**
7
+ * Opt in to the legacy structured playthrough gate. Ordinary React game
8
+ * projects should use independent Vitest scenarios and leave this disabled.
9
+ */
10
+ enablePlaythroughReporter?: boolean;
6
11
  aliases?: Record<string, string>;
7
12
  additionalSetupFiles?: string[];
8
13
  testTimeout?: number;
@@ -3,6 +3,11 @@ import { ViteUserConfig } from 'vitest/config';
3
3
  interface ReactGameVitestConfigOptions {
4
4
  /** React 模板的绝对根目录,通常传入 `import.meta.dirname`。 */
5
5
  projectRoot: string;
6
+ /**
7
+ * Opt in to the legacy structured playthrough gate. Ordinary React game
8
+ * projects should use independent Vitest scenarios and leave this disabled.
9
+ */
10
+ enablePlaythroughReporter?: boolean;
6
11
  aliases?: Record<string, string>;
7
12
  additionalSetupFiles?: string[];
8
13
  testTimeout?: number;