@forgeax/engine-import 0.1.2

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.
Files changed (75) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +226 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/decode-image-owner.test-d.d.ts +2 -0
  5. package/dist/__tests__/decode-image-owner.test-d.d.ts.map +1 -0
  6. package/dist/__tests__/import-contract-migration.test.d.ts +2 -0
  7. package/dist/__tests__/import-contract-migration.test.d.ts.map +1 -0
  8. package/dist/__tests__/import-dependencies.test.d.ts +2 -0
  9. package/dist/__tests__/import-dependencies.test.d.ts.map +1 -0
  10. package/dist/__tests__/import-finalizer-digest-retry.test.d.ts +2 -0
  11. package/dist/__tests__/import-finalizer-digest-retry.test.d.ts.map +1 -0
  12. package/dist/__tests__/import-local-artifacts.test.d.ts +2 -0
  13. package/dist/__tests__/import-local-artifacts.test.d.ts.map +1 -0
  14. package/dist/__tests__/import.unit.test.d.ts +2 -0
  15. package/dist/__tests__/import.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/material-import-product.unit.test.d.ts +2 -0
  17. package/dist/__tests__/material-import-product.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/mesh-bin-contract.test.d.ts +2 -0
  19. package/dist/__tests__/mesh-bin-contract.test.d.ts.map +1 -0
  20. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  21. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  22. package/dist/__tests__/mesh-bin.test.d.ts +2 -0
  23. package/dist/__tests__/mesh-bin.test.d.ts.map +1 -0
  24. package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts +2 -0
  25. package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts +2 -0
  27. package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts +2 -0
  29. package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/scriptable-pack.integration.test.d.ts +2 -0
  31. package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +1 -0
  32. package/dist/__tests__/source-overrides-import-context.unit.test.d.ts +2 -0
  33. package/dist/__tests__/source-overrides-import-context.unit.test.d.ts.map +1 -0
  34. package/dist/import-product.d.ts +36 -0
  35. package/dist/import-product.d.ts.map +1 -0
  36. package/dist/import-runner.d.ts +133 -0
  37. package/dist/import-runner.d.ts.map +1 -0
  38. package/dist/importer-registry.d.ts +40 -0
  39. package/dist/importer-registry.d.ts.map +1 -0
  40. package/dist/index.d.ts +10 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.mjs +1607 -0
  43. package/dist/index.mjs.map +1 -0
  44. package/dist/mesh-bin.d.ts +19 -0
  45. package/dist/mesh-bin.d.ts.map +1 -0
  46. package/dist/scriptable-pack-output-producers.d.ts +6 -0
  47. package/dist/scriptable-pack-output-producers.d.ts.map +1 -0
  48. package/dist/scriptable-pack-staged-snapshot.d.ts +22 -0
  49. package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -0
  50. package/dist/scriptable-pack.d.ts +153 -0
  51. package/dist/scriptable-pack.d.ts.map +1 -0
  52. package/package.json +63 -0
  53. package/src/__tests__/decode-image-owner.test-d.ts +53 -0
  54. package/src/__tests__/import-contract-migration.test.ts +190 -0
  55. package/src/__tests__/import-dependencies.test.ts +65 -0
  56. package/src/__tests__/import-finalizer-digest-retry.test.ts +113 -0
  57. package/src/__tests__/import-local-artifacts.test.ts +100 -0
  58. package/src/__tests__/import.unit.test.ts +429 -0
  59. package/src/__tests__/material-import-product.unit.test.ts +56 -0
  60. package/src/__tests__/mesh-bin-contract.test.ts +95 -0
  61. package/src/__tests__/mesh-bin-morph-v3.test.ts +44 -0
  62. package/src/__tests__/mesh-bin.test.ts +102 -0
  63. package/src/__tests__/scriptable-pack-output-producers.unit.test.ts +190 -0
  64. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +429 -0
  65. package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +258 -0
  66. package/src/__tests__/scriptable-pack.integration.test.ts +435 -0
  67. package/src/__tests__/source-overrides-import-context.unit.test.ts +92 -0
  68. package/src/import-product.ts +195 -0
  69. package/src/import-runner.ts +720 -0
  70. package/src/importer-registry.ts +80 -0
  71. package/src/index.ts +103 -0
  72. package/src/mesh-bin.ts +229 -0
  73. package/src/scriptable-pack-output-producers.ts +466 -0
  74. package/src/scriptable-pack-staged-snapshot.ts +161 -0
  75. package/src/scriptable-pack.ts +615 -0
