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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +424 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts +2 -0
  5. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/callback-deletion.unit.test.d.ts +2 -0
  7. package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
  9. package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
  11. package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
  13. package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/create-app-error-retention.integration.test.d.ts +2 -0
  15. package/dist/__tests__/create-app-error-retention.integration.test.d.ts.map +1 -0
  16. package/dist/__tests__/create-app-features.test.d.ts +2 -0
  17. package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
  18. package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
  19. package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
  20. package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
  21. package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
  22. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts +2 -0
  23. package/dist/__tests__/create-app-physics-plugin-failure.integration.test.d.ts.map +1 -0
  24. package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
  25. package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
  26. package/dist/__tests__/create-app-stop.test.d.ts +2 -0
  27. package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
  28. package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
  29. package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/create-app.test-d.d.ts +2 -0
  31. package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
  32. package/dist/__tests__/create-app.test.d.ts +2 -0
  33. package/dist/__tests__/create-app.test.d.ts.map +1 -0
  34. package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
  35. package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
  36. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
  37. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
  39. package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
  41. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
  42. package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
  43. package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
  44. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
  45. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
  47. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
  49. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
  50. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
  51. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
  53. package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
  54. package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
  55. package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
  57. package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
  59. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
  61. package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
  62. package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
  63. package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
  65. package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
  66. package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
  67. package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
  69. package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
  70. package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
  71. package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
  72. package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
  73. package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
  74. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
  75. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
  77. package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
  79. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
  81. package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
  83. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
  84. package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
  85. package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
  87. package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
  88. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
  89. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
  91. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
  92. package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
  93. package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
  94. package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
  95. package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
  96. package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
  97. package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
  98. package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
  99. package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
  100. package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
  101. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
  102. package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
  103. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
  104. package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
  105. package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
  106. package/dist/__tests__/remote-integration.test.d.ts +2 -0
  107. package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
  108. package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
  109. package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
  110. package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
  111. package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
  112. package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
  113. package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
  114. package/dist/__tests__/surface-handoff.test.d.ts +2 -0
  115. package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
  116. package/dist/__tests__/types-m2.test-d.d.ts +2 -0
  117. package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
  118. package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
  119. package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
  120. package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
  121. package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
  122. package/dist/animation-asset-lookup.d.ts +7 -0
  123. package/dist/animation-asset-lookup.d.ts.map +1 -0
  124. package/dist/browser-frame-signal.d.ts +17 -0
  125. package/dist/browser-frame-signal.d.ts.map +1 -0
  126. package/dist/canvas-policy.d.ts +4 -0
  127. package/dist/canvas-policy.d.ts.map +1 -0
  128. package/dist/create-app.d.ts +78 -0
  129. package/dist/create-app.d.ts.map +1 -0
  130. package/dist/engine-worker-runtime.mjs +827 -0
  131. package/dist/engine-worker-runtime.mjs.map +1 -0
  132. package/dist/errors.d.ts +254 -0
  133. package/dist/errors.d.ts.map +1 -0
  134. package/dist/execution/attached-world-swap.d.ts +10 -0
  135. package/dist/execution/attached-world-swap.d.ts.map +1 -0
  136. package/dist/execution/bootstrap-entry.d.ts +30 -0
  137. package/dist/execution/bootstrap-entry.d.ts.map +1 -0
  138. package/dist/execution/bootstrap-url.d.ts +5 -0
  139. package/dist/execution/bootstrap-url.d.ts.map +1 -0
  140. package/dist/execution/capabilities.d.ts +5 -0
  141. package/dist/execution/capabilities.d.ts.map +1 -0
  142. package/dist/execution/control.d.ts +12 -0
  143. package/dist/execution/control.d.ts.map +1 -0
  144. package/dist/execution/engine-worker-runtime.d.ts +2 -0
  145. package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
  146. package/dist/execution/engine-worker.d.ts +24 -0
  147. package/dist/execution/engine-worker.d.ts.map +1 -0
  148. package/dist/execution/host-controller.d.ts +16 -0
  149. package/dist/execution/host-controller.d.ts.map +1 -0
  150. package/dist/execution/index.d.ts +10 -0
  151. package/dist/execution/index.d.ts.map +1 -0
  152. package/dist/execution/kernel-pool.d.ts +14 -0
  153. package/dist/execution/kernel-pool.d.ts.map +1 -0
  154. package/dist/execution/kernel-worker-runtime.d.ts +2 -0
  155. package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
  156. package/dist/execution/measurement.d.ts +7 -0
  157. package/dist/execution/measurement.d.ts.map +1 -0
  158. package/dist/execution/protocol.d.ts +87 -0
  159. package/dist/execution/protocol.d.ts.map +1 -0
  160. package/dist/execution/report.d.ts +5 -0
  161. package/dist/execution/report.d.ts.map +1 -0
  162. package/dist/execution/schema.d.ts +4 -0
  163. package/dist/execution/schema.d.ts.map +1 -0
  164. package/dist/execution/selector.d.ts +10 -0
  165. package/dist/execution/selector.d.ts.map +1 -0
  166. package/dist/execution/types.d.ts +123 -0
  167. package/dist/execution/types.d.ts.map +1 -0
  168. package/dist/fallback-camera.d.ts +8 -0
  169. package/dist/fallback-camera.d.ts.map +1 -0
  170. package/dist/game-context.d.ts +98 -0
  171. package/dist/game-context.d.ts.map +1 -0
  172. package/dist/index.d.ts +27 -0
  173. package/dist/index.d.ts.map +1 -0
  174. package/dist/index.mjs +3980 -0
  175. package/dist/index.mjs.map +1 -0
  176. package/dist/input-map-plugin.d.ts +5 -0
  177. package/dist/input-map-plugin.d.ts.map +1 -0
  178. package/dist/input-plugin.d.ts +4 -0
  179. package/dist/input-plugin.d.ts.map +1 -0
  180. package/dist/internal/assembled-engine-profile.d.ts +5 -0
  181. package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
  182. package/dist/internal/assets-world-plugin.d.ts +7 -0
  183. package/dist/internal/assets-world-plugin.d.ts.map +1 -0
  184. package/dist/internal/browser-remote-bridge.d.ts +24 -0
  185. package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
  186. package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
  187. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
  188. package/dist/internal/component-introspection.d.ts +24 -0
  189. package/dist/internal/component-introspection.d.ts.map +1 -0
  190. package/dist/internal/debug-draw.d.ts +9 -0
  191. package/dist/internal/debug-draw.d.ts.map +1 -0
  192. package/dist/internal/ecs-import.d.ts +6 -0
  193. package/dist/internal/ecs-import.d.ts.map +1 -0
  194. package/dist/internal/engine-profile-common.d.ts +9 -0
  195. package/dist/internal/engine-profile-common.d.ts.map +1 -0
  196. package/dist/internal/error-fanout.d.ts +66 -0
  197. package/dist/internal/error-fanout.d.ts.map +1 -0
  198. package/dist/internal/frame-loop.d.ts +38 -0
  199. package/dist/internal/frame-loop.d.ts.map +1 -0
  200. package/dist/internal/input-attach.d.ts +44 -0
  201. package/dist/internal/input-attach.d.ts.map +1 -0
  202. package/dist/internal/main-engine-profile.d.ts +19 -0
  203. package/dist/internal/main-engine-profile.d.ts.map +1 -0
  204. package/dist/internal/remote-serve-flag.d.ts +19 -0
  205. package/dist/internal/remote-serve-flag.d.ts.map +1 -0
  206. package/dist/internal/remote-server-plugin.d.ts +8 -0
  207. package/dist/internal/remote-server-plugin.d.ts.map +1 -0
  208. package/dist/internal/rhi-capture.d.ts +19 -0
  209. package/dist/internal/rhi-capture.d.ts.map +1 -0
  210. package/dist/internal/rhi-debug-flag.d.ts +18 -0
  211. package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
  212. package/dist/internal/worker-engine-profile.d.ts +15 -0
  213. package/dist/internal/worker-engine-profile.d.ts.map +1 -0
  214. package/dist/kernel-worker-runtime.mjs +69 -0
  215. package/dist/kernel-worker-runtime.mjs.map +1 -0
  216. package/dist/load-game-errors.d.ts +131 -0
  217. package/dist/load-game-errors.d.ts.map +1 -0
  218. package/dist/load-game.d.ts +34 -0
  219. package/dist/load-game.d.ts.map +1 -0
  220. package/dist/plugin-factories.d.ts +3 -0
  221. package/dist/plugin-factories.d.ts.map +1 -0
  222. package/dist/renderer-plugin.d.ts +25 -0
  223. package/dist/renderer-plugin.d.ts.map +1 -0
  224. package/dist/tool-preview/bootstrap.d.ts +113 -0
  225. package/dist/tool-preview/bootstrap.d.ts.map +1 -0
  226. package/dist/tool-preview/evidence.d.ts +20 -0
  227. package/dist/tool-preview/evidence.d.ts.map +1 -0
  228. package/dist/tool-preview/framing.d.ts +13 -0
  229. package/dist/tool-preview/framing.d.ts.map +1 -0
  230. package/dist/tool-preview/recipe.d.ts +54 -0
  231. package/dist/tool-preview/recipe.d.ts.map +1 -0
  232. package/dist/types.d.ts +404 -0
  233. package/dist/types.d.ts.map +1 -0
  234. package/package.json +85 -0
  235. package/schema/execution-report.schema.json +168 -0
  236. package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
  237. package/src/__tests__/callback-deletion.unit.test.ts +14 -0
  238. package/src/__tests__/callback-inference.test-d.ts +36 -0
  239. package/src/__tests__/component-introspection.unit.test.ts +56 -0
  240. package/src/__tests__/consumer-migration.unit.test.ts +34 -0
  241. package/src/__tests__/create-app-error-retention.integration.test.ts +125 -0
  242. package/src/__tests__/create-app-features.test.ts +51 -0
  243. package/src/__tests__/create-app-listener-sync.test.ts +381 -0
  244. package/src/__tests__/create-app-lock-provider.test.ts +212 -0
  245. package/src/__tests__/create-app-physics-plugin-failure.integration.test.ts +142 -0
  246. package/src/__tests__/create-app-plugin.test.ts +217 -0
  247. package/src/__tests__/create-app-stop.test.ts +609 -0
  248. package/src/__tests__/create-app-time.unit.test.ts +26 -0
  249. package/src/__tests__/create-app.test-d.ts +83 -0
  250. package/src/__tests__/create-app.test.ts +287 -0
  251. package/src/__tests__/create-renderer-error.integration.test.ts +25 -0
  252. package/src/__tests__/create-renderer-lifecycle.integration.test.ts +24 -0
  253. package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
  254. package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +176 -0
  255. package/src/__tests__/draw-source-single-world-regression.test.ts +184 -0
  256. package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +82 -0
  257. package/src/__tests__/engine-worker-world-swap.unit.test.ts +69 -0
  258. package/src/__tests__/errors-pointer-lock-failed.test.ts +265 -0
  259. package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +79 -0
  260. package/src/__tests__/execution-error-detail.test-d.ts +13 -0
  261. package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
  262. package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
  263. package/src/__tests__/execution-frame-protocol.unit.test.ts +60 -0
  264. package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
  265. package/src/__tests__/execution-measurement.unit.test.ts +20 -0
  266. package/src/__tests__/execution-public-api.test-d.ts +14 -0
  267. package/src/__tests__/execution-public-api.unit.test.ts +9 -0
  268. package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
  269. package/src/__tests__/execution-report.contract.test.ts +50 -0
  270. package/src/__tests__/execution-report.types.test-d.ts +16 -0
  271. package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
  272. package/src/__tests__/execution-selector.unit.test.ts +72 -0
  273. package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
  274. package/src/__tests__/fallback-camera.unit.test.ts +32 -0
  275. package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
  276. package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
  277. package/src/__tests__/frame-loop-world-array.test.ts +450 -0
  278. package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
  279. package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
  280. package/src/__tests__/plugin-policy.integration.test.ts +15 -0
  281. package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
  282. package/src/__tests__/profiler-default-off.test.ts +53 -0
  283. package/src/__tests__/profiler-frame-token.test.ts +235 -0
  284. package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
  285. package/src/__tests__/profiler-phase-integration.test.ts +117 -0
  286. package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
  287. package/src/__tests__/remote-integration.test.ts +224 -0
  288. package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
  289. package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
  290. package/src/__tests__/rhi-capture.browser.test.ts +51 -0
  291. package/src/__tests__/surface-handoff.test.ts +89 -0
  292. package/src/__tests__/types-m2.test-d.ts +81 -0
  293. package/src/__tests__/visibility-remote.integration.test.ts +116 -0
  294. package/src/__tests__/worker-execution-stop.test.ts +173 -0
  295. package/src/animation-asset-lookup.ts +13 -0
  296. package/src/browser-frame-signal.ts +44 -0
  297. package/src/canvas-policy.ts +16 -0
  298. package/src/create-app.ts +1264 -0
  299. package/src/errors.ts +442 -0
  300. package/src/execution/attached-world-swap.ts +33 -0
  301. package/src/execution/bootstrap-entry.ts +122 -0
  302. package/src/execution/bootstrap-url.ts +25 -0
  303. package/src/execution/capabilities.ts +102 -0
  304. package/src/execution/control.ts +48 -0
  305. package/src/execution/engine-worker-runtime.ts +383 -0
  306. package/src/execution/engine-worker.ts +163 -0
  307. package/src/execution/host-controller.ts +460 -0
  308. package/src/execution/index.ts +44 -0
  309. package/src/execution/kernel-pool.ts +236 -0
  310. package/src/execution/kernel-worker-runtime.ts +103 -0
  311. package/src/execution/measurement.ts +39 -0
  312. package/src/execution/protocol.ts +134 -0
  313. package/src/execution/report.ts +66 -0
  314. package/src/execution/schema.ts +168 -0
  315. package/src/execution/selector.ts +98 -0
  316. package/src/execution/types.ts +163 -0
  317. package/src/fallback-camera.ts +20 -0
  318. package/src/game-context.ts +126 -0
  319. package/src/index.ts +190 -0
  320. package/src/input-map-plugin.ts +21 -0
  321. package/src/input-plugin.ts +26 -0
  322. package/src/internal/assembled-engine-profile.ts +14 -0
  323. package/src/internal/assets-world-plugin.ts +33 -0
  324. package/src/internal/browser-remote-bridge.ts +269 -0
  325. package/src/internal/browser-rhi-debug-runtime.ts +67 -0
  326. package/src/internal/component-introspection.ts +116 -0
  327. package/src/internal/debug-draw.ts +38 -0
  328. package/src/internal/ecs-import.ts +41 -0
  329. package/src/internal/engine-profile-common.ts +9 -0
  330. package/src/internal/error-fanout.ts +129 -0
  331. package/src/internal/frame-loop.ts +539 -0
  332. package/src/internal/input-attach.ts +104 -0
  333. package/src/internal/main-engine-profile.ts +84 -0
  334. package/src/internal/remote-serve-flag.ts +41 -0
  335. package/src/internal/remote-server-plugin.ts +16 -0
  336. package/src/internal/rhi-capture.ts +211 -0
  337. package/src/internal/rhi-debug-flag.ts +40 -0
  338. package/src/internal/worker-engine-profile.ts +47 -0
  339. package/src/load-game-errors.ts +211 -0
  340. package/src/load-game.ts +115 -0
  341. package/src/plugin-factories.ts +2 -0
  342. package/src/renderer-plugin.ts +96 -0
  343. package/src/tool-preview/bootstrap.ts +835 -0
  344. package/src/tool-preview/evidence.ts +59 -0
  345. package/src/tool-preview/framing.ts +52 -0
  346. package/src/tool-preview/recipe.ts +137 -0
  347. package/src/types.ts +450 -0
  348. package/src/vite-env.d.ts +10 -0
