@forgeax/engine-runtime 0.1.21 → 0.1.24

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 (410) hide show
  1. package/README.md +18 -9
  2. package/dist/__tests__/advance-animation-player.cases.unit.test.d.ts +2 -0
  3. package/dist/__tests__/advance-animation-player.cases.unit.test.d.ts.map +1 -0
  4. package/dist/__tests__/asset-registry-aabb.cases.unit.test.d.ts +2 -0
  5. package/dist/__tests__/asset-registry-aabb.cases.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/asset-registry-bootstrap.cases.unit.test.d.ts +2 -0
  7. package/dist/__tests__/asset-registry-bootstrap.cases.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/asset-registry-builtin-nineslice.cases.unit.test.d.ts +2 -0
  9. package/dist/__tests__/asset-registry-builtin-nineslice.cases.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/asset-registry-d9-tile-sampler-soft-warn.cases.unit.test.d.ts +2 -0
  11. package/dist/__tests__/asset-registry-d9-tile-sampler-soft-warn.cases.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/asset-registry-guid.cases.unit.test.d.ts +2 -0
  13. package/dist/__tests__/asset-registry-guid.cases.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/asset-registry-material-validate.cases.unit.test.d.ts +2 -0
  15. package/dist/__tests__/asset-registry-material-validate.cases.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/asset-registry-mesh-fail-fast.cases.unit.test.d.ts +2 -0
  17. package/dist/__tests__/asset-registry-mesh-fail-fast.cases.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/asset-registry-scene.cases.unit.test.d.ts +2 -0
  19. package/dist/__tests__/asset-registry-scene.cases.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/asset-registry-sprite-slices-validate.cases.unit.test.d.ts +2 -0
  21. package/dist/__tests__/asset-registry-sprite-slices-validate.cases.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/asset-renderer-assembly.cases.unit.test.d.ts +2 -0
  23. package/dist/__tests__/asset-renderer-assembly.cases.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/asset.cases.unit.test.d.ts +2 -0
  25. package/dist/__tests__/asset.cases.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/bloom-gating.cases.unit.test.d.ts +2 -0
  27. package/dist/__tests__/bloom-gating.cases.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/builtin-guid-ssot.cases.unit.test.d.ts +2 -0
  29. package/dist/__tests__/builtin-guid-ssot.cases.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/builtin-pack.cases.unit.test.d.ts +2 -0
  31. package/dist/__tests__/builtin-pack.cases.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/camera-antialias.cases.unit.test.d.ts +2 -0
  33. package/dist/__tests__/camera-antialias.cases.unit.test.d.ts.map +1 -0
  34. package/dist/__tests__/camera-bloom.cases.unit.test.d.ts +2 -0
  35. package/dist/__tests__/camera-bloom.cases.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/camera-clear-schema.cases.unit.test.d.ts +2 -0
  37. package/dist/__tests__/camera-clear-schema.cases.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/camera-ortho.cases.unit.test.d.ts +2 -0
  39. package/dist/__tests__/camera-ortho.cases.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/cluster-binner.cases.unit.test.d.ts +2 -0
  41. package/dist/__tests__/cluster-binner.cases.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/create-renderer-fallback-shader-manifest.cases.unit.test.d.ts +2 -0
  43. package/dist/__tests__/create-renderer-fallback-shader-manifest.cases.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/create-renderer-uniform-fallback.cases.unit.test.d.ts +2 -0
  45. package/dist/__tests__/create-renderer-uniform-fallback.cases.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/cubemap-upload.cases.unit.test.d.ts +2 -0
  47. package/dist/__tests__/cubemap-upload.cases.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/dev-import-transport.cases.unit.test.d.ts +2 -0
  49. package/dist/__tests__/dev-import-transport.cases.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/engine-metrics.cases.unit.test.d.ts +2 -0
  51. package/dist/__tests__/engine-metrics.cases.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/equirect-projection-error.cases.unit.test.d.ts +2 -0
  53. package/dist/__tests__/equirect-projection-error.cases.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/font-asset-load.cases.unit.test.d.ts +2 -0
  55. package/dist/__tests__/font-asset-load.cases.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/fxaa-intermediate-texture.cases.unit.test.d.ts +2 -0
  57. package/dist/__tests__/fxaa-intermediate-texture.cases.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/fxaa-pipeline.cases.unit.test.d.ts +2 -0
  59. package/dist/__tests__/fxaa-pipeline.cases.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/gpu-residency.cases.unit.test.d.ts +2 -0
  61. package/dist/__tests__/gpu-residency.cases.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/gpu-resource-release.cases.unit.test.d.ts +2 -0
  63. package/dist/__tests__/gpu-resource-release.cases.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/handle-quad.cases.unit.test.d.ts +2 -0
  65. package/dist/__tests__/handle-quad.cases.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/hdrp-bgl-slots.cases.unit.test.d.ts +2 -0
  67. package/dist/__tests__/hdrp-bgl-slots.cases.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/hdrp-grid-invalid.cases.unit.test.d.ts +2 -0
  69. package/dist/__tests__/hdrp-grid-invalid.cases.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/hdrp-index-list-overflow-once.cases.unit.test.d.ts +2 -0
  71. package/dist/__tests__/hdrp-index-list-overflow-once.cases.unit.test.d.ts.map +1 -0
  72. package/dist/__tests__/hdrp-light-budget.cases.unit.test.d.ts +2 -0
  73. package/dist/__tests__/hdrp-light-budget.cases.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/hdrp-pipeline-asset-config.cases.unit.test.d.ts +2 -0
  75. package/dist/__tests__/hdrp-pipeline-asset-config.cases.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
  77. package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -1
  78. package/dist/__tests__/helpers/renderer-device-loss-dawn.d.ts +25 -0
  79. package/dist/__tests__/helpers/renderer-device-loss-dawn.d.ts.map +1 -0
  80. package/dist/__tests__/helpers/standard-material-manifest.d.ts +0 -1
  81. package/dist/__tests__/helpers/standard-material-manifest.d.ts.map +1 -1
  82. package/dist/__tests__/ibl-pipeline-cache.cases.unit.test.d.ts +2 -0
  83. package/dist/__tests__/ibl-pipeline-cache.cases.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/lazy-catalog.cases.unit.test.d.ts +2 -0
  85. package/dist/__tests__/lazy-catalog.cases.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/load-by-guid-hdr.cases.unit.test.d.ts +2 -0
  87. package/dist/__tests__/load-by-guid-hdr.cases.unit.test.d.ts.map +1 -0
  88. package/dist/__tests__/load-by-guid-prod-material-parent.cases.unit.test.d.ts +2 -0
  89. package/dist/__tests__/load-by-guid-prod-material-parent.cases.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/load-by-guid-prod.cases.unit.test.d.ts +2 -0
  91. package/dist/__tests__/load-by-guid-prod.cases.unit.test.d.ts.map +1 -0
  92. package/dist/__tests__/loader-registry.cases.unit.test.d.ts +2 -0
  93. package/dist/__tests__/loader-registry.cases.unit.test.d.ts.map +1 -0
  94. package/dist/__tests__/m7-hdrp-demo-shape.cases.unit.test.d.ts +2 -0
  95. package/dist/__tests__/m7-hdrp-demo-shape.cases.unit.test.d.ts.map +1 -0
  96. package/dist/__tests__/mipmap-formula.cases.unit.test.d.ts +2 -0
  97. package/dist/__tests__/mipmap-formula.cases.unit.test.d.ts.map +1 -0
  98. package/dist/__tests__/mipmap-pipeline-cache.cases.unit.test.d.ts +2 -0
  99. package/dist/__tests__/mipmap-pipeline-cache.cases.unit.test.d.ts.map +1 -0
  100. package/dist/__tests__/parse-asset-payload-material.cases.unit.test.d.ts +2 -0
  101. package/dist/__tests__/parse-asset-payload-material.cases.unit.test.d.ts.map +1 -0
  102. package/dist/__tests__/parse-asset-payload-texture.cases.unit.test.d.ts +2 -0
  103. package/dist/__tests__/parse-asset-payload-texture.cases.unit.test.d.ts.map +1 -0
  104. package/dist/__tests__/parse-scene-payload-refs.cases.unit.test.d.ts +2 -0
  105. package/dist/__tests__/parse-scene-payload-refs.cases.unit.test.d.ts.map +1 -0
  106. package/dist/__tests__/pass-selector.cases.unit.test.d.ts +2 -0
  107. package/dist/__tests__/pass-selector.cases.unit.test.d.ts.map +1 -0
  108. package/dist/__tests__/pipeline-builder.cases.unit.test.d.ts +2 -0
  109. package/dist/__tests__/pipeline-builder.cases.unit.test.d.ts.map +1 -0
  110. package/dist/__tests__/pipeline-cache-key-topology.cases.unit.test.d.ts +2 -0
  111. package/dist/__tests__/pipeline-cache-key-topology.cases.unit.test.d.ts.map +1 -0
  112. package/dist/__tests__/pipeline-vertex-stride-branch.cases.unit.test.d.ts +2 -0
  113. package/dist/__tests__/pipeline-vertex-stride-branch.cases.unit.test.d.ts.map +1 -0
  114. package/dist/__tests__/pipeline.cases.unit.test.d.ts +2 -0
  115. package/dist/__tests__/pipeline.cases.unit.test.d.ts.map +1 -0
  116. package/dist/__tests__/post-process-register.cases.unit.test.d.ts +2 -0
  117. package/dist/__tests__/post-process-register.cases.unit.test.d.ts.map +1 -0
  118. package/dist/__tests__/propagate-transforms.cases.unit.test.d.ts +2 -0
  119. package/dist/__tests__/propagate-transforms.cases.unit.test.d.ts.map +1 -0
  120. package/dist/__tests__/record-all-topology.cases.unit.test.d.ts +2 -0
  121. package/dist/__tests__/record-all-topology.cases.unit.test.d.ts.map +1 -0
  122. package/dist/__tests__/record-draw-branch.cases.unit.test.d.ts +2 -0
  123. package/dist/__tests__/record-draw-branch.cases.unit.test.d.ts.map +1 -0
  124. package/dist/__tests__/record-strip-index-format.cases.unit.test.d.ts +2 -0
  125. package/dist/__tests__/record-strip-index-format.cases.unit.test.d.ts.map +1 -0
  126. package/dist/__tests__/register-with-guid-rgba16float.cases.unit.test.d.ts +2 -0
  127. package/dist/__tests__/register-with-guid-rgba16float.cases.unit.test.d.ts.map +1 -0
  128. package/dist/__tests__/render-data.cases.unit.test.d.ts +2 -0
  129. package/dist/__tests__/render-data.cases.unit.test.d.ts.map +1 -0
  130. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
  131. package/dist/__tests__/render-query-regression.cases.unit.test.d.ts +2 -0
  132. package/dist/__tests__/render-query-regression.cases.unit.test.d.ts.map +1 -0
  133. package/dist/__tests__/render-system-record-submesh.cases.unit.test.d.ts +2 -0
  134. package/dist/__tests__/render-system-record-submesh.cases.unit.test.d.ts.map +1 -0
  135. package/dist/__tests__/render-system-record-warn-once.cases.unit.test.d.ts +2 -0
  136. package/dist/__tests__/render-system-record-warn-once.cases.unit.test.d.ts.map +1 -0
  137. package/dist/__tests__/renderer-device-loss-multi-world.integration.test.d.ts +2 -0
  138. package/dist/__tests__/renderer-device-loss-multi-world.integration.test.d.ts.map +1 -0
  139. package/dist/__tests__/renderer-device-loss-provider-feasibility.dawn.test.d.ts +2 -0
  140. package/dist/__tests__/renderer-device-loss-provider-feasibility.dawn.test.d.ts.map +1 -0
  141. package/dist/__tests__/renderer-device-loss-recovery.dawn.test.d.ts +2 -0
  142. package/dist/__tests__/renderer-device-loss-recovery.dawn.test.d.ts.map +1 -0
  143. package/dist/__tests__/renderer-draw-world.cases.unit.test.d.ts +2 -0
  144. package/dist/__tests__/renderer-draw-world.cases.unit.test.d.ts.map +1 -0
  145. package/dist/__tests__/renderer-recovery-consumer.integration.test.d.ts +2 -0
  146. package/dist/__tests__/renderer-recovery-consumer.integration.test.d.ts.map +1 -0
  147. package/dist/__tests__/renderstate-pipeline-cache.cases.unit.test.d.ts +2 -0
  148. package/dist/__tests__/renderstate-pipeline-cache.cases.unit.test.d.ts.map +1 -0
  149. package/dist/__tests__/resolve-scene-guids.cases.unit.test.d.ts +2 -0
  150. package/dist/__tests__/resolve-scene-guids.cases.unit.test.d.ts.map +1 -0
  151. package/dist/__tests__/shadow-skip-non-triangle.cases.unit.test.d.ts +2 -0
  152. package/dist/__tests__/shadow-skip-non-triangle.cases.unit.test.d.ts.map +1 -0
  153. package/dist/__tests__/skin-errors-kebab-case.cases.unit.test.d.ts +2 -0
  154. package/dist/__tests__/skin-errors-kebab-case.cases.unit.test.d.ts.map +1 -0
  155. package/dist/__tests__/skin-instances-coexist.cases.unit.test.d.ts +2 -0
  156. package/dist/__tests__/skin-instances-coexist.cases.unit.test.d.ts.map +1 -0
  157. package/dist/__tests__/skin-m2-extract-happy.cases.unit.test.d.ts +2 -0
  158. package/dist/__tests__/skin-m2-extract-happy.cases.unit.test.d.ts.map +1 -0
  159. package/dist/__tests__/skin-m2-extract-joint-error.cases.unit.test.d.ts +2 -0
  160. package/dist/__tests__/skin-m2-extract-joint-error.cases.unit.test.d.ts.map +1 -0
  161. package/dist/__tests__/skin-m2-extract-null-assets.cases.unit.test.d.ts +2 -0
  162. package/dist/__tests__/skin-m2-extract-null-assets.cases.unit.test.d.ts.map +1 -0
  163. package/dist/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.d.ts +2 -0
  164. package/dist/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.d.ts.map +1 -0
  165. package/dist/__tests__/skin-m3-record-cache.cases.unit.test.d.ts +2 -0
  166. package/dist/__tests__/skin-m3-record-cache.cases.unit.test.d.ts.map +1 -0
  167. package/dist/__tests__/skin-palette-extract.cases.unit.test.d.ts +2 -0
  168. package/dist/__tests__/skin-palette-extract.cases.unit.test.d.ts.map +1 -0
  169. package/dist/__tests__/skin-pipeline-routing.cases.unit.test.d.ts +2 -0
  170. package/dist/__tests__/skin-pipeline-routing.cases.unit.test.d.ts.map +1 -0
  171. package/dist/__tests__/skin.cases.unit.test.d.ts +2 -0
  172. package/dist/__tests__/skin.cases.unit.test.d.ts.map +1 -0
  173. package/dist/__tests__/skybox-extract.cases.unit.test.d.ts +2 -0
  174. package/dist/__tests__/skybox-extract.cases.unit.test.d.ts.map +1 -0
  175. package/dist/__tests__/skybox-shader-compile.cases.unit.test.d.ts +2 -0
  176. package/dist/__tests__/skybox-shader-compile.cases.unit.test.d.ts.map +1 -0
  177. package/dist/__tests__/skylight-bind-group.cases.unit.test.d.ts +2 -0
  178. package/dist/__tests__/skylight-bind-group.cases.unit.test.d.ts.map +1 -0
  179. package/dist/__tests__/skylight-component.cases.unit.test.d.ts +2 -0
  180. package/dist/__tests__/skylight-component.cases.unit.test.d.ts.map +1 -0
  181. package/dist/__tests__/skylight-fallback-path.cases.unit.test.d.ts +2 -0
  182. package/dist/__tests__/skylight-fallback-path.cases.unit.test.d.ts.map +1 -0
  183. package/dist/__tests__/skylight-pipeline-layout.cases.unit.test.d.ts +2 -0
  184. package/dist/__tests__/skylight-pipeline-layout.cases.unit.test.d.ts.map +1 -0
  185. package/dist/__tests__/skylight-solid-color-extract.cases.unit.test.d.ts +2 -0
  186. package/dist/__tests__/skylight-solid-color-extract.cases.unit.test.d.ts.map +1 -0
  187. package/dist/__tests__/sprite-animation-tick-boundary.cases.unit.test.d.ts +2 -0
  188. package/dist/__tests__/sprite-animation-tick-boundary.cases.unit.test.d.ts.map +1 -0
  189. package/dist/__tests__/sprite-animation-tick-clamp.cases.unit.test.d.ts +2 -0
  190. package/dist/__tests__/sprite-animation-tick-clamp.cases.unit.test.d.ts.map +1 -0
  191. package/dist/__tests__/sprite-animation-tick-frame-duration-negative.cases.unit.test.d.ts +2 -0
  192. package/dist/__tests__/sprite-animation-tick-frame-duration-negative.cases.unit.test.d.ts.map +1 -0
  193. package/dist/__tests__/sprite-animation-tick-frame-duration-zero.cases.unit.test.d.ts +2 -0
  194. package/dist/__tests__/sprite-animation-tick-frame-duration-zero.cases.unit.test.d.ts.map +1 -0
  195. package/dist/__tests__/sprite-animation-tick-loop.cases.unit.test.d.ts +2 -0
  196. package/dist/__tests__/sprite-animation-tick-loop.cases.unit.test.d.ts.map +1 -0
  197. package/dist/__tests__/sprite-animation-tick-override-probe.cases.unit.test.d.ts +2 -0
  198. package/dist/__tests__/sprite-animation-tick-override-probe.cases.unit.test.d.ts.map +1 -0
  199. package/dist/__tests__/sprite-animation-tick-regions-mismatch.cases.unit.test.d.ts +2 -0
  200. package/dist/__tests__/sprite-animation-tick-regions-mismatch.cases.unit.test.d.ts.map +1 -0
  201. package/dist/__tests__/storage-buffer-caps.cases.unit.test.d.ts +2 -0
  202. package/dist/__tests__/storage-buffer-caps.cases.unit.test.d.ts.map +1 -0
  203. package/dist/__tests__/systems.cases.unit.test.d.ts +2 -0
  204. package/dist/__tests__/systems.cases.unit.test.d.ts.map +1 -0
  205. package/dist/__tests__/tonemap-hdr-target.cases.unit.test.d.ts +2 -0
  206. package/dist/__tests__/tonemap-hdr-target.cases.unit.test.d.ts.map +1 -0
  207. package/dist/__tests__/tonemap-pipeline-split.cases.unit.test.d.ts +2 -0
  208. package/dist/__tests__/tonemap-pipeline-split.cases.unit.test.d.ts.map +1 -0
  209. package/dist/__tests__/tonemap.cases.unit.test.d.ts +2 -0
  210. package/dist/__tests__/tonemap.cases.unit.test.d.ts.map +1 -0
  211. package/dist/__tests__/transparent-sort-config-get.cases.unit.test.d.ts +2 -0
  212. package/dist/__tests__/transparent-sort-config-get.cases.unit.test.d.ts.map +1 -0
  213. package/dist/__tests__/transparent-sort-config-set.cases.unit.test.d.ts +2 -0
  214. package/dist/__tests__/transparent-sort-config-set.cases.unit.test.d.ts.map +1 -0
  215. package/dist/__tests__/transparent-sort.cases.unit.test.d.ts +2 -0
  216. package/dist/__tests__/transparent-sort.cases.unit.test.d.ts.map +1 -0
  217. package/dist/__tests__/upload-texture-consistency.cases.unit.test.d.ts +2 -0
  218. package/dist/__tests__/upload-texture-consistency.cases.unit.test.d.ts.map +1 -0
  219. package/dist/__tests__/verify-revisions.cases.unit.test.d.ts +2 -0
  220. package/dist/__tests__/verify-revisions.cases.unit.test.d.ts.map +1 -0
  221. package/dist/__tests__/wire-default-loaders.cases.unit.test.d.ts +2 -0
  222. package/dist/__tests__/wire-default-loaders.cases.unit.test.d.ts.map +1 -0
  223. package/dist/__tests__/zero-camera-clear-fallback.cases.unit.test.d.ts +2 -0
  224. package/dist/__tests__/zero-camera-clear-fallback.cases.unit.test.d.ts.map +1 -0
  225. package/dist/createRenderer.d.ts.map +1 -1
  226. package/dist/dev-import-transport.d.ts.map +1 -1
  227. package/dist/index.mjs +4 -3
  228. package/dist/index.mjs.map +1 -1
  229. package/dist/sprite-param-values.d.ts +1 -1
  230. package/package.json +30 -30
  231. package/src/__tests__/advance-animation-player.cases.unit.test.ts +1666 -0
  232. package/src/__tests__/animation-graph-asset-union.test-d.ts +2 -0
  233. package/src/__tests__/asset-brand-and-name-retirement.test.ts +4 -3
  234. package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +676 -0
  235. package/src/__tests__/asset-registry-bootstrap.cases.unit.test.ts +472 -0
  236. package/src/__tests__/asset-registry-builtin-nineslice.cases.unit.test.ts +481 -0
  237. package/src/__tests__/asset-registry-d9-tile-sampler-soft-warn.cases.unit.test.ts +568 -0
  238. package/src/__tests__/asset-registry-guid.cases.unit.test.ts +500 -0
  239. package/src/__tests__/asset-registry-material-validate.cases.unit.test.ts +756 -0
  240. package/src/__tests__/asset-registry-mesh-fail-fast.cases.unit.test.ts +591 -0
  241. package/src/__tests__/asset-registry-scene.cases.unit.test.ts +501 -0
  242. package/src/__tests__/asset-registry-scene.test-d.ts +1 -0
  243. package/src/__tests__/asset-registry-sprite-slices-validate.cases.unit.test.ts +536 -0
  244. package/src/__tests__/asset-renderer-assembly.cases.unit.test.ts +496 -0
  245. package/src/__tests__/asset.cases.unit.test.ts +439 -0
  246. package/src/__tests__/bind-group-cache-keying.test.ts +6 -2
  247. package/src/__tests__/bloom-gating.cases.unit.test.ts +428 -0
  248. package/src/__tests__/builtin-guid-ssot.cases.unit.test.ts +472 -0
  249. package/src/__tests__/builtin-pack.cases.unit.test.ts +639 -0
  250. package/src/__tests__/builtin-shader-register-e2e.test.ts +1 -1
  251. package/src/__tests__/camera-antialias.cases.unit.test.ts +372 -0
  252. package/src/__tests__/camera-bloom.cases.unit.test.ts +396 -0
  253. package/src/__tests__/camera-clear-schema.cases.unit.test.ts +391 -0
  254. package/src/__tests__/camera-ortho.cases.unit.test.ts +484 -0
  255. package/src/__tests__/cluster-binner.cases.unit.test.ts +817 -0
  256. package/src/__tests__/collect-scene-mount-collapse.test.ts +5 -1
  257. package/src/__tests__/collect-transient-roundtrip.test.ts +14 -3
  258. package/src/__tests__/components.test-d.ts +9 -6
  259. package/src/__tests__/components.unit.test.ts +23 -19
  260. package/src/__tests__/create-renderer-fallback-shader-manifest.cases.unit.test.ts +345 -0
  261. package/src/__tests__/create-renderer-uniform-fallback.cases.unit.test.ts +445 -0
  262. package/src/__tests__/cubemap-upload.cases.unit.test.ts +593 -0
  263. package/src/__tests__/dawn/ibl-precompute-readback.dawn.test.ts +184 -17
  264. package/src/__tests__/dawn/material-alpha.dawn.test.ts +17 -4
  265. package/src/__tests__/derive-bgl-integration.test.ts +8 -8
  266. package/src/__tests__/dev-import-transport.cases.unit.test.ts +503 -0
  267. package/src/__tests__/engine-metrics.cases.unit.test.ts +371 -0
  268. package/src/__tests__/equirect-projection-error.cases.unit.test.ts +416 -0
  269. package/src/__tests__/errors.unit.test.ts +3 -0
  270. package/src/__tests__/extract-frames-merge.test.ts +3 -0
  271. package/src/__tests__/font-asset-load.cases.unit.test.ts +510 -0
  272. package/src/__tests__/fxaa-intermediate-texture.cases.unit.test.ts +690 -0
  273. package/src/__tests__/fxaa-pipeline.cases.unit.test.ts +546 -0
  274. package/src/__tests__/gpu-residency.cases.unit.test.ts +479 -0
  275. package/src/__tests__/gpu-resource-release.cases.unit.test.ts +929 -0
  276. package/src/__tests__/handle-quad.cases.unit.test.ts +491 -0
  277. package/src/__tests__/hdrp-bgl-slots.cases.unit.test.ts +451 -0
  278. package/src/__tests__/hdrp-grid-invalid.cases.unit.test.ts +408 -0
  279. package/src/__tests__/hdrp-index-list-overflow-once.cases.unit.test.ts +405 -0
  280. package/src/__tests__/hdrp-light-budget.cases.unit.test.ts +483 -0
  281. package/src/__tests__/hdrp-pipeline-asset-config.cases.unit.test.ts +441 -0
  282. package/src/__tests__/helpers/register-runtime-components.ts +9 -1
  283. package/src/__tests__/helpers/register-scene-components.ts +9 -2
  284. package/src/__tests__/helpers/renderer-device-loss-dawn.ts +69 -0
  285. package/src/__tests__/helpers/standard-material-manifest.ts +11 -15
  286. package/src/__tests__/ibl-pipeline-cache.cases.unit.test.ts +415 -0
  287. package/src/__tests__/lazy-catalog.cases.unit.test.ts +848 -0
  288. package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +14 -15
  289. package/src/__tests__/light-snapshot.test-d.ts +27 -1
  290. package/src/__tests__/lights-preservation.fixup.test.ts +19 -21
  291. package/src/__tests__/lights.unit.test.ts +2076 -1
  292. package/src/__tests__/load-by-guid-hdr.cases.unit.test.ts +582 -0
  293. package/src/__tests__/load-by-guid-prod-material-parent.cases.unit.test.ts +1514 -0
  294. package/src/__tests__/load-by-guid-prod.cases.unit.test.ts +557 -0
  295. package/src/__tests__/loader-name-survival.test.ts +8 -9
  296. package/src/__tests__/loader-registry.cases.unit.test.ts +767 -0
  297. package/src/__tests__/m7-hdrp-demo-shape.cases.unit.test.ts +361 -0
  298. package/src/__tests__/material-texture-uv-scale.unit.test.ts +2 -2
  299. package/src/__tests__/materials.unit.test.ts +29 -10
  300. package/src/__tests__/mipmap-formula.cases.unit.test.ts +434 -0
  301. package/src/__tests__/mipmap-pipeline-cache.cases.unit.test.ts +490 -0
  302. package/src/__tests__/package-promotion.test.ts +7 -11
  303. package/src/__tests__/parse-asset-payload-material.cases.unit.test.ts +565 -0
  304. package/src/__tests__/parse-asset-payload-texture.cases.unit.test.ts +720 -0
  305. package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +767 -0
  306. package/src/__tests__/pass-selector.cases.unit.test.ts +376 -0
  307. package/src/__tests__/pbr-pipeline.unit.test.ts +39 -21
  308. package/src/__tests__/pipeline-builder.cases.unit.test.ts +624 -0
  309. package/src/__tests__/pipeline-cache-key-topology.cases.unit.test.ts +807 -0
  310. package/src/__tests__/pipeline-vertex-stride-branch.cases.unit.test.ts +379 -0
  311. package/src/__tests__/pipeline.cases.unit.test.ts +370 -0
  312. package/src/__tests__/point-light-shadow.browser.test.ts +1 -0
  313. package/src/__tests__/point-light-shadow.unit.test.ts +18 -98
  314. package/src/__tests__/post-process-register.cases.unit.test.ts +405 -0
  315. package/src/__tests__/propagate-transforms.cases.unit.test.ts +502 -0
  316. package/src/__tests__/propagate-transforms.dawn.test.ts +34 -18
  317. package/src/__tests__/record-all-topology.cases.unit.test.ts +608 -0
  318. package/src/__tests__/record-draw-branch.cases.unit.test.ts +621 -0
  319. package/src/__tests__/record-strip-index-format.cases.unit.test.ts +419 -0
  320. package/src/__tests__/record-worldid-isolation.test.ts +16 -32
  321. package/src/__tests__/register-with-guid-rgba16float.cases.unit.test.ts +469 -0
  322. package/src/__tests__/render-data.cases.unit.test.ts +553 -0
  323. package/src/__tests__/render-error-exhaustive.test-d.ts +11 -0
  324. package/src/__tests__/render-query-regression.cases.unit.test.ts +531 -0
  325. package/src/__tests__/render-system-extract.test.ts +5 -5
  326. package/src/__tests__/render-system-record-multi-material-textureview.test.ts +1 -0
  327. package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +1 -0
  328. package/src/__tests__/render-system-record-submesh.cases.unit.test.ts +740 -0
  329. package/src/__tests__/render-system-record-warn-once.cases.unit.test.ts +418 -0
  330. package/src/__tests__/render-system-record.test.ts +15 -33
  331. package/src/__tests__/renderer-device-loss-multi-world.integration.test.ts +103 -0
  332. package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +124 -0
  333. package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +50 -0
  334. package/src/__tests__/renderer-draw-world.cases.unit.test.ts +514 -0
  335. package/src/__tests__/renderer-recovery-consumer.integration.test.ts +96 -0
  336. package/src/__tests__/renderer-surface.unit.test.ts +0 -2
  337. package/src/__tests__/renderstate-pipeline-cache.cases.unit.test.ts +631 -0
  338. package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +1123 -0
  339. package/src/__tests__/resolveName-unified-entry.test.ts +6 -6
  340. package/src/__tests__/resolveName.test.ts +14 -2
  341. package/src/__tests__/shadow-caster-empty-schema.test.ts +1 -1
  342. package/src/__tests__/shadow-csm-cascade-loadop.test.ts +1 -0
  343. package/src/__tests__/shadow-csm-tile-consistency.test.ts +1 -0
  344. package/src/__tests__/shadow-csm-ubo.test.ts +3 -1
  345. package/src/__tests__/shadow-fields-observable.dawn.test.ts +15 -4
  346. package/src/__tests__/shadow-m1.dawn.test.ts +1 -1
  347. package/src/__tests__/shadow-skip-non-triangle.cases.unit.test.ts +681 -0
  348. package/src/__tests__/skin-errors-kebab-case.cases.unit.test.ts +475 -0
  349. package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +5 -4
  350. package/src/__tests__/skin-instances-coexist.cases.unit.test.ts +375 -0
  351. package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +592 -0
  352. package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +619 -0
  353. package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +569 -0
  354. package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +585 -0
  355. package/src/__tests__/skin-m3-record-cache.cases.unit.test.ts +457 -0
  356. package/src/__tests__/skin-palette-extract.cases.unit.test.ts +635 -0
  357. package/src/__tests__/skin-parented-double-transform.test.ts +5 -5
  358. package/src/__tests__/skin-pipeline-routing.cases.unit.test.ts +411 -0
  359. package/src/__tests__/skin.cases.unit.test.ts +421 -0
  360. package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +1 -6
  361. package/src/__tests__/skybox-extract.cases.unit.test.ts +511 -0
  362. package/src/__tests__/skybox-shader-compile.cases.unit.test.ts +437 -0
  363. package/src/__tests__/skylight-bind-group.cases.unit.test.ts +642 -0
  364. package/src/__tests__/skylight-component.cases.unit.test.ts +386 -0
  365. package/src/__tests__/skylight-fallback-path.cases.unit.test.ts +566 -0
  366. package/src/__tests__/skylight-lazy-projection.test.ts +35 -20
  367. package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +506 -0
  368. package/src/__tests__/skylight-solid-color-extract.cases.unit.test.ts +399 -0
  369. package/src/__tests__/sprite-animation-tick-boundary.cases.unit.test.ts +521 -0
  370. package/src/__tests__/sprite-animation-tick-clamp.cases.unit.test.ts +453 -0
  371. package/src/__tests__/sprite-animation-tick-frame-duration-negative.cases.unit.test.ts +447 -0
  372. package/src/__tests__/sprite-animation-tick-frame-duration-zero.cases.unit.test.ts +395 -0
  373. package/src/__tests__/sprite-animation-tick-loop.cases.unit.test.ts +494 -0
  374. package/src/__tests__/sprite-animation-tick-override-probe.cases.unit.test.ts +439 -0
  375. package/src/__tests__/sprite-animation-tick-regions-mismatch.cases.unit.test.ts +466 -0
  376. package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +16 -18
  377. package/src/__tests__/sprite-lit-pipeline-cache-key.test.ts +1 -1
  378. package/src/__tests__/ssao-bgl.test.ts +2 -1
  379. package/src/__tests__/ssao-pipelines.test.ts +1 -1
  380. package/src/__tests__/storage-buffer-caps.cases.unit.test.ts +373 -0
  381. package/src/__tests__/systems.cases.unit.test.ts +425 -0
  382. package/src/__tests__/template-game-default-api-contract.test.ts +45 -23
  383. package/src/__tests__/tilemap-chunk-extract.overhang-bounds.test.ts +1 -1
  384. package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +10 -4
  385. package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +670 -0
  386. package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +867 -0
  387. package/src/__tests__/tonemap.cases.unit.test.ts +446 -0
  388. package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +2 -2
  389. package/src/__tests__/transparent-sort-config-get.cases.unit.test.ts +395 -0
  390. package/src/__tests__/transparent-sort-config-set.cases.unit.test.ts +424 -0
  391. package/src/__tests__/transparent-sort.cases.unit.test.ts +684 -0
  392. package/src/__tests__/upload-texture-consistency.cases.unit.test.ts +478 -0
  393. package/src/__tests__/verify-revisions.cases.unit.test.ts +619 -0
  394. package/src/__tests__/wire-default-loaders.cases.unit.test.ts +461 -0
  395. package/src/__tests__/zero-camera-clear-fallback.cases.unit.test.ts +374 -0
  396. package/src/collect-scene-asset.ts +1 -1
  397. package/src/components/__tests__/transform-vec-schema.test.ts +2 -2
  398. package/src/createRenderer.ts +1 -0
  399. package/src/dev-import-transport.ts +2 -1
  400. package/src/sprite-param-values.ts +1 -1
  401. package/dist/.tsbuildinfo +0 -1
  402. package/dist/__tests__/asset.unit.test.d.ts +0 -2
  403. package/dist/__tests__/asset.unit.test.d.ts.map +0 -1
  404. package/dist/__tests__/pipeline.unit.test.d.ts +0 -2
  405. package/dist/__tests__/pipeline.unit.test.d.ts.map +0 -1
  406. package/dist/__tests__/systems.unit.test.d.ts +0 -2
  407. package/dist/__tests__/systems.unit.test.d.ts.map +0 -1
  408. package/src/__tests__/asset.unit.test.ts +0 -7110
  409. package/src/__tests__/pipeline.unit.test.ts +0 -5283
  410. package/src/__tests__/systems.unit.test.ts +0 -9210