@@ -0,0 +1,435 @@
1
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
2
+ import type {
3
+ ScriptablePackAssetDeclarations,
4
+ ScriptablePackDefinition,
5
+ } from '@forgeax/engine-pack/source';
6
+ import type { Asset, AssetGuid as AssetGuidType, MeshAsset } from '@forgeax/engine-types';
7
+ import { AssetError, err, ok } from '@forgeax/engine-types';
8
+ import { describe, expect, it } from 'vitest';
9
+ import {
10
+ type AssetOutputPayload,
11
+ AssetOutputProducerRegistry,
12
+ buildScriptablePack,
13
+ type ScriptablePackAssetSnapshotSource,
14
+ } from '../scriptable-pack.js';
15
+
16
+ function fixturePayload(asset: Asset): AssetOutputPayload {
17
+ switch (asset.kind) {
18
+ case 'material':
19
+ return { kind: 'material', values: {} };
20
+ case 'scene':
21
+ return { kind: 'scene', entities: [] };
22
+ case 'font':
23
+ return {
24
+ kind: 'font',
25
+ glyphs: asset.glyphs,
26
+ common: asset.common,
27
+ atlasGuid: AssetGuid.format(asset.atlas),
28
+ samplerGuid: AssetGuid.format(asset.sampler),
29
+ };
30
+ case 'tileset':
31
+ return { ...asset, atlases: asset.atlases.map((_atlas, index) => index) };
32
+ case 'animation-graph':
33
+ return {
34
+ kind: 'animation-graph',
35
+ root: asset.root,
36
+ nodes: asset.nodes.map((node) => (node.type === 'clip' ? { ...node, clip: 0 } : node)),
37
+ };
38
+ default:
39
+ return asset;
40
+ }
41
+ }
42
+
43
+ function guid(value: string): AssetGuidType {
44
+ const parsed = AssetGuid.parse(value);
45
+ if (!parsed.ok) throw parsed.error;
46
+ return parsed.value;
47
+ }
48
+
49
+ const REFERENCE = guid('019ffa97-0000-7000-8000-000000000001');
50
+ const CONTENT = guid('019ffa97-0000-7000-8000-000000000002');
51
+ const BOTH = guid('019ffa97-0000-7000-8000-000000000003');
52
+ const UNUSED = guid('019ffa97-0000-7000-8000-000000000004');
53
+ const ASSETS = {
54
+ mesh: { guid: guid('019ffa97-1000-7000-8000-000000000001'), kind: 'mesh' },
55
+ material: { guid: guid('019ffa97-1000-7000-8000-000000000002'), kind: 'material' },
56
+ scene: { guid: guid('019ffa97-1000-7000-8000-000000000003'), kind: 'scene' },
57
+ } as const satisfies ScriptablePackAssetDeclarations;
58
+
59
+ function source(
60
+ generation = 4,
61
+ contentDigest = 'sha256:content',
62
+ bothDigest = 'sha256:both',
63
+ ): ScriptablePackAssetSnapshotSource & { readonly original: MeshAsset } {
64
+ const original = {
65
+ kind: 'mesh',
66
+ vertices: new Float32Array([1, 2, 3]),
67
+ attributes: {},
68
+ submeshes: [
69
+ {
70
+ topology: 'triangle-list',
71
+ indexOffset: 0,
72
+ indexCount: 0,
73
+ vertexCount: 3,
74
+ materialSlot: 0,
75
+ },
76
+ ],
77
+ materialSlots: [{ slotName: 'Default' }],
78
+ } satisfies MeshAsset;
79
+ const entries = new Map([
80
+ [AssetGuid.format(CONTENT), { asset: original as Asset, generation, digest: contentDigest }],
81
+ [
82
+ AssetGuid.format(BOTH),
83
+ { asset: original as Asset, generation: generation + 3, digest: bothDigest },
84
+ ],
85
+ ]);
86
+ return {
87
+ original,
88
+ async readByGuid(requested) {
89
+ const entry = entries.get(AssetGuid.format(requested));
90
+ if (entry === undefined) throw new Error('unexpected test GUID');
91
+ return ok(entry);
92
+ },
93
+ };
94
+ }
95
+
96
+ function fixture(): ScriptablePackDefinition<typeof ASSETS> {
97
+ return {
98
+ schemaVersion: '1.0.0',
99
+ packageId: guid('019ffa97-1000-7000-8000-000000000000'),
100
+ assets: ASSETS,
101
+ externalAssets: { reference: REFERENCE, content: CONTENT, both: BOTH },
102
+ async build(reader) {
103
+ const content = await reader.readByGuid<MeshAsset>(CONTENT);
104
+ if (!content.ok) return content;
105
+ content.value.vertices[0] = 99;
106
+ const both = await reader.readByGuid<MeshAsset>(BOTH);
107
+ if (!both.ok) return both;
108
+ return ok({
109
+ mesh: {
110
+ kind: 'mesh',
111
+ vertices: new Float32Array([0, 0, 0]),
112
+ attributes: {},
113
+ submeshes: [
114
+ {
115
+ topology: 'triangle-list',
116
+ indexOffset: 0,
117
+ indexCount: 0,
118
+ vertexCount: 3,
119
+ materialSlot: 0,
120
+ },
121
+ ],
122
+ materialSlots: [{ slotName: 'Default' }],
123
+ },
124
+ material: { kind: 'material', values: {} },
125
+ scene: { kind: 'scene', entities: [] },
126
+ });
127
+ },
128
+ };
129
+ }
130
+
131
+ function outputs(version = '1'): AssetOutputProducerRegistry {
132
+ const registry = new AssetOutputProducerRegistry();
133
+ for (const kind of ['mesh', 'material', 'scene']) {
134
+ registry.register({
135
+ kind,
136
+ version: `fixture/${version}`,
137
+ produce(input) {
138
+ const refs =
139
+ input.sourceKey === 'mesh'
140
+ ? [{ guid: AssetGuid.format(REFERENCE) }]
141
+ : input.sourceKey === 'scene'
142
+ ? [{ guid: AssetGuid.format(BOTH), sourceField: { fieldName: 'template' } }]
143
+ : [];
144
+ return ok({ payload: fixturePayload(input.asset), refs, artifacts: {} });
145
+ },
146
+ });
147
+ }
148
+ return registry;
149
+ }
150
+
151
+ describe('ScriptablePack build bridge', () => {
152
+ it('does not require an Asset snapshot source when build performs no content reads', async () => {
153
+ const definition: ScriptablePackDefinition = {
154
+ schemaVersion: '1.0.0',
155
+ packageId: guid('019ffa97-1000-7000-8000-000000000010'),
156
+ assets: {
157
+ material: {
158
+ guid: guid('019ffa97-1000-7000-8000-000000000011'),
159
+ kind: 'material',
160
+ },
161
+ },
162
+ externalAssets: {},
163
+ build: () => ok({ material: { kind: 'material', values: {} } }),
164
+ };
165
+ const registry = new AssetOutputProducerRegistry();
166
+ registry.register({
167
+ kind: 'material',
168
+ version: 'fixture/1',
169
+ produce: (input) => ok({ payload: fixturePayload(input.asset), refs: [], artifacts: {} }),
170
+ });
171
+
172
+ const result = await buildScriptablePack({
173
+ definition,
174
+ sourcePath: 'material.pack.ts',
175
+ outputs: registry,
176
+ sourceClosure: [],
177
+ authoringContractVersion: 'fixture/1',
178
+ });
179
+
180
+ expect(result.ok).toBe(true);
181
+ });
182
+
183
+ it('derives reference/content/both, protects input snapshots, and is deterministic', async () => {
184
+ const assetSource = source();
185
+ const options = {
186
+ definition: fixture(),
187
+ sourcePath: 'house.pack.ts',
188
+ assetSource,
189
+ outputs: outputs(),
190
+ sourceClosure: [{ path: 'house.pack.ts', digest: 'sha256:source' }],
191
+ authoringContractVersion: 'geometry/1',
192
+ } as const;
193
+ const first = await buildScriptablePack(options);
194
+ const second = await buildScriptablePack(options);
195
+ expect(first.ok).toBe(true);
196
+ expect(second.ok).toBe(true);
197
+ if (!first.ok || !second.ok) return;
198
+ expect(first.value.externalEvidence).toEqual([
199
+ { guid: AssetGuid.format(REFERENCE), usage: 'reference' },
200
+ {
201
+ guid: AssetGuid.format(CONTENT),
202
+ usage: 'content',
203
+ generation: 4,
204
+ digest: 'sha256:content',
205
+ },
206
+ {
207
+ guid: AssetGuid.format(BOTH),
208
+ usage: 'both',
209
+ generation: 7,
210
+ digest: 'sha256:both',
211
+ },
212
+ ]);
213
+ expect(assetSource.original.vertices[0]).toBe(1);
214
+ expect(first.value.inputFingerprint).toBe(second.value.inputFingerprint);
215
+ expect(first.value.product.assets.find((asset) => asset.kind === 'scene')?.refs).toEqual([
216
+ { guid: AssetGuid.format(BOTH), sourceField: { fieldName: 'template' } },
217
+ ]);
218
+ });
219
+
220
+ it('keeps the source fingerprint stable when dependency generations advance', async () => {
221
+ const base = {
222
+ definition: fixture(),
223
+ outputs: outputs(),
224
+ sourcePath: 'assets/house.pack.ts',
225
+ sourceClosure: [{ path: 'house.pack.ts', digest: 'sha256:source' }],
226
+ authoringContractVersion: 'geometry/1',
227
+ } as const;
228
+ const first = await buildScriptablePack({ ...base, assetSource: source(4) });
229
+ const second = await buildScriptablePack({ ...base, assetSource: source(104) });
230
+ expect(first.ok).toBe(true);
231
+ expect(second.ok).toBe(true);
232
+ if (!first.ok || !second.ok) return;
233
+ expect(
234
+ first.value.externalEvidence.find((entry) => entry.guid === AssetGuid.format(CONTENT))
235
+ ?.generation,
236
+ ).toBe(4);
237
+ expect(
238
+ second.value.externalEvidence.find((entry) => entry.guid === AssetGuid.format(CONTENT))
239
+ ?.generation,
240
+ ).toBe(104);
241
+ expect(first.value.inputFingerprint).toBe(second.value.inputFingerprint);
242
+ });
243
+
244
+ it('keeps the build fingerprint stable across host-specific source roots', async () => {
245
+ const base = {
246
+ definition: fixture(),
247
+ assetSource: source(),
248
+ outputs: outputs(),
249
+ authoringContractVersion: 'geometry/1',
250
+ } as const;
251
+ const editor = await buildScriptablePack({
252
+ ...base,
253
+ sourcePath: 'assets/house.pack.ts',
254
+ sourceClosure: [
255
+ { path: '/tmp/e2e/sample/assets/house.pack.ts', digest: 'sha256:source' },
256
+ { path: '/tmp/e2e/sample/assets/house.pack-lib.ts', digest: 'sha256:lib' },
257
+ ],
258
+ });
259
+ const carrier = await buildScriptablePack({
260
+ ...base,
261
+ sourcePath: 'assets/house.pack.ts',
262
+ sourceClosure: [
263
+ {
264
+ path: '/workspace/play-runtime/host-games/sample/assets/house.pack.ts',
265
+ digest: 'sha256:source',
266
+ },
267
+ {
268
+ path: '/workspace/play-runtime/host-games/sample/assets/house.pack-lib.ts',
269
+ digest: 'sha256:lib',
270
+ },
271
+ ],
272
+ });
273
+ expect(editor.ok).toBe(true);
274
+ expect(carrier.ok).toBe(true);
275
+ if (!editor.ok || !carrier.ok) return;
276
+ expect(editor.value.inputFingerprint).toBe(carrier.value.inputFingerprint);
277
+ });
278
+
279
+ it('invalidates the input fingerprint when closure, evidence, or producer versions change', async () => {
280
+ const definition = fixture();
281
+ const base = {
282
+ definition,
283
+ sourcePath: 'house.pack.ts',
284
+ assetSource: source(4, 'sha256:content-a', 'sha256:both-a'),
285
+ outputs: outputs('1'),
286
+ sourceClosure: [{ path: 'house.pack.ts', digest: 'sha256:source-a' }],
287
+ authoringContractVersion: 'geometry/1',
288
+ } as const;
289
+ const stable = await buildScriptablePack(base);
290
+ const closureChanged = await buildScriptablePack({
291
+ ...base,
292
+ sourceClosure: [{ path: 'house.pack.ts', digest: 'sha256:source-b' }],
293
+ });
294
+ const evidenceChanged = await buildScriptablePack({
295
+ ...base,
296
+ assetSource: source(5, 'sha256:content-b', 'sha256:both-b'),
297
+ });
298
+ const producerChanged = await buildScriptablePack({ ...base, outputs: outputs('2') });
299
+
300
+ expect(stable.ok && closureChanged.ok).toBe(true);
301
+ expect(stable.ok && evidenceChanged.ok).toBe(true);
302
+ expect(stable.ok && producerChanged.ok).toBe(true);
303
+ if (!stable.ok || !closureChanged.ok || !evidenceChanged.ok || !producerChanged.ok) return;
304
+ expect(closureChanged.value.inputFingerprint).not.toBe(stable.value.inputFingerprint);
305
+ expect(evidenceChanged.value.inputFingerprint).not.toBe(stable.value.inputFingerprint);
306
+ expect(producerChanged.value.inputFingerprint).not.toBe(stable.value.inputFingerprint);
307
+ });
308
+
309
+ it('fails when an external declaration is unused', async () => {
310
+ const definition = fixture();
311
+ const result = await buildScriptablePack({
312
+ definition: {
313
+ ...definition,
314
+ externalAssets: { ...definition.externalAssets, unused: UNUSED },
315
+ },
316
+ sourcePath: 'unused.pack.ts',
317
+ assetSource: source(),
318
+ outputs: outputs(),
319
+ sourceClosure: [],
320
+ authoringContractVersion: 'geometry/1',
321
+ });
322
+ expect(result).toMatchObject({
323
+ ok: false,
324
+ error: {
325
+ code: 'pack-source-external-closure-mismatch',
326
+ detail: { unusedDeclaredGuids: [AssetGuid.format(UNUSED)] },
327
+ },
328
+ });
329
+ });
330
+
331
+ it('fails when an output reference is not declared in the external closure', async () => {
332
+ const definition = fixture();
333
+ const result = await buildScriptablePack({
334
+ definition: {
335
+ ...definition,
336
+ externalAssets: { content: CONTENT, both: BOTH },
337
+ },
338
+ sourcePath: 'undeclared-reference.pack.ts',
339
+ assetSource: source(),
340
+ outputs: outputs(),
341
+ sourceClosure: [],
342
+ authoringContractVersion: 'geometry/1',
343
+ });
344
+ expect(result).toMatchObject({
345
+ ok: false,
346
+ error: {
347
+ code: 'pack-source-external-closure-mismatch',
348
+ detail: { undeclaredReferencedGuids: [AssetGuid.format(REFERENCE)] },
349
+ },
350
+ });
351
+ });
352
+
353
+ it('rejects malformed producer refs before publication', async () => {
354
+ const invalidOutputs = outputs();
355
+ invalidOutputs.register({
356
+ kind: 'mesh',
357
+ version: 'fixture/invalid-ref',
358
+ produce: (input) =>
359
+ ok({
360
+ payload: fixturePayload(input.asset),
361
+ refs: [{ guid: 'not-a-guid' }],
362
+ artifacts: {},
363
+ }),
364
+ });
365
+ const result = await buildScriptablePack({
366
+ definition: fixture(),
367
+ sourcePath: 'invalid-ref.pack.ts',
368
+ assetSource: source(),
369
+ outputs: invalidOutputs,
370
+ sourceClosure: [],
371
+ authoringContractVersion: 'geometry/1',
372
+ });
373
+ expect(result).toMatchObject({
374
+ ok: false,
375
+ error: {
376
+ code: 'pack-source-output-invalid',
377
+ detail: { kindMismatches: [{ actual: 'not-a-guid' }] },
378
+ },
379
+ });
380
+ });
381
+
382
+ it('returns a structured missing-dependency error before producing outputs', async () => {
383
+ const missing = guid('019ffa97-0000-7000-8000-000000000005');
384
+ const definition: ScriptablePackDefinition = {
385
+ schemaVersion: '1.0.0',
386
+ packageId: guid('019ffa97-1000-7000-8000-000000000010'),
387
+ assets: {
388
+ mesh: {
389
+ guid: guid('019ffa97-1000-7000-8000-000000000011'),
390
+ kind: 'mesh',
391
+ },
392
+ },
393
+ externalAssets: { missing },
394
+ async build(reader) {
395
+ const result = await reader.readByGuid<MeshAsset>(missing);
396
+ if (!result.ok) return result;
397
+ return ok({ mesh: result.value });
398
+ },
399
+ };
400
+ const registry = new AssetOutputProducerRegistry();
401
+ registry.register({
402
+ kind: 'mesh',
403
+ version: 'fixture/1',
404
+ produce: (input) => ok({ payload: fixturePayload(input.asset), refs: [], artifacts: {} }),
405
+ });
406
+
407
+ const result = await buildScriptablePack({
408
+ definition,
409
+ sourcePath: 'missing.pack.ts',
410
+ assetSource: {
411
+ async readByGuid() {
412
+ return err(
413
+ new AssetError({
414
+ code: 'asset-not-imported',
415
+ expected: 'the declared external asset to be available',
416
+ hint: 'publish the dependency before rebuilding the source package',
417
+ }),
418
+ );
419
+ },
420
+ },
421
+ outputs: registry,
422
+ sourceClosure: [],
423
+ authoringContractVersion: 'fixture/1',
424
+ });
425
+
426
+ expect(result).toMatchObject({
427
+ ok: false,
428
+ error: {
429
+ code: 'asset-not-imported',
430
+ expected: 'the declared external asset to be available',
431
+ hint: 'publish the dependency before rebuilding the source package',
432
+ },
433
+ });
434
+ });
435
+ });
@@ -0,0 +1,92 @@
1
+ import type { Asset, ImportContext, ImportedAsset, Importer } from '@forgeax/engine-types';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { type RunImportMeta, runImport } from '../import-runner.js';
4
+ import { ImporterRegistry } from '../importer-registry.js';
5
+
6
+ const GUID = '11111111-1111-4111-8111-111111111111';
7
+
8
+ function registryFor(impl: (ctx: ImportContext) => readonly ImportedAsset[]): ImporterRegistry {
9
+ const registry = new ImporterRegistry();
10
+ registry.register({
11
+ key: 'fixture',
12
+ import: (ctx) => ({
13
+ ok: true,
14
+ value: {
15
+ assets: impl(ctx),
16
+ sourceDependencies: [],
17
+ },
18
+ }),
19
+ } as Importer);
20
+ return registry;
21
+ }
22
+
23
+ function meta(sourceOverrides?: Record<string, Record<string, unknown>>): RunImportMeta {
24
+ return {
25
+ importer: 'fixture',
26
+ source: 'fixture.source',
27
+ ...(sourceOverrides === undefined ? {} : { sourceOverrides }),
28
+ subAssets: [{ guid: GUID, sourceIndex: 0, sourceKey: 'mesh/main', kind: 'mesh' }],
29
+ } as RunImportMeta;
30
+ }
31
+
32
+ function fs() {
33
+ return { readSource: async () => ({ ok: true as const, value: new Uint8Array([1]) }) };
34
+ }
35
+
36
+ function asset(): ImportedAsset {
37
+ return {
38
+ guid: GUID,
39
+ kind: 'mesh',
40
+ payload: { vertices: new Float32Array(), indices: new Uint16Array(), attributes: {} } as Asset,
41
+ refs: [],
42
+ artifacts: {},
43
+ };
44
+ }
45
+
46
+ describe('source override ImportContext', () => {
47
+ it('passes producer-owned values by exact sourceKey', async () => {
48
+ let received: unknown;
49
+ const result = await runImport(
50
+ meta({ 'mesh/main': { lod: 2, producerFlag: true } }),
51
+ registryFor((ctx) => {
52
+ received = ctx.sourceOverrides;
53
+ return [asset()];
54
+ }),
55
+ fs(),
56
+ );
57
+
58
+ expect(result.ok).toBe(true);
59
+ expect(received).toEqual({ 'mesh/main': { lod: 2, producerFlag: true } });
60
+ });
61
+
62
+ it('keeps legacy Meta calls free of overrides', async () => {
63
+ let received: unknown = 'sentinel';
64
+ const result = await runImport(
65
+ meta(),
66
+ registryFor((ctx) => {
67
+ received = ctx.sourceOverrides;
68
+ return [asset()];
69
+ }),
70
+ fs(),
71
+ );
72
+
73
+ expect(result.ok).toBe(true);
74
+ expect(received).toBeUndefined();
75
+ });
76
+
77
+ it('rejects an unknown key before invoking the producer', async () => {
78
+ const producer = vi.fn(() => [asset()]);
79
+ const result = await runImport(meta({ 'mesh/other': { lod: 1 } }), registryFor(producer), fs());
80
+
81
+ expect(result.ok).toBe(false);
82
+ if (!result.ok) {
83
+ expect(result.error).toMatchObject({
84
+ code: 'unknown-source-key',
85
+ expected: 'sourceKey to be declared by the producer topology',
86
+ actual: 'mesh/other',
87
+ hint: expect.any(String),
88
+ });
89
+ }
90
+ expect(producer).not.toHaveBeenCalled();
91
+ });
92
+ });