@auraindustry/aurajs 0.0.2 → 0.0.3
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/package.json +13 -1
- package/src/build-contract.mjs +708 -2
- package/src/cli.mjs +1584 -13
- package/src/conformance.mjs +1013 -86
- package/src/game-state-runtime.mjs +1067 -0
- package/src/headless-test.mjs +525 -10
- package/src/host-binary.mjs +33 -10
- package/src/react/aura-game.mjs +310 -0
- package/src/react/index.mjs +1 -0
- package/src/scaffold.mjs +267 -13
- package/src/web-api.mjs +454 -0
- package/templates/create/2d/aura.config.json +28 -0
- package/templates/create/2d/src/main.js +196 -0
- package/templates/create/3d/aura.config.json +28 -0
- package/templates/create/3d/src/main.js +306 -0
- package/templates/create/blank/aura.config.json +28 -0
- package/templates/create/blank/src/main.js +28 -0
- package/templates/create/shared/src/starter-utils/core.js +114 -0
- package/templates/create/shared/src/starter-utils/enemy-archetypes-2d.js +68 -0
- package/templates/create/shared/src/starter-utils/index.js +6 -0
- package/templates/create/shared/src/starter-utils/platformer-3d.js +101 -0
- package/templates/create/shared/src/starter-utils/wave-director.js +101 -0
- package/templates/skills/aurajs/SKILL.md +40 -0
- package/templates/skills/aurajs/api-contract-3d.md +7 -0
- package/templates/skills/aurajs/api-contract.md +7 -0
- package/templates/starter/src/main.js +48 -0
package/src/headless-test.mjs
CHANGED
|
@@ -3,6 +3,12 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import vm from 'node:vm';
|
|
4
4
|
|
|
5
5
|
import { bundleProject } from './bundler.mjs';
|
|
6
|
+
import {
|
|
7
|
+
createGameStateRuntimeHooks,
|
|
8
|
+
diffCanonicalGameState,
|
|
9
|
+
applyCanonicalStatePatch,
|
|
10
|
+
DEFAULT_STATE_MUTATION_GUARDRAILS,
|
|
11
|
+
} from './game-state-runtime.mjs';
|
|
6
12
|
|
|
7
13
|
export class HeadlessTestError extends Error {
|
|
8
14
|
constructor(message, details = {}) {
|
|
@@ -12,6 +18,16 @@ export class HeadlessTestError extends Error {
|
|
|
12
18
|
}
|
|
13
19
|
}
|
|
14
20
|
|
|
21
|
+
function createHeadlessTestState() {
|
|
22
|
+
return {
|
|
23
|
+
failures: [],
|
|
24
|
+
passes: 0,
|
|
25
|
+
drawCalls: 0,
|
|
26
|
+
audioCalls: 0,
|
|
27
|
+
logs: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
15
31
|
export function parseTestArgs(args) {
|
|
16
32
|
const parsed = {
|
|
17
33
|
file: 'src/test.js',
|
|
@@ -71,13 +87,7 @@ export async function runHeadlessTest(options = {}) {
|
|
|
71
87
|
outFile: resolve(projectRoot, '.aura/test/test.bundle.js'),
|
|
72
88
|
});
|
|
73
89
|
|
|
74
|
-
const testState =
|
|
75
|
-
failures: [],
|
|
76
|
-
passes: 0,
|
|
77
|
-
drawCalls: 0,
|
|
78
|
-
audioCalls: 0,
|
|
79
|
-
logs: [],
|
|
80
|
-
};
|
|
90
|
+
const testState = createHeadlessTestState();
|
|
81
91
|
|
|
82
92
|
const aura = createHeadlessAura({ width, height, testState });
|
|
83
93
|
const context = vm.createContext(createRuntimeContext(aura, testState));
|
|
@@ -116,6 +126,482 @@ export async function runHeadlessTest(options = {}) {
|
|
|
116
126
|
};
|
|
117
127
|
}
|
|
118
128
|
|
|
129
|
+
export async function runHeadlessStateExport(options = {}) {
|
|
130
|
+
const projectRoot = resolve(options.projectRoot || process.cwd());
|
|
131
|
+
const entryFile = resolve(projectRoot, options.file || 'src/main.js');
|
|
132
|
+
const width = options.width ?? 1280;
|
|
133
|
+
const height = options.height ?? 720;
|
|
134
|
+
const frames = options.frames ?? 1;
|
|
135
|
+
|
|
136
|
+
if (!existsSync(entryFile)) {
|
|
137
|
+
throw new HeadlessTestError(`State export entry file not found: ${entryFile}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const bundle = bundleProject({
|
|
141
|
+
projectRoot,
|
|
142
|
+
mode: 'test',
|
|
143
|
+
entryFile,
|
|
144
|
+
outFile: resolve(projectRoot, '.aura/test/state-export.bundle.js'),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const testState = createHeadlessTestState();
|
|
148
|
+
const aura = createHeadlessAura({ width, height, testState });
|
|
149
|
+
const context = vm.createContext(createRuntimeContext(aura, testState));
|
|
150
|
+
|
|
151
|
+
const source = readFileSync(bundle.outFile, 'utf8');
|
|
152
|
+
const script = new vm.Script(source, {
|
|
153
|
+
filename: bundle.outFile,
|
|
154
|
+
displayErrors: true,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
script.runInContext(context, { timeout: 5000 });
|
|
159
|
+
await executeLifecycle(aura, frames);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw new HeadlessTestError(`Headless state export execution failed: ${error.message}`, {
|
|
162
|
+
stack: error.stack,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!aura.state || typeof aura.state !== 'object' || typeof aura.state.export !== 'function') {
|
|
167
|
+
throw new HeadlessTestError('Headless state export failed: aura.state.export hook is unavailable.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let exportResult;
|
|
171
|
+
try {
|
|
172
|
+
exportResult = aura.state.export({
|
|
173
|
+
mode: options.mode,
|
|
174
|
+
seed: options.seed,
|
|
175
|
+
frameIndex: options.frameIndex ?? frames,
|
|
176
|
+
elapsedSeconds: options.elapsedSeconds,
|
|
177
|
+
capturedAt: options.capturedAt ?? null,
|
|
178
|
+
});
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw new HeadlessTestError(`Headless state export failed: ${error.message}`, {
|
|
181
|
+
stack: error.stack,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!exportResult || typeof exportResult !== 'object') {
|
|
186
|
+
throw new HeadlessTestError('Headless state export failed: runtime returned an invalid export result.');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
entryFile,
|
|
192
|
+
bundle,
|
|
193
|
+
frames,
|
|
194
|
+
width,
|
|
195
|
+
height,
|
|
196
|
+
exportResult,
|
|
197
|
+
passes: testState.passes,
|
|
198
|
+
drawCalls: testState.drawCalls,
|
|
199
|
+
audioCalls: testState.audioCalls,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function runHeadlessStateApply(options = {}) {
|
|
204
|
+
const projectRoot = resolve(options.projectRoot || process.cwd());
|
|
205
|
+
const entryFile = resolve(projectRoot, options.file || 'src/main.js');
|
|
206
|
+
const width = options.width ?? 1280;
|
|
207
|
+
const height = options.height ?? 720;
|
|
208
|
+
const frames = options.frames ?? 1;
|
|
209
|
+
const targetPayload = options.payload;
|
|
210
|
+
const dryRun = options.dryRun === true;
|
|
211
|
+
const verify = options.verify === true;
|
|
212
|
+
const rollbackOnFail = options.rollbackOnFail === true;
|
|
213
|
+
const guardrails = normalizeStateMutationGuardrails(options.guardrails);
|
|
214
|
+
const startedAtMs = Date.now();
|
|
215
|
+
const isTimedOut = () => (Date.now() - startedAtMs) >= guardrails.maxRuntimeMs;
|
|
216
|
+
|
|
217
|
+
if (!existsSync(entryFile)) {
|
|
218
|
+
throw new HeadlessTestError(`State apply entry file not found: ${entryFile}`);
|
|
219
|
+
}
|
|
220
|
+
if (!targetPayload || typeof targetPayload !== 'object' || Array.isArray(targetPayload)) {
|
|
221
|
+
throw new HeadlessTestError('Headless state apply failed: payload must be an object.');
|
|
222
|
+
}
|
|
223
|
+
const payloadBytes = jsonByteSize(targetPayload);
|
|
224
|
+
if (payloadBytes > guardrails.maxPayloadBytes) {
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
reasonCode: 'state_payload_too_large',
|
|
228
|
+
detail: `payload bytes exceed maxPayloadBytes=${guardrails.maxPayloadBytes}`,
|
|
229
|
+
dryRun,
|
|
230
|
+
verify,
|
|
231
|
+
rollbackOnFail,
|
|
232
|
+
payloadBytes,
|
|
233
|
+
maxPayloadBytes: guardrails.maxPayloadBytes,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const bundle = bundleProject({
|
|
238
|
+
projectRoot,
|
|
239
|
+
mode: 'test',
|
|
240
|
+
entryFile,
|
|
241
|
+
outFile: resolve(projectRoot, '.aura/test/state-apply.bundle.js'),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const testState = createHeadlessTestState();
|
|
245
|
+
const aura = createHeadlessAura({ width, height, testState });
|
|
246
|
+
const context = vm.createContext(createRuntimeContext(aura, testState));
|
|
247
|
+
|
|
248
|
+
const source = readFileSync(bundle.outFile, 'utf8');
|
|
249
|
+
const script = new vm.Script(source, {
|
|
250
|
+
filename: bundle.outFile,
|
|
251
|
+
displayErrors: true,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
script.runInContext(context, { timeout: 5000 });
|
|
256
|
+
await executeLifecycle(aura, frames);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw new HeadlessTestError(`Headless state apply execution failed: ${error.message}`, {
|
|
259
|
+
stack: error.stack,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (isTimedOut()) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
reasonCode: 'state_apply_timeout',
|
|
266
|
+
detail: `state apply exceeded maxRuntimeMs=${guardrails.maxRuntimeMs}`,
|
|
267
|
+
dryRun,
|
|
268
|
+
verify,
|
|
269
|
+
rollbackOnFail,
|
|
270
|
+
maxRuntimeMs: guardrails.maxRuntimeMs,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!aura.state || typeof aura.state !== 'object') {
|
|
275
|
+
throw new HeadlessTestError('Headless state apply failed: aura.state surface is unavailable.');
|
|
276
|
+
}
|
|
277
|
+
if (typeof aura.state.export !== 'function') {
|
|
278
|
+
throw new HeadlessTestError('Headless state apply failed: aura.state.export hook is unavailable.');
|
|
279
|
+
}
|
|
280
|
+
if (typeof aura.state.apply !== 'function') {
|
|
281
|
+
throw new HeadlessTestError('Headless state apply failed: aura.state.apply hook is unavailable.');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const captureExport = () => {
|
|
285
|
+
const result = aura.state.export({
|
|
286
|
+
mode: options.mode,
|
|
287
|
+
seed: options.seed,
|
|
288
|
+
frameIndex: options.frameIndex ?? frames,
|
|
289
|
+
elapsedSeconds: options.elapsedSeconds,
|
|
290
|
+
capturedAt: options.capturedAt ?? null,
|
|
291
|
+
});
|
|
292
|
+
if (!result || typeof result !== 'object' || result.ok !== true || !result.payload) {
|
|
293
|
+
throw new HeadlessTestError('Headless state apply failed: aura.state.export returned invalid result.');
|
|
294
|
+
}
|
|
295
|
+
return result.payload;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const baselinePayload = captureExport();
|
|
299
|
+
const baselineFingerprint = baselinePayload.export?.fingerprint || null;
|
|
300
|
+
const preview = diffCanonicalGameState(baselinePayload, targetPayload);
|
|
301
|
+
if (!preview.ok) {
|
|
302
|
+
return {
|
|
303
|
+
ok: false,
|
|
304
|
+
reasonCode: preview.reasonCode || (dryRun ? 'state_dry_run_failed' : 'state_apply_failed'),
|
|
305
|
+
detail: preview.detail || null,
|
|
306
|
+
dryRun,
|
|
307
|
+
verify,
|
|
308
|
+
rollbackOnFail,
|
|
309
|
+
baselinePayload,
|
|
310
|
+
preview,
|
|
311
|
+
baselineFingerprint,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
if (preview.patch.mutations.length > guardrails.maxMutations) {
|
|
315
|
+
return {
|
|
316
|
+
ok: false,
|
|
317
|
+
reasonCode: 'mutation_budget_exceeded',
|
|
318
|
+
detail: `mutation count ${preview.patch.mutations.length} exceeds maxMutations=${guardrails.maxMutations}`,
|
|
319
|
+
dryRun,
|
|
320
|
+
verify,
|
|
321
|
+
rollbackOnFail,
|
|
322
|
+
mutationCount: preview.patch.mutations.length,
|
|
323
|
+
maxMutations: guardrails.maxMutations,
|
|
324
|
+
baselineFingerprint,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const guardPreviewApply = applyCanonicalStatePatch(baselinePayload, preview.patch, {
|
|
328
|
+
maxMutations: guardrails.maxMutations,
|
|
329
|
+
maxPayloadBytes: guardrails.maxPayloadBytes,
|
|
330
|
+
maxRuntimeMs: guardrails.maxRuntimeMs,
|
|
331
|
+
allowlistPrefixes: guardrails.allowlistPrefixes,
|
|
332
|
+
});
|
|
333
|
+
if (!guardPreviewApply.ok) {
|
|
334
|
+
return {
|
|
335
|
+
ok: false,
|
|
336
|
+
reasonCode: guardPreviewApply.reasonCode || 'state_apply_failed',
|
|
337
|
+
detail: guardPreviewApply.detail || null,
|
|
338
|
+
dryRun,
|
|
339
|
+
verify,
|
|
340
|
+
rollbackOnFail,
|
|
341
|
+
mutationCount: preview.patch.mutations.length,
|
|
342
|
+
baselineFingerprint,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (isTimedOut()) {
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
reasonCode: 'state_apply_timeout',
|
|
349
|
+
detail: `state apply exceeded maxRuntimeMs=${guardrails.maxRuntimeMs}`,
|
|
350
|
+
dryRun,
|
|
351
|
+
verify,
|
|
352
|
+
rollbackOnFail,
|
|
353
|
+
mutationCount: preview.patch.mutations.length,
|
|
354
|
+
maxRuntimeMs: guardrails.maxRuntimeMs,
|
|
355
|
+
baselineFingerprint,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (dryRun) {
|
|
360
|
+
const afterDryRunPayload = captureExport();
|
|
361
|
+
const unchanged = diffCanonicalGameState(baselinePayload, afterDryRunPayload);
|
|
362
|
+
if (!unchanged.ok || unchanged.patch?.mutations?.length !== 0) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
reasonCode: 'state_dry_run_mutated',
|
|
366
|
+
detail: 'Dry-run preview mutated runtime state.',
|
|
367
|
+
dryRun: true,
|
|
368
|
+
verify,
|
|
369
|
+
rollbackOnFail,
|
|
370
|
+
baselinePayload,
|
|
371
|
+
preview,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
reasonCode: 'state_dry_run_ok',
|
|
378
|
+
dryRun: true,
|
|
379
|
+
verify,
|
|
380
|
+
rollbackOnFail,
|
|
381
|
+
applied: false,
|
|
382
|
+
verified: false,
|
|
383
|
+
rolledBack: false,
|
|
384
|
+
mutationCount: preview.patch.mutations.length,
|
|
385
|
+
previewPatch: preview.patch,
|
|
386
|
+
baselineFingerprint,
|
|
387
|
+
finalFingerprint: afterDryRunPayload.export?.fingerprint || null,
|
|
388
|
+
payloadBytes,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (isTimedOut()) {
|
|
393
|
+
return {
|
|
394
|
+
ok: false,
|
|
395
|
+
reasonCode: 'state_apply_timeout',
|
|
396
|
+
detail: `state apply exceeded maxRuntimeMs=${guardrails.maxRuntimeMs}`,
|
|
397
|
+
dryRun: false,
|
|
398
|
+
verify,
|
|
399
|
+
rollbackOnFail,
|
|
400
|
+
mutationCount: preview.patch.mutations.length,
|
|
401
|
+
maxRuntimeMs: guardrails.maxRuntimeMs,
|
|
402
|
+
baselineFingerprint,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const applyResult = aura.state.apply(targetPayload, {
|
|
407
|
+
dryRun,
|
|
408
|
+
verify,
|
|
409
|
+
rollbackOnFail,
|
|
410
|
+
});
|
|
411
|
+
if (!applyResult || typeof applyResult !== 'object') {
|
|
412
|
+
return {
|
|
413
|
+
ok: false,
|
|
414
|
+
reasonCode: 'state_apply_runtime_failed',
|
|
415
|
+
detail: 'aura.state.apply returned invalid result',
|
|
416
|
+
dryRun: false,
|
|
417
|
+
verify,
|
|
418
|
+
rollbackOnFail,
|
|
419
|
+
baselineFingerprint,
|
|
420
|
+
mutationCount: preview.patch.mutations.length,
|
|
421
|
+
payloadBytes,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const rollbackBaseline = () => {
|
|
426
|
+
try {
|
|
427
|
+
const rollback = aura.state.apply(baselinePayload, {
|
|
428
|
+
dryRun: false,
|
|
429
|
+
verify: false,
|
|
430
|
+
rollbackOnFail: false,
|
|
431
|
+
});
|
|
432
|
+
return rollback && typeof rollback === 'object' ? rollback : { ok: false, reasonCode: 'rollback_failed' };
|
|
433
|
+
} catch (error) {
|
|
434
|
+
return {
|
|
435
|
+
ok: false,
|
|
436
|
+
reasonCode: 'rollback_failed',
|
|
437
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
if (applyResult.ok !== true) {
|
|
443
|
+
if (!rollbackOnFail) {
|
|
444
|
+
return {
|
|
445
|
+
ok: false,
|
|
446
|
+
reasonCode: applyResult.reasonCode || 'state_apply_failed',
|
|
447
|
+
detail: applyResult.detail || null,
|
|
448
|
+
dryRun: false,
|
|
449
|
+
verify,
|
|
450
|
+
rollbackOnFail,
|
|
451
|
+
applied: false,
|
|
452
|
+
verified: false,
|
|
453
|
+
rolledBack: false,
|
|
454
|
+
baselineFingerprint,
|
|
455
|
+
mutationCount: preview.patch.mutations.length,
|
|
456
|
+
payloadBytes,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const rollback = rollbackBaseline();
|
|
461
|
+
return {
|
|
462
|
+
ok: false,
|
|
463
|
+
reasonCode: rollback.ok === true
|
|
464
|
+
? (applyResult.reasonCode || 'state_apply_failed')
|
|
465
|
+
: (rollback.reasonCode || 'rollback_failed'),
|
|
466
|
+
detail: applyResult.detail || rollback.detail || null,
|
|
467
|
+
dryRun: false,
|
|
468
|
+
verify,
|
|
469
|
+
rollbackOnFail,
|
|
470
|
+
applied: false,
|
|
471
|
+
verified: false,
|
|
472
|
+
rolledBack: rollback.ok === true,
|
|
473
|
+
rollbackReasonCode: rollback.reasonCode || null,
|
|
474
|
+
baselineFingerprint,
|
|
475
|
+
mutationCount: preview.patch.mutations.length,
|
|
476
|
+
payloadBytes,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const afterApplyPayload = captureExport();
|
|
481
|
+
if (isTimedOut()) {
|
|
482
|
+
return {
|
|
483
|
+
ok: false,
|
|
484
|
+
reasonCode: 'state_apply_timeout',
|
|
485
|
+
detail: `state apply exceeded maxRuntimeMs=${guardrails.maxRuntimeMs}`,
|
|
486
|
+
dryRun: false,
|
|
487
|
+
verify,
|
|
488
|
+
rollbackOnFail,
|
|
489
|
+
applied: true,
|
|
490
|
+
verified: false,
|
|
491
|
+
rolledBack: false,
|
|
492
|
+
mutationCount: preview.patch.mutations.length,
|
|
493
|
+
maxRuntimeMs: guardrails.maxRuntimeMs,
|
|
494
|
+
baselineFingerprint,
|
|
495
|
+
finalFingerprint: afterApplyPayload.export?.fingerprint || null,
|
|
496
|
+
payloadBytes,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
let verified = false;
|
|
500
|
+
if (verify) {
|
|
501
|
+
const verifyDiff = diffCanonicalGameState(targetPayload, afterApplyPayload);
|
|
502
|
+
verified = verifyDiff.ok === true && verifyDiff.patch?.mutations?.length === 0;
|
|
503
|
+
if (!verified) {
|
|
504
|
+
if (!rollbackOnFail) {
|
|
505
|
+
return {
|
|
506
|
+
ok: false,
|
|
507
|
+
reasonCode: 'verify_failed',
|
|
508
|
+
detail: 'Applied state does not match requested payload.',
|
|
509
|
+
dryRun: false,
|
|
510
|
+
verify,
|
|
511
|
+
rollbackOnFail,
|
|
512
|
+
applied: true,
|
|
513
|
+
verified: false,
|
|
514
|
+
rolledBack: false,
|
|
515
|
+
baselineFingerprint,
|
|
516
|
+
finalFingerprint: afterApplyPayload.export?.fingerprint || null,
|
|
517
|
+
mutationCount: preview.patch.mutations.length,
|
|
518
|
+
payloadBytes,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const rollback = rollbackBaseline();
|
|
523
|
+
return {
|
|
524
|
+
ok: false,
|
|
525
|
+
reasonCode: rollback.ok === true ? 'verify_failed' : (rollback.reasonCode || 'rollback_failed'),
|
|
526
|
+
detail: 'Applied state does not match requested payload.',
|
|
527
|
+
dryRun: false,
|
|
528
|
+
verify,
|
|
529
|
+
rollbackOnFail,
|
|
530
|
+
applied: true,
|
|
531
|
+
verified: false,
|
|
532
|
+
rolledBack: rollback.ok === true,
|
|
533
|
+
rollbackReasonCode: rollback.reasonCode || null,
|
|
534
|
+
baselineFingerprint,
|
|
535
|
+
finalFingerprint: afterApplyPayload.export?.fingerprint || null,
|
|
536
|
+
mutationCount: preview.patch.mutations.length,
|
|
537
|
+
payloadBytes,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return {
|
|
543
|
+
ok: true,
|
|
544
|
+
reasonCode: 'state_apply_ok',
|
|
545
|
+
dryRun: false,
|
|
546
|
+
verify,
|
|
547
|
+
rollbackOnFail,
|
|
548
|
+
applied: true,
|
|
549
|
+
verified,
|
|
550
|
+
rolledBack: false,
|
|
551
|
+
mutationCount: preview.patch.mutations.length,
|
|
552
|
+
baselineFingerprint,
|
|
553
|
+
finalFingerprint: afterApplyPayload.export?.fingerprint || null,
|
|
554
|
+
payloadBytes,
|
|
555
|
+
applyResult,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function normalizeStateMutationGuardrails(value) {
|
|
560
|
+
const source = value && typeof value === 'object' ? value : {};
|
|
561
|
+
const maxMutations = normalizeNonNegativeInteger(
|
|
562
|
+
source.maxMutations,
|
|
563
|
+
DEFAULT_STATE_MUTATION_GUARDRAILS.maxMutations,
|
|
564
|
+
);
|
|
565
|
+
const maxPayloadBytes = normalizeNonNegativeInteger(
|
|
566
|
+
source.maxPayloadBytes,
|
|
567
|
+
DEFAULT_STATE_MUTATION_GUARDRAILS.maxPayloadBytes,
|
|
568
|
+
);
|
|
569
|
+
const maxRuntimeMs = normalizeNonNegativeInteger(
|
|
570
|
+
source.maxRuntimeMs,
|
|
571
|
+
DEFAULT_STATE_MUTATION_GUARDRAILS.maxRuntimeMs,
|
|
572
|
+
);
|
|
573
|
+
const allowlistPrefixes = Array.isArray(source.allowlistPrefixes) && source.allowlistPrefixes.length > 0
|
|
574
|
+
? source.allowlistPrefixes
|
|
575
|
+
.filter((entry) => typeof entry === 'string' && entry.trim().startsWith('/'))
|
|
576
|
+
.map((entry) => entry.trim())
|
|
577
|
+
: [...DEFAULT_STATE_MUTATION_GUARDRAILS.allowlistPrefixes];
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
maxMutations,
|
|
581
|
+
maxPayloadBytes,
|
|
582
|
+
maxRuntimeMs,
|
|
583
|
+
allowlistPrefixes: [...new Set(allowlistPrefixes)].sort((a, b) => a.localeCompare(b)),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function normalizeNonNegativeInteger(value, fallback) {
|
|
588
|
+
const parsed = Number(value);
|
|
589
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
590
|
+
return fallback;
|
|
591
|
+
}
|
|
592
|
+
return parsed;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function jsonByteSize(value) {
|
|
596
|
+
let text = '';
|
|
597
|
+
try {
|
|
598
|
+
text = JSON.stringify(value);
|
|
599
|
+
} catch {
|
|
600
|
+
text = '';
|
|
601
|
+
}
|
|
602
|
+
return Buffer.byteLength(text || '', 'utf8');
|
|
603
|
+
}
|
|
604
|
+
|
|
119
605
|
function createRuntimeContext(aura, testState) {
|
|
120
606
|
const consoleShim = {
|
|
121
607
|
log: (...args) => {
|
|
@@ -149,6 +635,7 @@ function createHeadlessAura({ width, height, testState }) {
|
|
|
149
635
|
const drawNoop = () => {
|
|
150
636
|
testState.drawCalls += 1;
|
|
151
637
|
};
|
|
638
|
+
const stateNoop = () => {};
|
|
152
639
|
|
|
153
640
|
const makeVec2 = (x = 0, y = 0) => ({ x: Number(x), y: Number(y) });
|
|
154
641
|
const vec2 = function vec2(x = 0, y = 0) {
|
|
@@ -217,6 +704,14 @@ function createHeadlessAura({ width, height, testState }) {
|
|
|
217
704
|
? value
|
|
218
705
|
: null
|
|
219
706
|
);
|
|
707
|
+
const parseRectFromComponents = (x, y, w, h) => (
|
|
708
|
+
isFiniteNumber(x)
|
|
709
|
+
&& isFiniteNumber(y)
|
|
710
|
+
&& isFiniteNumber(w)
|
|
711
|
+
&& isFiniteNumber(h)
|
|
712
|
+
? { x, y, w, h }
|
|
713
|
+
: null
|
|
714
|
+
);
|
|
220
715
|
const parsePoint = (value) => (
|
|
221
716
|
value
|
|
222
717
|
&& isFiniteNumber(value.x)
|
|
@@ -249,9 +744,16 @@ function createHeadlessAura({ width, height, testState }) {
|
|
|
249
744
|
};
|
|
250
745
|
|
|
251
746
|
const collision = {
|
|
252
|
-
rectRect: (
|
|
253
|
-
|
|
254
|
-
|
|
747
|
+
rectRect: (...args) => {
|
|
748
|
+
let lhs = null;
|
|
749
|
+
let rhs = null;
|
|
750
|
+
if (args.length >= 2 && typeof args[0] === 'object' && typeof args[1] === 'object') {
|
|
751
|
+
lhs = parseRect(args[0]);
|
|
752
|
+
rhs = parseRect(args[1]);
|
|
753
|
+
} else if (args.length >= 8) {
|
|
754
|
+
lhs = parseRectFromComponents(args[0], args[1], args[2], args[3]);
|
|
755
|
+
rhs = parseRectFromComponents(args[4], args[5], args[6], args[7]);
|
|
756
|
+
}
|
|
255
757
|
return !!lhs && !!rhs && collisionRectRect(lhs, rhs);
|
|
256
758
|
},
|
|
257
759
|
rectPoint: (rect, point) => {
|
|
@@ -3818,7 +4320,14 @@ function createHeadlessAura({ width, height, testState }) {
|
|
|
3818
4320
|
},
|
|
3819
4321
|
|
|
3820
4322
|
draw3d: {
|
|
4323
|
+
drawMesh: drawNoop,
|
|
4324
|
+
clear3d: drawNoop,
|
|
4325
|
+
drawSkybox: drawNoop,
|
|
4326
|
+
billboard: drawNoop,
|
|
3821
4327
|
mesh: drawNoop,
|
|
4328
|
+
setEnvironmentMap: stateNoop,
|
|
4329
|
+
setFog: stateNoop,
|
|
4330
|
+
clearFog: stateNoop,
|
|
3822
4331
|
},
|
|
3823
4332
|
|
|
3824
4333
|
material: {
|
|
@@ -4269,6 +4778,12 @@ function createHeadlessAura({ width, height, testState }) {
|
|
|
4269
4778
|
vec3,
|
|
4270
4779
|
};
|
|
4271
4780
|
|
|
4781
|
+
const gameStateHooks = createGameStateRuntimeHooks({ aura, mode: 'headless' });
|
|
4782
|
+
aura.state = {
|
|
4783
|
+
export: (options = {}) => gameStateHooks.exportState(options),
|
|
4784
|
+
apply: (payload, options = {}) => gameStateHooks.applyState(payload, options),
|
|
4785
|
+
};
|
|
4786
|
+
|
|
4272
4787
|
return aura;
|
|
4273
4788
|
}
|
|
4274
4789
|
|
package/src/host-binary.mjs
CHANGED
|
@@ -314,8 +314,16 @@ function findInstalledPackageRoot(packageName, { requireResolve, readPackageJson
|
|
|
314
314
|
|
|
315
315
|
function resolveLocalHostBinaryCandidates(searchRoot, binaryName) {
|
|
316
316
|
return [
|
|
317
|
-
|
|
318
|
-
|
|
317
|
+
{
|
|
318
|
+
kind: 'release',
|
|
319
|
+
path: resolve(searchRoot, 'src', 'rust-host', 'target', 'release', binaryName),
|
|
320
|
+
reasonCode: 'host_binary_local_release_selected',
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
kind: 'debug',
|
|
324
|
+
path: resolve(searchRoot, 'src', 'rust-host', 'target', 'debug', binaryName),
|
|
325
|
+
reasonCode: 'host_binary_local_debug_fallback',
|
|
326
|
+
},
|
|
319
327
|
];
|
|
320
328
|
}
|
|
321
329
|
|
|
@@ -356,23 +364,35 @@ export function resolveGateHostBinary({
|
|
|
356
364
|
const binaryName = basename(spec.binaryRelativePath);
|
|
357
365
|
const repoRoot = findRepoRootWithRustHost(searchFrom);
|
|
358
366
|
const localCandidates = repoRoot ? resolveLocalHostBinaryCandidates(repoRoot, binaryName) : [];
|
|
367
|
+
const checkedCandidates = localCandidates.map((candidate) => candidate.path);
|
|
359
368
|
|
|
360
369
|
for (const candidate of localCandidates) {
|
|
361
|
-
if (!existsSync(candidate)) continue;
|
|
362
|
-
if (!canExecuteFile(candidate)) {
|
|
363
|
-
diagnostics.push(`Detected local host binary but it is not executable: ${candidate}`);
|
|
370
|
+
if (!existsSync(candidate.path)) continue;
|
|
371
|
+
if (!canExecuteFile(candidate.path)) {
|
|
372
|
+
diagnostics.push(`[host_binary_local_non_executable] Detected local ${candidate.kind} host binary but it is not executable: ${candidate.path}`);
|
|
364
373
|
continue;
|
|
365
374
|
}
|
|
375
|
+
if (candidate.kind === 'debug') {
|
|
376
|
+
diagnostics.push(
|
|
377
|
+
`[host_binary_local_debug_fallback] Selected local debug host because release host is unavailable or not executable: ${candidate.path}`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
366
380
|
|
|
367
381
|
return {
|
|
368
382
|
...spec,
|
|
369
|
-
binaryPath: candidate,
|
|
370
|
-
packageRoot: dirname(candidate),
|
|
383
|
+
binaryPath: candidate.path,
|
|
384
|
+
packageRoot: dirname(candidate.path),
|
|
371
385
|
source: 'local-build',
|
|
386
|
+
reasonCode: candidate.reasonCode,
|
|
372
387
|
diagnostics,
|
|
373
388
|
localBuild: {
|
|
374
389
|
repoRoot,
|
|
375
|
-
checked:
|
|
390
|
+
checked: checkedCandidates,
|
|
391
|
+
selected: {
|
|
392
|
+
kind: candidate.kind,
|
|
393
|
+
path: candidate.path,
|
|
394
|
+
reasonCode: candidate.reasonCode,
|
|
395
|
+
},
|
|
376
396
|
},
|
|
377
397
|
resolvedAt: new Date().toISOString(),
|
|
378
398
|
};
|
|
@@ -385,13 +405,15 @@ export function resolveGateHostBinary({
|
|
|
385
405
|
|
|
386
406
|
try {
|
|
387
407
|
const cached = resolveCachedBinary({ platform, arch, ...(cacheRoot ? { cacheRoot } : {}) });
|
|
408
|
+
const cachedReasonCode = cached.override ? 'host_binary_override_selected' : 'host_binary_cache_selected';
|
|
388
409
|
return {
|
|
389
410
|
...cached,
|
|
390
411
|
source: cached.override ? 'override' : 'cache',
|
|
412
|
+
reasonCode: cachedReasonCode,
|
|
391
413
|
diagnostics: [...diagnostics, ...(cached.diagnostics || [])],
|
|
392
414
|
localBuild: {
|
|
393
415
|
repoRoot,
|
|
394
|
-
checked:
|
|
416
|
+
checked: checkedCandidates,
|
|
395
417
|
},
|
|
396
418
|
resolvedAt: new Date().toISOString(),
|
|
397
419
|
};
|
|
@@ -403,10 +425,11 @@ export function resolveGateHostBinary({
|
|
|
403
425
|
return {
|
|
404
426
|
...packaged,
|
|
405
427
|
source: 'package',
|
|
428
|
+
reasonCode: 'host_binary_package_selected',
|
|
406
429
|
diagnostics,
|
|
407
430
|
localBuild: {
|
|
408
431
|
repoRoot,
|
|
409
|
-
checked:
|
|
432
|
+
checked: checkedCandidates,
|
|
410
433
|
},
|
|
411
434
|
resolvedAt: new Date().toISOString(),
|
|
412
435
|
};
|