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