From b48fcc42ae3468d5ff808b0f5af35e87113f8ea7 Mon Sep 17 00:00:00 2001 From: manjunath Date: Tue, 20 Oct 2026 10:00:24 +0530 Subject: [PATCH] fix(voice): refuse direct cctv toggles outside the shared lifecycle --- changelog.d/1205-cctv-tool-routing.md | 3 +-- docs/CURRENT-STATE.md | 5 +++-- src/voice/cctvRouting.test.mjs | 16 +++++++++++++++- src/voice/gevActions.js | 7 ++----- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/changelog.d/1205-cctv-tool-routing.md b/changelog.d/1205-cctv-tool-routing.md index ed7552591..72e8859c5 100644 --- a/changelog.d/1205-cctv-tool-routing.md +++ b/changelog.d/1205-cctv-tool-routing.md @@ -1,3 +1,2 @@ - Report actual outcomes when voice enables or disables CCTV through the shared - layer lifecycle, including failed and cancelled changes (manjunath22466, - #1205). + layer lifecycle, including failed and cancelled changes (Manjunath, #1205). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index d8cc977c9..af59b5429 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -628,8 +628,9 @@ Voice action argument schemas have one portable owner under `src/voice/actionSch layer visibility lifecycle. Results distinguish successful changes from manager-reported failures or cancellation and report enabled state and lifecycle uncertainty. A caller-supplied abort signal reaches the manager; a late abort -preserves an already committed change. Missing-layer errors, other CCTV actions -and the exported `controlCctv` helper retain their existing behavior. +preserves an already committed change. Missing-layer errors and other CCTV +actions retain their existing behavior. The exported `controlCctv` helper refuses direct +enable/disable requests and directs callers to `set_layer_visibility`. `src/voice/layerManifest.js` declares each catalog layer's voice capabilities: spoken aliases, `toggle`, in-view `context`, `track`, and analyst `query` fields with type and unit, or `noQuery` with the reason. The `set_layer_visibility`, `show_data_layers_menu`, `get_entity_context` and `analyst_query` layer enums, the runner's alias table and the analyst field hints are generated from it. `src/voice/layerManifest.test.mjs` fails when a catalog layer is neither listed nor named in `VOICE_OFF_LAYERS` (today only the two scene-driven Bhote Koshi layers). Global Context is listed with `toggle: false`; `set_context_mode` reaches it. `set_panel_open` also opens `weather-panel` and `recent-imagery-panel`. diff --git a/src/voice/cctvRouting.test.mjs b/src/voice/cctvRouting.test.mjs index 58f3a135b..0df5dd12c 100644 --- a/src/voice/cctvRouting.test.mjs +++ b/src/voice/cctvRouting.test.mjs @@ -2,7 +2,7 @@ import { after, before, test } from 'node:test'; import assert from 'node:assert/strict'; import { DataLayerManager } from '../data/manager.js'; import { CCTV_FOCUS_RESULT } from '../layers/cctv/index.js'; -import { createGevActionRunner } from './gevActions.js'; +import { controlCctv, createGevActionRunner } from './gevActions.js'; const originalWindow = globalThis.window; before(() => { @@ -294,6 +294,20 @@ test( }, ); +test('exported controlCctv refuses toggles without calling the manager', async () => { + const manager = { + layers: new Map([['cctv', { module: {} }]]), + setEnabled: () => assert.fail('direct helper must not change visibility'), + isEnabled: () => assert.fail('direct toggle must not read enabled state'), + }; + for (const action of ['ENABLE', 'disable']) { + const result = await controlCctv(manager, { action }); + assert.equal(result.ok, false); + assert.equal(result.action, 'control_cctv'); + assert.match(result.error, /set_layer_visibility/); + } +}); + test( 'CCTV fallback managers report rejection, transitional state, and uncertainty', { timeout: 2000 }, diff --git a/src/voice/gevActions.js b/src/voice/gevActions.js index 41d59221e..d8b63565f 100644 --- a/src/voice/gevActions.js +++ b/src/voice/gevActions.js @@ -1444,13 +1444,10 @@ export async function controlCctv(dataManager, args = {}, styleManager = null) { } if (action === 'enable' || action === 'disable') { - await dataManager.setEnabled('cctv', action === 'enable', { - origin: 'voice', - }); return { - ok: true, + ok: false, action: 'control_cctv', - enabled: dataManager.isEnabled('cctv'), + error: 'Enable or disable CCTV through set_layer_visibility', }; } if (!dataManager.isEnabled('cctv')) {