@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,377 @@
1
+ // feat-20260619-audio-resource-ownership-deterministic-reclaim / M7 / w24 fix-up.
2
+ //
3
+ // Listener sync real-consumption-path test: goes through the canvas form
4
+ // (createApp(canvas, opts)) so the w25 auto-registration code
5
+ // (create-app.ts:517-538) is actually executed. The prior version of this
6
+ // test used the assemble form with a hand-copied registerListenerSyncSystem
7
+ // helper — a shadow copy of the production code that left w25 untested.
8
+ //
9
+ // Approach: vi.mock createRenderer (only export overridden) so the canvas
10
+ // form does not attempt real WebGPU init; all other exports from
11
+ // @forgeax/engine-runtime remain real (Transform, registerPropagateTransforms,
12
+ // PROPAGATE_TRANSFORMS_SYSTEM, etc.). globalThis.AudioContext is stubbed
13
+ // with a mock whose .listener carries nine { value } AudioParam objects
14
+ // that syncListenerFromWorldMatrix writes to — the same nine fields the
15
+ // production code accesses. requestAnimationFrame is stubbed (same pattern
16
+ // as create-app-stop.test.ts) so start() runs one frame synchronously.
17
+ //
18
+ // Three scenarios (semantics unchanged from the prior version, but now
19
+ // exercising the real production path through the canvas form):
20
+ // (a) AudioListener+Transform entity at (5,0,0) — after start()+frame
21
+ // the mock listener.positionX.value === 5 (proves D-7 real addSystem
22
+ // auto-registration consumes the world mat4 from propagateTransforms).
23
+ // (b) No AudioListener entity — system no-ops, no throw, mock listener
24
+ // values remain at 0.
25
+ // (c) Frame-order regression guard (R-listener-frame-order): modify
26
+ // Transform.posX in the same frame-cycle and run another frame —
27
+ // listener pose reflects the CURRENT frame's world mat4, not a
28
+ // stale/previous value (proves after-propagate frame order is
29
+ // effective and the system does not suffer 1-frame lag).
30
+
31
+ import { AudioListener as AudioListenerComponent, audioPlugin } from '@forgeax/engine-audio';
32
+ import { webAudioPlugin } from '@forgeax/engine-audio-webaudio';
33
+ import { Transform } from '@forgeax/engine-scene';
34
+ import { describe, expect, it, vi } from 'vitest';
35
+ import { createApp } from '../create-app';
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Mock createRenderer so the canvas form does not attempt real WebGPU init.
39
+ // All other @forgeax/engine-runtime exports stay real (Transform,
40
+ // registerPropagateTransforms, advanceAnimationPlayer, etc.).
41
+ // ---------------------------------------------------------------------------
42
+
43
+ vi.mock('@forgeax/engine-runtime', async (importOriginal) => {
44
+ const actual = await importOriginal<typeof import('@forgeax/engine-runtime')>();
45
+ const ready: Promise<{ ok: true; value: undefined }> = Promise.resolve({
46
+ ok: true,
47
+ value: undefined,
48
+ });
49
+ const rendererStub = {
50
+ backend: 'webgpu' as const,
51
+ ready,
52
+ draw(): { ok: true; value: undefined } {
53
+ return { ok: true, value: undefined };
54
+ },
55
+ attachWorld(): { ok: true; value: undefined } {
56
+ return { ok: true, value: undefined };
57
+ },
58
+ detachWorld(): void {},
59
+ onError(): () => void {
60
+ return () => {};
61
+ },
62
+ onLost(): () => void {
63
+ return () => {};
64
+ },
65
+ dispose: vi.fn(),
66
+ assets: null,
67
+ };
68
+ return {
69
+ ...actual,
70
+ createRenderer: vi.fn().mockResolvedValue(rendererStub),
71
+ };
72
+ });
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Test helpers
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /**
79
+ * Build a mock AudioContext whose .listener carries the nine AudioParam
80
+ * objects (positionX/Y/Z, forwardX/Y/Z, upX/Y/Z) that
81
+ * syncListenerFromWorldMatrix writes to. The mock also carries the minimum
82
+ * surface WebAudioEngine.ensureContext() needs: createGain, destination,
83
+ * state, close, and the gesture-listener methods.
84
+ */
85
+ function makeMockAudioContextForListenerSync(): AudioContext {
86
+ const mkGain = vi.fn(
87
+ () =>
88
+ ({
89
+ gain: { value: 1 } as unknown as AudioParam,
90
+ context: undefined as unknown as BaseAudioContext,
91
+ numberOfInputs: 1,
92
+ numberOfOutputs: 1,
93
+ channelCount: 2,
94
+ channelCountMode: 'max' as const,
95
+ channelInterpretation: 'speakers' as const,
96
+ connect: vi.fn().mockReturnValue(undefined),
97
+ disconnect: vi.fn(),
98
+ addEventListener: vi.fn(),
99
+ removeEventListener: vi.fn(),
100
+ dispatchEvent: vi.fn(),
101
+ }) as unknown as GainNode,
102
+ );
103
+
104
+ const mkListener = (): AudioListener =>
105
+ ({
106
+ positionX: { value: 0 } as unknown as AudioParam,
107
+ positionY: { value: 0 } as unknown as AudioParam,
108
+ positionZ: { value: 0 } as unknown as AudioParam,
109
+ forwardX: { value: 0 } as unknown as AudioParam,
110
+ forwardY: { value: 0 } as unknown as AudioParam,
111
+ forwardZ: { value: 0 } as unknown as AudioParam,
112
+ upX: { value: 0 } as unknown as AudioParam,
113
+ upY: { value: 0 } as unknown as AudioParam,
114
+ upZ: { value: 0 } as unknown as AudioParam,
115
+ }) as unknown as AudioListener;
116
+
117
+ return {
118
+ listener: mkListener(),
119
+ state: 'suspended' as AudioContextState,
120
+ sampleRate: 48000,
121
+ destination: {
122
+ maxChannelCount: 2,
123
+ channelCount: 2,
124
+ channelCountMode: 'explicit' as const,
125
+ channelInterpretation: 'speakers' as const,
126
+ context: undefined as unknown as BaseAudioContext,
127
+ numberOfInputs: 1,
128
+ numberOfOutputs: 0,
129
+ connect: vi.fn().mockReturnValue(undefined),
130
+ disconnect: vi.fn(),
131
+ addEventListener: vi.fn(),
132
+ removeEventListener: vi.fn(),
133
+ dispatchEvent: vi.fn(),
134
+ } as unknown as AudioDestinationNode,
135
+ currentTime: 0,
136
+ resume: vi.fn().mockResolvedValue(undefined),
137
+ suspend: vi.fn().mockResolvedValue(undefined),
138
+ close: vi.fn().mockResolvedValue(undefined),
139
+ createGain: mkGain,
140
+ createBufferSource: vi.fn(),
141
+ createPanner: vi.fn(),
142
+ decodeAudioData: vi.fn(),
143
+ addEventListener: vi.fn(),
144
+ removeEventListener: vi.fn(),
145
+ dispatchEvent: vi.fn(),
146
+ onstatechange: null,
147
+ baseLatency: 0,
148
+ outputLatency: 0,
149
+ getOutputTimestamp: vi.fn(),
150
+ createMediaStreamDestination: vi.fn(),
151
+ createMediaStreamSource: vi.fn(),
152
+ createChannelMerger: vi.fn(),
153
+ createChannelSplitter: vi.fn(),
154
+ createDelay: vi.fn(),
155
+ createBiquadFilter: vi.fn(),
156
+ createConvolver: vi.fn(),
157
+ createDynamicsCompressor: vi.fn(),
158
+ createOscillator: vi.fn(),
159
+ createStereoPanner: vi.fn(),
160
+ createWaveShaper: vi.fn(),
161
+ createIIRFilter: vi.fn(),
162
+ createScriptProcessor: vi.fn(),
163
+ createAnalyser: vi.fn(),
164
+ createConstantSource: vi.fn(),
165
+ audioWorklet: undefined as unknown as AudioWorklet,
166
+ } as unknown as AudioContext;
167
+ }
168
+
169
+ /**
170
+ * Node-env rAF injection: createFrameLoop's resolveRaf reads
171
+ * globalThis.requestAnimationFrame at construction time (frame-loop.ts:141-153).
172
+ * Call this BEFORE createApp() so the frame loop captures the stub.
173
+ *
174
+ * Auto-invoke only on first raf call (from start()), not on subsequent
175
+ * calls (from tick()'s `raf(tick)` in frame-loop), to avoid infinite
176
+ * recursion. The stub state is returned so tests can read tickFn to drive
177
+ * additional frames.
178
+ */
179
+ function installRafStub(): { tickFn: (() => void) | null } {
180
+ const stub: { tickFn: (() => void) | null } = { tickFn: null };
181
+ let firstCall = true;
182
+ (globalThis as Record<string, unknown>).requestAnimationFrame = (cb: (t: number) => void) => {
183
+ stub.tickFn = () => cb(performance.now());
184
+ if (firstCall) {
185
+ firstCall = false;
186
+ stub.tickFn();
187
+ }
188
+ return 1;
189
+ };
190
+ (globalThis as Record<string, unknown>).cancelAnimationFrame = () => {
191
+ stub.tickFn = null;
192
+ };
193
+ return stub;
194
+ }
195
+
196
+ function uninstallRafStub() {
197
+ delete (globalThis as Record<string, unknown>).requestAnimationFrame;
198
+ delete (globalThis as Record<string, unknown>).cancelAnimationFrame;
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // Tests
203
+ // ---------------------------------------------------------------------------
204
+
205
+ describe('feat-20260619 M7: listener sync real consumption path (canvas form)', () => {
206
+ describe('w24 scenario (a): AudioListener+Transform entity syncs world translation', () => {
207
+ it('canvas form audio:true → after start() mock listener positionX === 5', async () => {
208
+ const OriginalAudioContext = globalThis.AudioContext;
209
+ vi.stubGlobal('document', {
210
+ addEventListener: vi.fn(),
211
+ removeEventListener: vi.fn(),
212
+ });
213
+ let context: AudioContext | undefined;
214
+ const mockCtor = vi.fn(function AudioContextMock(this: Record<string, unknown>) {
215
+ context = makeMockAudioContextForListenerSync();
216
+ Object.assign(this, context);
217
+ });
218
+ // biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor -- must use function (not arrow) so new AudioContext() is constructable
219
+ globalThis.AudioContext = mockCtor as any;
220
+
221
+ installRafStub();
222
+
223
+ try {
224
+ const canvas = {
225
+ tagName: 'canvas',
226
+ isConnected: true,
227
+ width: 800,
228
+ height: 600,
229
+ } as HTMLCanvasElement;
230
+
231
+ const result = await createApp(canvas, { plugins: [webAudioPlugin(), audioPlugin()] });
232
+ expect(result.ok).toBe(true);
233
+ if (!result.ok) return;
234
+ const app = result.value;
235
+
236
+ // Spawn entity AFTER createApp but BEFORE start(), so the listener-sync
237
+ // system (registered during createApp) finds it on the first frame.
238
+ app.world.spawn(
239
+ { component: Transform, data: { pos: [5, 0, 0] } },
240
+ { component: AudioListenerComponent, data: {} },
241
+ );
242
+
243
+ app.start();
244
+
245
+ const listener = context?.listener;
246
+ if (!listener) throw new Error('expected host AudioContext.listener');
247
+ expect(listener.positionX.value).toBe(5);
248
+ expect(listener.positionY.value).toBe(0);
249
+ expect(listener.positionZ.value).toBe(0);
250
+
251
+ app.stop();
252
+ } finally {
253
+ uninstallRafStub();
254
+ globalThis.AudioContext = OriginalAudioContext;
255
+ vi.restoreAllMocks();
256
+ vi.unstubAllGlobals();
257
+ }
258
+ });
259
+ });
260
+
261
+ describe('w24 scenario (b): no AudioListener entity → no-op, no throw', () => {
262
+ it('canvas form audio:true with no AudioListener entity → start() does not throw', async () => {
263
+ const OriginalAudioContext = globalThis.AudioContext;
264
+ vi.stubGlobal('document', {
265
+ addEventListener: vi.fn(),
266
+ removeEventListener: vi.fn(),
267
+ });
268
+ const mockCtor = vi.fn(function AudioContextMock(this: Record<string, unknown>) {
269
+ Object.assign(this, makeMockAudioContextForListenerSync());
270
+ });
271
+ // biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor -- must use function (not arrow) so new AudioContext() is constructable
272
+ globalThis.AudioContext = mockCtor as any;
273
+
274
+ installRafStub();
275
+
276
+ try {
277
+ const canvas = {
278
+ tagName: 'canvas',
279
+ isConnected: true,
280
+ width: 800,
281
+ height: 600,
282
+ } as HTMLCanvasElement;
283
+
284
+ const result = await createApp(canvas, { plugins: [webAudioPlugin(), audioPlugin()] });
285
+ expect(result.ok).toBe(true);
286
+ if (!result.ok) return;
287
+ const app = result.value;
288
+
289
+ // No AudioListener entity spawned — the listener-sync system runs,
290
+ // finds nothing, and silently no-ops. No throw, no crash.
291
+ expect(() => app.start()).not.toThrow();
292
+
293
+ expect(mockCtor).not.toHaveBeenCalled();
294
+
295
+ app.stop();
296
+ } finally {
297
+ uninstallRafStub();
298
+ globalThis.AudioContext = OriginalAudioContext;
299
+ vi.restoreAllMocks();
300
+ vi.unstubAllGlobals();
301
+ }
302
+ });
303
+ });
304
+
305
+ describe('w24 scenario (c): frame-order regression guard — listener pose reads current frame', () => {
306
+ it('modify Transform.posX before second frame → listener reflects new value, not stale', async () => {
307
+ const OriginalAudioContext = globalThis.AudioContext;
308
+ vi.stubGlobal('document', {
309
+ addEventListener: vi.fn(),
310
+ removeEventListener: vi.fn(),
311
+ });
312
+ let context: AudioContext | undefined;
313
+ const mockCtor = vi.fn(function AudioContextMock(this: Record<string, unknown>) {
314
+ context = makeMockAudioContextForListenerSync();
315
+ Object.assign(this, context);
316
+ });
317
+ // biome-ignore lint/suspicious/noExplicitAny: vitest mock for AudioContext constructor -- must use function (not arrow) so new AudioContext() is constructable
318
+ globalThis.AudioContext = mockCtor as any;
319
+
320
+ const rafStub = installRafStub();
321
+
322
+ try {
323
+ const canvas = {
324
+ tagName: 'canvas',
325
+ isConnected: true,
326
+ width: 800,
327
+ height: 600,
328
+ } as HTMLCanvasElement;
329
+
330
+ const result = await createApp(canvas, { plugins: [webAudioPlugin(), audioPlugin()] });
331
+ expect(result.ok).toBe(true);
332
+ if (!result.ok) return;
333
+ const app = result.value;
334
+
335
+ // Spawn entity at (0,0,0).
336
+ const spawnResult = app.world.spawn(
337
+ { component: Transform, data: { pos: [0, 0, 0] } },
338
+ { component: AudioListenerComponent, data: {} },
339
+ );
340
+ expect(spawnResult.ok).toBe(true);
341
+ if (!spawnResult.ok) return;
342
+ const entity = spawnResult.value;
343
+
344
+ // Frame 1: propagateTransforms computes world with posX=0,
345
+ // listener-sync reads it → positionX === 0.
346
+ app.start();
347
+ const listener = context?.listener;
348
+ if (!listener) throw new Error('expected host AudioContext.listener');
349
+ expect(listener.positionX.value).toBe(0);
350
+
351
+ // Modify local position: simulate a frame callback that moves the
352
+ // listener entity. If the listener-sync system ran BEFORE
353
+ // propagateTransforms, the next frame would still read the old
354
+ // world mat4 with posX=0 (1-frame lag).
355
+ const setResult = app.world.set(entity, Transform, { pos: [8, 0, 0] });
356
+ expect(setResult.ok).toBe(true);
357
+
358
+ // Frame 2: propagateTransforms recomputes world with posX=8,
359
+ // listener-sync reads the CURRENT frame's world → positionX === 8.
360
+ // This assertion is the R-listener-frame-order regression guard.
361
+ expect(rafStub.tickFn).not.toBeNull();
362
+ if (rafStub.tickFn) {
363
+ rafStub.tickFn();
364
+ }
365
+
366
+ expect(listener.positionX.value).toBe(8);
367
+
368
+ app.stop();
369
+ } finally {
370
+ uninstallRafStub();
371
+ globalThis.AudioContext = OriginalAudioContext;
372
+ vi.restoreAllMocks();
373
+ vi.unstubAllGlobals();
374
+ }
375
+ });
376
+ });
377
+ });
@@ -0,0 +1,212 @@
1
+ // create-app-lock-provider.test.ts -- M2 w9: three-hop passthrough real
2
+ // link integration test.
3
+ //
4
+ // Validates that lockProvider flows from CreateAppOptions through
5
+ // InputAttachOptions to BrowserInputBackendOptions without breakage
6
+ // (D-2 three-hop passthrough). Uses a fake canvas + synthetic click
7
+ // event to verify that requestLock is called and sample().pointerLocked
8
+ // reflects the provider lock state.
9
+ //
10
+ // TDD red phase: the lockProvider field on CreateAppOptions /
11
+ // InputAttachOptions is added in w12 (committed), but the full pipeline
12
+ // test validates the end-to-end wiring. This test is green when
13
+ // attachInputAuto correctly forwards lockProvider to the backend.
14
+ //
15
+ // charter awareness:
16
+ // F1 single-entry indexability -- all three-hop tests in one file
17
+ // P3 explicit failure -- each assertion produces a clear diagnostic
18
+ // P4 consistent abstraction -- tests use fake canvas, not real browser PointerLock
19
+
20
+ import { snapshotFromSample } from '@forgeax/engine-input';
21
+ import { describe, expect, it, vi } from 'vitest';
22
+
23
+ import type { AppError } from '../errors';
24
+ import { attachInputAuto, type InputAttachOptions } from '../internal/input-attach';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Fake DOM infrastructure (mirrors input tests pattern: buildBBFakes).
28
+ // ---------------------------------------------------------------------------
29
+
30
+ interface FakeTarget {
31
+ addEventListener(kind: string, handler: EventListener): void;
32
+ removeEventListener(kind: string, handler: EventListener): void;
33
+ }
34
+
35
+ function makeFakeDom() {
36
+ // Listener store: Map<target, Map<kind, Set<handler>>>
37
+ const listeners = new Map<string, Map<string, Set<EventListener>>>();
38
+
39
+ function makeTarget(label: string): FakeTarget {
40
+ return {
41
+ addEventListener(kind: string, handler: EventListener): void {
42
+ let perKind = listeners.get(label);
43
+ if (!perKind) {
44
+ perKind = new Map();
45
+ listeners.set(label, perKind);
46
+ }
47
+ let set = perKind.get(kind);
48
+ if (!set) {
49
+ set = new Set();
50
+ perKind.set(kind, set);
51
+ }
52
+ set.add(handler);
53
+ },
54
+ removeEventListener(kind: string, handler: EventListener): void {
55
+ listeners.get(label)?.get(kind)?.delete(handler);
56
+ },
57
+ };
58
+ }
59
+
60
+ const canvas = {
61
+ ...makeTarget('canvas'),
62
+ requestPointerLock: vi.fn(),
63
+ getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 600 }) as DOMRect,
64
+ width: 800,
65
+ height: 600,
66
+ style: { touchAction: '' },
67
+ setPointerCapture: vi.fn(),
68
+ } as unknown as HTMLCanvasElement;
69
+
70
+ const doc = {
71
+ ...makeTarget('document'),
72
+ hasFocus(): boolean {
73
+ return true;
74
+ },
75
+ visibilityState: 'visible',
76
+ pointerLockElement: null as Element | null,
77
+ exitPointerLock: vi.fn(),
78
+ } as unknown as Document;
79
+
80
+ const win = makeTarget('window') as unknown as Window;
81
+
82
+ function dispatchClick(): void {
83
+ const handlers = listeners.get('canvas')?.get('click');
84
+ if (handlers) {
85
+ const ev = new Event('click', { bubbles: true });
86
+ for (const h of handlers) {
87
+ h(ev as Event);
88
+ }
89
+ }
90
+ }
91
+
92
+ // Simulate W3C pointerlockchange: canvas enters pointer-lock.
93
+ function simulateW3cLock(): void {
94
+ (doc as unknown as { pointerLockElement: Element | null }).pointerLockElement = canvas;
95
+ const handlers = listeners.get('document')?.get('pointerlockchange');
96
+ if (handlers) {
97
+ const ev = new Event('pointerlockchange');
98
+ for (const h of handlers) {
99
+ h(ev as Event);
100
+ }
101
+ }
102
+ }
103
+
104
+ return { canvas, doc, win, dispatchClick, simulateW3cLock, listeners };
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Tests
109
+ // ---------------------------------------------------------------------------
110
+
111
+ describe('create-app-lock-provider (w9)', () => {
112
+ describe('three-hop passthrough: CreateAppOptions -> InputAttachOptions -> BrowserInputBackendOptions', () => {
113
+ it('lockProvider.requestLock is called on canvas click when lockProvider is injected', () => {
114
+ const { canvas, doc, win, dispatchClick } = makeFakeDom();
115
+
116
+ // Stub browser globals (attachBrowserInputBackend reads these).
117
+ vi.stubGlobal('document', doc);
118
+ vi.stubGlobal('window', win);
119
+ vi.stubGlobal('HTMLElement', class {});
120
+
121
+ // Patch performance.now() for gesture debounce (backend checks time
122
+ // since mousedown for trusted-gesture purposes).
123
+ vi.stubGlobal(
124
+ 'performance',
125
+ Object.freeze({
126
+ now: () => 0,
127
+ }),
128
+ );
129
+
130
+ const requestLock = vi.fn();
131
+ const exitLock = vi.fn();
132
+ const lockProvider = { requestLock, exitLock };
133
+
134
+ const opts: InputAttachOptions = { lockProvider };
135
+ const handle = attachInputAuto(canvas, opts);
136
+
137
+ // Dispatch a click — the backend's onCanvasClick handler should call
138
+ // lockProvider.requestLock() (provider path takes priority over W3C).
139
+ dispatchClick();
140
+
141
+ expect(requestLock).toHaveBeenCalledOnce();
142
+
143
+ // After provider requestLock succeeds (synchronous return, D-7 optimistic
144
+ // placement), the providerLocked flag should be set, so sample() should
145
+ // report pointerLocked === true.
146
+ const snap = snapshotFromSample(handle.backend.sample());
147
+ expect(snap.mouse.pointerLocked).toBe(true);
148
+
149
+ // Cleanup.
150
+ handle.cleanup();
151
+
152
+ vi.unstubAllGlobals();
153
+ });
154
+
155
+ it('without lockProvider, W3C path is used (pointerlockchange-driven)', () => {
156
+ const { canvas, doc, win, dispatchClick, simulateW3cLock } = makeFakeDom();
157
+
158
+ vi.stubGlobal('document', doc);
159
+ vi.stubGlobal('window', win);
160
+ vi.stubGlobal('HTMLElement', class {});
161
+ vi.stubGlobal(
162
+ 'performance',
163
+ Object.freeze({
164
+ now: () => 0,
165
+ }),
166
+ );
167
+
168
+ const opts: InputAttachOptions = {};
169
+ const handle = attachInputAuto(canvas, opts);
170
+
171
+ // Dispatch a click — the backend should call requestPointerLock on
172
+ // the canvas (W3C path, no provider).
173
+ dispatchClick();
174
+ expect(canvas.requestPointerLock).toHaveBeenCalledOnce();
175
+
176
+ // Simulate W3C pointerlockchange event.
177
+ simulateW3cLock();
178
+
179
+ const snap = snapshotFromSample(handle.backend.sample());
180
+ expect(snap.mouse.pointerLocked).toBe(true);
181
+
182
+ handle.cleanup();
183
+ vi.unstubAllGlobals();
184
+ });
185
+
186
+ it('setOnErrorDispatch receives onLockError when w12 is wired', () => {
187
+ // This tests the onLockError wrapping path. When the backend's
188
+ // onLockError fires (e.g. W3C requestPointerLock rejection), the
189
+ // setOnErrorDispatch callback should be invoked with an AppError
190
+ // tagged 'app-pointer-lock-failed'.
191
+
192
+ const onErrorReceived: AppError[] = [];
193
+ const handle = attachInputAuto({} as HTMLCanvasElement);
194
+
195
+ handle.setOnErrorDispatch((err) => {
196
+ onErrorReceived.push(err);
197
+ });
198
+
199
+ // Call setOnErrorDispatch again — idempotent replacement.
200
+ handle.setOnErrorDispatch((err) => {
201
+ onErrorReceived.push(err);
202
+ });
203
+
204
+ // Verify the setter does not crash and the handle is valid.
205
+ // The actual onLockError firing is covered implicitly when the
206
+ // real backend rejects — but this test validates the wiring seam.
207
+ expect(onErrorReceived).toHaveLength(0);
208
+
209
+ handle.cleanup();
210
+ });
211
+ });
212
+ });