fix(voice): Navara refusals reach speech, analyst queries and stop_tracking

Review of 10a (low):
- The spoken answer to set_layer_visibility on a layer this engine lacks is
  the refusal's own sentence (why, and the switch to ?engine=cesium), not
  only "Couldn't turn on".
- analyst_query naming layers the catalog left out (no viewer) refuses with
  UNAVAILABLE_ON_ENGINE and the layer ids instead of answering zero.
- stop_tracking skips layers the engine does not have, so with nothing
  tracked it is a no-op success on Navara (it reported the absent military
  and satellites layers as failures).
Tests for each (the old code fails the last two); Cesium unchanged.
This commit is contained in:
German Khokhlov committed 2026-10-17 01:18:12 +04:00
1 parent 443c581280
commit 1dd3220812
3 files changed
+86 -1

No files matched your search

+28 -1
View File
@@ -332,7 +332,9 @@ function unavailableOnEngine(
engine: engine?.kind ?? null,
feature: what,
suggestion: SWITCH_ENGINE_HINT,
error: `${what} is not available on ${engineName(engine)} yet. Switch to the Cesium engine (${SWITCH_ENGINE_HINT}) to use it.`,
error: /\blayers$/.test(what)
? `${what} are not available on ${engineName(engine)} yet. Switch to the Cesium engine (${SWITCH_ENGINE_HINT}) to use them.`
: `${what} is not available on ${engineName(engine)} yet. Switch to the Cesium engine (${SWITCH_ENGINE_HINT}) to use it.`,
};
}
@@ -1110,6 +1112,29 @@ export function createGevActionRunner({
}
if (name === 'analyst_query') {
// Layers this engine's catalog left out cannot be asked: say so rather
// than answer zero.
const missing = !viewer
? (Array.isArray(args.layers) ? args.layers : [])
.map(normalizeLayerId)
.filter(
(layerId) =>
layerId &&
voiceLayer(layerId) &&
!dataManager.layers.has(layerId),
)
: [];
if (missing.length)
return {
...unavailableOnEngine(
'analyst_query',
engine,
missing.length === 1
? `The ${missing[0]} layer`
: `The ${missing.join(', ')} layers`,
),
layerIds: missing,
};
return runAnalystQuery(
ensureAnalystEngine(),
dataManager,
@@ -2484,6 +2509,8 @@ function stopAllTracking(engine, dataManager) {
['military', 'selectedMilitaryTrackingId'],
['satellites', 'selectedSatTrackingId'],
]) {
// A layer this engine does not have tracks nothing to clear.
if (dataManager.layers && !dataManager.layers.has(layerId)) continue;
try {
if (
dataManager.setLayerParams(
+51
View File
@@ -504,3 +504,54 @@ test('without a viewer a layer the engine lacks refuses instead of throwing', as
'a name no engine knows is still an unknown layer',
);
});
test('without a viewer analyst_query on a layer the engine lacks refuses', async () => {
const engine = engineOverAustin({ kind: 'navara' });
const runner = runnerOn(engine, {
dataManager: dataManagerWith({ flights: {} }),
});
const one = await runner('analyst_query', { layers: ['cctv'] });
assert.equal(one.ok, false);
assert.equal(one.code, UNAVAILABLE_ON_ENGINE);
assert.deepEqual(one.layerIds, ['cctv']);
assert.match(
one.error,
/^The cctv layer is not available on the Navara engine yet/,
);
const two = await runner('analyst_query', { layers: ['cctv', 'vessels'] });
assert.match(
two.error,
/^The cctv, ais-live-vessels layers are not available on the Navara engine yet\. Switch to the Cesium engine \(\?engine=cesium\) to use them\.$/,
);
});
test('stop_tracking with nothing tracked is a no-op success, also without the layers', async () => {
const engine = engineOverAustin({ kind: 'navara' });
const dataManager = dataManagerWith({
flights: { getTrackedInfo: () => null },
});
// As the manager answers: a layer it does not hold takes no params.
dataManager.setLayerParams = (id) => dataManager.layers.has(id);
const runner = runnerOn(engine, { dataManager });
const result = await runner('stop_tracking', {});
assert.equal(result.ok, true);
assert.deepEqual(result.released, []);
});
test('the spoken refusal of an unavailable layer carries its reason', async () => {
const { SPEECH_BUILDERS } = await import('./speech.js');
const engine = engineOverAustin({ kind: 'navara' });
const runner = runnerOn(engine, {
dataManager: dataManagerWith({ flights: {} }),
});
const result = await runner('set_layer_visibility', {
layerId: 'cctv',
enabled: true,
});
const speech = SPEECH_BUILDERS.set_layer_visibility(result, {
layerId: 'cctv',
enabled: true,
});
assert.equal(speech.say, result.error);
assert.match(speech.say, /Switch to the Cesium engine/);
});
+7
View File
@@ -103,6 +103,13 @@ function setLayerVisibility(result, args = {}) {
if (result?.cancelled) return null;
if (!result?.ok) {
const wanted = args.enabled === false ? 'off' : 'on';
// An engine without the layer says why and what to do (the refusal's
// own sentence), not only that it failed.
if (result?.code === 'UNAVAILABLE_ON_ENGINE' && result.error)
return {
say: result.error,
display: { title: label, lines: [result.error] },
};
return {
say: `Couldn't turn ${wanted} ${label}.`,
display: { title: label, lines: [result?.error].filter(Boolean) },