@@ -0,0 +1,609 @@
1
+ // feat-20260612-rhi-destroy-renderer-dispose-gpu-lifecycle / M6 / w22.
2
+ //
3
+ // Runtime unit test for assemble ownership: createApp({renderer, world}).stop()
4
+ // stops the App loop but leaves the host-owned Renderer alive.
5
+ //
6
+ // Mock approach (mirrors packages/app/__tests__/app.unit.test.ts
7
+ // `makeRendererStub` pattern): build a minimal Renderer stub with
8
+ // resolved `ready` + a `vi.fn()` `dispose` spy. createApp's assemble
9
+ // form awaits `renderer.ready` so the stub must surface a settled
10
+ // Result.ok promise. Subscribing renderer.onError is internal to
11
+ // createApp.start() (R-1 timing); we return a noop unsubscribe.
12
+
13
+ import { World } from '@forgeax/engine-ecs';
14
+ import type { Renderer } from '@forgeax/engine-render';
15
+ import type { RhiError } from '@forgeax/engine-rhi/errors';
16
+ import { ok } from '@forgeax/engine-types';
17
+ import { describe, expect, it, vi } from 'vitest';
18
+ import { RendererContractFailureError } from '../../../render/src/errors/render';
19
+
20
+ import { createApp } from '../create-app';
21
+
22
+ type ReadyResult = { ok: true; value: undefined } | { ok: false; error: RhiError };
23
+
24
+ interface StubRenderer {
25
+ readonly renderer: Renderer;
26
+ readonly disposeSpy: ReturnType<typeof vi.fn>;
27
+ readonly releaseSurfaceSpy: ReturnType<typeof vi.fn>;
28
+ readonly restoreSurfaceSpy: ReturnType<typeof vi.fn>;
29
+ }
30
+
31
+ function makeRendererStubForStop(): StubRenderer {
32
+ const ready: Promise<ReadyResult> = Promise.resolve({ ok: true, value: undefined });
33
+ const disposeSpy = vi.fn<() => void>();
34
+ const releaseSurfaceSpy = vi.fn(() => ok(undefined));
35
+ const restoreSurfaceSpy = vi.fn(() => ok(undefined));
36
+ const renderer = {
37
+ backend: 'webgpu' as const,
38
+ ready,
39
+ draw(): { ok: true; value: undefined } {
40
+ return { ok: true, value: undefined };
41
+ },
42
+ attach(): { ok: true; value: undefined } {
43
+ return { ok: true, value: undefined };
44
+ },
45
+ detachWorld(): void {},
46
+ subscribe(): () => void {
47
+ return () => {
48
+ // no-op unsubscribe
49
+ };
50
+ },
51
+ releaseSurface: releaseSurfaceSpy,
52
+ restoreSurface: restoreSurfaceSpy,
53
+ dispose: disposeSpy,
54
+ } as unknown as Renderer;
55
+ return { renderer, disposeSpy, releaseSurfaceSpy, restoreSurfaceSpy };
56
+ }
57
+
58
+ describe('create-app-stop.test.ts', () => {
59
+ describe('App presentation-surface lease', () => {
60
+ it('releases and restores presentation without replacing World or Renderer', async () => {
61
+ const { renderer, releaseSurfaceSpy, restoreSurfaceSpy } = makeRendererStubForStop();
62
+ const world = new World();
63
+ const result = await createApp({ renderer, world });
64
+ expect(result.ok).toBe(true);
65
+ if (!result.ok) return;
66
+ const app = result.value;
67
+
68
+ expect(app.start().ok).toBe(true);
69
+ expect((await app.releaseSurfacePreserveWorld()).ok).toBe(true);
70
+ expect(releaseSurfaceSpy).toHaveBeenCalledTimes(1);
71
+ expect(app.world).toBe(world);
72
+ expect(app.renderer).toBe(renderer);
73
+
74
+ expect((await app.restoreSurface()).ok).toBe(true);
75
+ expect(restoreSurfaceSpy).toHaveBeenCalledTimes(1);
76
+ expect(app.world).toBe(world);
77
+ expect(app.renderer).toBe(renderer);
78
+ });
79
+
80
+ it('does not restore a running loop when surface release fails', async () => {
81
+ const { renderer } = makeRendererStubForStop();
82
+ const releaseError = new RendererContractFailureError('draw', 'forced failure');
83
+ vi.mocked(renderer.releaseSurface).mockReturnValue({ ok: false, error: releaseError });
84
+ const result = await createApp({ renderer, world: new World() });
85
+ expect(result.ok).toBe(true);
86
+ if (!result.ok) return;
87
+
88
+ expect(result.value.start().ok).toBe(true);
89
+ const released = await result.value.releaseSurfacePreserveWorld();
90
+ expect(released.ok).toBe(false);
91
+ expect(result.value.execution.report().engine.health).toBe('running');
92
+ });
93
+ });
94
+
95
+ describe('assemble App stop preserves its host-owned Renderer', () => {
96
+ it('start -> stop does not call renderer.dispose()', async () => {
97
+ const { renderer, disposeSpy } = makeRendererStubForStop();
98
+ const result = await createApp({ renderer, world: new World() });
99
+ expect(result.ok).toBe(true);
100
+ if (!result.ok) return;
101
+ const app = result.value;
102
+
103
+ const startResult = app.start();
104
+ expect(startResult.ok).toBe(true);
105
+
106
+ const stopResult = app.stop();
107
+ expect(stopResult.ok).toBe(true);
108
+ expect(disposeSpy).not.toHaveBeenCalled();
109
+ });
110
+
111
+ it('second stop() remains inert for renderer ownership', async () => {
112
+ const { renderer, disposeSpy } = makeRendererStubForStop();
113
+ const result = await createApp({ renderer, world: new World() });
114
+ expect(result.ok).toBe(true);
115
+ if (!result.ok) return;
116
+ const app = result.value;
117
+
118
+ app.start();
119
+ app.stop();
120
+ // Second stop returns app-not-started err (frame-loop already idle),
121
+ // but the call itself must not throw and must not re-fire dispose.
122
+ const second = app.stop();
123
+ expect(second.ok).toBe(false);
124
+ expect(disposeSpy).not.toHaveBeenCalled();
125
+ });
126
+
127
+ it('stop() before start() does not call renderer.dispose()', async () => {
128
+ const { renderer, disposeSpy } = makeRendererStubForStop();
129
+ const result = await createApp({ renderer, world: new World() });
130
+ expect(result.ok).toBe(true);
131
+ if (!result.ok) return;
132
+ const app = result.value;
133
+
134
+ const stopResult = app.stop();
135
+ expect(stopResult.ok).toBe(false);
136
+ expect(disposeSpy).not.toHaveBeenCalled();
137
+
138
+ app.stop();
139
+ expect(disposeSpy).not.toHaveBeenCalled();
140
+ });
141
+ });
142
+ });
143
+
144
+ // w4: AC-04 -- assemble form does not auto-destroy host-managed audioBackend.
145
+ describe('w4: AC-04 assemble form does not auto-destroy external backend', () => {
146
+ it('stop() on assemble-form app with external audioBackend does not call destroy()', async () => {
147
+ const { renderer, disposeSpy: _disposeSpy } = makeRendererStubForStop();
148
+ const destroySpy = vi.fn<() => void>();
149
+ const audioBackend = {
150
+ play: vi.fn(),
151
+ stop: vi.fn(),
152
+ setVolume: vi.fn(),
153
+ setBusVolume: vi.fn(),
154
+ setBusMute: vi.fn(),
155
+ setListenerPose: vi.fn(),
156
+ getState: vi.fn(() => ({
157
+ contextState: 'running' as const,
158
+ activeSourceCount: 0,
159
+ lastError: null,
160
+ })),
161
+ getActiveSourceCount: vi.fn(() => 0),
162
+ destroy: destroySpy,
163
+ };
164
+
165
+ const result = await createApp({
166
+ renderer,
167
+ world: (() => {
168
+ const w = new World();
169
+ w.insertResource(AUDIO_ENGINE_RESOURCE_KEY, audioBackend);
170
+ return w;
171
+ })(),
172
+ });
173
+ expect(result.ok).toBe(true);
174
+ if (!result.ok) return;
175
+ const app = result.value;
176
+
177
+ const startResult = app.start();
178
+ expect(startResult.ok).toBe(true);
179
+
180
+ const stopResult = app.stop();
181
+ expect(stopResult.ok).toBe(true);
182
+
183
+ // The external backend is host-managed; the engine MUST NOT call destroy().
184
+ expect(destroySpy).not.toHaveBeenCalled();
185
+ });
186
+ });
187
+
188
+ // ---------------------------------------------------------------------------
189
+ // feat-20260619 M3: auto-register audioTickSystem (w13/w14 tests, w15 impl)
190
+ // ---------------------------------------------------------------------------
191
+
192
+ import {
193
+ AUDIO_ENGINE_RESOURCE_KEY,
194
+ AudioSource,
195
+ audioBackendPlugin,
196
+ audioPlugin,
197
+ } from '@forgeax/engine-audio';
198
+ import { WebAudioEngine } from '@forgeax/engine-audio-webaudio';
199
+
200
+ function createTestBufferForApp(duration = 1, sampleRate = 48000): AudioBuffer {
201
+ return {
202
+ sampleRate,
203
+ length: duration * sampleRate,
204
+ duration,
205
+ numberOfChannels: 2,
206
+ getChannelData: vi.fn(() => new Float32Array(duration * sampleRate)),
207
+ copyFromChannel: vi.fn(),
208
+ copyToChannel: vi.fn(),
209
+ } as unknown as AudioBuffer;
210
+ }
211
+
212
+ function makeMockAudioContextForApp(): AudioContext {
213
+ const mkGain = vi.fn(
214
+ () =>
215
+ ({
216
+ gain: { value: 1 } as unknown as AudioParam,
217
+ context: undefined as unknown as BaseAudioContext,
218
+ numberOfInputs: 1,
219
+ numberOfOutputs: 1,
220
+ channelCount: 2,
221
+ channelCountMode: 'max' as const,
222
+ channelInterpretation: 'speakers' as const,
223
+ connect: vi.fn().mockReturnValue(undefined),
224
+ disconnect: vi.fn(),
225
+ addEventListener: vi.fn(),
226
+ removeEventListener: vi.fn(),
227
+ dispatchEvent: vi.fn(),
228
+ }) as unknown as GainNode,
229
+ );
230
+
231
+ const mkSrc = vi.fn(
232
+ () =>
233
+ ({
234
+ buffer: null,
235
+ playbackRate: { value: 1 } as unknown as AudioParam,
236
+ detune: { value: 0 } as unknown as AudioParam,
237
+ loop: false,
238
+ loopStart: 0,
239
+ loopEnd: 0,
240
+ context: undefined as unknown as BaseAudioContext,
241
+ numberOfInputs: 0,
242
+ numberOfOutputs: 1,
243
+ channelCount: 2,
244
+ channelCountMode: 'max' as const,
245
+ channelInterpretation: 'speakers' as const,
246
+ connect: vi.fn().mockReturnValue(undefined),
247
+ disconnect: vi.fn(),
248
+ start: vi.fn(),
249
+ stop: vi.fn(),
250
+ addEventListener: vi.fn(),
251
+ removeEventListener: vi.fn(),
252
+ dispatchEvent: vi.fn(),
253
+ onended: null,
254
+ }) as unknown as AudioBufferSourceNode,
255
+ );
256
+
257
+ const mkPanner = vi.fn(
258
+ () =>
259
+ ({
260
+ panningModel: 'equalpower' as const,
261
+ distanceModel: 'inverse' as const,
262
+ refDistance: 1,
263
+ maxDistance: 10000,
264
+ rolloffFactor: 1,
265
+ coneInnerAngle: 360,
266
+ coneOuterAngle: 360,
267
+ coneOuterGain: 0,
268
+ positionX: { value: 0 } as unknown as AudioParam,
269
+ positionY: { value: 0 } as unknown as AudioParam,
270
+ positionZ: { value: 0 } as unknown as AudioParam,
271
+ orientationX: { value: 1 } as unknown as AudioParam,
272
+ orientationY: { value: 0 } as unknown as AudioParam,
273
+ orientationZ: { value: 0 } as unknown as AudioParam,
274
+ context: undefined as unknown as BaseAudioContext,
275
+ numberOfInputs: 1,
276
+ numberOfOutputs: 1,
277
+ channelCount: 2,
278
+ channelCountMode: 'explicit' as const,
279
+ channelInterpretation: 'speakers' as const,
280
+ connect: vi.fn().mockReturnValue(undefined),
281
+ disconnect: vi.fn(),
282
+ addEventListener: vi.fn(),
283
+ removeEventListener: vi.fn(),
284
+ dispatchEvent: vi.fn(),
285
+ }) as unknown as PannerNode,
286
+ );
287
+
288
+ return {
289
+ state: 'suspended' as AudioContextState,
290
+ sampleRate: 48000,
291
+ destination: {
292
+ maxChannelCount: 2,
293
+ channelCount: 2,
294
+ channelCountMode: 'explicit' as const,
295
+ channelInterpretation: 'speakers' as const,
296
+ context: undefined as unknown as BaseAudioContext,
297
+ numberOfInputs: 1,
298
+ numberOfOutputs: 0,
299
+ connect: vi.fn().mockReturnValue(undefined),
300
+ disconnect: vi.fn(),
301
+ addEventListener: vi.fn(),
302
+ removeEventListener: vi.fn(),
303
+ dispatchEvent: vi.fn(),
304
+ } as unknown as AudioDestinationNode,
305
+ currentTime: 0,
306
+ listener: {} as unknown as AudioListener,
307
+ resume: vi.fn().mockResolvedValue(undefined),
308
+ suspend: vi.fn().mockResolvedValue(undefined),
309
+ close: vi.fn().mockResolvedValue(undefined),
310
+ createGain: mkGain,
311
+ createBufferSource: mkSrc,
312
+ createPanner: mkPanner,
313
+ decodeAudioData: vi.fn().mockResolvedValue(createTestBufferForApp()),
314
+ addEventListener: vi.fn(),
315
+ removeEventListener: vi.fn(),
316
+ dispatchEvent: vi.fn(),
317
+ onstatechange: null,
318
+ baseLatency: 0,
319
+ outputLatency: 0,
320
+ getOutputTimestamp: vi.fn(),
321
+ createMediaStreamDestination: vi.fn(),
322
+ createMediaStreamSource: vi.fn(),
323
+ createChannelMerger: vi.fn(),
324
+ createChannelSplitter: vi.fn(),
325
+ createDelay: vi.fn(),
326
+ createBiquadFilter: vi.fn(),
327
+ createConvolver: vi.fn(),
328
+ createDynamicsCompressor: vi.fn(),
329
+ createOscillator: vi.fn(),
330
+ createStereoPanner: vi.fn(),
331
+ createWaveShaper: vi.fn(),
332
+ createIIRFilter: vi.fn(),
333
+ createScriptProcessor: vi.fn(),
334
+ createAnalyser: vi.fn(),
335
+ createConstantSource: vi.fn(),
336
+ audioWorklet: undefined as unknown as AudioWorklet,
337
+ } as unknown as AudioContext;
338
+ }
339
+
340
+ /**
341
+ * Node-env rAF injection: in node, `requestAnimationFrame` is undefined,
342
+ * so resolveRaf (frame-loop.ts:141-153) returns `() => 0` — the tick
343
+ * callback is never invoked. We stub it so `start()` runs one frame
344
+ * synchronously, and the captured tick callback can be driven repeatedly
345
+ * by the test for additional frames.
346
+ *
347
+ * Auto-invoke only on first raf call (from start()), not on subsequent
348
+ * calls (from tick()'s `raf(tick)` at frame-loop.ts:257), to avoid
349
+ * infinite recursion.
350
+ */
351
+ function installRafStub(): { tickFn: (() => void) | null } {
352
+ const stub: { tickFn: (() => void) | null } = { tickFn: null };
353
+ let firstCall = true;
354
+ // biome-ignore lint/suspicious/noExplicitAny: vitest stub of a browser Global
355
+ (globalThis as any).requestAnimationFrame = (cb: (t: number) => void) => {
356
+ stub.tickFn = () => cb(performance.now());
357
+ if (firstCall) {
358
+ firstCall = false;
359
+ stub.tickFn();
360
+ }
361
+ return 1;
362
+ };
363
+ // biome-ignore lint/suspicious/noExplicitAny: vitest stub of a browser Global
364
+ (globalThis as any).cancelAnimationFrame = () => {
365
+ stub.tickFn = null;
366
+ };
367
+ return stub;
368
+ }
369
+
370
+ function uninstallRafStub() {
371
+ delete (globalThis as Record<string, unknown>).requestAnimationFrame;
372
+ delete (globalThis as Record<string, unknown>).cancelAnimationFrame;
373
+ }
374
+
375
+ describe('feat-20260619 M3: auto-register audioTickSystem', () => {
376
+ // w13: AC-12 — audioPlugin registers the audio-tick world system.
377
+ // feat-20260623-plugin-system-unify (M2 / D-2 / D-4): the assemble form no
378
+ // longer auto-wires audio; the host inserts AUDIO_ENGINE_RESOURCE_KEY and
379
+ // passes audioPlugin() in plugins. The audio-tick system then runs inside
380
+ // world.update(1 / 60).unwrap() each frame. Verification goes through the full consumption
381
+ // path: createApp → app.start() → frames tick → AudioSource.playing edge
382
+ // detected → backend.play triggered.
383
+ //
384
+ // Edge-detection semantics (getPrevState, audio-tick-system.ts:187-197):
385
+ // first observation stores current as prev and returns current → no edge.
386
+ // A false→true edge requires two frames: frame 1 sees false (prev=false),
387
+ // frame 2 sees true (prev=false, current=true → play-start).
388
+ describe('w13: AC-12 auto-register wiring verification', () => {
389
+ it('audioPlugin → backend.play triggered after playing edge (full consumption path)', async () => {
390
+ const OriginalAudioContext = globalThis.AudioContext;
391
+ vi.stubGlobal('document', {
392
+ addEventListener: vi.fn(),
393
+ removeEventListener: vi.fn(),
394
+ });
395
+ // biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor
396
+ globalThis.AudioContext = vi.fn(() => makeMockAudioContextForApp()) as any;
397
+
398
+ const rafStub = installRafStub();
399
+
400
+ try {
401
+ const world = new World();
402
+
403
+ // Alloc a clip handle so clipResolver can resolve it.
404
+ const clipHandle = world.sharedRefs.alloc('AudioClipAsset', {
405
+ kind: 'audio',
406
+ sourceKey: 'stop-fixture-a',
407
+ bytes: new Uint8Array([1]),
408
+ });
409
+
410
+ // Spawn entity with playing=false. After frame 1, turn playing=true
411
+ // to produce a false→true edge on frame 2.
412
+ const spawnResult = world.spawn({
413
+ // biome-ignore lint/suspicious/noExplicitAny: handle type is opaque branded via alloc
414
+ component: AudioSource as any,
415
+ data: {
416
+ clip: clipHandle,
417
+ playing: false,
418
+ loop: false,
419
+ volume: 1,
420
+ spatialBlend: 0,
421
+ bus: 'sfx',
422
+ },
423
+ });
424
+ expect(spawnResult.ok).toBe(true);
425
+ if (!spawnResult.ok) return;
426
+ const entity = spawnResult.value;
427
+
428
+ const backend = new WebAudioEngine();
429
+ const playSpy = vi.spyOn(backend, 'play');
430
+
431
+ const { renderer } = makeRendererStubForStop();
432
+ // Attach sharedRefs as renderer.assets so buildApp inserts
433
+ // ASSET_REGISTRY_RESOURCE_KEY (clipResolver reads world.sharedRefs
434
+ // directly though, so this is primarily for buildApp coverage).
435
+ const rendererWithAssets = { ...renderer, assets: world.sharedRefs } as unknown as Renderer;
436
+ // New assemble contract (D-2 / D-4): host inserts the audio backend
437
+ // resource + passes audioPlugin() in the plugin list. audioPlugin reads
438
+ // the resource and registers the audio-tick world system.
439
+ const result = await createApp({
440
+ renderer: rendererWithAssets,
441
+ world,
442
+ plugins: [audioBackendPlugin(backend), audioPlugin()],
443
+ });
444
+ expect(result.ok).toBe(true);
445
+ if (!result.ok) return;
446
+ const app = result.value;
447
+
448
+ // Frame 1: playing=false (first obs → prev=false → no edge → no play).
449
+ app.start();
450
+ expect(playSpy).not.toHaveBeenCalled();
451
+
452
+ // Set playing=true so the next frame detects the false→true edge.
453
+ const setResult = world.set(entity, AudioSource, { playing: true });
454
+ expect(setResult.ok).toBe(true);
455
+
456
+ // Frame 2: captured rAF tick runs. The auto-registered audioTickSystem
457
+ // calls getPrevState which sees prev=false, current=true → play-start.
458
+ expect(rafStub.tickFn).not.toBeNull();
459
+ rafStub.tickFn?.();
460
+
461
+ expect(playSpy).toHaveBeenCalledTimes(1);
462
+
463
+ app.stop();
464
+ playSpy.mockRestore();
465
+ } finally {
466
+ uninstallRafStub();
467
+ globalThis.AudioContext = OriginalAudioContext;
468
+ vi.restoreAllMocks();
469
+ vi.unstubAllGlobals();
470
+ }
471
+ });
472
+
473
+ // Reverse: no audio:true → no registration → no play.
474
+ it('no audio:true → audioTickSystem not registered, backend.play not triggered', async () => {
475
+ const OriginalAudioContext = globalThis.AudioContext;
476
+ vi.stubGlobal('document', {
477
+ addEventListener: vi.fn(),
478
+ removeEventListener: vi.fn(),
479
+ });
480
+ // biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor
481
+ globalThis.AudioContext = vi.fn(() => makeMockAudioContextForApp()) as any;
482
+
483
+ const rafStub = installRafStub();
484
+
485
+ try {
486
+ const world = new World();
487
+ const clipHandle = world.sharedRefs.alloc('AudioClipAsset', {
488
+ kind: 'audio',
489
+ sourceKey: 'stop-fixture-b',
490
+ bytes: new Uint8Array([2]),
491
+ });
492
+ world.spawn({
493
+ // biome-ignore lint/suspicious/noExplicitAny: handle type is opaque branded via alloc
494
+ component: AudioSource as any,
495
+ data: {
496
+ clip: clipHandle,
497
+ playing: true,
498
+ loop: false,
499
+ volume: 1,
500
+ spatialBlend: 0,
501
+ bus: 'sfx',
502
+ },
503
+ });
504
+
505
+ const backend = new WebAudioEngine();
506
+ const playSpy = vi.spyOn(backend, 'play');
507
+
508
+ // Build WITHOUT audio backend → no tick system registration.
509
+ const { renderer } = makeRendererStubForStop();
510
+ const result = await createApp({ renderer, world });
511
+ expect(result.ok).toBe(true);
512
+ if (!result.ok) return;
513
+ const app = result.value;
514
+
515
+ app.start();
516
+ // Since audioTickSystem is never registered, play should never fire.
517
+ expect(playSpy).not.toHaveBeenCalled();
518
+
519
+ // Drive additional frames exhaustively.
520
+ for (let i = 0; i < 5; i++) {
521
+ if (rafStub.tickFn) rafStub.tickFn();
522
+ }
523
+ expect(playSpy).not.toHaveBeenCalled();
524
+
525
+ app.stop();
526
+ playSpy.mockRestore();
527
+ } finally {
528
+ uninstallRafStub();
529
+ globalThis.AudioContext = OriginalAudioContext;
530
+ vi.restoreAllMocks();
531
+ vi.unstubAllGlobals();
532
+ }
533
+ });
534
+ });
535
+
536
+ // w14: AC-13 — auto-registered audioTickSystem can resolve both
537
+ // AUDIO_ENGINE_RESOURCE_KEY and ASSET_REGISTRY_RESOURCE_KEY on the
538
+ // first frame (no exception, no silent skip due to missing resources).
539
+ describe('w14: AC-13 registration timing — first-frame resolution', () => {
540
+ it('audioTickSystem resolves resources on first frame without error', async () => {
541
+ const OriginalAudioContext = globalThis.AudioContext;
542
+ vi.stubGlobal('document', {
543
+ addEventListener: vi.fn(),
544
+ removeEventListener: vi.fn(),
545
+ });
546
+ // biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor
547
+ globalThis.AudioContext = vi.fn(() => makeMockAudioContextForApp()) as any;
548
+
549
+ const rafStub = installRafStub();
550
+
551
+ try {
552
+ const world = new World();
553
+ const clipHandle = world.sharedRefs.alloc('AudioClipAsset', {
554
+ kind: 'audio',
555
+ sourceKey: 'stop-fixture-c',
556
+ bytes: new Uint8Array([3]),
557
+ });
558
+
559
+ // Spawn entity with playing=false so first frame walks the
560
+ // archetype without attempting to play — the key assertion is
561
+ // "no exception thrown during first-frame execution".
562
+ world.spawn({
563
+ // biome-ignore lint/suspicious/noExplicitAny: handle type is opaque branded via alloc
564
+ component: AudioSource as any,
565
+ data: {
566
+ clip: clipHandle,
567
+ playing: false,
568
+ loop: false,
569
+ volume: 1,
570
+ spatialBlend: 0,
571
+ bus: 'sfx',
572
+ },
573
+ });
574
+
575
+ const backend = new WebAudioEngine();
576
+ const { renderer } = makeRendererStubForStop();
577
+ const rendererWithAssets = { ...renderer, assets: world.sharedRefs } as unknown as Renderer;
578
+ // New assemble contract (D-2 / D-4): host inserts the audio resource +
579
+ // passes audioPlugin() so the audio-tick world system is registered.
580
+ const result = await createApp({
581
+ renderer: rendererWithAssets,
582
+ world,
583
+ plugins: [audioBackendPlugin(backend), audioPlugin()],
584
+ });
585
+ expect(result.ok).toBe(true);
586
+ if (!result.ok) return;
587
+ const app = result.value;
588
+
589
+ // First frame: the audio-tick world system walks the
590
+ // AudioSource archetype. clipResolver uses world.sharedRefs (always
591
+ // available on World, not a resource-key lookup). The tick system
592
+ // should not throw due to missing resources.
593
+ expect(() => app.start()).not.toThrow();
594
+
595
+ // Drive a few more frames to ensure continuing resolution is stable.
596
+ for (let i = 0; i < 3; i++) {
597
+ if (rafStub.tickFn) rafStub.tickFn();
598
+ }
599
+
600
+ app.stop();
601
+ } finally {
602
+ uninstallRafStub();
603
+ globalThis.AudioContext = OriginalAudioContext;
604
+ vi.restoreAllMocks();
605
+ vi.unstubAllGlobals();
606
+ }
607
+ });
608
+ });
609
+ });
@@ -0,0 +1,26 @@
1
+ import { FixedTime, Time, World } from '@forgeax/engine-ecs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import type { AppAssembleArgs, CreateAppOptions } from '../types';
5
+
6
+ describe('createApp time policy surface', () => {
7
+ it('exposes time policy only on the canvas-form options', () => {
8
+ const canvasOptions: CreateAppOptions = {
9
+ time: { fixedDeltaSeconds: 1 / 30, maxStepsPerUpdate: 2, maxDeltaSeconds: 0.1 },
10
+ };
11
+ expect(canvasOptions.time?.fixedDeltaSeconds).toBeCloseTo(1 / 30);
12
+
13
+ const world = new World({
14
+ time: { fixedDeltaSeconds: 1 / 30, maxStepsPerUpdate: 2, maxDeltaSeconds: 0.1 },
15
+ });
16
+ expect(world.getResource(Time).maxDeltaSeconds).toBeCloseTo(0.1);
17
+ expect(world.getResource(FixedTime).delta).toBeCloseTo(1 / 30);
18
+ expect(world.getResource(FixedTime).maxStepsPerUpdate).toBe(2);
19
+ });
20
+
21
+ it('keeps an assemble-form World policy host-owned', () => {
22
+ const world = new World({ time: { fixedDeltaSeconds: 1 / 30 } });
23
+ const args = { world } as Pick<AppAssembleArgs, 'world'>;
24
+ expect(args.world.getResource(FixedTime).delta).toBeCloseTo(1 / 30);
25
+ });
26
+ });