Merge branch 'main' into prereq-10-osm-overlays

This commit is contained in:
Said Turksever authored and GitHub committed 2026-11-03 14:34:32 +00:00
commit 8e40d2689b
424 files changed
+25311 -956

No files matched your search

+6 -1
View File
@@ -2227,7 +2227,11 @@ export async function controlRadio(
if (!radioActionIsCurrent(options)) return cancelled(summarize);
const prepared = state.selected
? true
: radio.cycleStation?.(1, { focus: false, autoplay: false });
: radio.cycleStation?.(1, {
focus: false,
autoplay: false,
fallback: true,
});
return {
ok: Boolean(prepared),
action: 'control_radio',
@@ -2251,6 +2255,7 @@ export async function controlRadio(
const selected = radio.cycleStation?.(action === 'next' ? 1 : -1, {
focus: false,
autoplay: false,
fallback: true,
});
return {
ok: Boolean(selected),
+18 -1
View File
@@ -1905,7 +1905,7 @@ test('voice Radio resolves Austin and exposes semantic selection, volume, pause,
assert.equal(result.radioAction, 'play');
assert.equal(result.stationId, 'nearest');
assert.equal('station' in result, false);
assert.deepEqual(calls.at(-1), ['cycle', 1, { focus: false, autoplay: false }]);
assert.deepEqual(calls.at(-1), ['cycle', 1, { focus: false, autoplay: false, fallback: true }]);
for (const invalidCoordinates of [
{ latitude: null, longitude: null },
@@ -4032,3 +4032,20 @@ test('set_panel_open reaches the Utilities panel by its name, Draw or Import', a
}
assert.deepEqual(calls.at(-1), ['utilities-panel', false, { explicit: true }]);
});
test('a partial analyst card shows one partial chip', () => {
const card = presentResult('analyst_query', {
ok: true,
action: 'analyst_query',
count: 3,
feedState: 'partial',
partial: true,
unanswered: ['vessels'],
summary: '3 contacts',
scopeLabel: 'within 5 km of X',
display: { scope: 'within 5 km of X' },
coverage: {},
});
const labels = card.display.chips.map((chip) => chip.label);
assert.equal(new Set(labels).size, labels.length, JSON.stringify(labels));
});
+41 -32
View File
@@ -591,41 +591,50 @@ test('replacement input invalidates an in-flight Radio preflight immediately', (
assert.equal(controller.radioHandoffEpoch, priorEpoch + 1);
});
test('direct Radio pause or stop cancels a pending voice handoff', async () => {
const order = [];
let playbackControl = null;
const ui = createTestVoiceUi();
const controller = new GevRealtimeController({
ui,
runner: async () => ({ ok: true }),
radioLayer: {
subscribePlaybackControls(listener) {
playbackControl = listener;
return () => { playbackControl = null; };
for (const [name, control, message] of [
['direct Radio pause or stop cancels a pending voice handoff', 'stop', 'the explicit stop remains authoritative'],
[
'a direct Radio Next that does not play cancels a pending voice handoff',
{ action: 'select', origin: 'user', attemptId: null },
'the user selection is not overridden by voice playback',
],
]) {
test(name, async () => {
const order = [];
let playbackControl = null;
const ui = createTestVoiceUi();
const controller = new GevRealtimeController({
ui,
runner: async () => ({ ok: true }),
radioLayer: {
subscribePlaybackControls(listener) {
playbackControl = listener;
return () => { playbackControl = null; };
},
playForVoice: async () => {
order.push('radio-play');
return true;
},
stopPlayback: () => order.push('radio-stop'),
},
playForVoice: async () => {
order.push('radio-play');
return true;
},
stopPlayback: () => order.push('radio-stop'),
},
});
controller.debugLog = () => {};
controller.dc = {
readyState: 'open',
send() {},
close() { order.push('voice-stop'); },
};
controller.pendingRadioPlaybackResult = { ok: true, radioPlaybackRequested: true };
});
controller.debugLog = () => {};
controller.dc = {
readyState: 'open',
send() {},
close() { order.push('voice-stop'); },
};
controller.pendingRadioPlaybackResult = { ok: true, radioPlaybackRequested: true };
playbackControl('stop');
await controller.handleRealtimeEvent({
data: JSON.stringify({ type: 'response.done', response: { status: 'completed' } }),
});
playbackControl(control);
await controller.handleRealtimeEvent({
data: JSON.stringify({ type: 'response.done', response: { status: 'completed' } }),
});
assert.equal(controller.pendingRadioPlaybackResult, null);
assert.deepEqual(order, [], 'the explicit stop remains authoritative');
});
assert.equal(controller.pendingRadioPlaybackResult, null);
assert.deepEqual(order, [], message);
});
}
test('confirmed manual Radio playback closes active voice without stopping Radio', () => {
const order = [];
+4
View File
@@ -376,6 +376,10 @@ export class RealtimeConnection {
this.stream = null;
}
if (this.audioEl) {
// This session owns the element's remote media; peer closure is not track release.
const remoteStream = this.audioEl.srcObject;
this.audioEl.srcObject = null;
remoteStream?.getTracks?.().forEach((track) => track.stop());
this.audioEl.remove();
this.audioEl = null;
}
+19 -5
View File
@@ -54,6 +54,10 @@ function browser(t) {
test('retained peer and channel callbacks cannot act after stop or enter a replacement session', async (t) => {
const { peers, streams } = browser(t);
const actions = [];
const remote = () => {
const track = { stops: 0, readyState: 'live', stop() { this.stops++; this.readyState = 'ended'; } };
return { getTracks: () => [track], track };
};
const controller = new GevRealtimeController({
runner: async (name) => { actions.push(name); return { ok: true }; },
backend: {
@@ -67,28 +71,38 @@ test('retained peer and channel callbacks cannot act after stop or enter a repla
const old = peers[0];
old.channel.handlers.get('open')();
assert.equal(controller.status, 'listening');
const oldAudio = controller.audioEl;
const oldRemote = remote();
old.ontrack({ streams: [oldRemote] });
const message = { data: JSON.stringify({ type: 'response.function_call_arguments.done', name: 'get_entity_context', call_id: 'stale', arguments: '{}' }) };
controller.stop();
assert.equal(oldRemote.track.stops, 1, 'Stop releases its owned remote track');
assert.equal(oldRemote.track.readyState, 'ended');
assert.equal(oldAudio.srcObject, null);
assert.doesNotThrow(() => old.ontrack({ streams: [{}] }));
await old.channel.handlers.get('message')(message);
await controller.start();
const current = peers[1];
current.channel.handlers.get('open')();
const audio = controller.audioEl;
const incoming = remote();
current.ontrack({ streams: [incoming] });
old.channel.readyState = 'open'; // Model an already queued callback from the old transport.
old.channel.handlers.get('open')();
await old.channel.handlers.get('message')(message);
old.ontrack({ streams: [{}] });
old.ontrack({ streams: [oldRemote] });
old.onicecandidateerror({ errorText: 'stale error' });
assert.equal(controller.status, 'listening');
assert.equal(controller.pc, current);
assert.equal(controller.audioEl, audio);
assert.equal(audio.srcObject, undefined);
assert.equal(audio.srcObject, incoming, 'a stale track cannot replace the current remote stream');
assert.equal(incoming.track.stops, 0, 'a stale callback cannot stop the current remote track');
assert.deepEqual(actions, []);
const incoming = {};
current.ontrack({ streams: [incoming] });
assert.equal(audio.srcObject, incoming, 'the current peer still delivers audio');
controller.stop();
controller.stop();
assert.equal(audio.srcObject, null);
assert.deepEqual([oldRemote, incoming].map(s => s.track.stops), [1, 1], 'repeated Stop does not restop remote tracks');
assert.equal(incoming.track.readyState, 'ended');
assert.deepEqual(streams.map(s => s.track.stops), [1, 1]);
});
+1 -1
View File
@@ -65,7 +65,7 @@ export class RealtimeRadio {
: control || {};
if (
event.origin === 'user' &&
(event.action === 'pause' || event.action === 'stop')
['pause', 'stop', 'select'].includes(event.action)
) {
this.cancelRadioHandoff();
} else if (
+4 -1
View File
@@ -422,7 +422,10 @@ function analystCard(result) {
...(result.feedState && result.feedState !== 'nominal'
? [{ label: shownFeedState(result.feedState) }]
: []),
...(result.partial ? [{ label: t('partial') }] : []),
// A partial feed state already says it.
...(result.partial && result.feedState !== 'partial'
? [{ label: t('partial') }]
: []),
],
// Coverage qualifications lead the feed detail. The bounded card groups
// overflow in its existing Notes disclosure rather than dropping them.