@forgeax/engine-import 0.0.0-dev.8d955ade1c79

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 (118) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +227 -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__/mesh-bin-contract.test.d.ts +2 -0
  17. package/dist/__tests__/mesh-bin-contract.test.d.ts.map +1 -0
  18. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  19. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  20. package/dist/__tests__/mesh-bin.test.d.ts +2 -0
  21. package/dist/__tests__/mesh-bin.test.d.ts.map +1 -0
  22. package/dist/__tests__/owner-chain.integration.test.d.ts +2 -0
  23. package/dist/__tests__/owner-chain.integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/scriptable-pack-host.unit.test.d.ts +2 -0
  25. package/dist/__tests__/scriptable-pack-host.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts +2 -0
  27. package/dist/__tests__/scriptable-pack-output-producers.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts +2 -0
  29. package/dist/__tests__/scriptable-pack-product.contract.test.d.ts.map +1 -0
  30. package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts +2 -0
  31. package/dist/__tests__/scriptable-pack-production-producers.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts +2 -0
  33. package/dist/__tests__/scriptable-pack-staged-snapshot.unit.test.d.ts.map +1 -0
  34. package/dist/__tests__/scriptable-pack.integration.test.d.ts +2 -0
  35. package/dist/__tests__/scriptable-pack.integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/source-overrides-import-context.unit.test.d.ts +2 -0
  37. package/dist/__tests__/source-overrides-import-context.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/source-package-publication.integration.test.d.ts +2 -0
  39. package/dist/__tests__/source-package-publication.integration.test.d.ts.map +1 -0
  40. package/dist/browser.d.ts +5 -0
  41. package/dist/browser.d.ts.map +1 -0
  42. package/dist/browser.mjs +782 -0
  43. package/dist/browser.mjs.map +1 -0
  44. package/dist/build-production.d.ts +48 -0
  45. package/dist/build-production.d.ts.map +1 -0
  46. package/dist/catalog-importer-policy.d.ts +13 -0
  47. package/dist/catalog-importer-policy.d.ts.map +1 -0
  48. package/dist/catalog-inventory.d.ts +4 -0
  49. package/dist/catalog-inventory.d.ts.map +1 -0
  50. package/dist/import-product.d.ts +27 -0
  51. package/dist/import-product.d.ts.map +1 -0
  52. package/dist/import-runner.d.ts +136 -0
  53. package/dist/import-runner.d.ts.map +1 -0
  54. package/dist/importer-registry.d.ts +48 -0
  55. package/dist/importer-registry.d.ts.map +1 -0
  56. package/dist/index.d.ts +19 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.mjs +2849 -0
  59. package/dist/index.mjs.map +1 -0
  60. package/dist/mesh-bin.d.ts +19 -0
  61. package/dist/mesh-bin.d.ts.map +1 -0
  62. package/dist/mesh-bin.mjs +182 -0
  63. package/dist/mesh-bin.mjs.map +1 -0
  64. package/dist/pack-projection.d.ts +5 -0
  65. package/dist/pack-projection.d.ts.map +1 -0
  66. package/dist/scriptable-pack-host.d.ts +72 -0
  67. package/dist/scriptable-pack-host.d.ts.map +1 -0
  68. package/dist/scriptable-pack-output-producers.d.ts +7 -0
  69. package/dist/scriptable-pack-output-producers.d.ts.map +1 -0
  70. package/dist/scriptable-pack-staged-snapshot.d.ts +21 -0
  71. package/dist/scriptable-pack-staged-snapshot.d.ts.map +1 -0
  72. package/dist/scriptable-pack.d.ts +141 -0
  73. package/dist/scriptable-pack.d.ts.map +1 -0
  74. package/dist/scriptable-source-package.d.ts +14 -0
  75. package/dist/scriptable-source-package.d.ts.map +1 -0
  76. package/dist/source-package-errors.d.ts +52 -0
  77. package/dist/source-package-errors.d.ts.map +1 -0
  78. package/dist/source-package-publication.d.ts +63 -0
  79. package/dist/source-package-publication.d.ts.map +1 -0
  80. package/dist/source-package.d.ts +60 -0
  81. package/dist/source-package.d.ts.map +1 -0
  82. package/package.json +69 -0
  83. package/src/__tests__/decode-image-owner.test-d.ts +53 -0
  84. package/src/__tests__/import-contract-migration.test.ts +223 -0
  85. package/src/__tests__/import-dependencies.test.ts +65 -0
  86. package/src/__tests__/import-finalizer-digest-retry.test.ts +113 -0
  87. package/src/__tests__/import-local-artifacts.test.ts +102 -0
  88. package/src/__tests__/import.unit.test.ts +429 -0
  89. package/src/__tests__/mesh-bin-contract.test.ts +95 -0
  90. package/src/__tests__/mesh-bin-morph-v3.test.ts +44 -0
  91. package/src/__tests__/mesh-bin.test.ts +102 -0
  92. package/src/__tests__/owner-chain.integration.test.ts +37 -0
  93. package/src/__tests__/scriptable-pack-host.unit.test.ts +86 -0
  94. package/src/__tests__/scriptable-pack-output-producers.unit.test.ts +190 -0
  95. package/src/__tests__/scriptable-pack-product.contract.test.ts +47 -0
  96. package/src/__tests__/scriptable-pack-production-producers.unit.test.ts +488 -0
  97. package/src/__tests__/scriptable-pack-staged-snapshot.unit.test.ts +290 -0
  98. package/src/__tests__/scriptable-pack.integration.test.ts +435 -0
  99. package/src/__tests__/source-overrides-import-context.unit.test.ts +92 -0
  100. package/src/__tests__/source-package-publication.integration.test.ts +75 -0
  101. package/src/browser.ts +14 -0
  102. package/src/build-production.ts +493 -0
  103. package/src/catalog-importer-policy.ts +58 -0
  104. package/src/catalog-inventory.ts +24 -0
  105. package/src/import-product.ts +163 -0
  106. package/src/import-runner.ts +749 -0
  107. package/src/importer-registry.ts +103 -0
  108. package/src/index.ts +175 -0
  109. package/src/mesh-bin.ts +229 -0
  110. package/src/pack-projection.ts +21 -0
  111. package/src/scriptable-pack-host.ts +544 -0
  112. package/src/scriptable-pack-output-producers.ts +456 -0
  113. package/src/scriptable-pack-staged-snapshot.ts +172 -0
  114. package/src/scriptable-pack.ts +626 -0
  115. package/src/scriptable-source-package.ts +88 -0
  116. package/src/source-package-errors.ts +201 -0
  117. package/src/source-package-publication.ts +326 -0
  118. package/src/source-package.ts +187 -0
