From 6d7fe53570465f0fabf19a678ae1ba3795cdf23b Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 5 Nov 2026 16:55:54 -0800 Subject: [PATCH] Let a shell without its navigation owner report no camera handoffs The voice runner subscribes to camera handoffs as it is created. The Cyber sonar test builds its runner on a StyleManager made from the prototype, which has no navigation owner, so the facade's delegation threw and both Node jobs failed. The facade now returns nothing there, like its other optional owners; the real shell builds its navigation owner before voice creates a runner. --- scripts/format-scope.json | 1 + src/ui/shellFacade.js | 2 +- src/ui/shellFacade.test.mjs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/ui/shellFacade.test.mjs diff --git a/scripts/format-scope.json b/scripts/format-scope.json index a28a8c53c..40bca019a 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -1128,6 +1128,7 @@ "src/dockerTrustedPeers.test.mjs", "src/cameraGlobeRotation.test.mjs", "src/cameraTilt.test.mjs", + "src/ui/shellFacade.test.mjs", "src/voice/globeResetConfirmation.test.mjs", "src/hudDms.test.mjs", "src/hudSummaryGate.test.mjs", diff --git a/src/ui/shellFacade.js b/src/ui/shellFacade.js index 95bf791d4..678b69c79 100644 --- a/src/ui/shellFacade.js +++ b/src/ui/shellFacade.js @@ -6,7 +6,7 @@ export class ShellFacade { /** Observe camera ownership transfers through the navigation owner. */ subscribeCameraHandoff(listener) { - return this._navigation.subscribeCameraHandoff(listener); + return this._navigation?.subscribeCameraHandoff(listener); } get _models3dEnabled() { diff --git a/src/ui/shellFacade.test.mjs b/src/ui/shellFacade.test.mjs new file mode 100644 index 000000000..abb892b37 --- /dev/null +++ b/src/ui/shellFacade.test.mjs @@ -0,0 +1,33 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { ShellFacade } from './shellFacade.js'; + +// A shell assembled without its navigation owner (as unit tests build one +// from the prototype) has no handoffs to report, like the facade's other +// optional owners; the voice runner subscribes as it is created. +test('camera handoffs without a navigation owner subscribe to nothing', () => { + const shell = Object.create(ShellFacade.prototype); + assert.equal( + shell.subscribeCameraHandoff(() => {}), + undefined, + ); +}); + +test('camera handoffs reach listeners through the navigation owner', () => { + const listeners = new Set(); + const shell = Object.create(ShellFacade.prototype); + shell._navigation = { + subscribeCameraHandoff(listener) { + listeners.add(listener); + return () => listeners.delete(listener); + }, + }; + let handoffs = 0; + const unsubscribe = shell.subscribeCameraHandoff(() => { + handoffs += 1; + }); + for (const listener of listeners) listener(); + assert.equal(handoffs, 1); + unsubscribe(); + assert.equal(listeners.size, 0); +});