@forgeax/engine-assets-runtime 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 (320) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +491 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/aabb.unit.test.d.ts +2 -0
  5. package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
  7. package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
  9. package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
  11. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
  12. package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
  13. package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
  15. package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
  17. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
  18. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
  19. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
  21. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
  23. package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
  24. package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
  25. package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
  27. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
  28. package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
  29. package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
  31. package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
  33. package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
  34. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
  35. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
  37. package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
  39. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
  40. package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
  41. package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
  42. package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
  43. package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
  44. package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
  45. package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
  46. package/dist/__tests__/decode-image-mime-owner.test.d.ts +2 -0
  47. package/dist/__tests__/decode-image-mime-owner.test.d.ts.map +1 -0
  48. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
  49. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
  51. package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
  53. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
  55. package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
  57. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
  59. package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
  61. package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
  63. package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
  65. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
  67. package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
  69. package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
  70. package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
  71. package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
  72. package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
  73. package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  75. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  76. package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
  77. package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
  79. package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
  81. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
  82. package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
  83. package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
  84. package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
  85. package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/plugin.test.d.ts +2 -0
  87. package/dist/__tests__/plugin.test.d.ts.map +1 -0
  88. package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
  89. package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
  90. package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
  91. package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
  92. package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
  93. package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
  94. package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
  95. package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
  96. package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
  97. package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
  98. package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
  99. package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
  100. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
  101. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
  102. package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
  103. package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
  104. package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
  105. package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
  106. package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
  107. package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
  108. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
  109. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
  110. package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
  111. package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
  112. package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
  113. package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
  114. package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
  115. package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
  116. package/dist/aabb.d.ts +3 -0
  117. package/dist/aabb.d.ts.map +1 -0
  118. package/dist/asset-kind.d.ts +4 -0
  119. package/dist/asset-kind.d.ts.map +1 -0
  120. package/dist/asset-registry.d.ts +554 -0
  121. package/dist/asset-registry.d.ts.map +1 -0
  122. package/dist/builtin-asset-registry.d.ts +22 -0
  123. package/dist/builtin-asset-registry.d.ts.map +1 -0
  124. package/dist/catalog-source.d.ts +24 -0
  125. package/dist/catalog-source.d.ts.map +1 -0
  126. package/dist/decode-image-bytes.d.ts +57 -0
  127. package/dist/decode-image-bytes.d.ts.map +1 -0
  128. package/dist/dynamic-texture-store.d.ts +89 -0
  129. package/dist/dynamic-texture-store.d.ts.map +1 -0
  130. package/dist/errors/asset.d.ts +201 -0
  131. package/dist/errors/asset.d.ts.map +1 -0
  132. package/dist/handles.d.ts +143 -0
  133. package/dist/handles.d.ts.map +1 -0
  134. package/dist/image-error.d.ts +6 -0
  135. package/dist/image-error.d.ts.map +1 -0
  136. package/dist/index.d.ts +34 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.mjs +8359 -0
  139. package/dist/index.mjs.map +1 -0
  140. package/dist/internal/artifact-cache.d.ts +17 -0
  141. package/dist/internal/artifact-cache.d.ts.map +1 -0
  142. package/dist/internal/asset-graph.d.ts +70 -0
  143. package/dist/internal/asset-graph.d.ts.map +1 -0
  144. package/dist/internal/catalog-session.d.ts +60 -0
  145. package/dist/internal/catalog-session.d.ts.map +1 -0
  146. package/dist/internal/decoder-registry.d.ts +19 -0
  147. package/dist/internal/decoder-registry.d.ts.map +1 -0
  148. package/dist/internal/immutable-payload.d.ts +9 -0
  149. package/dist/internal/immutable-payload.d.ts.map +1 -0
  150. package/dist/internal/load-asset.d.ts +31 -0
  151. package/dist/internal/load-asset.d.ts.map +1 -0
  152. package/dist/internal/pack-reader.d.ts +18 -0
  153. package/dist/internal/pack-reader.d.ts.map +1 -0
  154. package/dist/internal/validate-runtime-row.d.ts +7 -0
  155. package/dist/internal/validate-runtime-row.d.ts.map +1 -0
  156. package/dist/internal.d.ts +3 -0
  157. package/dist/internal.d.ts.map +1 -0
  158. package/dist/internal.mjs +329 -0
  159. package/dist/internal.mjs.map +1 -0
  160. package/dist/loader-registry.d.ts +63 -0
  161. package/dist/loader-registry.d.ts.map +1 -0
  162. package/dist/loaders/inline-pack.d.ts +72 -0
  163. package/dist/loaders/inline-pack.d.ts.map +1 -0
  164. package/dist/loaders/mesh-bin.d.ts +21 -0
  165. package/dist/loaders/mesh-bin.d.ts.map +1 -0
  166. package/dist/loaders/pack-artifact.d.ts +20 -0
  167. package/dist/loaders/pack-artifact.d.ts.map +1 -0
  168. package/dist/material/dependency-graph.d.ts +9 -0
  169. package/dist/material/dependency-graph.d.ts.map +1 -0
  170. package/dist/material/generation-cache.d.ts +24 -0
  171. package/dist/material/generation-cache.d.ts.map +1 -0
  172. package/dist/material/inspection.d.ts +24 -0
  173. package/dist/material/inspection.d.ts.map +1 -0
  174. package/dist/material/loader.d.ts +59 -0
  175. package/dist/material/loader.d.ts.map +1 -0
  176. package/dist/material/runtime-shader.d.ts +14 -0
  177. package/dist/material/runtime-shader.d.ts.map +1 -0
  178. package/dist/mesh-bin.d.ts +3 -0
  179. package/dist/mesh-bin.d.ts.map +1 -0
  180. package/dist/mipmap-generator.d.ts +132 -0
  181. package/dist/mipmap-generator.d.ts.map +1 -0
  182. package/dist/payload-validate.d.ts +60 -0
  183. package/dist/payload-validate.d.ts.map +1 -0
  184. package/dist/plugin.d.ts +16 -0
  185. package/dist/plugin.d.ts.map +1 -0
  186. package/dist/registry/artifact-io.d.ts +16 -0
  187. package/dist/registry/artifact-io.d.ts.map +1 -0
  188. package/dist/registry/asset-evidence.d.ts +13 -0
  189. package/dist/registry/asset-evidence.d.ts.map +1 -0
  190. package/dist/registry/catalog-state.d.ts +47 -0
  191. package/dist/registry/catalog-state.d.ts.map +1 -0
  192. package/dist/registry/catalog.d.ts +48 -0
  193. package/dist/registry/catalog.d.ts.map +1 -0
  194. package/dist/registry/instantiate.d.ts +153 -0
  195. package/dist/registry/instantiate.d.ts.map +1 -0
  196. package/dist/registry/load-by-guid.d.ts +247 -0
  197. package/dist/registry/load-by-guid.d.ts.map +1 -0
  198. package/dist/registry/load-state.d.ts +26 -0
  199. package/dist/registry/load-state.d.ts.map +1 -0
  200. package/dist/registry/runtime-projection.d.ts +16 -0
  201. package/dist/registry/runtime-projection.d.ts.map +1 -0
  202. package/dist/registry/scene-publication-fence.d.ts +38 -0
  203. package/dist/registry/scene-publication-fence.d.ts.map +1 -0
  204. package/dist/registry/validate-material.d.ts +77 -0
  205. package/dist/registry/validate-material.d.ts.map +1 -0
  206. package/dist/resolve-asset-handle.d.ts +43 -0
  207. package/dist/resolve-asset-handle.d.ts.map +1 -0
  208. package/dist/resolve-tileset-runtime.d.ts +23 -0
  209. package/dist/resolve-tileset-runtime.d.ts.map +1 -0
  210. package/dist/scene-handle-fields.d.ts +88 -0
  211. package/dist/scene-handle-fields.d.ts.map +1 -0
  212. package/dist/scene-payload.d.ts +15 -0
  213. package/dist/scene-payload.d.ts.map +1 -0
  214. package/dist/wire-default-loaders.d.ts +29 -0
  215. package/dist/wire-default-loaders.d.ts.map +1 -0
  216. package/package.json +76 -0
  217. package/src/__tests__/aabb.unit.test.ts +97 -0
  218. package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
  219. package/src/__tests__/artifact-io.unit.test.ts +127 -0
  220. package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
  221. package/src/__tests__/asset-evidence.unit.test.ts +32 -0
  222. package/src/__tests__/asset-kind.unit.test.ts +14 -0
  223. package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
  224. package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
  225. package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
  226. package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
  227. package/src/__tests__/asset-registry.unit.test.ts +511 -0
  228. package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
  229. package/src/__tests__/catalog-replica.unit.test.ts +196 -0
  230. package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
  231. package/src/__tests__/catalog-source.unit.test.ts +126 -0
  232. package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
  233. package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
  234. package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
  235. package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
  236. package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
  237. package/src/__tests__/decode-image-bytes.test.ts +90 -0
  238. package/src/__tests__/decode-image-mime-owner.test.ts +38 -0
  239. package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
  240. package/src/__tests__/errors-asset.unit.test.ts +92 -0
  241. package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
  242. package/src/__tests__/instantiate-context.unit.test.ts +65 -0
  243. package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
  244. package/src/__tests__/instantiate.unit.test.ts +684 -0
  245. package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
  246. package/src/__tests__/loader-registry.unit.test.ts +101 -0
  247. package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
  248. package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
  249. package/src/__tests__/material-load-consumer.test-d.ts +42 -0
  250. package/src/__tests__/material-ready.integration.test.ts +508 -0
  251. package/src/__tests__/material-stale-generation.unit.test.ts +147 -0
  252. package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
  253. package/src/__tests__/mesh-bin.unit.test.ts +152 -0
  254. package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
  255. package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
  256. package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
  257. package/src/__tests__/payload-validate.unit.test.ts +285 -0
  258. package/src/__tests__/plugin.test.ts +55 -0
  259. package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
  260. package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
  261. package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
  262. package/src/__tests__/registry-load-state.unit.test.ts +35 -0
  263. package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
  264. package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
  265. package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
  266. package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
  267. package/src/__tests__/scene-payload.unit.test.ts +151 -0
  268. package/src/__tests__/scene-publication-fence.test.ts +129 -0
  269. package/src/__tests__/scriptable-pack-public-surface.test-d.ts +27 -0
  270. package/src/__tests__/static-rig.integration.test.ts +8 -0
  271. package/src/__tests__/validate-material.unit.test.ts +359 -0
  272. package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
  273. package/src/aabb.ts +107 -0
  274. package/src/asset-kind.ts +6 -0
  275. package/src/asset-registry.ts +2045 -0
  276. package/src/builtin-asset-registry.ts +164 -0
  277. package/src/catalog-source.ts +89 -0
  278. package/src/decode-image-bytes.ts +126 -0
  279. package/src/dynamic-texture-store.ts +234 -0
  280. package/src/errors/asset.ts +347 -0
  281. package/src/handles.ts +186 -0
  282. package/src/image-error.ts +45 -0
  283. package/src/index.ts +189 -0
  284. package/src/internal/artifact-cache.ts +65 -0
  285. package/src/internal/asset-graph.ts +420 -0
  286. package/src/internal/catalog-session.ts +345 -0
  287. package/src/internal/decoder-registry.ts +175 -0
  288. package/src/internal/immutable-payload.ts +20 -0
  289. package/src/internal/load-asset.ts +254 -0
  290. package/src/internal/pack-reader.ts +183 -0
  291. package/src/internal/validate-runtime-row.ts +51 -0
  292. package/src/internal.ts +6 -0
  293. package/src/loader-registry.ts +157 -0
  294. package/src/loaders/inline-pack.ts +837 -0
  295. package/src/loaders/mesh-bin.ts +304 -0
  296. package/src/loaders/pack-artifact.ts +665 -0
  297. package/src/material/dependency-graph.ts +46 -0
  298. package/src/material/generation-cache.ts +177 -0
  299. package/src/material/inspection.ts +46 -0
  300. package/src/material/loader.ts +326 -0
  301. package/src/material/runtime-shader.ts +133 -0
  302. package/src/mesh-bin.ts +5 -0
  303. package/src/mipmap-generator.ts +450 -0
  304. package/src/payload-validate.ts +573 -0
  305. package/src/plugin.ts +48 -0
  306. package/src/registry/artifact-io.ts +259 -0
  307. package/src/registry/asset-evidence.ts +51 -0
  308. package/src/registry/catalog-state.ts +409 -0
  309. package/src/registry/catalog.ts +369 -0
  310. package/src/registry/instantiate.ts +1056 -0
  311. package/src/registry/load-by-guid.ts +1657 -0
  312. package/src/registry/load-state.ts +115 -0
  313. package/src/registry/runtime-projection.ts +63 -0
  314. package/src/registry/scene-publication-fence.ts +256 -0
  315. package/src/registry/validate-material.ts +385 -0
  316. package/src/resolve-asset-handle.ts +147 -0
  317. package/src/resolve-tileset-runtime.ts +72 -0
  318. package/src/scene-handle-fields.ts +221 -0
  319. package/src/scene-payload.ts +207 -0
  320. package/src/wire-default-loaders.ts +79 -0
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { LoadStateStore } from '../registry/load-state';
3
+
4
+ const ROOT = '11111111-1111-4111-8111-111111111111';
5
+ const REF = '22222222-2222-4222-8222-222222222222';
6
+
7
+ describe('registry load state', () => {
8
+ it('keeps provisional records out of the public-ready fast path', () => {
9
+ const states = new LoadStateStore();
10
+ states.begin(ROOT, [REF]);
11
+ states.resolveAsset(ROOT, { kind: 'root' });
12
+
13
+ expect(states.get(ROOT)?.status).toBe('provisional');
14
+ expect(states.getReady(ROOT)).toBeUndefined();
15
+
16
+ states.begin(REF, []);
17
+ states.resolveAsset(REF, { kind: 'ref' });
18
+ states.promoteReady(REF);
19
+ states.promoteReady(ROOT);
20
+ expect(states.getReady(ROOT)).toEqual({ kind: 'root' });
21
+ });
22
+
23
+ it('purges a failed closure and returns to Unloaded for retry', () => {
24
+ const states = new LoadStateStore();
25
+ states.begin(ROOT, [REF]);
26
+ states.begin(REF, []);
27
+ states.resolveAsset(ROOT, { kind: 'root' });
28
+ states.fail(ROOT, { code: 'artifact-failed' });
29
+
30
+ expect(states.get(ROOT)?.status).toBe('unloaded');
31
+ expect(states.get(REF)?.status).toBe('unloaded');
32
+ expect(states.getReady(ROOT)).toBeUndefined();
33
+ expect(states.begin(ROOT, [REF]).status).toBe('provisional');
34
+ });
35
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AssetRegistry } from '../asset-registry';
3
+
4
+ const GUID_A = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
5
+ const GUID_B = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
6
+
7
+ function pack() {
8
+ return {
9
+ schemaVersion: '2.0.0',
10
+ kind: 'internal-text-package',
11
+ assets: [
12
+ { guid: GUID_A, kind: 'host-blob', payload: { value: 'a' }, refs: [], artifacts: {} },
13
+ { guid: GUID_B, kind: 'host-blob', payload: { value: 'b' }, refs: [], artifacts: {} },
14
+ ],
15
+ };
16
+ }
17
+
18
+ function registryWithLoader() {
19
+ const registry = new AssetRegistry({} as never);
20
+ registry.loaders.register({
21
+ kind: 'host-blob',
22
+ load: (payload) => ({ kind: 'host-blob', ...payload }),
23
+ });
24
+ return registry;
25
+ }
26
+
27
+ describe('registry package cache', () => {
28
+ it('fetches and parses a shared package once for concurrent assets', async () => {
29
+ const registry = registryWithLoader();
30
+ registry.configurePackIndex('/pack-index.json');
31
+ const fetcher = vi.fn(async (url: string) => {
32
+ if (url.endsWith('pack-index.json')) {
33
+ return new Response(
34
+ JSON.stringify([
35
+ { guid: GUID_A, packageUrl: '/shared.pack.json', kind: 'host-blob', sourcePath: 'a' },
36
+ { guid: GUID_B, packageUrl: '/shared.pack.json', kind: 'host-blob', sourcePath: 'b' },
37
+ ]),
38
+ );
39
+ }
40
+ return new Response(JSON.stringify(pack()));
41
+ });
42
+ vi.stubGlobal('fetch', fetcher);
43
+
44
+ const [a, b] = await Promise.all([
45
+ registry.loadByGuid<{ kind: string; value: string }>(registry.parseGuid(GUID_A)),
46
+ registry.loadByGuid<{ kind: string; value: string }>(registry.parseGuid(GUID_B)),
47
+ ]);
48
+
49
+ expect(a.ok).toBe(true);
50
+ expect(b.ok).toBe(true);
51
+ expect(fetcher.mock.calls.filter(([url]) => url.endsWith('shared.pack.json'))).toHaveLength(1);
52
+ });
53
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { LoadStateStore } from '../registry/load-state';
3
+
4
+ const ROOT = '11111111-1111-4111-8111-111111111111';
5
+ const REF = '22222222-2222-4222-8222-222222222222';
6
+
7
+ describe('public-ready fast path', () => {
8
+ it('returns only SCC-promoted values and never provisional or failed records', () => {
9
+ const states = new LoadStateStore();
10
+ states.begin(ROOT, [REF]);
11
+ states.resolveAsset(ROOT, { kind: 'root' });
12
+ expect(states.getReady(ROOT)).toBeUndefined();
13
+
14
+ states.begin(REF, []);
15
+ states.resolveAsset(REF, { kind: 'ref' });
16
+ states.fail(REF, { code: 'missing-artifact' });
17
+ expect(states.getReady(ROOT)).toBeUndefined();
18
+ expect(states.getReady(REF)).toBeUndefined();
19
+
20
+ states.begin(ROOT, [REF]);
21
+ states.begin(REF, []);
22
+ states.resolveAsset(REF, { kind: 'ref' });
23
+ states.resolveAsset(ROOT, { kind: 'root' });
24
+ states.promoteReady(REF);
25
+ states.promoteReady(ROOT);
26
+ expect(states.getReady(ROOT)).toEqual({ kind: 'root' });
27
+ });
28
+
29
+ it('purge removes the old failure so retry uses a new record', () => {
30
+ const states = new LoadStateStore();
31
+ states.begin(ROOT, []);
32
+ states.fail(ROOT, { code: 'decode-failed' });
33
+ expect(states.get(ROOT)?.status).toBe('unloaded');
34
+ const retry = states.begin(ROOT, []);
35
+ expect(retry.status).toBe('provisional');
36
+ expect(retry.error).toBeUndefined();
37
+ });
38
+ });
@@ -0,0 +1,238 @@
1
+ // @forgeax/engine-assets-runtime -- resolveAssetHandle + material-walk coverage
2
+ // (fix issue #709). Exercises the two-tier slot dispatch (builtin vs user-tier),
3
+ // the stale/released/not-found error arms, and the material parent-chain walk
4
+ // (single, inherited, override-by-name, missing-parent, cycle).
5
+
6
+ import { World } from '@forgeax/engine-ecs';
7
+ import type { Asset, Handle, MaterialAsset } from '@forgeax/engine-types';
8
+ import { BUILTIN_BASE, handleGeneration, handleSlot, toShared } from '@forgeax/engine-types';
9
+ import { describe, expect, it } from 'vitest';
10
+ import { HANDLE_CUBE } from '../handles';
11
+ import { resolveAssetHandle, walkMaterialPassesOverSharedRefs } from '../resolve-asset-handle';
12
+
13
+ describe('resolveAssetHandle two-tier dispatch', () => {
14
+ it('resolves a builtin mesh handle (slot < BUILTIN_BASE)', () => {
15
+ const res = resolveAssetHandle(new World(), HANDLE_CUBE as Handle<string, 'shared'>);
16
+ expect(res.ok).toBe(true);
17
+ if (!res.ok) return;
18
+ expect((res.value as { kind: string }).kind).toBe('mesh');
19
+ });
20
+
21
+ it('errors with asset-not-found for an unoccupied builtin slot', () => {
22
+ const bogus = toShared<'MeshAsset'>(999); // in [1, BUILTIN_BASE) but not a real builtin
23
+ const res = resolveAssetHandle(new World(), bogus as Handle<string, 'shared'>);
24
+ expect(res.ok).toBe(false);
25
+ if (res.ok) return;
26
+ expect((res.error as { code: string }).code).toBe('asset-not-found');
27
+ });
28
+
29
+ it('resolves a user-tier handle minted via allocSharedRef', () => {
30
+ const world = new World();
31
+ const payload = { kind: 'material', passes: [] } as unknown as Asset;
32
+ const handle = world.allocSharedRef('MaterialAsset', payload);
33
+ const res = resolveAssetHandle(world, handle as Handle<string, 'shared'>);
34
+ expect(res.ok).toBe(true);
35
+ if (!res.ok) return;
36
+ expect(res.value).toBe(payload);
37
+ });
38
+
39
+ it('forwards the stale error after the handle slot is released + re-allocated', () => {
40
+ const world = new World();
41
+ const handle = world.allocSharedRef('MaterialAsset', { kind: 'material' } as unknown as Asset);
42
+ world.sharedRefs.release(handle as Handle<string, 'shared'>);
43
+ // Releasing bumps the slot generation, so the stale handle now resolves to
44
+ // the transparently-forwarded shared-ref-stale error (D-3 / AC-10), not a
45
+ // flattened asset-not-found.
46
+ const res = resolveAssetHandle(world, handle as Handle<string, 'shared'>);
47
+ expect(res.ok).toBe(false);
48
+ if (res.ok) return;
49
+ expect((res.error as { code: string }).code).toBe('shared-ref-stale');
50
+ });
51
+
52
+ it('forwards exact stale detail without exposing the replacement payload', () => {
53
+ const world = new World();
54
+ const oldPayload = { kind: 'material', values: { baseColor: [1, 0, 0] } } as unknown as Asset;
55
+ const oldHandle = world.allocSharedRef('MaterialAsset', oldPayload);
56
+ const oldSlot = handleSlot(oldHandle);
57
+ const oldGeneration = handleGeneration(oldHandle);
58
+
59
+ expect(world.sharedRefs.release(oldHandle).ok).toBe(true);
60
+ const replacementPayload = {
61
+ kind: 'material',
62
+ values: { baseColor: [0, 0, 1] },
63
+ } as unknown as Asset;
64
+ const replacementHandle = world.allocSharedRef('MaterialAsset', replacementPayload);
65
+
66
+ expect(handleSlot(replacementHandle)).toBe(oldSlot);
67
+ expect(handleGeneration(replacementHandle)).toBe(oldGeneration + 1);
68
+
69
+ const res = resolveAssetHandle(world, oldHandle as Handle<string, 'shared'>);
70
+ expect(res.ok).toBe(false);
71
+ if (res.ok) return;
72
+ expect(res.error.code).toBe('shared-ref-stale');
73
+ expect(res.error.detail).toEqual({
74
+ slot: oldSlot,
75
+ expectedGeneration: oldGeneration,
76
+ actualGeneration: handleGeneration(replacementHandle),
77
+ });
78
+ expect('value' in res.error).toBe(false);
79
+
80
+ const foreignModuleWorld = {
81
+ sharedRefs: {
82
+ resolve: () => ({
83
+ ok: false,
84
+ error: {
85
+ code: 'shared-ref-stale' as const,
86
+ detail: {
87
+ slot: oldSlot,
88
+ expectedGeneration: oldGeneration,
89
+ actualGeneration: handleGeneration(replacementHandle),
90
+ },
91
+ },
92
+ }),
93
+ },
94
+ } as unknown as World;
95
+ const bridged = resolveAssetHandle(foreignModuleWorld, oldHandle as Handle<string, 'shared'>);
96
+ expect(bridged).toEqual({
97
+ ok: false,
98
+ error: {
99
+ code: 'shared-ref-stale',
100
+ detail: {
101
+ slot: oldSlot,
102
+ expectedGeneration: oldGeneration,
103
+ actualGeneration: handleGeneration(replacementHandle),
104
+ },
105
+ },
106
+ });
107
+
108
+ const replacement = resolveAssetHandle(world, replacementHandle as Handle<string, 'shared'>);
109
+ expect(replacement).toMatchObject({ ok: true, value: replacementPayload });
110
+ expect(replacement.ok && replacement.value).not.toBe(oldPayload);
111
+ });
112
+
113
+ it('errors with asset-not-found for an unallocated user-tier slot', () => {
114
+ const res = resolveAssetHandle(
115
+ new World(),
116
+ toShared<'MeshAsset'>(BUILTIN_BASE + 5) as Handle<string, 'shared'>,
117
+ );
118
+ expect(res.ok).toBe(false);
119
+ });
120
+ });
121
+
122
+ function mat(over: Partial<MaterialAsset>): MaterialAsset {
123
+ return { kind: 'material', ...over } as MaterialAsset;
124
+ }
125
+
126
+ describe('walkMaterialPassesOverSharedRefs', () => {
127
+ it('returns the passes of a single childless material', () => {
128
+ const world = new World();
129
+ const handle = world.allocSharedRef(
130
+ 'MaterialAsset',
131
+ mat({
132
+ passes: [{ name: 'main', program: { module: 'forgeax::standard' } }],
133
+ values: { a: 1 },
134
+ }),
135
+ );
136
+ const res = walkMaterialPassesOverSharedRefs(world, handle, { lookup: () => undefined });
137
+ expect(res.ok).toBe(true);
138
+ if (!res.ok) return;
139
+ expect(res.value.passes.map((p) => p.name)).toEqual(['main']);
140
+ expect(res.value.values).toEqual({ a: 1 });
141
+ });
142
+
143
+ it('inherits parent passes when the child declares none, merging values', () => {
144
+ const world = new World();
145
+ const parent = mat({
146
+ passes: [{ name: 'base', program: { module: 'forgeax::standard' } }],
147
+ values: { a: 1, b: 2 },
148
+ });
149
+ const child = mat({ parent: 'p-guid' as never, values: { b: 20 } });
150
+ const handle = world.allocSharedRef('MaterialAsset', child);
151
+ const res = walkMaterialPassesOverSharedRefs(world, handle, { lookup: () => parent });
152
+ expect(res.ok).toBe(true);
153
+ if (!res.ok) return;
154
+ expect(res.value.passes.map((p) => p.name)).toEqual(['base']);
155
+ expect(res.value.values).toEqual({ a: 1, b: 20 }); // child overrides
156
+ });
157
+
158
+ it('inherits the root parameter contract without child declaration merging', () => {
159
+ const world = new World();
160
+ const parent = mat({
161
+ passes: [{ name: 'base', program: { module: 'forgeax::standard' } }],
162
+ parameters: [
163
+ { name: 'baseColor', type: 'color' },
164
+ { name: 'roughness', type: 'f32' },
165
+ ],
166
+ });
167
+ const child = mat({
168
+ parent: 'p-guid' as never,
169
+ parameters: [
170
+ { name: 'roughness', type: 'f32' },
171
+ { name: 'normalTexture', type: 'texture' },
172
+ ],
173
+ });
174
+ const handle = world.allocSharedRef('MaterialAsset', child);
175
+ const res = walkMaterialPassesOverSharedRefs(world, handle, { lookup: () => parent });
176
+ expect(res.ok).toBe(true);
177
+ if (!res.ok) return;
178
+ expect(res.value.parameters).toEqual([
179
+ { name: 'baseColor', type: 'color' },
180
+ { name: 'roughness', type: 'f32' },
181
+ ]);
182
+ });
183
+
184
+ it('child passes override parent by name and append new ones', () => {
185
+ const world = new World();
186
+ const parent = mat({
187
+ passes: [
188
+ { name: 'shadow', program: { module: 'forgeax::shadow' } },
189
+ { name: 'main', program: { module: 'forgeax::parent-main' } },
190
+ ],
191
+ });
192
+ const child = mat({
193
+ parent: 'p-guid' as never,
194
+ passes: [
195
+ { name: 'main', program: { module: 'forgeax::child-main' } },
196
+ { name: 'extra', program: { module: 'forgeax::extra' } },
197
+ ],
198
+ });
199
+ const handle = world.allocSharedRef('MaterialAsset', child);
200
+ const res = walkMaterialPassesOverSharedRefs(world, handle, { lookup: () => parent });
201
+ expect(res.ok).toBe(true);
202
+ if (!res.ok) return;
203
+ const byName = new Map(res.value.passes.map((p) => [p.name, p.program.module]));
204
+ expect(byName.get('main')).toBe('forgeax::child-main'); // overridden
205
+ expect(byName.get('extra')).toBe('forgeax::extra'); // appended
206
+ expect(byName.get('shadow')).toBe('forgeax::shadow'); // inherited
207
+ });
208
+
209
+ it('errors when the parent is not catalogued', () => {
210
+ const world = new World();
211
+ const child = mat({ parent: 'missing-guid' as never });
212
+ const handle = world.allocSharedRef('MaterialAsset', child);
213
+ const res = walkMaterialPassesOverSharedRefs(world, handle, { lookup: () => undefined });
214
+ expect(res.ok).toBe(false);
215
+ if (res.ok) return;
216
+ expect((res.error as { code: string }).code).toBe('material-parent-not-found');
217
+ });
218
+
219
+ it('detects a parent cycle', () => {
220
+ const world = new World();
221
+ // A -> B -> A: lookup returns a material whose parent points back.
222
+ const a = mat({ parent: 'guid-b' as never });
223
+ const b = mat({ parent: 'guid-a' as never });
224
+ const handle = world.allocSharedRef('MaterialAsset', a);
225
+ const registry = {
226
+ lookup: (guid: string | { toString(): string }) => {
227
+ const g = String(guid).toLowerCase();
228
+ if (g.includes('guid-b')) return b;
229
+ if (g.includes('guid-a')) return a;
230
+ return undefined;
231
+ },
232
+ };
233
+ const res = walkMaterialPassesOverSharedRefs(world, handle, registry as never);
234
+ expect(res.ok).toBe(false);
235
+ if (res.ok) return;
236
+ expect((res.error as { code: string }).code).toBe('material-circular-inheritance');
237
+ });
238
+ });
@@ -0,0 +1,129 @@
1
+ // @forgeax/engine-assets-runtime -- scene-handle-fields coverage (fix issue #709).
2
+ // extractSceneEntityHandleGuids + extractMountOverrideHandleGuids identify
3
+ // shared<T> / array<shared<T>> schema fields via resolveComponent and read the
4
+ // raw GUID string(s). Uses test-unique component names to avoid colliding with
5
+ // the engine's real component registry in the shared coverage run.
6
+
7
+ import { type Component, defineComponent } from '@forgeax/engine-ecs';
8
+ import type { MountOverride } from '@forgeax/engine-types';
9
+ import { describe, expect, it } from 'vitest';
10
+ import {
11
+ extractMountOverrideHandleGuids,
12
+ extractSceneEntityHandleGuids,
13
+ } from '../scene-handle-fields';
14
+
15
+ const T709MeshFilter = defineComponent('T709MeshFilter', { assetHandle: 'shared<MeshAsset>' });
16
+ const T709MeshRenderer = defineComponent('T709MeshRenderer', {
17
+ materials: 'array<shared<MaterialAsset>>',
18
+ });
19
+ const T709Transform = defineComponent('T709Transform', { x: 'f32' });
20
+ const components = new Map<string, Component>([
21
+ [T709MeshFilter.name, T709MeshFilter],
22
+ [T709MeshRenderer.name, T709MeshRenderer],
23
+ [T709Transform.name, T709Transform],
24
+ ]);
25
+
26
+ describe('extractSceneEntityHandleGuids', () => {
27
+ it('extracts scalar shared<T> GUID strings', () => {
28
+ const entries = extractSceneEntityHandleGuids(components, [
29
+ { localId: 5, components: { T709MeshFilter: { assetHandle: 'mesh-guid' } } },
30
+ ]);
31
+ expect(entries).toEqual([
32
+ {
33
+ entityLocalId: 5,
34
+ componentName: 'T709MeshFilter',
35
+ fieldName: 'assetHandle',
36
+ guidString: 'mesh-guid',
37
+ },
38
+ ]);
39
+ });
40
+
41
+ it('extracts array<shared<T>> element GUIDs with their arrayIndex', () => {
42
+ const entries = extractSceneEntityHandleGuids(components, [
43
+ { localId: 1, components: { T709MeshRenderer: { materials: ['m0', 'm1'] } } },
44
+ ]);
45
+ expect(entries).toEqual([
46
+ {
47
+ entityLocalId: 1,
48
+ componentName: 'T709MeshRenderer',
49
+ fieldName: 'materials',
50
+ guidString: 'm0',
51
+ arrayIndex: 0,
52
+ },
53
+ {
54
+ entityLocalId: 1,
55
+ componentName: 'T709MeshRenderer',
56
+ fieldName: 'materials',
57
+ guidString: 'm1',
58
+ arrayIndex: 1,
59
+ },
60
+ ]);
61
+ });
62
+
63
+ it('skips unknown components, non-shared fields, and already-resolved numbers', () => {
64
+ const entries = extractSceneEntityHandleGuids(components, [
65
+ { localId: 0, components: { NotRegistered709: { assetHandle: 'x' } } },
66
+ { localId: 1, components: { T709Transform: { x: 3 } } },
67
+ { localId: 2, components: { T709MeshFilter: { assetHandle: 42 } } },
68
+ { localId: 3, components: { T709MeshRenderer: { materials: [7, 'm-str'] } } },
69
+ ]);
70
+ // Only the string array element survives.
71
+ expect(entries).toEqual([
72
+ {
73
+ entityLocalId: 3,
74
+ componentName: 'T709MeshRenderer',
75
+ fieldName: 'materials',
76
+ guidString: 'm-str',
77
+ arrayIndex: 1,
78
+ },
79
+ ]);
80
+ });
81
+ });
82
+
83
+ describe('extractMountOverrideHandleGuids', () => {
84
+ it('patch form (field present) extracts one GUID from a shared field', () => {
85
+ const overrides: MountOverride[] = [
86
+ { localId: 1 as never, comp: 'T709MeshFilter', field: 'assetHandle', value: 'mesh-guid' },
87
+ ];
88
+ const entries = extractMountOverrideHandleGuids(components, overrides);
89
+ expect(entries).toEqual([
90
+ {
91
+ overrideIndex: 0,
92
+ componentName: 'T709MeshFilter',
93
+ fieldName: 'assetHandle',
94
+ guidString: 'mesh-guid',
95
+ },
96
+ ]);
97
+ });
98
+
99
+ it('add form (field absent, value is a per-field map) extracts array GUIDs', () => {
100
+ const overrides: MountOverride[] = [
101
+ { localId: 1 as never, comp: 'T709MeshRenderer', value: { materials: ['a', 'b'] } },
102
+ ];
103
+ const entries = extractMountOverrideHandleGuids(components, overrides);
104
+ expect(entries).toEqual([
105
+ {
106
+ overrideIndex: 0,
107
+ componentName: 'T709MeshRenderer',
108
+ fieldName: 'materials',
109
+ guidString: 'a',
110
+ arrayIndex: 0,
111
+ },
112
+ {
113
+ overrideIndex: 0,
114
+ componentName: 'T709MeshRenderer',
115
+ fieldName: 'materials',
116
+ guidString: 'b',
117
+ arrayIndex: 1,
118
+ },
119
+ ]);
120
+ });
121
+
122
+ it('skips unknown components and non-object add values', () => {
123
+ const overrides: MountOverride[] = [
124
+ { localId: 1 as never, comp: 'NotRegistered709', value: { assetHandle: 'x' } },
125
+ { localId: 2 as never, comp: 'T709MeshFilter', value: 5 },
126
+ ];
127
+ expect(extractMountOverrideHandleGuids(components, overrides)).toEqual([]);
128
+ });
129
+ });
@@ -0,0 +1,151 @@
1
+ // @forgeax/engine-assets-runtime -- parseScenePayload coverage (fix issue #709).
2
+ // Exercises the entities/mounts/skinGuids refs-resolution branches + fail-fast
3
+ // return shapes. parseScenePayload keys handle fields off the HANDLE_FIELD_NAMES
4
+ // / HANDLE_ARRAY_FIELD_NAMES name allowlists, so no component registration needed.
5
+
6
+ import type { SceneAsset } from '@forgeax/engine-types';
7
+ import { describe, expect, it } from 'vitest';
8
+ import { parseScenePayload } from '../scene-payload';
9
+
10
+ function isScene(v: unknown): v is SceneAsset {
11
+ return typeof v === 'object' && v !== null && (v as { kind?: string }).kind === 'scene';
12
+ }
13
+
14
+ describe('parseScenePayload without refs', () => {
15
+ it('returns undefined when entities is not an array', () => {
16
+ expect(parseScenePayload({})).toBeUndefined();
17
+ expect(parseScenePayload({ entities: 'nope' })).toBeUndefined();
18
+ });
19
+
20
+ it('returns undefined when an entity localId is not a number', () => {
21
+ expect(parseScenePayload({ entities: [{ localId: 'x', components: {} }] })).toBeUndefined();
22
+ });
23
+
24
+ it('passes component fields through verbatim (no refs table)', () => {
25
+ const out = parseScenePayload({
26
+ entities: [{ localId: 0, components: { MeshFilter: { assetHandle: 3 } } }],
27
+ });
28
+ expect(isScene(out)).toBe(true);
29
+ if (!isScene(out)) return;
30
+ expect(out.entities[0]?.components.MeshFilter?.assetHandle).toBe(3);
31
+ });
32
+ });
33
+
34
+ describe('parseScenePayload with refs', () => {
35
+ it('resolves ParticleEffectPlayer.effect through the scalar refs contract', () => {
36
+ const out = parseScenePayload(
37
+ {
38
+ entities: [
39
+ { localId: 0, components: { ParticleEffectPlayer: { effect: 1, seed: 424242 } } },
40
+ ],
41
+ },
42
+ ['scene-guid', 'particle-effect-guid'],
43
+ );
44
+ expect(out).toMatchObject({
45
+ entities: [
46
+ { components: { ParticleEffectPlayer: { effect: 'particle-effect-guid', seed: 424242 } } },
47
+ ],
48
+ });
49
+ });
50
+
51
+ const refs = ['guid-a', 'guid-b', 'guid-c'];
52
+
53
+ it('resolves a scalar handle field index to its GUID string', () => {
54
+ const out = parseScenePayload(
55
+ { entities: [{ localId: 0, components: { MeshFilter: { assetHandle: 1 } } }] },
56
+ refs,
57
+ );
58
+ expect(isScene(out)).toBe(true);
59
+ if (!isScene(out)) return;
60
+ expect(out.entities[0]?.components.MeshFilter?.assetHandle).toBe('guid-b');
61
+ });
62
+
63
+ it('keeps non-handle integer fields as-is (e.g. ChildOf.parent)', () => {
64
+ const out = parseScenePayload(
65
+ { entities: [{ localId: 0, components: { ChildOf: { parent: 2 } } }] },
66
+ refs,
67
+ );
68
+ if (!isScene(out)) throw new Error('expected scene');
69
+ expect(out.entities[0]?.components.ChildOf?.parent).toBe(2);
70
+ });
71
+
72
+ it('resolves array<handle> fields (materials) element-wise', () => {
73
+ const out = parseScenePayload(
74
+ { entities: [{ localId: 0, components: { MeshRenderer: { materials: [0, 2] } } }] },
75
+ refs,
76
+ );
77
+ if (!isScene(out)) throw new Error('expected scene');
78
+ expect(out.entities[0]?.components.MeshRenderer?.materials).toEqual(['guid-a', 'guid-c']);
79
+ });
80
+
81
+ it('returns a ParseSceneError for an out-of-bounds scalar handle index', () => {
82
+ const out = parseScenePayload(
83
+ { entities: [{ localId: 7, components: { MeshFilter: { assetHandle: 9 } } }] },
84
+ refs,
85
+ );
86
+ expect(isScene(out)).toBe(false);
87
+ expect(out).toMatchObject({
88
+ localId: 7,
89
+ component: 'MeshFilter',
90
+ field: 'assetHandle',
91
+ index: 9,
92
+ refsLength: 3,
93
+ });
94
+ });
95
+
96
+ it('returns a ParseSceneError for an out-of-bounds array handle element', () => {
97
+ const out = parseScenePayload(
98
+ { entities: [{ localId: 1, components: { MeshRenderer: { materials: [0, 99] } } }] },
99
+ refs,
100
+ );
101
+ expect(isScene(out)).toBe(false);
102
+ expect(out).toMatchObject({ localId: 1, field: 'materials[1]', index: 99 });
103
+ });
104
+
105
+ it('resolves mounts[].source through refs', () => {
106
+ const out = parseScenePayload(
107
+ {
108
+ entities: [{ localId: 0, components: {} }],
109
+ mounts: [{ localId: 1, source: 2, memberFirst: 2, memberCount: 0 }],
110
+ },
111
+ refs,
112
+ );
113
+ if (!isScene(out)) throw new Error('expected scene');
114
+ expect(out.mounts?.[0]?.source).toBe('guid-c');
115
+ });
116
+
117
+ it('returns undefined when a mount source index is out of bounds', () => {
118
+ const out = parseScenePayload(
119
+ {
120
+ entities: [{ localId: 0, components: {} }],
121
+ mounts: [{ localId: 1, source: 42, memberFirst: 2, memberCount: 0 }],
122
+ },
123
+ refs,
124
+ );
125
+ expect(out).toBeUndefined();
126
+ });
127
+
128
+ it('resolves skinGuids from refs indices and passes pre-resolved strings through', () => {
129
+ const idx = parseScenePayload(
130
+ { entities: [{ localId: 0, components: {} }], skinGuids: [0, 2] },
131
+ refs,
132
+ );
133
+ if (!isScene(idx)) throw new Error('expected scene');
134
+ expect(idx.skinGuids).toEqual(['guid-a', 'guid-c']);
135
+
136
+ const strs = parseScenePayload({
137
+ entities: [{ localId: 0, components: {} }],
138
+ skinGuids: ['g1'],
139
+ });
140
+ if (!isScene(strs)) throw new Error('expected scene');
141
+ expect(strs.skinGuids).toEqual(['g1']);
142
+ });
143
+
144
+ it('returns undefined when a skinGuids index is out of bounds', () => {
145
+ const out = parseScenePayload(
146
+ { entities: [{ localId: 0, components: {} }], skinGuids: [99] },
147
+ refs,
148
+ );
149
+ expect(out).toBeUndefined();
150
+ });
151
+ });