@forgeax/engine-app 0.1.2

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