@forgeax/engine-assets-runtime 0.1.4 → 0.1.7

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 (311) hide show
  1. package/README.md +453 -175
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/aabb.unit.test.d.ts +2 -0
  4. package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
  6. package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
  8. package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
  9. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
  10. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
  12. package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
  13. package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
  14. package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
  15. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
  16. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
  18. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
  19. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
  20. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
  21. package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
  22. package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
  23. package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
  24. package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
  25. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
  26. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
  27. package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
  28. package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
  29. package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
  30. package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
  31. package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
  32. package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
  33. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
  34. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
  35. package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
  36. package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
  37. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
  38. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
  39. package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
  40. package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
  41. package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
  42. package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
  43. package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
  44. package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
  45. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
  46. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
  47. package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
  48. package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
  49. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
  50. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
  51. package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
  52. package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
  53. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
  54. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
  55. package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
  56. package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
  57. package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
  58. package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
  59. package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
  60. package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
  61. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
  62. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
  63. package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
  64. package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
  65. package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
  66. package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
  67. package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
  68. package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
  69. package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
  70. package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
  71. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  72. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  73. package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
  74. package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
  75. package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
  76. package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
  77. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
  78. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
  79. package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
  80. package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
  81. package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
  82. package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
  83. package/dist/__tests__/plugin.test.d.ts +2 -0
  84. package/dist/__tests__/plugin.test.d.ts.map +1 -0
  85. package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
  86. package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
  87. package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
  88. package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
  89. package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
  90. package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
  91. package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
  92. package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
  93. package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
  94. package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
  95. package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
  96. package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
  97. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
  98. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
  99. package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
  100. package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
  101. package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
  102. package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
  103. package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
  104. package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
  105. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
  106. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
  107. package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
  108. package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
  109. package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
  110. package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
  111. package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
  112. package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
  113. package/dist/aabb.d.ts +3 -0
  114. package/dist/aabb.d.ts.map +1 -0
  115. package/dist/asset-registry.d.ts +554 -0
  116. package/dist/asset-registry.d.ts.map +1 -0
  117. package/dist/builtin-asset-registry.d.ts +22 -0
  118. package/dist/builtin-asset-registry.d.ts.map +1 -0
  119. package/dist/catalog-source.d.ts +2 -1
  120. package/dist/catalog-source.d.ts.map +1 -1
  121. package/dist/decode-image-bytes.d.ts +57 -0
  122. package/dist/decode-image-bytes.d.ts.map +1 -0
  123. package/dist/dynamic-texture-store.d.ts +89 -0
  124. package/dist/dynamic-texture-store.d.ts.map +1 -0
  125. package/dist/errors/asset.d.ts +201 -0
  126. package/dist/errors/asset.d.ts.map +1 -0
  127. package/dist/handles.d.ts +143 -0
  128. package/dist/handles.d.ts.map +1 -0
  129. package/dist/image-error.d.ts +6 -0
  130. package/dist/image-error.d.ts.map +1 -0
  131. package/dist/index.d.ts +32 -3
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.mjs +7233 -114
  134. package/dist/index.mjs.map +1 -1
  135. package/dist/internal.d.ts +1 -1
  136. package/dist/internal.d.ts.map +1 -1
  137. package/dist/loader-registry.d.ts +63 -0
  138. package/dist/loader-registry.d.ts.map +1 -0
  139. package/dist/loaders/inline-pack.d.ts +72 -0
  140. package/dist/loaders/inline-pack.d.ts.map +1 -0
  141. package/dist/loaders/mesh-bin.d.ts +21 -0
  142. package/dist/loaders/mesh-bin.d.ts.map +1 -0
  143. package/dist/loaders/pack-artifact.d.ts +20 -0
  144. package/dist/loaders/pack-artifact.d.ts.map +1 -0
  145. package/dist/material/dependency-graph.d.ts +9 -0
  146. package/dist/material/dependency-graph.d.ts.map +1 -0
  147. package/dist/material/generation-cache.d.ts +24 -0
  148. package/dist/material/generation-cache.d.ts.map +1 -0
  149. package/dist/material/inspection.d.ts +24 -0
  150. package/dist/material/inspection.d.ts.map +1 -0
  151. package/dist/material/loader.d.ts +59 -0
  152. package/dist/material/loader.d.ts.map +1 -0
  153. package/dist/material/runtime-shader.d.ts +14 -0
  154. package/dist/material/runtime-shader.d.ts.map +1 -0
  155. package/dist/mesh-bin.d.ts +3 -0
  156. package/dist/mesh-bin.d.ts.map +1 -0
  157. package/dist/mipmap-generator.d.ts +132 -0
  158. package/dist/mipmap-generator.d.ts.map +1 -0
  159. package/dist/payload-validate.d.ts +60 -0
  160. package/dist/payload-validate.d.ts.map +1 -0
  161. package/dist/plugin.d.ts +16 -0
  162. package/dist/plugin.d.ts.map +1 -0
  163. package/dist/registry/artifact-io.d.ts +16 -0
  164. package/dist/registry/artifact-io.d.ts.map +1 -0
  165. package/dist/registry/asset-evidence.d.ts +13 -0
  166. package/dist/registry/asset-evidence.d.ts.map +1 -0
  167. package/dist/registry/catalog-state.d.ts +47 -0
  168. package/dist/registry/catalog-state.d.ts.map +1 -0
  169. package/dist/registry/catalog.d.ts +48 -0
  170. package/dist/registry/catalog.d.ts.map +1 -0
  171. package/dist/registry/instantiate.d.ts +153 -0
  172. package/dist/registry/instantiate.d.ts.map +1 -0
  173. package/dist/registry/load-by-guid.d.ts +247 -0
  174. package/dist/registry/load-by-guid.d.ts.map +1 -0
  175. package/dist/registry/load-state.d.ts +26 -0
  176. package/dist/registry/load-state.d.ts.map +1 -0
  177. package/dist/registry/runtime-projection.d.ts +16 -0
  178. package/dist/registry/runtime-projection.d.ts.map +1 -0
  179. package/dist/registry/scene-publication-fence.d.ts +38 -0
  180. package/dist/registry/scene-publication-fence.d.ts.map +1 -0
  181. package/dist/registry/validate-material.d.ts +77 -0
  182. package/dist/registry/validate-material.d.ts.map +1 -0
  183. package/dist/resolve-asset-handle.d.ts +43 -0
  184. package/dist/resolve-asset-handle.d.ts.map +1 -0
  185. package/dist/resolve-tileset-runtime.d.ts +23 -0
  186. package/dist/resolve-tileset-runtime.d.ts.map +1 -0
  187. package/dist/scene-handle-fields.d.ts +88 -0
  188. package/dist/scene-handle-fields.d.ts.map +1 -0
  189. package/dist/scene-payload.d.ts +15 -0
  190. package/dist/scene-payload.d.ts.map +1 -0
  191. package/dist/wire-default-loaders.d.ts +29 -0
  192. package/dist/wire-default-loaders.d.ts.map +1 -0
  193. package/package.json +22 -9
  194. package/src/__tests__/aabb.unit.test.ts +97 -0
  195. package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
  196. package/src/__tests__/artifact-io.unit.test.ts +127 -0
  197. package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
  198. package/src/__tests__/asset-evidence.unit.test.ts +32 -0
  199. package/src/__tests__/{asset-kind.test.ts → asset-kind.unit.test.ts} +1 -1
  200. package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
  201. package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
  202. package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
  203. package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
  204. package/src/__tests__/asset-registry.unit.test.ts +511 -0
  205. package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
  206. package/src/__tests__/catalog-replica.unit.test.ts +196 -0
  207. package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
  208. package/src/__tests__/catalog-source.unit.test.ts +126 -0
  209. package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
  210. package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
  211. package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
  212. package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
  213. package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
  214. package/src/__tests__/decode-image-bytes.test.ts +90 -0
  215. package/src/__tests__/decode-image-mime-owner.test.ts +2 -5
  216. package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
  217. package/src/__tests__/errors-asset.unit.test.ts +92 -0
  218. package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
  219. package/src/__tests__/instantiate-context.unit.test.ts +65 -0
  220. package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
  221. package/src/__tests__/instantiate.unit.test.ts +684 -0
  222. package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
  223. package/src/__tests__/loader-registry.unit.test.ts +101 -0
  224. package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
  225. package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
  226. package/src/__tests__/material-load-consumer.test-d.ts +42 -0
  227. package/src/__tests__/material-ready.integration.test.ts +508 -0
  228. package/src/__tests__/material-stale-generation.unit.test.ts +147 -0
  229. package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
  230. package/src/__tests__/mesh-bin.unit.test.ts +152 -0
  231. package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
  232. package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
  233. package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
  234. package/src/__tests__/payload-validate.unit.test.ts +285 -0
  235. package/src/__tests__/plugin.test.ts +55 -0
  236. package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
  237. package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
  238. package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
  239. package/src/__tests__/registry-load-state.unit.test.ts +35 -0
  240. package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
  241. package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
  242. package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
  243. package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
  244. package/src/__tests__/scene-payload.unit.test.ts +151 -0
  245. package/src/__tests__/scene-publication-fence.test.ts +129 -0
  246. package/src/__tests__/scriptable-pack-public-surface.test-d.ts +27 -0
  247. package/src/__tests__/static-rig.integration.test.ts +8 -0
  248. package/src/__tests__/validate-material.unit.test.ts +359 -0
  249. package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
  250. package/src/aabb.ts +107 -0
  251. package/src/asset-registry.ts +2045 -0
  252. package/src/builtin-asset-registry.ts +164 -0
  253. package/src/catalog-source.ts +14 -70
  254. package/src/decode-image-bytes.ts +126 -0
  255. package/src/dynamic-texture-store.ts +234 -0
  256. package/src/errors/asset.ts +347 -0
  257. package/src/handles.ts +186 -0
  258. package/src/image-error.ts +45 -0
  259. package/src/index.ts +183 -16
  260. package/src/internal.ts +1 -0
  261. package/src/loader-registry.ts +157 -0
  262. package/src/loaders/inline-pack.ts +837 -0
  263. package/src/loaders/mesh-bin.ts +304 -0
  264. package/src/loaders/pack-artifact.ts +665 -0
  265. package/src/material/dependency-graph.ts +46 -0
  266. package/src/material/generation-cache.ts +177 -0
  267. package/src/material/inspection.ts +46 -0
  268. package/src/material/loader.ts +326 -0
  269. package/src/material/runtime-shader.ts +133 -0
  270. package/src/mesh-bin.ts +5 -0
  271. package/src/mipmap-generator.ts +450 -0
  272. package/src/payload-validate.ts +573 -0
  273. package/src/plugin.ts +48 -0
  274. package/src/registry/artifact-io.ts +259 -0
  275. package/src/registry/asset-evidence.ts +51 -0
  276. package/src/registry/catalog-state.ts +409 -0
  277. package/src/registry/catalog.ts +369 -0
  278. package/src/registry/instantiate.ts +1056 -0
  279. package/src/registry/load-by-guid.ts +1657 -0
  280. package/src/registry/load-state.ts +115 -0
  281. package/src/registry/runtime-projection.ts +63 -0
  282. package/src/registry/scene-publication-fence.ts +256 -0
  283. package/src/registry/validate-material.ts +385 -0
  284. package/src/resolve-asset-handle.ts +147 -0
  285. package/src/resolve-tileset-runtime.ts +72 -0
  286. package/src/scene-handle-fields.ts +221 -0
  287. package/src/scene-payload.ts +207 -0
  288. package/src/wire-default-loaders.ts +79 -0
  289. package/dist/__tests__/asset-graph-red.integration.test.d.ts +0 -2
  290. package/dist/__tests__/asset-graph-red.integration.test.d.ts.map +0 -1
  291. package/dist/__tests__/asset-kind.test.d.ts +0 -2
  292. package/dist/__tests__/asset-kind.test.d.ts.map +0 -1
  293. package/dist/__tests__/asset-registry-core.integration.test.d.ts +0 -2
  294. package/dist/__tests__/asset-registry-core.integration.test.d.ts.map +0 -1
  295. package/dist/__tests__/asset-registry-public-api.test-d.d.ts +0 -2
  296. package/dist/__tests__/asset-registry-public-api.test-d.d.ts.map +0 -1
  297. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts +0 -2
  298. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts.map +0 -1
  299. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts +0 -2
  300. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts.map +0 -1
  301. package/dist/__tests__/catalog-session-red.unit.test.d.ts +0 -2
  302. package/dist/__tests__/catalog-session-red.unit.test.d.ts.map +0 -1
  303. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts +0 -2
  304. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts.map +0 -1
  305. package/src/__tests__/asset-graph-red.integration.test.ts +0 -113
  306. package/src/__tests__/asset-registry-core.integration.test.ts +0 -161
  307. package/src/__tests__/asset-registry-public-api.test-d.ts +0 -31
  308. package/src/__tests__/asset-runtime-core-lifecycle.integration.test.ts +0 -79
  309. package/src/__tests__/asset-runtime-snapshot.unit.test.ts +0 -23
  310. package/src/__tests__/catalog-session-red.unit.test.ts +0 -276
  311. package/src/__tests__/registry-lifecycle-red.integration.test.ts +0 -80