@@ -0,0 +1,1514 @@
1
+ // @ts-nocheck — merged file: cross-source type narrowing failures from blocks originally outside src/ rootDir
2
+ // Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
3
+ // biome-ignore-all lint/complexity/noUselessLoneBlockStatements: block-scope isolation between merged source files (consolidation paradigm)
4
+ //
5
+ // Source files (N=33):
6
+ // - packages/runtime/__tests__/asset-registry.test.ts
7
+ // - packages/runtime/__tests__/loader-registry.test.ts
8
+ // - packages/runtime/__tests__/wire-default-loaders.test.ts
9
+ // - packages/runtime/src/__tests__/asset-registry-aabb.test.ts
10
+ // - packages/runtime/src/__tests__/asset-registry-builtin-nineslice.test.ts
11
+ // - packages/runtime/src/__tests__/asset-registry-d9-tile-sampler-soft-warn.test.ts
12
+ // - packages/runtime/src/__tests__/asset-registry-guid.test.ts
13
+ // - packages/runtime/src/__tests__/asset-registry-material-validate.test.ts
14
+ // - packages/runtime/src/__tests__/asset-registry-mesh-fail-fast.test.ts
15
+ // - packages/runtime/src/__tests__/asset-registry-scene.test.ts
16
+ // - packages/runtime/src/__tests__/asset-registry-sprite-slices-validate.test.ts
17
+ // - packages/runtime/src/__tests__/auto-select.test.ts
18
+ // - packages/runtime/src/__tests__/bindgroup-resize-invalidation.test.ts
19
+ // - packages/runtime/src/__tests__/builtin-guid-ssot.test.ts
20
+ // - packages/runtime/src/__tests__/builtin-pack.test.ts
21
+ // - packages/runtime/src/__tests__/cube-texture-narrowing.test.ts
22
+ // - packages/runtime/src/__tests__/cubemap-upload.test.ts
23
+ // - packages/runtime/src/__tests__/dev-import-transport.test.ts
24
+ // - packages/runtime/src/__tests__/font-asset-load.test.ts
25
+ // - packages/runtime/src/__tests__/handle-quad.test.ts
26
+ // - packages/runtime/src/__tests__/lazy-catalog.test.ts
27
+ // - packages/runtime/src/__tests__/load-by-guid-hdr.test.ts
28
+ // - packages/runtime/src/__tests__/load-by-guid-prod-material-parent.test.ts
29
+ // - packages/runtime/src/__tests__/load-by-guid-prod.test.ts
30
+ // - packages/runtime/src/__tests__/mipmap-formula.test.ts
31
+ // - packages/runtime/src/__tests__/mipmap-pipeline-cache.test.ts
32
+ // - packages/runtime/src/__tests__/parse-asset-payload-material.test.ts
33
+ // - packages/runtime/src/__tests__/parse-asset-payload-texture.test.ts
34
+ // - packages/runtime/src/__tests__/parse-scene-payload-refs.test.ts
35
+ // - packages/runtime/src/__tests__/register-with-guid-rgba16float.test.ts
36
+ // - packages/runtime/src/__tests__/resolve-scene-guids.test.ts
37
+ // - packages/runtime/src/__tests__/upload-texture-consistency.test.ts
38
+ // - packages/runtime/src/__tests__/verify-revisions.test.ts
39
+ //
40
+ // Paradigm: each block-scope wraps a source file. ancestorTitles[0] is the
41
+ // source-preserved inner describe (NOT the source filename for these 3 files
42
+ // — recovery path: vitest report ancestorTitles -> grep this file -> upstream
43
+ // `// ─── from <name>.test.ts ───` block separator -> source filename).
44
+ // Top-level imports merged + deduped.
45
+
46
+ import { readFileSync } from 'node:fs';
47
+ import { fileURLToPath } from 'node:url';
48
+ import * as assetRegistryModule from '@forgeax/engine-assets-runtime';
49
+ import {
50
+ AssetRegistry,
51
+ animationClipLoader,
52
+ buildSceneChildContext,
53
+ fontLoader,
54
+ getOrCreateMipmapPipeline,
55
+ HANDLE_CUBE,
56
+ HANDLE_CYLINDER,
57
+ HANDLE_QUAD,
58
+ HANDLE_SPHERE,
59
+ HANDLE_TRIANGLE,
60
+ INLINE_PACK_LOADERS,
61
+ LoaderRegistry,
62
+ materialLoader,
63
+ meshLoader,
64
+ mipmapCacheSize,
65
+ numMipLevels,
66
+ PACK_ARTIFACT_LOADERS,
67
+ resolveAssetHandle,
68
+ sceneLoader,
69
+ skeletonLoader,
70
+ skinLoader,
71
+ textureLoader,
72
+ walkMaterialPassesOverSharedRefs,
73
+ wireDefaultLoaders,
74
+ } from '@forgeax/engine-assets-runtime';
75
+ import { audioLoader } from '@forgeax/engine-audio-webaudio';
76
+ import { defineComponent, World } from '@forgeax/engine-ecs';
77
+ import {
78
+ createBoxGeometry,
79
+ createPlaneGeometry,
80
+ meshFromInterleaved,
81
+ PROCEDURAL_FLOATS_PER_VERTEX,
82
+ } from '@forgeax/engine-geometry';
83
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
84
+ import { ok, ok as rhiOk } from '@forgeax/engine-rhi';
85
+ import { rhi } from '@forgeax/engine-rhi-null';
86
+ import { ShaderRegistry } from '@forgeax/engine-shader';
87
+ import type {
88
+ EquirectAsset,
89
+ Handle,
90
+ MaterialAsset,
91
+ MaterialPass,
92
+ SamplerAsset,
93
+ } from '@forgeax/engine-types';
94
+ import { AssetError, toShared, toUnique, unwrapHandle } from '@forgeax/engine-types';
95
+ import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
96
+ import { deriveBuiltin } from '../../../pack/src/builtin';
97
+ import { DeviceScope } from '../../../render/src/device/device-scope';
98
+ import { GpuResidencyCache } from '../../../render/src/device/gpu-residency';
99
+ import { createEngineMetrics } from '../../../render/src/engine-metrics';
100
+ import { createRenderer as createRuntimeRenderer } from '../createRenderer';
101
+ import { createDevImportTransport, type ImportTransport } from '../dev-import-transport';
102
+ import { EngineEnvironmentError } from '../errors/environment';
103
+ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
104
+ import { requireRenderer } from './renderer-test-utils';
105
+
106
+ vi.mock('@forgeax/engine-rhi-webgpu', async () => {
107
+ spies.webgpuImportCount += 1;
108
+ const actualRhi =
109
+ await vi.importActual<typeof import('@forgeax/engine-rhi')>('@forgeax/engine-rhi');
110
+ spies.rhiErrorCtor = actualRhi.RhiError;
111
+ const fakeAdapter = {
112
+ features: new Set<string>(),
113
+ limits: {} as Readonly<Record<string, number>>,
114
+ async requestDevice() {
115
+ return actualRhi.ok(makeFakeRhiDevice());
116
+ },
117
+ };
118
+ return {
119
+ rhi: {
120
+ requestAdapter: async () => {
121
+ if (spies.rhiWebgpuRequestAdapterShould === 'reject-rhi-not-available') {
122
+ return actualRhi.err(
123
+ new actualRhi.RhiError({
124
+ code: 'rhi-not-available',
125
+ expected: 'navigator.gpu available',
126
+ hint: 'unit-test: forced rhi-not-available',
127
+ }),
128
+ );
129
+ }
130
+ if (spies.rhiWebgpuRequestAdapterShould === 'reject-adapter-null') {
131
+ return actualRhi.err(
132
+ new actualRhi.RhiError({
133
+ code: 'adapter-unavailable',
134
+ expected: 'requestAdapter returns non-null',
135
+ hint: 'unit-test: forced adapter-unavailable',
136
+ }),
137
+ );
138
+ }
139
+ return actualRhi.ok(fakeAdapter);
140
+ },
141
+ acquireCanvasContext: (_canvas: unknown) => {
142
+ return actualRhi.ok({
143
+ configure: () => actualRhi.ok(undefined),
144
+ unconfigure: () => actualRhi.ok(undefined),
145
+ getCurrentTexture: () => actualRhi.ok({ __brand: 'TextureView' }),
146
+ });
147
+ },
148
+ },
149
+ createShaderModule: async () => actualRhi.ok({ __brand: 'ShaderModule' } as unknown as object),
150
+ translateErrorEventToRhiError: () =>
151
+ actualRhi.err(
152
+ new actualRhi.RhiError({
153
+ code: 'webgpu-runtime-error',
154
+ expected: 'GPU error event translates to RhiError',
155
+ hint: 'unit-test mock event translation',
156
+ }),
157
+ ),
158
+ _internal_getRawDevice: () => undefined,
159
+ };
160
+ });
161
+ vi.mock('@forgeax/engine-rhi-wgpu', async () => {
162
+ spies.wgpuImportCount += 1;
163
+ const actualRhi =
164
+ await vi.importActual<typeof import('@forgeax/engine-rhi')>('@forgeax/engine-rhi');
165
+ const fakeAdapter = {
166
+ features: new Set<string>(),
167
+ limits: {} as Readonly<Record<string, number>>,
168
+ async requestDevice() {
169
+ return actualRhi.ok(makeFakeRhiDevice());
170
+ },
171
+ };
172
+ return {
173
+ rhi: {
174
+ requestAdapter: async () => {
175
+ if (spies.rhiWgpuRequestAdapterShould === 'reject-rhi-not-available') {
176
+ return actualRhi.err(
177
+ new actualRhi.RhiError({
178
+ code: 'rhi-not-available',
179
+ expected: 'wgpu webgl backend available',
180
+ hint: 'unit-test: forced rhi-not-available on rhi-wgpu',
181
+ }),
182
+ );
183
+ }
184
+ return actualRhi.ok(fakeAdapter);
185
+ },
186
+ acquireCanvasContext: (_canvas: unknown) => {
187
+ return actualRhi.ok({
188
+ configure: () => actualRhi.ok(undefined),
189
+ unconfigure: () => actualRhi.ok(undefined),
190
+ getCurrentTexture: () => actualRhi.ok({ __brand: 'TextureView' }),
191
+ });
192
+ },
193
+ },
194
+ ensureReady: async () => {
195
+ spies.ensureReadyCount += 1;
196
+ if (spies.rhiWgpuEnsureReadyShould === 'reject-load-failed') {
197
+ throw new Error('unit-test: forced rhi-wgpu wasm load failure');
198
+ }
199
+ return undefined;
200
+ },
201
+ createShaderModule: async () => actualRhi.ok({ __brand: 'ShaderModule' } as unknown as object),
202
+ translateErrorEventToRhiError: () =>
203
+ actualRhi.err(
204
+ new actualRhi.RhiError({
205
+ code: 'webgpu-runtime-error',
206
+ expected: 'GPU error event translates to RhiError',
207
+ hint: 'unit-test mock event translation',
208
+ }),
209
+ ),
210
+ _internal_getRawDevice: () => undefined,
211
+ };
212
+ });
213
+ const spies = vi.hoisted(() => ({
214
+ webgpuImportCount: 0,
215
+ wgpuImportCount: 0,
216
+ ensureReadyCount: 0,
217
+ // Failure toggles set by individual tests before invoking createRenderer.
218
+ rhiWebgpuRequestAdapterShould: 'success' as
219
+ | 'success'
220
+ | 'reject-rhi-not-available'
221
+ | 'reject-adapter-null',
222
+ rhiWgpuEnsureReadyShould: 'success' as 'success' | 'reject-load-failed',
223
+ rhiWgpuRequestAdapterShould: 'success' as 'success' | 'reject-rhi-not-available',
224
+ rhiErrorCtor: undefined as undefined | (new (...args: never[]) => Error),
225
+ reset(): void {
226
+ this.webgpuImportCount = 0;
227
+ this.wgpuImportCount = 0;
228
+ this.ensureReadyCount = 0;
229
+ this.rhiWebgpuRequestAdapterShould = 'success';
230
+ this.rhiWgpuEnsureReadyShould = 'success';
231
+ this.rhiWgpuRequestAdapterShould = 'success';
232
+ },
233
+ }));
234
+ function makeFakeRhiDevice(): Record<string, unknown> {
235
+ let resolveLost!: (info: unknown) => void;
236
+ const lost = new Promise<unknown>((res) => {
237
+ resolveLost = res;
238
+ });
239
+ void resolveLost;
240
+ return {
241
+ __brand: 'RhiDevice',
242
+ lost,
243
+ features: new Set<string>(),
244
+ limits: {},
245
+ // feat-20260707 M5 / w33: createRenderer projects RhiCaps.textureCompression*
246
+ // into TranscodeCaps right after building the registry, so the fake device
247
+ // must expose a caps object (the real RhiDevice always carries one).
248
+ caps: {
249
+ textureCompressionBc: false,
250
+ textureCompressionEtc2: false,
251
+ textureCompressionAstc: false,
252
+ },
253
+ queue: {
254
+ submit: () => undefined,
255
+ writeBuffer: () => undefined,
256
+ },
257
+ createBuffer: () => ({ ok: true, value: { __brand: 'Buffer' } }),
258
+ createTexture: () => ({ ok: true, value: { __brand: 'Texture' } }),
259
+ createBindGroupLayout: () => ({ ok: true, value: { __brand: 'BindGroupLayout' } }),
260
+ createBindGroup: () => ({ ok: true, value: { __brand: 'BindGroup' } }),
261
+ createPipelineLayout: () => ({ ok: true, value: { __brand: 'PipelineLayout' } }),
262
+ createRenderPipeline: () => ({ ok: true, value: { __brand: 'RenderPipeline' } }),
263
+ createSampler: () => ({ ok: true, value: { __brand: 'Sampler' } }),
264
+ createShaderModule: () => ({
265
+ ok: true,
266
+ value: { __brand: 'ShaderModule' },
267
+ }),
268
+ createTextureView: () => ({ ok: true, value: { __brand: 'TextureView' } }),
269
+ createCommandEncoder: () => ({
270
+ ok: true,
271
+ value: {
272
+ beginRenderPass: () => ({
273
+ setPipeline: () => undefined,
274
+ setBindGroup: () => undefined,
275
+ setVertexBuffer: () => undefined,
276
+ setIndexBuffer: () => undefined,
277
+ draw: () => undefined,
278
+ drawIndexed: () => undefined,
279
+ end: () => undefined,
280
+ }),
281
+ finish: () => ({ __brand: 'CommandBuffer' }),
282
+ },
283
+ }),
284
+ };
285
+ }
286
+ function makeMockCanvas(opts: { webgpu?: 'context' | 'null'; webgl2?: 'context' | 'null' }) {
287
+ return {
288
+ width: 800,
289
+ height: 600,
290
+ getContext(kind: string): unknown {
291
+ if (kind === 'webgpu') {
292
+ if (opts.webgpu === 'context') {
293
+ return {
294
+ __mockTag: 'webgpu-canvas-context',
295
+ configure: () => undefined,
296
+ unconfigure: () => undefined,
297
+ getCurrentTexture: () => ({ createView: () => ({}) }),
298
+ };
299
+ }
300
+ return null;
301
+ }
302
+ if (kind === 'webgl2') {
303
+ if (opts.webgl2 === 'context') {
304
+ return {
305
+ __mockTag: 'webgl2',
306
+ getExtension: () => null,
307
+ getParameter: () => 1,
308
+ isContextLost: () => false,
309
+ };
310
+ }
311
+ return null;
312
+ }
313
+ return null;
314
+ },
315
+ addEventListener() {},
316
+ removeEventListener() {},
317
+ } as unknown as HTMLCanvasElement;
318
+ }
319
+ function _makeStubGPU(): unknown {
320
+ return {
321
+ requestAdapter: async () => ({
322
+ features: new Set<string>(),
323
+ limits: {},
324
+ requestDevice: async () => makeFakeRhiDevice(),
325
+ }),
326
+ getPreferredCanvasFormat: () => 'bgra8unorm',
327
+ };
328
+ }
329
+
330
+ void [
331
+ AssetError,
332
+ AssetGuid,
333
+ AssetRegistry,
334
+ DeviceScope,
335
+ EngineEnvironmentError,
336
+ GpuResidencyCache,
337
+ HANDLE_CUBE,
338
+ HANDLE_CYLINDER,
339
+ HANDLE_QUAD,
340
+ HANDLE_SPHERE,
341
+ HANDLE_TRIANGLE,
342
+ INLINE_PACK_LOADERS,
343
+ LoaderRegistry,
344
+ PACK_ARTIFACT_LOADERS,
345
+ PROCEDURAL_FLOATS_PER_VERTEX,
346
+ ShaderRegistry,
347
+ World,
348
+ _makeStubGPU,
349
+ afterEach,
350
+ animationClipLoader,
351
+ assetRegistryModule,
352
+ audioLoader,
353
+ beforeEach,
354
+ buildSceneChildContext,
355
+ createBoxGeometry,
356
+ createDevImportTransport,
357
+ createEngineMetrics,
358
+ createPlaneGeometry,
359
+ createRuntimeRenderer,
360
+ defineComponent,
361
+ deriveBuiltin,
362
+ describe,
363
+ expect,
364
+ expectTypeOf,
365
+ fileURLToPath,
366
+ fontLoader,
367
+ getOrCreateMipmapPipeline,
368
+ it,
369
+ makeFakeRhiDevice,
370
+ makeMockCanvas,
371
+ makeMockShaderRegistry,
372
+ materialLoader,
373
+ meshFromInterleaved,
374
+ meshLoader,
375
+ mipmapCacheSize,
376
+ numMipLevels,
377
+ ok,
378
+ readFileSync,
379
+ requireRenderer,
380
+ resolveAssetHandle,
381
+ rhi,
382
+ rhiOk,
383
+ sceneLoader,
384
+ skeletonLoader,
385
+ skinLoader,
386
+ spies,
387
+ textureLoader,
388
+ toShared,
389
+ toUnique,
390
+ unwrapHandle,
391
+ vi,
392
+ walkMaterialPassesOverSharedRefs,
393
+ wireDefaultLoaders,
394
+ ];
395
+ type __MergedKeep =
396
+ | EquirectAsset
397
+ | Handle
398
+ | ImportTransport
399
+ | MaterialAsset
400
+ | MaterialPass
401
+ | SamplerAsset;
402
+
403
+ {
404
+ // --- from load-by-guid-prod-material-parent.test.ts ---
405
+ const PARENT_GUID = '00000000-0000-7000-8000-000000000001';
406
+ const CHILD_GUID = '00000000-0000-7000-8000-000000000002';
407
+ const CHILD_SAME_PACK_GUID = '00000000-0000-7000-8000-000000000003';
408
+ const NON_MATERIAL_GUID = '00000000-0000-7000-8000-000000000004';
409
+
410
+ // pack-index catalog fixture with:
411
+ // - parent: material in parent.pack.json
412
+ // - child: material in child.pack.json (parent ref to PARENT_GUID)
413
+ // - child-same: material in same-pack.pack.json (parent ref to PARENT_GUID,
414
+ // both in same file)
415
+ // - non-material: mesh entry for AC-05
416
+ const PACK_INDEX_FIXTURE = [
417
+ {
418
+ guid: PARENT_GUID,
419
+ packageUrl: '/assets/parent.pack.json',
420
+ kind: 'material',
421
+ sourcePath: 'assets/parent.pack.json',
422
+ },
423
+ {
424
+ guid: CHILD_GUID,
425
+ packageUrl: '/assets/child.pack.json',
426
+ kind: 'material',
427
+ sourcePath: 'assets/child.pack.json',
428
+ },
429
+ {
430
+ guid: CHILD_SAME_PACK_GUID,
431
+ packageUrl: '/assets/same-pack.pack.json',
432
+ kind: 'material',
433
+ sourcePath: 'assets/same-pack.pack.json',
434
+ },
435
+ {
436
+ guid: NON_MATERIAL_GUID,
437
+ packageUrl: '/assets/mesh.pack.json',
438
+ kind: 'mesh',
439
+ sourcePath: 'assets/mesh.pack.json',
440
+ submeshes: [
441
+ {
442
+ indexOffset: 0,
443
+ indexCount: 0,
444
+ vertexCount: 0,
445
+ materialSlot: 0,
446
+ topology: 'triangle-list',
447
+ },
448
+ ],
449
+ },
450
+ ];
451
+
452
+ // Parent material pack — standalone, has passes.
453
+ const PARENT_PACK = {
454
+ schemaVersion: '2.0.0',
455
+ kind: 'internal-text-package',
456
+ assets: [
457
+ {
458
+ guid: PARENT_GUID,
459
+ kind: 'material',
460
+ payload: {
461
+ kind: 'material',
462
+ passes: [
463
+ {
464
+ name: 'Forward',
465
+ program: { module: 'test::standard' },
466
+ renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
467
+ },
468
+ ],
469
+ values: {
470
+ baseColor: [0.5, 0.5, 0.5, 1],
471
+ metallic: 0,
472
+ roughness: 0.8,
473
+ },
474
+ },
475
+ refs: [],
476
+ },
477
+ ],
478
+ };
479
+
480
+ // Child material pack — no passes, only parent ref.
481
+ const CHILD_PACK = {
482
+ schemaVersion: '2.0.0',
483
+ kind: 'internal-text-package',
484
+ assets: [
485
+ {
486
+ guid: CHILD_GUID,
487
+ kind: 'material',
488
+ payload: {
489
+ kind: 'material',
490
+ parent: 0,
491
+ values: {
492
+ baseColor: [0.8, 0.2, 0.1, 1],
493
+ roughness: 0.3,
494
+ },
495
+ },
496
+ refs: [PARENT_GUID],
497
+ },
498
+ ],
499
+ };
500
+
501
+ // Same-pack file — parent + child in one pack (AC-08).
502
+ const SAME_PACK = {
503
+ schemaVersion: '2.0.0',
504
+ kind: 'internal-text-package',
505
+ assets: [
506
+ {
507
+ guid: PARENT_GUID,
508
+ kind: 'material',
509
+ payload: {
510
+ kind: 'material',
511
+ passes: [
512
+ {
513
+ name: 'Forward',
514
+ program: { module: 'test::standard' },
515
+ renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
516
+ },
517
+ ],
518
+ values: {
519
+ baseColor: [0.3, 0.3, 0.3, 1],
520
+ metallic: 0.2,
521
+ roughness: 0.6,
522
+ },
523
+ },
524
+ refs: [],
525
+ },
526
+ {
527
+ guid: CHILD_SAME_PACK_GUID,
528
+ kind: 'material',
529
+ payload: {
530
+ kind: 'material',
531
+ parent: 0,
532
+ values: {
533
+ roughness: 0.2,
534
+ },
535
+ },
536
+ refs: [PARENT_GUID],
537
+ },
538
+ ],
539
+ };
540
+
541
+ // Mesh pack for AC-05 (parent ref points to non-material).
542
+ const MESH_PACK = {
543
+ schemaVersion: '2.0.0',
544
+ kind: 'internal-text-package',
545
+ assets: [
546
+ {
547
+ guid: NON_MATERIAL_GUID,
548
+ kind: 'mesh',
549
+ payload: {
550
+ vertices: [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
551
+ indices: [0],
552
+ attributes: {},
553
+ },
554
+ refs: [],
555
+ submeshes: [
556
+ {
557
+ indexOffset: 0,
558
+ indexCount: 0,
559
+ vertexCount: 0,
560
+ materialSlot: 0,
561
+ topology: 'triangle-list',
562
+ },
563
+ ],
564
+ },
565
+ ],
566
+ };
567
+
568
+ // Child that references NON_MATERIAL_GUID as parent — invalid.
569
+ const CHILD_WITH_NON_MATERIAL_PARENT_PACK = {
570
+ schemaVersion: '2.0.0',
571
+ kind: 'internal-text-package',
572
+ assets: [
573
+ {
574
+ guid: '00000000-0000-7000-8000-000000000005',
575
+ kind: 'material',
576
+ payload: {
577
+ kind: 'material',
578
+ parent: 0,
579
+ values: {
580
+ baseColor: [0.8, 0.2, 0.1, 1],
581
+ },
582
+ },
583
+ refs: [NON_MATERIAL_GUID],
584
+ },
585
+ ],
586
+ };
587
+
588
+ // feat-20260614 M8: passesOf/paramValueOf are gone. The material parent-chain
589
+ // walk now runs over a World column handle via walkMaterialPassesOverSharedRefs;
590
+ // the child payload (returned by loadByGuid) is minted into a fresh World and
591
+ // its parent chain resolved against the AssetRegistry catalogue (D-19).
592
+ function resolveMaterialChain(reg: AssetRegistry, childPayload: MaterialAsset) {
593
+ const world = new World();
594
+ const childHandle = world.allocSharedRef('MaterialAsset', childPayload);
595
+ return walkMaterialPassesOverSharedRefs(world, childHandle, reg);
596
+ }
597
+
598
+ describe('loadByGuid prod :: material parent inheritance (feat-20260528 M2 / w4)', () => {
599
+ let originalFetch: typeof globalThis.fetch | undefined;
600
+
601
+ beforeEach(() => {
602
+ originalFetch = globalThis.fetch;
603
+ });
604
+
605
+ afterEach(() => {
606
+ if (originalFetch !== undefined) {
607
+ globalThis.fetch = originalFetch;
608
+ } else {
609
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown requires deleting globalThis.fetch
610
+ delete (globalThis as any).fetch;
611
+ }
612
+ });
613
+
614
+ // --------------- AC-03: successful parent preload ---------------
615
+
616
+ it('(AC-03) child with parent ref (no passes) — parent loaded first, passesOf/paramValueOf inherits parent passes', async () => {
617
+ const reg = new AssetRegistry(makeMockShaderRegistry());
618
+
619
+ if (typeof reg.configurePackIndex !== 'function') {
620
+ return;
621
+ }
622
+
623
+ reg.configurePackIndex('/pack-index.json');
624
+
625
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
626
+ if (url === '/pack-index.json') {
627
+ return Promise.resolve({
628
+ ok: true,
629
+ json: () => Promise.resolve(PACK_INDEX_FIXTURE),
630
+ });
631
+ }
632
+ if (url === '/assets/parent.pack.json') {
633
+ return Promise.resolve({
634
+ ok: true,
635
+ json: () => Promise.resolve(PARENT_PACK),
636
+ });
637
+ }
638
+ if (url === '/assets/child.pack.json') {
639
+ return Promise.resolve({
640
+ ok: true,
641
+ json: () => Promise.resolve(CHILD_PACK),
642
+ });
643
+ }
644
+ return Promise.resolve({ ok: false, status: 404 });
645
+ });
646
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
647
+ globalThis.fetch = fetchMock as any;
648
+
649
+ const childGuid = AssetGuid.parse(CHILD_GUID);
650
+ if (!childGuid.ok) throw new Error('expected ok');
651
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
652
+
653
+ expect(result.ok).toBe(true);
654
+ if (!result.ok) return;
655
+
656
+ // resolve the child material — should inherit parent passes
657
+ const walk = resolveMaterialChain(reg, result.value);
658
+ expect(walk.ok).toBe(true);
659
+ if (!walk.ok) return;
660
+
661
+ // Inherited parent passes
662
+ expect(walk.value.passes.length).toBe(1);
663
+ if (walk.value.passes[0]) {
664
+ expect(walk.value.passes[0].name).toBe('Forward');
665
+ expect(walk.value.passes[0].program.module).toBe('test::standard');
666
+ }
667
+
668
+ // Child values override parent: baseColor + roughness from child, metallic from parent
669
+ expect(walk.value.values.baseColor).toEqual([0.8, 0.2, 0.1, 1]);
670
+ expect(walk.value.values.metallic).toBe(0);
671
+ expect(walk.value.values.roughness).toBe(0.3);
672
+ });
673
+
674
+ it('(AC-03) parent already catalogued — idempotent fast-path, child resolves correctly', async () => {
675
+ const reg = new AssetRegistry(makeMockShaderRegistry());
676
+
677
+ if (typeof reg.configurePackIndex !== 'function') {
678
+ return;
679
+ }
680
+
681
+ reg.configurePackIndex('/pack-index.json');
682
+
683
+ // Pre-catalogue the parent (simulates parent already loaded). feat-20260614
684
+ // M8: the registry holds no handle maps; "already loaded" == catalogued.
685
+ const parentGuid = AssetGuid.parse(PARENT_GUID);
686
+ if (!parentGuid.ok) throw new Error('expected ok');
687
+ reg.catalog<MaterialAsset>(parentGuid.value, {
688
+ kind: 'material',
689
+ passes: [
690
+ {
691
+ name: 'Forward',
692
+ program: { module: 'test::standard' },
693
+ renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
694
+ },
695
+ ],
696
+ values: {
697
+ baseColor: [0.5, 0.5, 0.5, 1],
698
+ metallic: 0,
699
+ roughness: 0.8,
700
+ },
701
+ } as MaterialAsset);
702
+
703
+ // Now request child — loadByGuid should see parent via the catalogue fast-path
704
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
705
+ if (url === '/pack-index.json') {
706
+ return Promise.resolve({
707
+ ok: true,
708
+ json: () => Promise.resolve(PACK_INDEX_FIXTURE),
709
+ });
710
+ }
711
+ if (url === '/assets/child.pack.json') {
712
+ return Promise.resolve({
713
+ ok: true,
714
+ json: () => Promise.resolve(CHILD_PACK),
715
+ });
716
+ }
717
+ return Promise.resolve({ ok: false, status: 404 });
718
+ });
719
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
720
+ globalThis.fetch = fetchMock as any;
721
+
722
+ const childGuid = AssetGuid.parse(CHILD_GUID);
723
+ if (!childGuid.ok) throw new Error('expected ok');
724
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
725
+
726
+ // Parent already catalogued — loadByGuidProd resolves it from the catalogue.
727
+ expect(result.ok).toBe(true);
728
+ if (!result.ok) return;
729
+
730
+ const walk = resolveMaterialChain(reg, result.value);
731
+ expect(walk.ok).toBe(true);
732
+ if (!walk.ok) return;
733
+
734
+ // Inherited parent passes
735
+ expect(walk.value.passes.length).toBe(1);
736
+ if (walk.value.passes[0]) {
737
+ expect(walk.value.passes[0].name).toBe('Forward');
738
+ }
739
+
740
+ // Child values override parent
741
+ expect(walk.value.values.baseColor).toEqual([0.8, 0.2, 0.1, 1]);
742
+ expect(walk.value.values.metallic).toBe(0);
743
+ expect(walk.value.values.roughness).toBe(0.3);
744
+ });
745
+
746
+ // --------------- AC-04: parent GUID not in pack-index ---------------
747
+
748
+ it('(AC-04) parent GUID not in pack-index — loadByGuid returns Err(asset-not-imported)', async () => {
749
+ const reg = new AssetRegistry(makeMockShaderRegistry());
750
+
751
+ if (typeof reg.configurePackIndex !== 'function') {
752
+ return;
753
+ }
754
+
755
+ reg.configurePackIndex('/pack-index.json');
756
+
757
+ // pack-index only has CHILD_GUID, not PARENT_GUID
758
+ const CATALOG_WITHOUT_PARENT = [
759
+ {
760
+ guid: CHILD_GUID,
761
+ packageUrl: '/assets/child.pack.json',
762
+ kind: 'material',
763
+ sourcePath: 'assets/child.pack.json',
764
+ },
765
+ ];
766
+
767
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
768
+ if (url === '/pack-index.json') {
769
+ return Promise.resolve({
770
+ ok: true,
771
+ json: () => Promise.resolve(CATALOG_WITHOUT_PARENT),
772
+ });
773
+ }
774
+ if (url === '/assets/child.pack.json') {
775
+ return Promise.resolve({
776
+ ok: true,
777
+ json: () => Promise.resolve(CHILD_PACK),
778
+ });
779
+ }
780
+ return Promise.resolve({ ok: false, status: 404 });
781
+ });
782
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
783
+ globalThis.fetch = fetchMock as any;
784
+
785
+ const childGuid = AssetGuid.parse(CHILD_GUID);
786
+ if (!childGuid.ok) throw new Error('expected ok');
787
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
788
+
789
+ expect(result.ok).toBe(false);
790
+ if (!result.ok) {
791
+ // M4 shipped form (no import transport): DDC miss -> asset-not-imported (AC-22).
792
+ expect(result.error.code).toBe('asset-not-imported');
793
+ // AC-04: hint prefix must contain parent + child GUID info (D-3)
794
+ expect(result.error.hint).toContain(
795
+ `loading parent material ${PARENT_GUID} for child ${CHILD_GUID}`,
796
+ );
797
+ }
798
+ });
799
+
800
+ // --------------- AC-05: parent ref points to non-material kind ---------------
801
+
802
+ it('(AC-05) parent ref points to mesh kind — loadByGuid returns Err(asset-parse-failed)', async () => {
803
+ const reg = new AssetRegistry(makeMockShaderRegistry());
804
+
805
+ if (typeof reg.configurePackIndex !== 'function') {
806
+ return;
807
+ }
808
+
809
+ reg.configurePackIndex('/pack-index.json');
810
+
811
+ const GUID_NON_MATERIAL_CHILD = '00000000-0000-7000-8000-000000000005';
812
+
813
+ const CATALOG_WITH_MESH = [
814
+ {
815
+ guid: NON_MATERIAL_GUID,
816
+ packageUrl: '/assets/mesh.pack.json',
817
+ kind: 'mesh',
818
+ sourcePath: 'assets/mesh.pack.json',
819
+ submeshes: [
820
+ {
821
+ indexOffset: 0,
822
+ indexCount: 0,
823
+ vertexCount: 0,
824
+ topology: 'triangle-list',
825
+ },
826
+ ],
827
+ },
828
+ {
829
+ guid: GUID_NON_MATERIAL_CHILD,
830
+ packageUrl: '/assets/child-nonmat-parent.pack.json',
831
+ kind: 'material',
832
+ sourcePath: 'assets/child-nonmat-parent.pack.json',
833
+ },
834
+ ];
835
+
836
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
837
+ if (url === '/pack-index.json') {
838
+ return Promise.resolve({
839
+ ok: true,
840
+ json: () => Promise.resolve(CATALOG_WITH_MESH),
841
+ });
842
+ }
843
+ if (url === '/assets/mesh.pack.json') {
844
+ return Promise.resolve({
845
+ ok: true,
846
+ json: () => Promise.resolve(MESH_PACK),
847
+ });
848
+ }
849
+ if (url === '/assets/child-nonmat-parent.pack.json') {
850
+ return Promise.resolve({
851
+ ok: true,
852
+ json: () => Promise.resolve(CHILD_WITH_NON_MATERIAL_PARENT_PACK),
853
+ });
854
+ }
855
+ return Promise.resolve({ ok: false, status: 404 });
856
+ });
857
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
858
+ globalThis.fetch = fetchMock as any;
859
+
860
+ const childGuid = AssetGuid.parse(GUID_NON_MATERIAL_CHILD);
861
+ if (!childGuid.ok) throw new Error('expected ok');
862
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
863
+
864
+ expect(result.ok).toBe(false);
865
+ if (!result.ok) {
866
+ // AC-05: parent is mesh, not material — runtime guard catches this
867
+ expect(result.error.code).toBe('asset-parse-failed');
868
+ // hint must include parent GUID + type mismatch info
869
+ expect(result.error.hint).toContain(
870
+ `loading parent material ${NON_MATERIAL_GUID} for child ${GUID_NON_MATERIAL_CHILD}`,
871
+ );
872
+ expect(result.error.hint).toContain("not 'material'");
873
+ }
874
+ });
875
+
876
+ // --------------- AC-08: same pack + different pack ---------------
877
+
878
+ it('(AC-08) parent and child in same pack file — recursive load works', async () => {
879
+ const reg = new AssetRegistry(makeMockShaderRegistry());
880
+
881
+ if (typeof reg.configurePackIndex !== 'function') {
882
+ return;
883
+ }
884
+
885
+ reg.configurePackIndex('/pack-index.json');
886
+
887
+ // Same pack file contains both parent and child — both entries map to
888
+ // the same packageUrl so loadByGuidProd fetches the same file twice
889
+ // (once for parent, once for child), but fast-path idempotency on the
890
+ // second loadByGuid(parentGuid) avoids a duplicate fetch.
891
+ const SAME_PACK_CATALOG = [
892
+ {
893
+ guid: PARENT_GUID,
894
+ packageUrl: '/assets/same-pack.pack.json',
895
+ kind: 'material',
896
+ sourcePath: 'assets/same-pack.pack.json',
897
+ },
898
+ {
899
+ guid: CHILD_SAME_PACK_GUID,
900
+ packageUrl: '/assets/same-pack.pack.json',
901
+ kind: 'material',
902
+ sourcePath: 'assets/same-pack.pack.json',
903
+ },
904
+ ];
905
+
906
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
907
+ if (url === '/pack-index.json') {
908
+ return Promise.resolve({
909
+ ok: true,
910
+ json: () => Promise.resolve(SAME_PACK_CATALOG),
911
+ });
912
+ }
913
+ if (url === '/assets/same-pack.pack.json') {
914
+ return Promise.resolve({
915
+ ok: true,
916
+ json: () => Promise.resolve(SAME_PACK),
917
+ });
918
+ }
919
+ return Promise.resolve({ ok: false, status: 404 });
920
+ });
921
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
922
+ globalThis.fetch = fetchMock as any;
923
+
924
+ const childGuid = AssetGuid.parse(CHILD_SAME_PACK_GUID);
925
+ if (!childGuid.ok) throw new Error('expected ok');
926
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
927
+
928
+ // Same pack: parent entry fetched from same pack file, catalogued first.
929
+ expect(result.ok).toBe(true);
930
+ if (!result.ok) return;
931
+
932
+ const walk = resolveMaterialChain(reg, result.value);
933
+ expect(walk.ok).toBe(true);
934
+ if (!walk.ok) return;
935
+
936
+ // Parent passes inherited
937
+ expect(walk.value.passes.length).toBe(1);
938
+ if (walk.value.passes[0]) {
939
+ expect(walk.value.passes[0].program.module).toBe('test::standard');
940
+ }
941
+
942
+ // Child roughness overrides parent (0.2 vs 0.6), other params from parent
943
+ expect(walk.value.values.baseColor).toEqual([0.3, 0.3, 0.3, 1]);
944
+ expect(walk.value.values.metallic).toBe(0.2);
945
+ expect(walk.value.values.roughness).toBe(0.2);
946
+ });
947
+
948
+ it('(AC-08) parent and child in different pack files — recursive load works', async () => {
949
+ const reg = new AssetRegistry(makeMockShaderRegistry());
950
+
951
+ if (typeof reg.configurePackIndex !== 'function') {
952
+ return;
953
+ }
954
+
955
+ reg.configurePackIndex('/pack-index.json');
956
+
957
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
958
+ if (url === '/pack-index.json') {
959
+ return Promise.resolve({
960
+ ok: true,
961
+ json: () => Promise.resolve(PACK_INDEX_FIXTURE),
962
+ });
963
+ }
964
+ if (url === '/assets/parent.pack.json') {
965
+ return Promise.resolve({
966
+ ok: true,
967
+ json: () => Promise.resolve(PARENT_PACK),
968
+ });
969
+ }
970
+ if (url === '/assets/child.pack.json') {
971
+ return Promise.resolve({
972
+ ok: true,
973
+ json: () => Promise.resolve(CHILD_PACK),
974
+ });
975
+ }
976
+ return Promise.resolve({ ok: false, status: 404 });
977
+ });
978
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
979
+ globalThis.fetch = fetchMock as any;
980
+
981
+ const childGuid = AssetGuid.parse(CHILD_GUID);
982
+ if (!childGuid.ok) throw new Error('expected ok');
983
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
984
+
985
+ // Different pack files: parent fetched from parent.pack.json, child from
986
+ // child.pack.json. Both catalogued.
987
+ expect(result.ok).toBe(true);
988
+ if (!result.ok) return;
989
+
990
+ const walk = resolveMaterialChain(reg, result.value);
991
+ expect(walk.ok).toBe(true);
992
+ if (!walk.ok) return;
993
+
994
+ // Parent passes inherited
995
+ expect(walk.value.passes.length).toBe(1);
996
+
997
+ // Child values override parent
998
+ expect(walk.value.values.baseColor).toEqual([0.8, 0.2, 0.1, 1]);
999
+ expect(walk.value.values.metallic).toBe(0);
1000
+ expect(walk.value.values.roughness).toBe(0.3);
1001
+ });
1002
+
1003
+ // --------------- W6: error boundary tests ---------------
1004
+
1005
+ it('(w6) parent load failure error hint has correct prefix format "loading parent material <GUID> for child <GUID>: "', async () => {
1006
+ const reg = new AssetRegistry(makeMockShaderRegistry());
1007
+
1008
+ if (typeof reg.configurePackIndex !== 'function') {
1009
+ return;
1010
+ }
1011
+
1012
+ reg.configurePackIndex('/pack-index.json');
1013
+
1014
+ // Catalog: child in index but parent not — parent loadByGuid will
1015
+ // return asset-not-found.
1016
+ const CATALOG_WITH_PARENT_MISSING = [
1017
+ {
1018
+ guid: CHILD_GUID,
1019
+ packageUrl: '/assets/child.pack.json',
1020
+ kind: 'material',
1021
+ sourcePath: 'assets/child.pack.json',
1022
+ },
1023
+ ];
1024
+
1025
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1026
+ if (url === '/pack-index.json') {
1027
+ return Promise.resolve({
1028
+ ok: true,
1029
+ json: () => Promise.resolve(CATALOG_WITH_PARENT_MISSING),
1030
+ });
1031
+ }
1032
+ if (url === '/assets/child.pack.json') {
1033
+ return Promise.resolve({
1034
+ ok: true,
1035
+ json: () => Promise.resolve(CHILD_PACK),
1036
+ });
1037
+ }
1038
+ return Promise.resolve({ ok: false, status: 404 });
1039
+ });
1040
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
1041
+ globalThis.fetch = fetchMock as any;
1042
+
1043
+ const childGuid = AssetGuid.parse(CHILD_GUID);
1044
+ if (!childGuid.ok) throw new Error('expected ok');
1045
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
1046
+
1047
+ expect(result.ok).toBe(false);
1048
+ if (!result.ok) {
1049
+ // M4 shipped form (no import transport): DDC miss -> asset-not-imported (AC-22).
1050
+ expect(result.error.code).toBe('asset-not-imported');
1051
+ // D-3: exact hint prefix format
1052
+ expect(result.error.hint).toContain(
1053
+ `loading parent material ${PARENT_GUID} for child ${CHILD_GUID}`,
1054
+ );
1055
+ // hint should start with the prefix
1056
+ expect(result.error.hint).toMatch(/^loading parent material /);
1057
+ }
1058
+ });
1059
+
1060
+ it('(w6) parent GUID not a valid UUID format —loadByGuid returns Err(asset-parse-failed)', async () => {
1061
+ const reg = new AssetRegistry(makeMockShaderRegistry());
1062
+
1063
+ if (typeof reg.configurePackIndex !== 'function') {
1064
+ return;
1065
+ }
1066
+
1067
+ reg.configurePackIndex('/pack-index.json');
1068
+
1069
+ const INVALID_GUID = 'not-a-valid-uuid';
1070
+ const CHILD_WITH_INVALID_PARENT_GUID = '00000000-0000-7000-8000-000000000009';
1071
+
1072
+ const CATALOG = [
1073
+ {
1074
+ guid: CHILD_WITH_INVALID_PARENT_GUID,
1075
+ packageUrl: '/assets/child-invalid-parent.pack.json',
1076
+ kind: 'material',
1077
+ sourcePath: 'assets/child-invalid-parent.pack.json',
1078
+ },
1079
+ ];
1080
+
1081
+ const CHILD_PACK_INVALID = {
1082
+ schemaVersion: '2.0.0',
1083
+ kind: 'internal-text-package',
1084
+ assets: [
1085
+ {
1086
+ guid: CHILD_WITH_INVALID_PARENT_GUID,
1087
+ kind: 'material',
1088
+ payload: {
1089
+ kind: 'material',
1090
+ parent: 0,
1091
+ values: { baseColor: [0.8, 0.2, 0.1, 1] },
1092
+ },
1093
+ refs: [INVALID_GUID],
1094
+ },
1095
+ ],
1096
+ };
1097
+
1098
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1099
+ if (url === '/pack-index.json') {
1100
+ return Promise.resolve({
1101
+ ok: true,
1102
+ json: () => Promise.resolve(CATALOG),
1103
+ });
1104
+ }
1105
+ if (url === '/assets/child-invalid-parent.pack.json') {
1106
+ return Promise.resolve({
1107
+ ok: true,
1108
+ json: () => Promise.resolve(CHILD_PACK_INVALID),
1109
+ });
1110
+ }
1111
+ return Promise.resolve({ ok: false, status: 404 });
1112
+ });
1113
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
1114
+ globalThis.fetch = fetchMock as any;
1115
+
1116
+ const childGuid = AssetGuid.parse(CHILD_WITH_INVALID_PARENT_GUID);
1117
+ if (!childGuid.ok) throw new Error('expected ok');
1118
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
1119
+
1120
+ // parseAssetPayload returns an asset with parentGuid=<invalid string>,
1121
+ // but then AssetGuid.parse(parentGuidStr) in loadByGuidProd fails
1122
+ // because 'not-a-valid-uuid' is not a valid UUID format.
1123
+ expect(result.ok).toBe(false);
1124
+ if (!result.ok) {
1125
+ expect(result.error.code).toBe('asset-parse-failed');
1126
+ expect(result.error.hint).toContain(INVALID_GUID);
1127
+ expect(result.error.hint).toContain('not a valid UUID format');
1128
+ }
1129
+ });
1130
+ });
1131
+
1132
+ // feat-20260622 M5 / w16 (R5): AC-10 parent breadcrumb literal-form contract.
1133
+ // The material parent edge currently loads via the independent "Path B"
1134
+ // preload (asset-registry.ts), which carries the precise breadcrumb hint
1135
+ // `loading parent material <PARENT> for child <CHILD>` that downstream code
1136
+ // asserts on. M5 (w17) folds Path B into the unified envelope.refs for-loop;
1137
+ // these tests lock the exact contract BEFORE the fold so the move is verified
1138
+ // to preserve it. They must be green pre-fold (against current Path B) and
1139
+ // stay green post-fold (against the unified for-loop sourceField==='parent' /
1140
+ // parent-edge branch).
1141
+ describe('(w16) AC-10 material parent breadcrumb literal-form contract', () => {
1142
+ let originalFetch: typeof globalThis.fetch | undefined;
1143
+
1144
+ beforeEach(() => {
1145
+ originalFetch = globalThis.fetch;
1146
+ });
1147
+
1148
+ afterEach(() => {
1149
+ if (originalFetch !== undefined) {
1150
+ globalThis.fetch = originalFetch;
1151
+ } else {
1152
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown requires deleting globalThis.fetch
1153
+ delete (globalThis as any).fetch;
1154
+ }
1155
+ });
1156
+
1157
+ it('(w16) parent load failure: hint contains parent GUID, child GUID, and the literal substrings "loading parent material" + "for child"', async () => {
1158
+ const reg = new AssetRegistry(makeMockShaderRegistry());
1159
+ if (typeof reg.configurePackIndex !== 'function') return;
1160
+ reg.configurePackIndex('/pack-index.json');
1161
+
1162
+ // Child catalogued, parent absent from index -> parent load fails.
1163
+ const CATALOG_PARENT_MISSING = [
1164
+ {
1165
+ guid: CHILD_GUID,
1166
+ packageUrl: '/assets/child.pack.json',
1167
+ kind: 'material',
1168
+ sourcePath: 'assets/child.pack.json',
1169
+ },
1170
+ ];
1171
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1172
+ if (url === '/pack-index.json') {
1173
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CATALOG_PARENT_MISSING) });
1174
+ }
1175
+ if (url === '/assets/child.pack.json') {
1176
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CHILD_PACK) });
1177
+ }
1178
+ return Promise.resolve({ ok: false, status: 404 });
1179
+ });
1180
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
1181
+ globalThis.fetch = fetchMock as any;
1182
+
1183
+ const childGuid = AssetGuid.parse(CHILD_GUID);
1184
+ if (!childGuid.ok) throw new Error('expected ok');
1185
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
1186
+
1187
+ expect(result.ok).toBe(false);
1188
+ if (result.ok) return;
1189
+ const hint = result.error.hint ?? '';
1190
+ // Literal form: `loading parent material X for child Y` (research Finding 7;
1191
+ // NOT the buildSceneChildContext "sub-asset X referenced by ..." form).
1192
+ expect(hint).toContain('loading parent material');
1193
+ expect(hint).toContain('for child');
1194
+ expect(hint).toContain(PARENT_GUID);
1195
+ expect(hint).toContain(CHILD_GUID);
1196
+ expect(hint).toContain(`loading parent material ${PARENT_GUID} for child ${CHILD_GUID}`);
1197
+ });
1198
+
1199
+ it('(w16) parent load failure: error CODE propagates from the parent load (not replaced with a generic code)', async () => {
1200
+ const reg = new AssetRegistry(makeMockShaderRegistry());
1201
+ if (typeof reg.configurePackIndex !== 'function') return;
1202
+ reg.configurePackIndex('/pack-index.json');
1203
+
1204
+ // Parent absent from index -> parent load is a catalog miss, which (no
1205
+ // import transport) yields `asset-not-imported`. The child load must
1206
+ // surface THAT code, not a flattened generic `asset-parse-failed`.
1207
+ const CATALOG_PARENT_MISSING = [
1208
+ {
1209
+ guid: CHILD_GUID,
1210
+ packageUrl: '/assets/child.pack.json',
1211
+ kind: 'material',
1212
+ sourcePath: 'assets/child.pack.json',
1213
+ },
1214
+ ];
1215
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1216
+ if (url === '/pack-index.json') {
1217
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CATALOG_PARENT_MISSING) });
1218
+ }
1219
+ if (url === '/assets/child.pack.json') {
1220
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CHILD_PACK) });
1221
+ }
1222
+ return Promise.resolve({ ok: false, status: 404 });
1223
+ });
1224
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
1225
+ globalThis.fetch = fetchMock as any;
1226
+
1227
+ const childGuid = AssetGuid.parse(CHILD_GUID);
1228
+ if (!childGuid.ok) throw new Error('expected ok');
1229
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
1230
+
1231
+ expect(result.ok).toBe(false);
1232
+ if (result.ok) return;
1233
+ // Propagated code from the parent catalog miss (AC-22 shipped form).
1234
+ expect(result.error.code).toBe('asset-not-imported');
1235
+ });
1236
+
1237
+ it('(w16) parent edge load failure carries enough info to identify the parent GUID', async () => {
1238
+ const reg = new AssetRegistry(makeMockShaderRegistry());
1239
+ if (typeof reg.configurePackIndex !== 'function') return;
1240
+ reg.configurePackIndex('/pack-index.json');
1241
+
1242
+ // Parent present in index but its pack file 404s -> parent load fails
1243
+ // mid-fetch. The breadcrumb must still name the parent GUID so an AI user
1244
+ // can locate the failing parent edge.
1245
+ const CATALOG_PARENT_FETCH_FAILS = [
1246
+ {
1247
+ guid: PARENT_GUID,
1248
+ packageUrl: '/assets/parent.pack.json',
1249
+ kind: 'material',
1250
+ sourcePath: 'assets/parent.pack.json',
1251
+ },
1252
+ {
1253
+ guid: CHILD_GUID,
1254
+ packageUrl: '/assets/child.pack.json',
1255
+ kind: 'material',
1256
+ sourcePath: 'assets/child.pack.json',
1257
+ },
1258
+ ];
1259
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1260
+ if (url === '/pack-index.json') {
1261
+ return Promise.resolve({
1262
+ ok: true,
1263
+ json: () => Promise.resolve(CATALOG_PARENT_FETCH_FAILS),
1264
+ });
1265
+ }
1266
+ if (url === '/assets/child.pack.json') {
1267
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CHILD_PACK) });
1268
+ }
1269
+ // parent.pack.json fetch fails
1270
+ return Promise.resolve({ ok: false, status: 404 });
1271
+ });
1272
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
1273
+ globalThis.fetch = fetchMock as any;
1274
+
1275
+ const childGuid = AssetGuid.parse(CHILD_GUID);
1276
+ if (!childGuid.ok) throw new Error('expected ok');
1277
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
1278
+
1279
+ expect(result.ok).toBe(false);
1280
+ if (result.ok) return;
1281
+ const hint = result.error.hint ?? '';
1282
+ expect(hint).toContain(`loading parent material ${PARENT_GUID} for child ${CHILD_GUID}`);
1283
+ });
1284
+
1285
+ it('(w16) parent ref points to non-material kind: hint preserves the literal parent breadcrumb + "not \'material\'"', async () => {
1286
+ const reg = new AssetRegistry(makeMockShaderRegistry());
1287
+ if (typeof reg.configurePackIndex !== 'function') return;
1288
+ reg.configurePackIndex('/pack-index.json');
1289
+
1290
+ const GUID_NON_MATERIAL_CHILD = '00000000-0000-7000-8000-000000000005';
1291
+ const CATALOG_WITH_MESH = [
1292
+ {
1293
+ guid: NON_MATERIAL_GUID,
1294
+ packageUrl: '/assets/mesh.pack.json',
1295
+ kind: 'mesh',
1296
+ sourcePath: 'assets/mesh.pack.json',
1297
+ submeshes: [
1298
+ {
1299
+ indexOffset: 0,
1300
+ indexCount: 0,
1301
+ vertexCount: 0,
1302
+ materialSlot: 0,
1303
+ topology: 'triangle-list',
1304
+ },
1305
+ ],
1306
+ },
1307
+ {
1308
+ guid: GUID_NON_MATERIAL_CHILD,
1309
+ packageUrl: '/assets/child-nonmat-parent.pack.json',
1310
+ kind: 'material',
1311
+ sourcePath: 'assets/child-nonmat-parent.pack.json',
1312
+ },
1313
+ ];
1314
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1315
+ if (url === '/pack-index.json') {
1316
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CATALOG_WITH_MESH) });
1317
+ }
1318
+ if (url === '/assets/mesh.pack.json') {
1319
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(MESH_PACK) });
1320
+ }
1321
+ if (url === '/assets/child-nonmat-parent.pack.json') {
1322
+ return Promise.resolve({
1323
+ ok: true,
1324
+ json: () => Promise.resolve(CHILD_WITH_NON_MATERIAL_PARENT_PACK),
1325
+ });
1326
+ }
1327
+ return Promise.resolve({ ok: false, status: 404 });
1328
+ });
1329
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
1330
+ globalThis.fetch = fetchMock as any;
1331
+
1332
+ const childGuid = AssetGuid.parse(GUID_NON_MATERIAL_CHILD);
1333
+ if (!childGuid.ok) throw new Error('expected ok');
1334
+ const result = await reg.loadByGuid<MaterialAsset>(childGuid.value);
1335
+
1336
+ expect(result.ok).toBe(false);
1337
+ if (result.ok) return;
1338
+ expect(result.error.code).toBe('asset-parse-failed');
1339
+ const hint = result.error.hint ?? '';
1340
+ expect(hint).toContain(
1341
+ `loading parent material ${NON_MATERIAL_GUID} for child ${GUID_NON_MATERIAL_CHILD}`,
1342
+ );
1343
+ expect(hint).toContain("not 'material'");
1344
+ });
1345
+ });
1346
+
1347
+ // feat-20260622 M5 / w18: end-to-end material-with-parent load + Path B
1348
+ // deletion verification. Proves the parent edge now flows through the unified
1349
+ // envelope.refs for-loop (w17 fold) end-to-end, and that the independent Path
1350
+ // B early-return block is gone from the source.
1351
+ describe('(w18) material-with-parent end-to-end + Path B deletion', () => {
1352
+ let originalFetch: typeof globalThis.fetch | undefined;
1353
+
1354
+ beforeEach(() => {
1355
+ originalFetch = globalThis.fetch;
1356
+ });
1357
+
1358
+ afterEach(() => {
1359
+ if (originalFetch !== undefined) {
1360
+ globalThis.fetch = originalFetch;
1361
+ } else {
1362
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown requires deleting globalThis.fetch
1363
+ delete (globalThis as any).fetch;
1364
+ }
1365
+ });
1366
+
1367
+ it('(w18a) child material with parent edge: both child and parent end up in the catalog, child.payload.parent set to the parent AssetGuid', async () => {
1368
+ const reg = new AssetRegistry(makeMockShaderRegistry());
1369
+ if (typeof reg.configurePackIndex !== 'function') return;
1370
+ reg.configurePackIndex('/pack-index.json');
1371
+
1372
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1373
+ if (url === '/pack-index.json') {
1374
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(PACK_INDEX_FIXTURE) });
1375
+ }
1376
+ if (url === '/assets/parent.pack.json') {
1377
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(PARENT_PACK) });
1378
+ }
1379
+ if (url === '/assets/child.pack.json') {
1380
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CHILD_PACK) });
1381
+ }
1382
+ return Promise.resolve({ ok: false, status: 404 });
1383
+ });
1384
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
1385
+ globalThis.fetch = fetchMock as any;
1386
+
1387
+ const childGuidParsed = AssetGuid.parse(CHILD_GUID);
1388
+ const parentGuidParsed = AssetGuid.parse(PARENT_GUID);
1389
+ if (!childGuidParsed.ok || !parentGuidParsed.ok) throw new Error('expected ok');
1390
+ const result = await reg.loadByGuid<MaterialAsset>(childGuidParsed.value);
1391
+ expect(result.ok).toBe(true);
1392
+ if (!result.ok) return;
1393
+
1394
+ // Child catalogued with parent stamped as the parent AssetGuid (renderer
1395
+ // field read by walkMaterialPassesOverSharedRefs), NOT the parentGuid
1396
+ // string intermediate.
1397
+ const childInCatalog = reg.lookup(childGuidParsed.value) as MaterialAsset | undefined;
1398
+ expect(childInCatalog?.kind).toBe('material');
1399
+ expect(childInCatalog?.parent).toBeDefined();
1400
+ expect(AssetGuid.format(childInCatalog?.parent as AssetGuid).toLowerCase()).toBe(
1401
+ PARENT_GUID.toLowerCase(),
1402
+ );
1403
+
1404
+ // Parent catalogued by the unified for-loop recursion (formerly Path B's
1405
+ // independent preload).
1406
+ const parentInCatalog = reg.lookup(parentGuidParsed.value) as MaterialAsset | undefined;
1407
+ expect(parentInCatalog?.kind).toBe('material');
1408
+ expect(parentInCatalog?.passes?.length).toBe(1);
1409
+
1410
+ // Inheritance still resolves end-to-end through the catalogued chain.
1411
+ const walk = resolveMaterialChain(reg, result.value);
1412
+ expect(walk.ok).toBe(true);
1413
+ if (!walk.ok) return;
1414
+ expect(walk.value.passes.length).toBe(1);
1415
+ expect(walk.value.values.baseColor).toEqual([0.8, 0.2, 0.1, 1]);
1416
+ expect(walk.value.values.roughness).toBe(0.3);
1417
+ });
1418
+
1419
+ it('(w18b) parent load failure -> error breadcrumb matches "loading parent material X for child Y" (post-fold contract preserved)', async () => {
1420
+ const reg = new AssetRegistry(makeMockShaderRegistry());
1421
+ if (typeof reg.configurePackIndex !== 'function') return;
1422
+ reg.configurePackIndex('/pack-index.json');
1423
+
1424
+ const CATALOG_PARENT_MISSING = [
1425
+ {
1426
+ guid: CHILD_GUID,
1427
+ packageUrl: '/assets/child.pack.json',
1428
+ kind: 'material',
1429
+ sourcePath: 'assets/child.pack.json',
1430
+ },
1431
+ ];
1432
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1433
+ if (url === '/pack-index.json') {
1434
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CATALOG_PARENT_MISSING) });
1435
+ }
1436
+ if (url === '/assets/child.pack.json') {
1437
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CHILD_PACK) });
1438
+ }
1439
+ return Promise.resolve({ ok: false, status: 404 });
1440
+ });
1441
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
1442
+ globalThis.fetch = fetchMock as any;
1443
+
1444
+ const childGuidParsed = AssetGuid.parse(CHILD_GUID);
1445
+ if (!childGuidParsed.ok) throw new Error('expected ok');
1446
+ const result = await reg.loadByGuid<MaterialAsset>(childGuidParsed.value);
1447
+ expect(result.ok).toBe(false);
1448
+ if (result.ok) return;
1449
+ expect(result.error.hint ?? '').toContain(
1450
+ `loading parent material ${PARENT_GUID} for child ${CHILD_GUID}`,
1451
+ );
1452
+ });
1453
+
1454
+ it('(w18c) Path B independent early-return block is deleted from load-by-guid source', () => {
1455
+ // feat-20260705-runtime-tier2-decomposition M1 / w7 (D-4): the loadByGuid
1456
+ // + DDC pipeline moved from asset-registry.ts into registry/load-by-guid.ts.
1457
+ const src = readFileSync(
1458
+ fileURLToPath(
1459
+ new URL('../../../assets-runtime/src/registry/load-by-guid.ts', import.meta.url),
1460
+ ),
1461
+ 'utf-8',
1462
+ );
1463
+ // The unique parent breadcrumb literal must NOT appear inside an
1464
+ // independent `loadByGuid<MaterialAsset>(parentGuid` preload anymore —
1465
+ // that whole Path B early-return is folded into the unified for-loop.
1466
+ expect(src).not.toContain('loadByGuid<MaterialAsset>(parentGuid');
1467
+ // No early-return that registers a separately rebuilt `resolvedAsset`
1468
+ // (post-w7 the free function form is registerParsedAsset(registry, guid, ...)).
1469
+ expect(src).not.toContain('registerParsedAsset(registry, guid, resolvedAsset');
1470
+ // The `loading parent material` literal now lives only in the unified
1471
+ // for-loop branch (template form `for child ${guidKey}`). The old Path B
1472
+ // used `for child ${guidKey}` against a `parentGuidStr` local — confirm
1473
+ // the new template references `${subGuidKey}` (the for-loop edge GUID).
1474
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: matching the literal source text, which deliberately contains the `${...}` placeholders.
1475
+ expect(src).toContain('loading parent material ${subGuidKey} for child ${guidKey}');
1476
+ });
1477
+
1478
+ it('(w18d) material WITHOUT a parent still loads (no parent edge in refs[]; unified for-loop has nothing to recurse on)', async () => {
1479
+ const reg = new AssetRegistry(makeMockShaderRegistry());
1480
+ if (typeof reg.configurePackIndex !== 'function') return;
1481
+ reg.configurePackIndex('/pack-index.json');
1482
+
1483
+ // PARENT_PACK is a standalone material with passes and no parent ref.
1484
+ const CATALOG_STANDALONE = [
1485
+ {
1486
+ guid: PARENT_GUID,
1487
+ packageUrl: '/assets/parent.pack.json',
1488
+ kind: 'material',
1489
+ sourcePath: 'assets/parent.pack.json',
1490
+ },
1491
+ ];
1492
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1493
+ if (url === '/pack-index.json') {
1494
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(CATALOG_STANDALONE) });
1495
+ }
1496
+ if (url === '/assets/parent.pack.json') {
1497
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(PARENT_PACK) });
1498
+ }
1499
+ return Promise.resolve({ ok: false, status: 404 });
1500
+ });
1501
+ // biome-ignore lint/suspicious/noExplicitAny: test mock requires unsafe cast
1502
+ globalThis.fetch = fetchMock as any;
1503
+
1504
+ const guidParsed = AssetGuid.parse(PARENT_GUID);
1505
+ if (!guidParsed.ok) throw new Error('expected ok');
1506
+ const result = await reg.loadByGuid<MaterialAsset>(guidParsed.value);
1507
+ expect(result.ok).toBe(true);
1508
+ if (!result.ok) return;
1509
+ expect(result.value.kind).toBe('material');
1510
+ expect(result.value.parent).toBeUndefined();
1511
+ expect(result.value.passes?.length).toBe(1);
1512
+ });
1513
+ });
1514
+ }