fix(voice): refuse direct cctv toggles outside the shared lifecycle

This commit is contained in:
manjunath committed 2026-10-20 10:00:24 +05:30
1 parent b920d5f404
commit b48fcc42ae
4 files changed
+21 -10

No files matched your search

+1 -2
View File
@@ -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).
+3 -2
View File
@@ -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`.
+15 -1
View File
@@ -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 },
+2 -5
View File
@@ -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')) {