@@ -0,0 +1,508 @@
1
+ import {
2
+ type CookedMaterialRecord,
3
+ createMaterialArtifactDigest,
4
+ serializeCookedMaterialRecord,
5
+ } from '@forgeax/engine-pack';
6
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
7
+ import { ShaderRegistry } from '@forgeax/engine-shader';
8
+ import { describe, expect, it, vi } from 'vitest';
9
+ import { AssetRegistry } from '../asset-registry';
10
+ import { MaterialGenerationCache } from '../material/generation-cache';
11
+ import { inspectMaterialRuntime } from '../material/inspection.js';
12
+ import { createMaterialLoader, type MaterialPublication } from '../material/loader.js';
13
+ import { materialParametersToParamSchema } from '../material/runtime-shader.js';
14
+
15
+ const MATERIAL_GUID = 'mat-ready';
16
+ const ARTIFACT_BYTES = new TextEncoder().encode('published material artifact');
17
+ const PRODUCTION_GUID = '019f0000-0000-7000-8000-000000000701';
18
+
19
+ function cookedRecord(guid = MATERIAL_GUID): CookedMaterialRecord {
20
+ const artifactDigest = createMaterialArtifactDigest(ARTIFACT_BYTES);
21
+ return {
22
+ schemaVersion: 'material-cook/3',
23
+ guid,
24
+ materialGuid: guid,
25
+ publicationGeneration: 7,
26
+ specializationKey: 'material-specialization/7/ready',
27
+ artifactDigest,
28
+ sourceClosure: ['materials/ready.material.json', 'shaders/ready.wgsl'],
29
+ parameterContract: { parameters: [], values: {} },
30
+ resolved: { passes: [], parameters: [], values: {} },
31
+ refs: { parent: [], textures: [], samplers: [], modules: [] },
32
+ artifact: {
33
+ mediaType: 'text/wgsl',
34
+ path: `materials/${guid}/shader.wgsl`,
35
+ digest: artifactDigest,
36
+ bytes: ARTIFACT_BYTES,
37
+ },
38
+ receipt: {
39
+ schemaVersion: 'material-cook/3',
40
+ sourceClosure: ['materials/ready.material.json', 'shaders/ready.wgsl'],
41
+ profile: 'webgpu/v1',
42
+ compilerVersion: 'compiler/1',
43
+ identity: {
44
+ materialContractDigest: 'sha256:ready-contract',
45
+ sourceRevision: 'sha256:ready-source',
46
+ sourceClosureDigest: 'sha256:ready-closure',
47
+ layoutIdentity: 'sha256:ready-layout',
48
+ programIdentity: 'sha256:ready-program',
49
+ pipelineIdentity: 'sha256:ready-pipeline',
50
+ materialPublicationIdentity: 'sha256:ready-publication',
51
+ cookIdentity: 'sha256:ready-cook',
52
+ compilerFingerprint: 'sha256:ready-compiler',
53
+ wasm: {
54
+ sourceContentKey: 'unavailable',
55
+ artifactSha256: 'unavailable',
56
+ glueSha256: 'unavailable',
57
+ },
58
+ artifactDigest,
59
+ valueGeneration: 7,
60
+ dependencyGeneration: 7,
61
+ cookGeneration: 7,
62
+ },
63
+ derivedInterface: { layoutIdentity: 'sha256:ready-layout' },
64
+ },
65
+ };
66
+ }
67
+
68
+ function publication(
69
+ overrides: {
70
+ readonly record?: unknown;
71
+ readonly artifact?: MaterialPublication['artifact'] | null;
72
+ } = {},
73
+ ): MaterialPublication {
74
+ const record = cookedRecord();
75
+ return {
76
+ guid: MATERIAL_GUID,
77
+ record: overrides.record ?? record,
78
+ ...(overrides.artifact === null
79
+ ? {}
80
+ : { artifact: overrides.artifact ?? { bytes: ARTIFACT_BYTES } }),
81
+ };
82
+ }
83
+
84
+ function createLoader(loadPublication: () => Promise<MaterialPublication | undefined>) {
85
+ return createMaterialLoader({ loadPublication, loadReference: async () => true });
86
+ }
87
+
88
+ describe('material runtime readiness', () => {
89
+ it('preserves authored numeric defaults in the runtime parameter contract', () => {
90
+ expect(
91
+ materialParametersToParamSchema(
92
+ [
93
+ { name: 'baseColor', type: 'color', default: [1, 1, 1, 1] },
94
+ { name: 'roughness', type: 'f32', default: 0.5 },
95
+ { name: 'baseColorTexture', type: 'texture' },
96
+ ],
97
+ 'published::custom',
98
+ ),
99
+ ).toEqual([
100
+ { name: 'baseColor', type: 'color', default: [1, 1, 1, 1] },
101
+ { name: 'roughness', type: 'f32', default: 0.5 },
102
+ { name: 'baseColorTexture', type: 'texture2d' },
103
+ ]);
104
+ });
105
+
106
+ it('loads only a cooked record with its artifact and complete references', async () => {
107
+ const loader = createLoader(async () => publication());
108
+
109
+ const result = await loader.load({
110
+ guid: MATERIAL_GUID,
111
+ specializationKey: 'material-specialization/7/ready',
112
+ });
113
+
114
+ expect(result.status).toBe('Ready');
115
+ if (result.status !== 'Ready') return;
116
+ expect(result).toMatchObject({
117
+ materialGuid: MATERIAL_GUID,
118
+ publicationGeneration: 7,
119
+ specializationKey: 'material-specialization/7/ready',
120
+ artifactDigest: expect.stringMatching(/^sha256:/),
121
+ sourceClosure: ['materials/ready.material.json', 'shaders/ready.wgsl'],
122
+ parameterContract: { parameters: [], values: {} },
123
+ });
124
+ expect(inspectMaterialRuntime(result)).toMatchObject({
125
+ materialGuid: MATERIAL_GUID,
126
+ readiness: 'ready',
127
+ publicationGeneration: 7,
128
+ specializationKey: 'material-specialization/7/ready',
129
+ artifactDigest: expect.stringMatching(/^sha256:/),
130
+ sourceClosure: ['materials/ready.material.json', 'shaders/ready.wgsl'],
131
+ parameterContract: { parameters: [], values: {} },
132
+ status: 'Ready',
133
+ });
134
+ });
135
+
136
+ it('returns a structured missing-cook failure without compiling at runtime', async () => {
137
+ const loader = createLoader(async () => undefined);
138
+ const result = await loader.load({ guid: 'mat-missing', specializationKey: 'key-a' });
139
+
140
+ expect(result).toMatchObject({
141
+ status: 'Error',
142
+ error: { code: 'material-specialization-not-cooked' },
143
+ });
144
+ });
145
+
146
+ it.each([
147
+ ['artifact missing', () => ({ artifact: null }), 'asset-artifact-missing'],
148
+ [
149
+ 'artifact bytes tampered',
150
+ () => ({ artifact: { bytes: new TextEncoder().encode('tampered artifact') } }),
151
+ 'asset-artifact-integrity-mismatch',
152
+ ],
153
+ [
154
+ 'record field tampered',
155
+ () => ({ record: { ...cookedRecord(), publicationGeneration: undefined } }),
156
+ 'material-cook-record-invalid',
157
+ ],
158
+ ] as const)('rejects %s on the published tuple', async (_label, mutate, code) => {
159
+ const loader = createLoader(async () => publication(mutate()));
160
+ const result = await loader.load({
161
+ guid: MATERIAL_GUID,
162
+ specializationKey: 'material-specialization/7/ready',
163
+ });
164
+
165
+ expect(result).toMatchObject({ status: 'Error', error: { code } });
166
+ });
167
+
168
+ it('keeps publication generations distinct in the material cache', async () => {
169
+ const cache = new MaterialGenerationCache();
170
+ let calls = 0;
171
+ const load = async () => {
172
+ calls += 1;
173
+ return calls;
174
+ };
175
+
176
+ const first = await cache.resolve(MATERIAL_GUID, 'material-specialization/7/ready', load, 7);
177
+ const second = await cache.resolve(MATERIAL_GUID, 'material-specialization/7/ready', load, 8);
178
+
179
+ expect(first).toBe(1);
180
+ expect(second).toBe(2);
181
+ expect(calls).toBe(2);
182
+ });
183
+
184
+ it('loads the published record sidecar through the production GUID route', async () => {
185
+ const record = cookedRecord(PRODUCTION_GUID);
186
+ const packageUrl = `/materials/${PRODUCTION_GUID}.pack.json`;
187
+ const descriptor = {
188
+ path: record.artifact.path,
189
+ mediaType: record.artifact.mediaType,
190
+ byteLength: ARTIFACT_BYTES.byteLength,
191
+ integrity: { algorithm: 'sha256' as const, digest: record.artifactDigest },
192
+ };
193
+ const pack = {
194
+ schemaVersion: '2.0.0',
195
+ kind: 'internal-text-package',
196
+ assets: [
197
+ {
198
+ guid: PRODUCTION_GUID,
199
+ kind: 'material',
200
+ payload: {
201
+ kind: 'material',
202
+ passes: [{ name: 'Forward', program: { module: 'core/pbr' } }],
203
+ parameters: [],
204
+ values: {},
205
+ },
206
+ refs: [],
207
+ artifacts: { [record.artifact.path]: descriptor },
208
+ },
209
+ ],
210
+ };
211
+ const fetcher = vi.fn(async (input: RequestInfo | URL) => {
212
+ const url = String(input);
213
+ if (url.endsWith('/pack-index.json')) {
214
+ return new Response(
215
+ JSON.stringify([{ guid: PRODUCTION_GUID, packageUrl, kind: 'material' }]),
216
+ );
217
+ }
218
+ if (url.endsWith('.record.json')) {
219
+ return new Response(serializeCookedMaterialRecord(record), {
220
+ headers: { 'content-type': 'application/json' },
221
+ });
222
+ }
223
+ if (url.endsWith('.pack.json')) return new Response(JSON.stringify(pack));
224
+ if (url.endsWith('.wgsl')) return new Response(ARTIFACT_BYTES);
225
+ return new Response('not found', { status: 404 });
226
+ });
227
+ vi.stubGlobal('fetch', fetcher);
228
+
229
+ try {
230
+ const registry = new AssetRegistry({} as never);
231
+ registry.configurePackIndex('/pack-index.json');
232
+ const parsedGuid = AssetGuid.parse(PRODUCTION_GUID);
233
+ expect(parsedGuid.ok).toBe(true);
234
+ if (!parsedGuid.ok) return;
235
+ const payloadResult = await registry.loadByGuid(parsedGuid.value);
236
+
237
+ expect(payloadResult).toMatchObject({ ok: true });
238
+ expect(registry.getMaterialReadiness(PRODUCTION_GUID)).toMatchObject({
239
+ status: 'Ready',
240
+ materialGuid: PRODUCTION_GUID,
241
+ publicationGeneration: 7,
242
+ artifactDigest: record.artifactDigest,
243
+ });
244
+ expect(fetcher).toHaveBeenCalled();
245
+ } finally {
246
+ vi.unstubAllGlobals();
247
+ }
248
+ });
249
+
250
+ it('does not require a cooked artifact for an Engine-owned material module', async () => {
251
+ const packageUrl = `/materials/${PRODUCTION_GUID}.pack.json`;
252
+ const pack = {
253
+ schemaVersion: '2.0.0',
254
+ kind: 'internal-text-package',
255
+ assets: [
256
+ {
257
+ guid: PRODUCTION_GUID,
258
+ kind: 'material',
259
+ payload: {
260
+ kind: 'material',
261
+ passes: [{ name: 'Forward', program: { module: 'forgeax::default-standard-pbr' } }],
262
+ values: { baseColor: [1, 0.2, 0.05, 1] },
263
+ },
264
+ refs: [],
265
+ artifacts: {},
266
+ },
267
+ ],
268
+ };
269
+ vi.stubGlobal(
270
+ 'fetch',
271
+ vi.fn(async (input: RequestInfo | URL) => {
272
+ const url = String(input);
273
+ if (url.endsWith('/pack-index.json')) {
274
+ return new Response(
275
+ JSON.stringify([{ guid: PRODUCTION_GUID, packageUrl, kind: 'material' }]),
276
+ );
277
+ }
278
+ return new Response(JSON.stringify(pack));
279
+ }),
280
+ );
281
+
282
+ try {
283
+ const registry = new AssetRegistry({} as never);
284
+ registry.configurePackIndex('/pack-index.json');
285
+ const parsedGuid = AssetGuid.parse(PRODUCTION_GUID);
286
+ expect(parsedGuid.ok).toBe(true);
287
+ if (!parsedGuid.ok) return;
288
+
289
+ const result = await registry.loadByGuid(parsedGuid.value);
290
+
291
+ expect(result).toMatchObject({ ok: true, value: { kind: 'material' } });
292
+ expect(registry.getMaterialReadiness(PRODUCTION_GUID)).toBeUndefined();
293
+ } finally {
294
+ vi.unstubAllGlobals();
295
+ }
296
+ });
297
+
298
+ it('installs authored pass modules from MaterialReady without a host registration step', async () => {
299
+ const customShaderId = 'published::custom';
300
+ const baseRecord = cookedRecord(PRODUCTION_GUID);
301
+ const record: CookedMaterialRecord = {
302
+ ...baseRecord,
303
+ resolved: {
304
+ ...baseRecord.resolved,
305
+ passes: [{ name: 'Forward', program: { module: customShaderId } }],
306
+ },
307
+ refs: { ...baseRecord.refs, modules: [customShaderId] },
308
+ };
309
+ const packageUrl = `/materials/${PRODUCTION_GUID}.pack.json`;
310
+ const descriptor = {
311
+ path: record.artifact.path,
312
+ mediaType: record.artifact.mediaType,
313
+ byteLength: ARTIFACT_BYTES.byteLength,
314
+ integrity: { algorithm: 'sha256' as const, digest: record.artifactDigest },
315
+ };
316
+ const pack = {
317
+ schemaVersion: '2.0.0',
318
+ kind: 'internal-text-package',
319
+ assets: [
320
+ {
321
+ guid: PRODUCTION_GUID,
322
+ kind: 'material',
323
+ payload: {
324
+ kind: 'material',
325
+ passes: [{ name: 'Forward', program: { module: customShaderId } }],
326
+ parameters: [],
327
+ values: {},
328
+ },
329
+ refs: [],
330
+ artifacts: { [record.artifact.path]: descriptor },
331
+ },
332
+ ],
333
+ };
334
+ const fetcher = vi.fn(async (input: RequestInfo | URL) => {
335
+ const url = String(input);
336
+ if (url.endsWith('/pack-index.json')) {
337
+ return new Response(
338
+ JSON.stringify([{ guid: PRODUCTION_GUID, packageUrl, kind: 'material' }]),
339
+ );
340
+ }
341
+ if (url.endsWith('.record.json')) {
342
+ return new Response(serializeCookedMaterialRecord(record), {
343
+ headers: { 'content-type': 'application/json' },
344
+ });
345
+ }
346
+ if (url.endsWith('.pack.json')) return new Response(JSON.stringify(pack));
347
+ if (url.endsWith('.wgsl')) return new Response(ARTIFACT_BYTES);
348
+ return new Response('not found', { status: 404 });
349
+ });
350
+ vi.stubGlobal('fetch', fetcher);
351
+
352
+ const shaderRegistry = new ShaderRegistry({
353
+ device: {
354
+ createShaderModule: () => {
355
+ throw new Error('not used');
356
+ },
357
+ } as never,
358
+ manifestUrl: undefined,
359
+ });
360
+ try {
361
+ const registry = new AssetRegistry(shaderRegistry);
362
+ registry.configurePackIndex('/pack-index.json');
363
+ const parsedGuid = AssetGuid.parse(PRODUCTION_GUID);
364
+ expect(parsedGuid.ok).toBe(true);
365
+ if (!parsedGuid.ok) return;
366
+
367
+ const result = await registry.loadByGuid(parsedGuid.value);
368
+
369
+ expect(result).toMatchObject({ ok: true });
370
+ const lookup = shaderRegistry.findMaterialArtifact(customShaderId);
371
+ expect(lookup).toMatchObject({
372
+ ok: true,
373
+ value: { source: new TextDecoder().decode(ARTIFACT_BYTES), paramSchema: [] },
374
+ });
375
+ } finally {
376
+ vi.unstubAllGlobals();
377
+ }
378
+ });
379
+
380
+ it('installs authored pass modules from an inline cooked artifact', async () => {
381
+ const customShaderId = 'published::inline-custom';
382
+ const baseRecord = cookedRecord(PRODUCTION_GUID);
383
+ const record: CookedMaterialRecord = {
384
+ ...baseRecord,
385
+ resolved: {
386
+ ...baseRecord.resolved,
387
+ passes: [{ name: 'Forward', program: { module: customShaderId } }],
388
+ },
389
+ refs: { ...baseRecord.refs, modules: [customShaderId] },
390
+ };
391
+ const packageUrl = `/materials/${PRODUCTION_GUID}.inline.pack.json`;
392
+ const pack = {
393
+ schemaVersion: '2.0.0',
394
+ kind: 'internal-text-package',
395
+ assets: [
396
+ {
397
+ guid: PRODUCTION_GUID,
398
+ kind: 'material',
399
+ payload: {
400
+ kind: 'material',
401
+ passes: [{ name: 'Forward', program: { module: customShaderId } }],
402
+ parameters: [],
403
+ values: {},
404
+ cooked: JSON.parse(serializeCookedMaterialRecord(record)),
405
+ },
406
+ refs: [],
407
+ artifacts: {},
408
+ },
409
+ ],
410
+ };
411
+ vi.stubGlobal(
412
+ 'fetch',
413
+ vi.fn(async (input: RequestInfo | URL) => {
414
+ const url = String(input);
415
+ if (url.endsWith('/pack-index.json')) {
416
+ return new Response(
417
+ JSON.stringify([{ guid: PRODUCTION_GUID, packageUrl, kind: 'material' }]),
418
+ );
419
+ }
420
+ if (url.endsWith('.inline.pack.json')) return new Response(JSON.stringify(pack));
421
+ return new Response('not found', { status: 404 });
422
+ }),
423
+ );
424
+
425
+ const shaderRegistry = new ShaderRegistry({
426
+ device: {
427
+ createShaderModule: () => {
428
+ throw new Error('not used');
429
+ },
430
+ } as never,
431
+ manifestUrl: undefined,
432
+ });
433
+ try {
434
+ const registry = new AssetRegistry(shaderRegistry);
435
+ registry.configurePackIndex('/pack-index.json');
436
+ const parsedGuid = AssetGuid.parse(PRODUCTION_GUID);
437
+ expect(parsedGuid.ok).toBe(true);
438
+ if (!parsedGuid.ok) return;
439
+
440
+ const result = await registry.loadByGuid(parsedGuid.value);
441
+
442
+ expect(result).toMatchObject({ ok: true });
443
+ expect(shaderRegistry.findMaterialArtifact(customShaderId)).toMatchObject({
444
+ ok: true,
445
+ value: { source: new TextDecoder().decode(ARTIFACT_BYTES), paramSchema: [] },
446
+ });
447
+ } finally {
448
+ vi.unstubAllGlobals();
449
+ }
450
+ });
451
+
452
+ it('does not reinterpret the Pack descriptor base64 digest as a material digest', async () => {
453
+ const record = cookedRecord(PRODUCTION_GUID);
454
+ const packageUrl = `/materials/${PRODUCTION_GUID}.pack.json`;
455
+ const descriptor = {
456
+ path: record.artifact.path,
457
+ mediaType: record.artifact.mediaType,
458
+ byteLength: ARTIFACT_BYTES.byteLength,
459
+ integrity: {
460
+ algorithm: 'sha256' as const,
461
+ digest: 'xEjSZ79vI9pvhM0qf+ot+tnK32nBxZHXVAn4OYdaJ3M=',
462
+ },
463
+ };
464
+ const pack = {
465
+ schemaVersion: '2.0.0',
466
+ kind: 'internal-text-package',
467
+ assets: [
468
+ {
469
+ guid: PRODUCTION_GUID,
470
+ kind: 'material',
471
+ payload: {
472
+ kind: 'material',
473
+ passes: [{ name: 'Forward', program: { module: 'core/pbr' } }],
474
+ parameters: [],
475
+ values: {},
476
+ },
477
+ refs: [],
478
+ artifacts: { [record.artifact.path]: descriptor },
479
+ },
480
+ ],
481
+ };
482
+ const fetcher = vi.fn(async (input: RequestInfo | URL) => {
483
+ const url = String(input);
484
+ if (url.endsWith('/pack-index.json')) {
485
+ return new Response(
486
+ JSON.stringify([{ guid: PRODUCTION_GUID, packageUrl, kind: 'material' }]),
487
+ );
488
+ }
489
+ if (url.endsWith('.record.json')) return new Response(serializeCookedMaterialRecord(record));
490
+ if (url.endsWith('.pack.json')) return new Response(JSON.stringify(pack));
491
+ if (url.endsWith('.wgsl')) return new Response(ARTIFACT_BYTES);
492
+ return new Response('not found', { status: 404 });
493
+ });
494
+ vi.stubGlobal('fetch', fetcher);
495
+ try {
496
+ const registry = new AssetRegistry({} as never);
497
+ registry.configurePackIndex('/pack-index.json');
498
+ const parsedGuid = AssetGuid.parse(PRODUCTION_GUID);
499
+ expect(parsedGuid.ok).toBe(true);
500
+ if (!parsedGuid.ok) return;
501
+ const result = await registry.loadByGuid(parsedGuid.value);
502
+ expect(result).toMatchObject({ ok: true });
503
+ expect(registry.getMaterialReadiness(PRODUCTION_GUID)).toMatchObject({ status: 'Ready' });
504
+ } finally {
505
+ vi.unstubAllGlobals();
506
+ }
507
+ });
508
+ });
@@ -0,0 +1,147 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { type CookedMaterialRecord, createMaterialArtifactDigest } from '@forgeax/engine-pack';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { MaterialGenerationCache } from '../material/generation-cache.js';
5
+ import { createMaterialLoader } from '../material/loader.js';
6
+
7
+ const CUSTOM_SHADER_FIXTURE = new URL(
8
+ '../../../../apps/hello/custom-shader/assets/pulse-material.pack.json',
9
+ import.meta.url,
10
+ );
11
+
12
+ const DERIVED_MATERIAL_GUID = '01935b00-7d8c-7c4e-9f12-345678abcd03';
13
+ const SPECIALIZATION_KEY = 'my-game::pulse-material';
14
+ const SPECIALIZATION_DEPENDENCIES = ['my-game::pulse-material', 'pack:pulse-material'] as const;
15
+
16
+ function createCookedMaterialLoader() {
17
+ const fixture = JSON.parse(readFileSync(CUSTOM_SHADER_FIXTURE, 'utf8')) as {
18
+ assets: readonly { guid: string; payload?: { cooked?: unknown } }[];
19
+ };
20
+ const cookedByGuid = new Map(
21
+ fixture.assets.map((entry) => [entry.guid.toLowerCase(), entry.payload?.cooked]),
22
+ );
23
+ return createMaterialLoader({
24
+ loadPublication: async (guid) => {
25
+ const raw = cookedByGuid.get(guid.toLowerCase()) as CookedMaterialRecord | undefined;
26
+ if (raw === undefined) return undefined;
27
+ const bytes = new TextEncoder().encode('published pulse artifact');
28
+ const digest = createMaterialArtifactDigest(bytes);
29
+ return {
30
+ guid,
31
+ record: {
32
+ ...raw,
33
+ materialGuid: raw.guid,
34
+ publicationGeneration: 1,
35
+ specializationKey: SPECIALIZATION_KEY,
36
+ artifactDigest: digest,
37
+ sourceClosure: raw.receipt.sourceClosure,
38
+ parameterContract: { parameters: raw.resolved.parameters, values: raw.resolved.values },
39
+ artifact: { ...raw.artifact, digest, bytes },
40
+ receipt: {
41
+ ...raw.receipt,
42
+ identity: { ...raw.receipt.identity, artifactDigest: digest, cookGeneration: 1 },
43
+ },
44
+ },
45
+ artifact: { bytes },
46
+ };
47
+ },
48
+ loadReference: async () => true,
49
+ });
50
+ }
51
+
52
+ describe('material stale generation publication', () => {
53
+ it('retries once and reports the generation vector when dependencies change', async () => {
54
+ const cache = new MaterialGenerationCache();
55
+ let calls = 0;
56
+ const result = await cache.loadWithGeneration('mat-a', ['texture/a'], async (generation) => {
57
+ calls += 1;
58
+ if (calls === 1) cache.bump('texture/a');
59
+ return { generation, value: calls };
60
+ });
61
+
62
+ expect(result.ok).toBe(true);
63
+ expect(calls).toBe(2);
64
+ expect(cache.generationError('mat-a')).toBeUndefined();
65
+ });
66
+
67
+ it('exposes stale-generation details after the retry budget is exhausted', async () => {
68
+ const cache = new MaterialGenerationCache();
69
+ const result = await cache.loadWithGeneration('mat-a', ['texture/a'], async (generation) => {
70
+ cache.bump('texture/a');
71
+ return { generation, value: 1 };
72
+ });
73
+
74
+ expect(result).toMatchObject({
75
+ ok: false,
76
+ error: { code: 'material-specialization-stale-generation' },
77
+ });
78
+ });
79
+
80
+ it('retries a cooked material after the stale promise is rejected', async () => {
81
+ const cache = new MaterialGenerationCache();
82
+ const loader = createCookedMaterialLoader();
83
+ const dependencies: string[] = [...SPECIALIZATION_DEPENDENCIES];
84
+ const shaderDependency = SPECIALIZATION_DEPENDENCIES[0];
85
+ let unstable = true;
86
+ let loadCalls = 0;
87
+
88
+ const loadSpecialization = () =>
89
+ cache.loadWithGeneration(DERIVED_MATERIAL_GUID, dependencies, async (generation) => {
90
+ loadCalls += 1;
91
+ const loaded = await loader.load({
92
+ guid: DERIVED_MATERIAL_GUID,
93
+ specializationKey: SPECIALIZATION_KEY,
94
+ });
95
+ expect(loaded.status).toBe('Ready');
96
+ if (loaded.status !== 'Ready') throw new Error('fixture material did not load');
97
+ if (unstable) cache.bump(shaderDependency);
98
+ return { generation, value: loaded.record };
99
+ });
100
+
101
+ const stale = await cache.resolve(
102
+ DERIVED_MATERIAL_GUID,
103
+ SPECIALIZATION_KEY,
104
+ loadSpecialization,
105
+ );
106
+ expect(stale).toMatchObject({
107
+ ok: false,
108
+ error: { code: 'material-specialization-stale-generation' },
109
+ });
110
+ expect(loadCalls).toBe(2);
111
+ expect(cache.generationError(DERIVED_MATERIAL_GUID)?.detail).toMatchObject({
112
+ material: DERIVED_MATERIAL_GUID,
113
+ dependencies: [...SPECIALIZATION_DEPENDENCIES],
114
+ observed: {
115
+ dependencies: { [SPECIALIZATION_DEPENDENCIES[0]]: 1, [SPECIALIZATION_DEPENDENCIES[1]]: 0 },
116
+ },
117
+ current: {
118
+ dependencies: { [SPECIALIZATION_DEPENDENCIES[0]]: 2, [SPECIALIZATION_DEPENDENCIES[1]]: 0 },
119
+ },
120
+ });
121
+ const staleError = cache.generationError(DERIVED_MATERIAL_GUID);
122
+ expect(staleError?.code).toBe('material-specialization-stale-generation');
123
+ expect(staleError?.detail.code).toBe('material-specialization-stale-generation');
124
+ expect(Object.isFrozen(staleError?.detail)).toBe(true);
125
+ expect(Object.isFrozen(staleError?.detail.dependencies)).toBe(true);
126
+ expect(Object.isFrozen(staleError?.detail.observed.dependencies)).toBe(true);
127
+ expect(Object.isFrozen(staleError?.detail.current.dependencies)).toBe(true);
128
+ dependencies.push('mutated-after-publication');
129
+ expect(staleError?.detail.dependencies).toEqual([...SPECIALIZATION_DEPENDENCIES]);
130
+
131
+ const published: unknown[] = [];
132
+ if (stale.ok) published.push(stale.value);
133
+ expect(published).toHaveLength(0);
134
+
135
+ unstable = false;
136
+ const fresh = await cache.resolve(
137
+ DERIVED_MATERIAL_GUID,
138
+ SPECIALIZATION_KEY,
139
+ loadSpecialization,
140
+ );
141
+ expect(fresh.ok).toBe(true);
142
+ expect(loadCalls).toBe(3);
143
+ if (fresh.ok) published.push(fresh.value);
144
+ expect(published).toHaveLength(1);
145
+ expect(cache.generationError(DERIVED_MATERIAL_GUID)).toBeUndefined();
146
+ });
147
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { unpackMeshBinV4 } from '../loaders/mesh-bin';
3
+
4
+ describe('mesh-bin v4 morph validation', () => {
5
+ it('rejects trailing bytes instead of silently accepting a malformed artifact', () => {
6
+ const bytes = new Uint8Array(80 + 1);
7
+ const view = new DataView(bytes.buffer);
8
+ view.setUint32(0, 4, true);
9
+ expect(unpackMeshBinV4(bytes, 'gltf://morph-trailing').ok).toBe(false);
10
+ });
11
+
12
+ it('rejects malformed metadata with a structured recovery error', () => {
13
+ const bytes = new Uint8Array(80);
14
+ const view = new DataView(bytes.buffer);
15
+ view.setUint32(0, 4, true);
16
+ const result = unpackMeshBinV4(bytes, 'gltf://morph-invalid');
17
+ expect(result.ok).toBe(false);
18
+ if (result.ok) return;
19
+ expect(result.error.sourceKey).toBe('gltf://morph-invalid');
20
+ expect(result.error.recovery).toContain('re-cook');
21
+ });
22
+ });