@@ -0,0 +1,102 @@
1
+ import type { ImportContext, ImportResult } from '@forgeax/engine-types';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { type RunImportMeta, runImport } from '../import-runner.js';
4
+ import { ImporterRegistry } from '../importer-registry.js';
5
+
6
+ const MESH_GUID = '019e2cc6-0c86-79da-aa76-b0984c86d45c';
7
+ const TEXTURE_GUID = '019e2cc6-0c86-79da-aa76-b0984c86d45d';
8
+
9
+ const meta: RunImportMeta = {
10
+ importer: 'local-artifacts-fixture',
11
+ source: 'fixture.source',
12
+ subAssets: [
13
+ { guid: MESH_GUID, sourceIndex: 0, sourceKey: 'fixture:mesh', kind: 'mesh' },
14
+ { guid: TEXTURE_GUID, sourceIndex: 1, sourceKey: 'fixture:texture', kind: 'texture' },
15
+ ],
16
+ };
17
+
18
+ function registryFor(value: unknown): ImporterRegistry {
19
+ const registry = new ImporterRegistry();
20
+ registry.register({
21
+ key: meta.importer,
22
+ import: async (_ctx: ImportContext): Promise<ImportResult> =>
23
+ ({ ok: true, value }) as ImportResult,
24
+ });
25
+ return registry;
26
+ }
27
+
28
+ describe('asset-local ImportProduct contract', () => {
29
+ it('keeps artifact ownership on each asset and preserves same keys across assets', async () => {
30
+ const value = {
31
+ assets: [
32
+ {
33
+ guid: MESH_GUID,
34
+ kind: 'mesh',
35
+ payload: { kind: 'mesh', vertices: [] },
36
+ refs: [],
37
+ artifacts: {
38
+ body: {
39
+ mediaType: 'application/x-forgeax-mesh',
40
+ assetCodec: { name: 'mesh-binary', version: '4' },
41
+ bytes: new Uint8Array([1]),
42
+ },
43
+ },
44
+ },
45
+ {
46
+ guid: TEXTURE_GUID,
47
+ kind: 'texture',
48
+ payload: { kind: 'texture', width: 1, height: 1 },
49
+ refs: [],
50
+ artifacts: {
51
+ body: {
52
+ mediaType: 'image/ktx2',
53
+ assetCodec: { name: 'basis', profile: 'uastc' },
54
+ bytes: new Uint8Array([2]),
55
+ },
56
+ },
57
+ },
58
+ ],
59
+ sourceDependencies: ['fixture.source'],
60
+ };
61
+
62
+ const result = await runImport(meta, registryFor(value), {
63
+ readSource: async () => ({ ok: true as const, value: new Uint8Array([0]) }),
64
+ });
65
+
66
+ expect(result.ok).toBe(true);
67
+ if (result.ok && 'product' in result.value) {
68
+ expect(result.value).not.toHaveProperty('bins');
69
+ expect(result.value.product.artifacts).toHaveProperty(`${MESH_GUID}/body`);
70
+ expect(result.value.product.receipts).toHaveLength(2);
71
+ expect(result.value.product.sourceRevision).toBe('source:fixture.source');
72
+ expect(result.value.pack.assets[0]?.artifacts).toHaveProperty('body');
73
+ expect(result.value.pack.assets[1]?.artifacts).toHaveProperty('body');
74
+ }
75
+ });
76
+
77
+ it('rejects a package-global artifact list instead of accepting an ownerless body', async () => {
78
+ const firstSubAsset = meta.subAssets[0];
79
+ if (firstSubAsset === undefined) throw new Error('fixture must declare a sub-asset');
80
+ const result = await runImport(
81
+ { ...meta, subAssets: [firstSubAsset] },
82
+ registryFor({
83
+ assets: [
84
+ {
85
+ guid: MESH_GUID,
86
+ kind: 'mesh',
87
+ payload: { kind: 'mesh', vertices: [] },
88
+ refs: [],
89
+ },
90
+ ],
91
+ artifacts: [
92
+ { path: 'mesh.bin', mimeType: 'application/octet-stream', bytes: new Uint8Array([1]) },
93
+ ],
94
+ sourceDependencies: ['fixture.source'],
95
+ }),
96
+ { readSource: async () => ({ ok: true as const, value: new Uint8Array([0]) }) },
97
+ );
98
+
99
+ expect(result.ok).toBe(false);
100
+ if (!result.ok) expect(result.error.code).toBe('import-internal-error');
101
+ });
102
+ });
@@ -0,0 +1,429 @@
1
+ // Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
2
+ // biome-ignore-all lint/complexity/noUselessLoneBlockStatements: scope isolation between merged source files
3
+ //
4
+ // Source files (N=2):
5
+ // - packages/import/src/__tests__/import-runner.test.ts
6
+ // - packages/import/src/__tests__/importer-registry.test.ts
7
+ //
8
+ // Paradigm: each block-scoped describe('<source-filename>.test.ts', ...) preserves
9
+ // source as ancestorTitles[0]. Top-level imports merged + deduped.
10
+
11
+ import type {
12
+ Asset,
13
+ ImportContext,
14
+ ImportedAsset,
15
+ Importer,
16
+ ImportResult,
17
+ } from '@forgeax/engine-types';
18
+ import { IMPORT_ERROR_HINTS, ImportError } from '@forgeax/engine-types';
19
+ import { describe, expect, it } from 'vitest';
20
+ import { type RunImportMeta, runImport } from '../import-runner.js';
21
+ import { ImporterRegistry } from '../importer-registry.js';
22
+
23
+ function stubImporter(key: string): Importer {
24
+ return {
25
+ key,
26
+ import: (_ctx: ImportContext): ImportResult => ({
27
+ ok: true,
28
+ value: { assets: [], sourceDependencies: [] },
29
+ }),
30
+ };
31
+ }
32
+
33
+ {
34
+ // ─── from import-runner.test.ts ───
35
+
36
+ const GUID_A = '019e2cc6-0c86-79da-aa76-b0984c86d45c';
37
+ const GUID_B = '019e2cc6-0c86-79da-aa76-b0984c86d45d';
38
+ const GUID_UNDECLARED = 'ffffffff-0c86-79da-aa76-b0984c86d45c';
39
+
40
+ const MESH_POD = {
41
+ kind: 'mesh' as const,
42
+ vertices: new Float32Array(),
43
+ indices: new Uint16Array(),
44
+ attributes: {},
45
+ } as unknown as Asset;
46
+
47
+ function okFs(bytes = new Uint8Array([1, 2, 3])) {
48
+ return {
49
+ readSource: async () => ({ ok: true as const, value: bytes }),
50
+ };
51
+ }
52
+
53
+ function failFs() {
54
+ return {
55
+ readSource: async () => ({ ok: false as const, error: new Error('ENOENT no such file') }),
56
+ };
57
+ }
58
+
59
+ function meta(importer: string, guids: readonly string[]): RunImportMeta {
60
+ return {
61
+ importer,
62
+ source: 'model.gltf',
63
+ subAssets: guids.map((g, i) => ({
64
+ guid: g,
65
+ sourceIndex: i,
66
+ sourceKey: `fixture:mesh:${g}`,
67
+ kind: 'mesh',
68
+ })),
69
+ };
70
+ }
71
+
72
+ function registryWith(
73
+ key: string,
74
+ impl: (ctx: ImportContext) => readonly ImportedAsset[] | Promise<readonly ImportedAsset[]>,
75
+ ): ImporterRegistry {
76
+ const reg = new ImporterRegistry();
77
+ reg.register({
78
+ key,
79
+ import: async (ctx) => ({
80
+ ok: true,
81
+ value: {
82
+ assets: (await impl(ctx)).map((asset) => ({
83
+ ...asset,
84
+ artifacts:
85
+ (asset as unknown as { artifacts?: Readonly<Record<string, unknown>> }).artifacts ??
86
+ {},
87
+ })),
88
+ sourceDependencies: [],
89
+ },
90
+ }),
91
+ });
92
+ return reg;
93
+ }
94
+
95
+ describe('import-runner.test.ts', () => {
96
+ describe('import runner (w15 / w17)', () => {
97
+ it('(a) happy path: produces a DDC pack with one row per produced asset', async () => {
98
+ const reg = registryWith('gltf', () => [
99
+ { guid: GUID_A, kind: 'mesh', payload: MESH_POD, refs: [], artifacts: {} },
100
+ { guid: GUID_B, kind: 'mesh', payload: MESH_POD, refs: [], artifacts: {} },
101
+ ]);
102
+ const res = await runImport(meta('gltf', [GUID_A, GUID_B]), reg, okFs());
103
+ expect(res.ok).toBe(true);
104
+ if (res.ok && !('skipped' in res.value)) {
105
+ expect(res.value.pack.kind).toBe('internal-text-package');
106
+ expect(res.value.pack.assets.map((a) => a.guid)).toEqual([GUID_A, GUID_B]);
107
+ }
108
+ });
109
+
110
+ it('(a2) product-only mode leaves pack publication to the downstream finalizer', async () => {
111
+ const reg = registryWith('gltf', () => [
112
+ { guid: GUID_A, kind: 'mesh', payload: MESH_POD, refs: [], artifacts: {} },
113
+ ]);
114
+ const res = await runImport({ ...meta('gltf', [GUID_A]), buildPack: false }, reg, okFs());
115
+ expect(res.ok).toBe(true);
116
+ if (res.ok && !('skipped' in res.value)) {
117
+ expect(res.value.product.assets).toHaveLength(1);
118
+ expect('pack' in res.value).toBe(false);
119
+ }
120
+ });
121
+
122
+ it('(b) import-produced-no-assets: importer returns []', async () => {
123
+ const reg = registryWith('gltf', () => []);
124
+ const res = await runImport(meta('gltf', [GUID_A]), reg, okFs());
125
+ expect(res.ok).toBe(false);
126
+ if (!res.ok) {
127
+ expect(res.error.code).toBe('import-produced-no-assets');
128
+ expect(res.error.detail).toMatchObject({ missingGuids: [GUID_A] });
129
+ }
130
+ });
131
+
132
+ it('(c) guid-mismatch: produced a GUID not declared in subAssets[]', async () => {
133
+ const reg = registryWith('gltf', () => [
134
+ { guid: GUID_UNDECLARED, kind: 'mesh', payload: MESH_POD, refs: [], artifacts: {} },
135
+ ]);
136
+ const res = await runImport(meta('gltf', [GUID_A]), reg, okFs());
137
+ expect(res.ok).toBe(false);
138
+ if (!res.ok) {
139
+ expect(res.error.code).toBe('guid-mismatch');
140
+ expect(res.error.detail).toMatchObject({ unexpectedGuids: [GUID_UNDECLARED] });
141
+ }
142
+ });
143
+
144
+ it('(d) import-produced-no-assets: a declared GUID is missing from the produced set', async () => {
145
+ const reg = registryWith('gltf', () => [
146
+ { guid: GUID_A, kind: 'mesh', payload: MESH_POD, refs: [], artifacts: {} },
147
+ ]);
148
+ const res = await runImport(meta('gltf', [GUID_A, GUID_B]), reg, okFs());
149
+ expect(res.ok).toBe(false);
150
+ if (!res.ok) {
151
+ expect(res.error.code).toBe('import-produced-no-assets');
152
+ expect(res.error.detail).toMatchObject({ missingGuids: [GUID_B] });
153
+ }
154
+ });
155
+
156
+ it('(e) importer-not-registered: no importer for meta.importer', async () => {
157
+ const reg = new ImporterRegistry();
158
+ reg.register({
159
+ key: 'image',
160
+ import: () => ({
161
+ ok: true,
162
+ value: { assets: [], sourceDependencies: [] },
163
+ }),
164
+ });
165
+ const res = await runImport(meta('gltf', [GUID_A]), reg, okFs());
166
+ expect(res.ok).toBe(false);
167
+ if (!res.ok) {
168
+ expect(res.error.code).toBe('importer-not-registered');
169
+ expect(res.error.detail).toMatchObject({
170
+ importer: 'gltf',
171
+ registeredImporters: ['image'],
172
+ });
173
+ }
174
+ });
175
+
176
+ it('(f) source-read-failed: readSource rejects', async () => {
177
+ const reg = registryWith('gltf', () => [
178
+ { guid: GUID_A, kind: 'mesh', payload: MESH_POD, refs: [], artifacts: {} },
179
+ ]);
180
+ const res = await runImport(meta('gltf', [GUID_A]), reg, failFs());
181
+ expect(res.ok).toBe(false);
182
+ if (!res.ok) {
183
+ expect(res.error.code).toBe('source-read-failed');
184
+ expect(res.error.detail).toMatchObject({ source: 'model.gltf' });
185
+ }
186
+ });
187
+
188
+ it.each([
189
+ 'throw',
190
+ 'reject',
191
+ ] as const)('(f2) contains %s source-reader failures and retries the same runner inputs', async (failureMode) => {
192
+ const badSource = 'model.gltf';
193
+ const healthySource = 'healthy-sibling.gltf';
194
+ const secretReason = 'secret source bytes and GUID must stay private';
195
+ const failure = Object.assign(new Error(secretReason), { code: 'EAGAIN' });
196
+ let mode: 'failing' | 'healthy' = 'failing';
197
+ const reads: string[] = [];
198
+ let importerCalls = 0;
199
+
200
+ const importer: Importer = {
201
+ key: 'gltf',
202
+ import: async (ctx) => {
203
+ importerCalls += 1;
204
+ const source = await ctx.readSource();
205
+ if (!source.ok) throw new Error('unexpected importer source refusal');
206
+ const guid = ctx.subAssets[0]?.guid ?? GUID_A;
207
+ return {
208
+ ok: true,
209
+ value: {
210
+ assets: [
211
+ {
212
+ guid,
213
+ kind: 'mesh',
214
+ payload: MESH_POD,
215
+ refs: [],
216
+ artifacts: {},
217
+ },
218
+ ],
219
+ sourceDependencies: [],
220
+ },
221
+ };
222
+ },
223
+ };
224
+ const registry = new ImporterRegistry();
225
+ registry.register(importer);
226
+ const fs = {
227
+ readSource: (sourcePath: string) => {
228
+ reads.push(sourcePath);
229
+ if (sourcePath === badSource && mode === 'failing') {
230
+ return failureMode === 'throw'
231
+ ? (() => {
232
+ throw failure;
233
+ })()
234
+ : Promise.reject(failure);
235
+ }
236
+ return Promise.resolve({ ok: true as const, value: new Uint8Array([1, 2, 3]) });
237
+ },
238
+ };
239
+ const badMeta = meta('gltf', [GUID_A]);
240
+ const healthyMeta = { ...meta('gltf', [GUID_B]), source: healthySource };
241
+
242
+ const first = await runImport(badMeta, registry, fs);
243
+ expect(first.ok).toBe(false);
244
+ expect(importerCalls).toBe(0);
245
+ expect(reads).toEqual([badSource]);
246
+ if (!first.ok) {
247
+ expect(first.error).toBeInstanceOf(ImportError);
248
+ expect(first.error.code).toBe('source-read-failed');
249
+ expect(first.error.expected).toBe(`readable source file at meta.source "${badSource}"`);
250
+ expect(first.error.hint).toBe(IMPORT_ERROR_HINTS['source-read-failed']);
251
+ expect(first.error.detail).toEqual({ source: badSource, reason: 'transient' });
252
+ expect(JSON.stringify(first.error)).not.toContain(secretReason);
253
+ expect(JSON.stringify(first.error)).not.toContain(GUID_A);
254
+ }
255
+
256
+ const healthySibling = await runImport(healthyMeta, registry, fs);
257
+ expect(healthySibling.ok).toBe(true);
258
+ expect(importerCalls).toBe(1);
259
+ if (healthySibling.ok && !('skipped' in healthySibling.value)) {
260
+ expect(healthySibling.value.product.sourceDependencies).toEqual([healthySource]);
261
+ expect(healthySibling.value.pack.assets[0]?.guid).toBe(GUID_B);
262
+ }
263
+
264
+ mode = 'healthy';
265
+ const second = await runImport(badMeta, registry, fs);
266
+ const third = await runImport(badMeta, registry, fs);
267
+ expect(second.ok).toBe(true);
268
+ expect(third.ok).toBe(true);
269
+ expect(importerCalls).toBe(3);
270
+ expect(reads.filter((path) => path === badSource)).toHaveLength(5);
271
+ expect(reads.filter((path) => path === healthySource)).toHaveLength(2);
272
+ if (second.ok && !('skipped' in second.value) && third.ok && !('skipped' in third.value)) {
273
+ expect(second.value.product.sourceDependencies).toEqual([badSource]);
274
+ expect(second.value.pack).toEqual(third.value.pack);
275
+ expect(second.value.cookProducts).toEqual(third.value.cookProducts);
276
+ }
277
+ });
278
+
279
+ it('(g) import-internal-error: the importer throws', async () => {
280
+ const reg = registryWith('gltf', () => {
281
+ throw new Error('boom inside importer');
282
+ });
283
+ const res = await runImport(meta('gltf', [GUID_A]), reg, okFs());
284
+ expect(res.ok).toBe(false);
285
+ if (!res.ok) {
286
+ expect(res.error.code).toBe('import-internal-error');
287
+ expect(res.error.detail).toMatchObject({ reason: 'boom inside importer' });
288
+ }
289
+ });
290
+
291
+ it('(h) reserved shader key is skipped (no DDC, no importer call)', async () => {
292
+ const reg = new ImporterRegistry();
293
+ const res = await runImport(meta('shader', [GUID_A]), reg, okFs());
294
+ expect(res.ok).toBe(true);
295
+ if (res.ok) {
296
+ expect(res.value).toEqual({ skipped: 'shader' });
297
+ }
298
+ });
299
+
300
+ it('(i) dispatch: the correct importer is invoked for meta.importer', async () => {
301
+ let invoked = '';
302
+ const reg = new ImporterRegistry();
303
+ reg.register({
304
+ key: 'gltf',
305
+ import: () => {
306
+ invoked = 'gltf';
307
+ return {
308
+ ok: true,
309
+ value: {
310
+ assets: [
311
+ { guid: GUID_A, kind: 'mesh', payload: MESH_POD, refs: [], artifacts: {} },
312
+ ],
313
+ sourceDependencies: [],
314
+ },
315
+ };
316
+ },
317
+ });
318
+ reg.register({
319
+ key: 'image',
320
+ import: () => {
321
+ invoked = 'image';
322
+ return {
323
+ ok: true,
324
+ value: {
325
+ assets: [
326
+ { guid: GUID_A, kind: 'texture', payload: MESH_POD, refs: [], artifacts: {} },
327
+ ],
328
+ sourceDependencies: [],
329
+ },
330
+ };
331
+ },
332
+ });
333
+ await runImport(meta('image', [GUID_A]), reg, okFs());
334
+ expect(invoked).toBe('image');
335
+ });
336
+
337
+ it('(j) importer failures preserve structured code and actionable hint', async () => {
338
+ const reg = new ImporterRegistry();
339
+ reg.register({
340
+ key: 'gltf',
341
+ import: () => ({
342
+ ok: false,
343
+ error: new ImportError({
344
+ code: 'source-read-failed',
345
+ expected: 'a readable source',
346
+ hint: IMPORT_ERROR_HINTS['source-read-failed'],
347
+ detail: { source: 'model.gltf', reason: 'ENOENT' },
348
+ }),
349
+ }),
350
+ });
351
+ const result = await runImport(meta('gltf', [GUID_A]), reg, okFs());
352
+ expect(result.ok).toBe(false);
353
+ if (!result.ok) {
354
+ expect(result.error.code).toBe('source-read-failed');
355
+ expect(result.error.hint).toContain('check the path');
356
+ expect(result.error.detail).toMatchObject({ source: 'model.gltf' });
357
+ }
358
+ });
359
+
360
+ it('(k) malformed importer results become structured internal errors', async () => {
361
+ const reg = new ImporterRegistry();
362
+ reg.register({ key: 'gltf', import: () => ({ ok: true }) as never });
363
+ const result = await runImport(meta('gltf', [GUID_A]), reg, okFs());
364
+ expect(result.ok).toBe(false);
365
+ if (!result.ok) {
366
+ expect(result.error.code).toBe('import-internal-error');
367
+ expect(result.error.hint).toContain('branch on err.detail');
368
+ }
369
+ });
370
+ });
371
+ });
372
+ }
373
+
374
+ {
375
+ // ─── from importer-registry.test.ts ───
376
+
377
+ describe('importer-registry.test.ts', () => {
378
+ describe('ImporterRegistry (w14 / w17)', () => {
379
+ it('register then get returns the registered importer', () => {
380
+ const reg = new ImporterRegistry();
381
+ const gltf = stubImporter('gltf');
382
+ reg.register(gltf);
383
+ expect(reg.get('gltf')).toBe(gltf);
384
+ });
385
+
386
+ it('get on an unregistered key returns undefined', () => {
387
+ const reg = new ImporterRegistry();
388
+ expect(reg.get('gltf')).toBeUndefined();
389
+ });
390
+
391
+ it('re-registering the same key is idempotent (last write wins, no throw)', () => {
392
+ const reg = new ImporterRegistry();
393
+ const first = stubImporter('gltf');
394
+ const second = stubImporter('gltf');
395
+ reg.register(first);
396
+ expect(() => reg.register(second)).not.toThrow();
397
+ expect(reg.get('gltf')).toBe(second);
398
+ expect(reg.registeredImporters().filter((k) => k === 'gltf')).toHaveLength(1);
399
+ });
400
+
401
+ it('registeredImporters reflects insertion order', () => {
402
+ const reg = new ImporterRegistry();
403
+ reg.register(stubImporter('gltf'));
404
+ reg.register(stubImporter('image'));
405
+ expect(reg.registeredImporters()).toEqual(['gltf', 'image']);
406
+ });
407
+
408
+ it('fail-fast: register throws on empty key', () => {
409
+ const reg = new ImporterRegistry();
410
+ expect(() =>
411
+ reg.register({
412
+ key: '',
413
+ import: () => ({
414
+ ok: true,
415
+ value: { assets: [], sourceDependencies: [] },
416
+ }),
417
+ }),
418
+ ).toThrow(TypeError);
419
+ });
420
+
421
+ it('fail-fast: register throws when import is not a function', () => {
422
+ const reg = new ImporterRegistry();
423
+ expect(() =>
424
+ reg.register({ key: 'gltf', import: undefined as unknown as Importer['import'] }),
425
+ ).toThrow(TypeError);
426
+ });
427
+ });
428
+ });
429
+ }
@@ -0,0 +1,95 @@
1
+ // mesh-bin-contract.test.ts -- feat-20260629-multi-uv-set-support m2-w2
2
+ //
3
+ // Import-side contract violation test for mesh-bin header v2 encode. Assert
4
+ // that packMeshBin with uvSetCount=9 or invalid stride produces a structured
5
+ // error (via Fail Fast at encode exit).
6
+ //
7
+ // RED at this commit: packMeshBin doesn't validate uvSetCount or stride;
8
+ // AssetErrorCode lacks 'mesh-bin-contract-violation'.
9
+ //
10
+ // GREEN after m2-w3 (encode validation) + m2-w5 (AssetErrorCode member).
11
+
12
+ import { packMeshBinV4 } from '@forgeax/engine-import';
13
+ import { describe, expect, it } from 'vitest';
14
+
15
+ describe('mesh-bin contract violation (import-side) (feat-20260629 m2-w2)', () => {
16
+ it('encode: uvSetCount=9 is rejected (Fail Fast at encode exit)', () => {
17
+ // Build a payload that implies uvSetCount=9 (uv1..uv8 present = 9 total)
18
+ const attrs: Record<string, unknown> = {
19
+ position: new Float32Array(12),
20
+ normal: new Float32Array(12),
21
+ uv: new Float32Array(8),
22
+ tangent: new Float32Array(16),
23
+ uv1: new Float32Array(8),
24
+ uv2: new Float32Array(8),
25
+ uv3: new Float32Array(8),
26
+ uv4: new Float32Array(8),
27
+ uv5: new Float32Array(8),
28
+ uv6: new Float32Array(8),
29
+ uv7: new Float32Array(8),
30
+ uv8: new Float32Array(8), // 9th set — out of bounds
31
+ };
32
+ const vertices = new Float32Array(4 * 28); // 12 + 8*2 = 28
33
+ const indices = new Uint16Array([0, 1, 2]);
34
+
35
+ const result = packMeshBinV4({ vertices, indices, attributes: attrs }, 'import://invalid-uv');
36
+ expect(result.ok).toBe(false);
37
+ });
38
+ });
39
+
40
+ describe('mesh-bin v4 producer red matrix', () => {
41
+ const attrs = {
42
+ position: new Float32Array([0, 0, 0]),
43
+ normal: new Float32Array([0, 1, 0]),
44
+ uv: new Float32Array([0, 0]),
45
+ tangent: new Float32Array([1, 0, 0, 1]),
46
+ color: new Float32Array([1, 0.5, 0.25, 1]),
47
+ };
48
+
49
+ it('returns deterministic v4 bytes with projection digest, stride, and RGBA payload facts', () => {
50
+ const result = packMeshBinV4(
51
+ {
52
+ vertices: new Float32Array([0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0.5, 0.25, 1]),
53
+ indices: new Uint16Array([0, 0, 0]),
54
+ attributes: attrs,
55
+ vertexCount: 1,
56
+ },
57
+ 'gltf://color',
58
+ );
59
+ expect(result.ok).toBe(true);
60
+ if (!result.ok) return;
61
+ const repeat = packMeshBinV4(
62
+ {
63
+ vertices: new Float32Array([0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0.5, 0.25, 1]),
64
+ indices: new Uint16Array([0, 0, 0]),
65
+ attributes: attrs,
66
+ vertexCount: 1,
67
+ },
68
+ 'gltf://color',
69
+ );
70
+ expect(repeat.ok).toBe(true);
71
+ if (!repeat.ok) return;
72
+ expect(result.value).toEqual(repeat.value);
73
+ });
74
+
75
+ it('rejects cardinality and RGBA shape corruption without publishing bytes', () => {
76
+ const result = packMeshBinV4(
77
+ {
78
+ vertices: new Float32Array(16),
79
+ indices: new Uint16Array([0, 1, 2]),
80
+ attributes: { ...attrs, color: new Float32Array([1, 0, 0]) },
81
+ vertexCount: 2,
82
+ },
83
+ 'gltf://bad-color',
84
+ );
85
+ expect(result).toMatchObject({
86
+ ok: false,
87
+ error: {
88
+ sourceKey: 'gltf://bad-color',
89
+ expected: expect.any(String),
90
+ actual: expect.any(String),
91
+ recovery: expect.stringContaining('re-cook'),
92
+ },
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,44 @@
1
+ import { packMeshBinV4 } from '@forgeax/engine-import';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ const attributes = {
5
+ position: new Float32Array(9),
6
+ normal: new Float32Array(9),
7
+ uv: new Float32Array(6),
8
+ tangent: new Float32Array(12),
9
+ };
10
+
11
+ describe('mesh-bin v4 morph contract', () => {
12
+ it('round-trips bounded target streams and default weights into metadata', () => {
13
+ const vertices = new Float32Array(3 * 12);
14
+ const result = packMeshBinV4(
15
+ {
16
+ vertices,
17
+ attributes,
18
+ morphTargets: [{ position: new Float32Array(9), normal: new Float32Array(9) }],
19
+ morphWeights: new Float32Array([0.25]),
20
+ },
21
+ 'gltf://morph',
22
+ );
23
+ expect(result.ok).toBe(true);
24
+ if (!result.ok) return;
25
+ expect(new DataView(result.value.buffer).getUint32(0, true)).toBe(4);
26
+ expect(new TextDecoder().decode(result.value)).toContain('morphWeights');
27
+ });
28
+
29
+ it('preserves deferred target payloads without truncation', () => {
30
+ const result = packMeshBinV4(
31
+ {
32
+ vertices: new Float32Array(3 * 12),
33
+ attributes,
34
+ morphTargets: Array.from({ length: 9 }, () => ({ position: new Float32Array(9) })),
35
+ },
36
+ 'gltf://morph-invalid',
37
+ );
38
+ expect(result.ok).toBe(true);
39
+ if (!result.ok) return;
40
+ const encoded = new TextDecoder().decode(result.value);
41
+ expect(encoded).toContain('morphTargets');
42
+ expect(encoded.match(/"position"/g)?.length).toBe(9);
43
+ });
44
+ });