mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
A scenario whose body ran past about 19 s idled until 45 s after its start, and twice one never exited and hit the runner's 300 s timeout after it had passed: something of Chrome outlived Browser.close and kept its stdout and stderr open. - scripts/qa/browser.mjs closes Chrome within 5 s, kills its process group and closes its pipes; the harness then finishes and exits with the scenario's code. QA_EXIT_DIAGNOSTICS=1 keeps the old ending and prints the open handles and Chrome's process tree 5 s after finish. - Every strict page starts with its clock at the 18:30 UTC nearest to the real time, so Navara's sun is up over the test cities whenever the acceptance runs; a scenario can ask for its own instant or the real clock (Wildlife does, for its freshness fixtures). - The runner records a scenario that passed but did not exit as hung, kills the whole process group on a timeout, runs a failed scenario once more and reports one that passes then as flaky, and appends each run to --history=FILE; scripts/report-acceptance-flake.mjs reads it.
264 lines
9.4 KiB
JavaScript
264 lines
9.4 KiB
JavaScript
import { closeBrowser, exitAfterFlush } from './qa/browser.mjs';
|
|
import { localPreview, waitForEngine } from './qa-strict-harness.mjs';
|
|
// An active voice session survives engine switches. With a fake microphone
|
|
// and a fake Realtime transport (no model is reached: /api/openai, /api/realtime
|
|
// and api.openai.com are blocked or answered in the page), this gate starts
|
|
// voice on Cesium, switches Cesium→Navara→Cesium→Navara and checks that:
|
|
// - the microphone was requested once, one peer connection was made, and
|
|
// neither it nor its data channel closed; no WebSocket opened;
|
|
// - every session object kept its identity and each switch attached once;
|
|
// - a set_layer_visibility call after each switch ran on the engine shown;
|
|
// - a call made during a switch waited and ran on the incoming engine, and
|
|
// one whose wait ran out was refused with ENGINE_SWITCHING;
|
|
// - annotate_map works on both engines and marks survive each switch;
|
|
// - the session heard which engine it drives after each switch.
|
|
//
|
|
// node scripts/qa-voice-engine-switch.mjs http://127.0.0.1:<port>/
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
idDifference,
|
|
launch,
|
|
openSessionPage,
|
|
readSession,
|
|
readVoiceEvidence,
|
|
requestEngine,
|
|
sessionAddress,
|
|
switchEngine,
|
|
} from './qa/appSessionHarness.mjs';
|
|
|
|
const base = localPreview(process.argv[2]);
|
|
|
|
// Play one server function call on the fake data channel and wait for the
|
|
// client's function_call_output.
|
|
function callTool(page, name, args, { beforeCall = null } = {}) {
|
|
return page.evaluate(
|
|
async (name, args, beforeCall) => {
|
|
const evidence = window.__qaVoice;
|
|
const callId = `qa-call-${(window.__qaCalls = (window.__qaCalls ?? 0) + 1)}`;
|
|
const responseId = `qa-response-${callId}`;
|
|
const deliver = (event) =>
|
|
evidence.channel.dispatchEvent(
|
|
new MessageEvent('message', { data: JSON.stringify(event) }),
|
|
);
|
|
if (beforeCall === 'switch-navara' || beforeCall === 'switch-cesium') {
|
|
const select = document.querySelector('#globe-engine');
|
|
select.value = beforeCall.slice('switch-'.length);
|
|
select.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}
|
|
deliver({ type: 'response.created', response: { id: responseId } });
|
|
deliver({
|
|
type: 'response.function_call_arguments.done',
|
|
response_id: responseId,
|
|
item_id: `item-${callId}`,
|
|
call_id: callId,
|
|
name,
|
|
arguments: JSON.stringify(args),
|
|
});
|
|
const started = performance.now();
|
|
for (;;) {
|
|
const sent = evidence.sent.find(
|
|
(event) =>
|
|
event?.type === 'conversation.item.create' &&
|
|
event.item?.type === 'function_call_output' &&
|
|
event.item.call_id === callId,
|
|
);
|
|
if (sent) {
|
|
deliver({
|
|
type: 'response.done',
|
|
response: { id: responseId, status: 'completed', output: [] },
|
|
});
|
|
return JSON.parse(sent.item.output);
|
|
}
|
|
if (performance.now() - started > 60_000)
|
|
throw new Error(`No output for ${name}`);
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
},
|
|
name,
|
|
args,
|
|
beforeCall,
|
|
);
|
|
}
|
|
|
|
const engineChanges = (page) =>
|
|
page.evaluate(() =>
|
|
window.__qaVoice.sent
|
|
.filter(
|
|
(event) =>
|
|
event?.type === 'conversation.item.create' &&
|
|
event.item?.role === 'system',
|
|
)
|
|
.map((event) => {
|
|
try {
|
|
return JSON.parse(event.item.content[0].text);
|
|
} catch {
|
|
return null;
|
|
}
|
|
})
|
|
.filter((event) => event?.type === 'engine-changed')
|
|
.map((event) => event.engine),
|
|
);
|
|
|
|
const ANNOTATION = {
|
|
annotations: [
|
|
{
|
|
type: 'pin',
|
|
latitude: 42.3554,
|
|
longitude: -71.0605,
|
|
label: 'QA voice mark',
|
|
},
|
|
],
|
|
};
|
|
|
|
const browser = await launch();
|
|
const results = [];
|
|
try {
|
|
const { page, errors } = await openSessionPage(
|
|
browser,
|
|
sessionAddress(base, { engine: 'cesium' }),
|
|
);
|
|
await waitForEngine(page, 'cesium', { timeout: 90_000 });
|
|
await page.evaluate(async () => {
|
|
const close = document.querySelector('[data-key-setup-close]');
|
|
if (close?.offsetParent) close.click();
|
|
await window.__godsEyeView.voiceCommands.start({ pushToTalk: false });
|
|
});
|
|
await page.waitForFunction(
|
|
() =>
|
|
window.__qaVoice.channel?.readyState === 'open' &&
|
|
window.__godsEyeView.voiceCommands.state === 'listening',
|
|
{ timeout: 30_000 },
|
|
);
|
|
const started = await readVoiceEvidence(page);
|
|
const boot = await readSession(page);
|
|
assert.equal(started.getUserMedia, 1, 'voice asks for the microphone once');
|
|
assert.equal(started.peerConnections, 1);
|
|
|
|
const annotated = await callTool(page, 'annotate_map', ANNOTATION);
|
|
assert.equal(annotated.ok, true, JSON.stringify(annotated));
|
|
assert.equal(annotated.engine, 'cesium');
|
|
let marks = await page.evaluate(() => window.__gevAnnotations.count());
|
|
assert.ok(marks >= 1, 'the mark is on the board');
|
|
results.push('PASS annotate_map drew a mark on Cesium');
|
|
|
|
const sequence = ['navara', 'cesium', 'navara'];
|
|
for (const kind of sequence) {
|
|
await switchEngine(page, kind);
|
|
const layer = await callTool(page, 'set_layer_visibility', {
|
|
layerId: 'earthquakes',
|
|
enabled: kind === 'navara',
|
|
});
|
|
assert.equal(layer.engine, kind, JSON.stringify(layer));
|
|
assert.equal(
|
|
await page.evaluate(() => window.__godsEyeView.engine.kind),
|
|
kind,
|
|
);
|
|
const count = await page.evaluate(() => window.__gevAnnotations.count());
|
|
assert.equal(count, marks, 'the marks survived the switch');
|
|
const annotation = await callTool(page, 'annotate_map', {
|
|
annotations: [
|
|
{
|
|
type: 'pin',
|
|
latitude: 42.3554 + marks * 0.01,
|
|
longitude: -71.0605,
|
|
label: `QA voice mark ${marks}`,
|
|
},
|
|
],
|
|
});
|
|
assert.equal(annotation.ok, true, JSON.stringify(annotation));
|
|
assert.equal(annotation.engine, kind);
|
|
marks = await page.evaluate(() => window.__gevAnnotations.count());
|
|
assert.equal(marks, count + 1, 'voice added a visible mark on this engine');
|
|
assert.equal(
|
|
await page.evaluate(() => window.__gevAnnotations.attached),
|
|
true,
|
|
);
|
|
results.push(`PASS ${kind}: annotate_map drew a mark`);
|
|
results.push(`PASS ${kind}: set_layer_visibility ran on ${layer.engine}`);
|
|
}
|
|
|
|
// A call made during a switch waits for the incoming engine.
|
|
const midSwitch = await callTool(
|
|
page,
|
|
'set_layer_visibility',
|
|
{ layerId: 'earthquakes', enabled: true },
|
|
{ beforeCall: 'switch-cesium' },
|
|
);
|
|
await waitForEngine(page, 'cesium', { timeout: 90_000 });
|
|
assert.equal(midSwitch.ok, true, JSON.stringify(midSwitch));
|
|
assert.equal(midSwitch.engine, 'cesium', JSON.stringify(midSwitch));
|
|
results.push('PASS a call made during the switch ran on the incoming engine');
|
|
assert.equal(
|
|
await page.evaluate(() => window.__gevAnnotations.count()),
|
|
marks,
|
|
'the mark is still on the board after Cesium→Navara→Cesium→Navara→Cesium',
|
|
);
|
|
|
|
// A call whose wait runs out is refused with a clear code.
|
|
await page.evaluate(() => {
|
|
window.__godsEyeView.voiceCommands.runner.switchWaitMs = 1;
|
|
});
|
|
const refused = await callTool(
|
|
page,
|
|
'set_layer_visibility',
|
|
{ layerId: 'earthquakes', enabled: false },
|
|
{ beforeCall: 'switch-navara' },
|
|
);
|
|
await waitForEngine(page, 'navara', { timeout: 90_000 });
|
|
await page.evaluate(() => {
|
|
window.__godsEyeView.voiceCommands.runner.switchWaitMs = 15_000;
|
|
});
|
|
assert.equal(refused.ok, false, JSON.stringify(refused));
|
|
assert.equal(refused.code, 'ENGINE_SWITCHING', JSON.stringify(refused));
|
|
results.push(
|
|
'PASS a call whose wait ran out was refused with ENGINE_SWITCHING',
|
|
);
|
|
|
|
await switchEngine(page, 'cesium');
|
|
const after = await readVoiceEvidence(page);
|
|
const state = await readSession(page);
|
|
const changes = await engineChanges(page);
|
|
const switches = sequence.length + 3;
|
|
const report = {
|
|
started,
|
|
after,
|
|
ids: idDifference(boot.ids, state.ids),
|
|
counters: state.counters,
|
|
changes,
|
|
voiceState: await page.evaluate(
|
|
() => window.__godsEyeView.voiceCommands.state,
|
|
),
|
|
};
|
|
console.log(JSON.stringify(report, null, 2));
|
|
assert.equal(after.getUserMedia, 1, 'no new microphone request');
|
|
assert.equal(after.peerConnections, 1, 'no new peer connection');
|
|
assert.equal(after.peerConnectionsClosed, 0, 'the connection stayed open');
|
|
assert.equal(after.channelsClosed, 0, 'the data channel stayed open');
|
|
assert.equal(after.channelState, 'open');
|
|
assert.equal(after.webSockets, started.webSockets, 'no new WebSocket');
|
|
assert.equal(after.tokenRequests, 1, 'no new token');
|
|
assert.equal(report.voiceState, 'listening');
|
|
assert.deepEqual(report.ids, {}, 'session objects kept their identity');
|
|
assert.equal(
|
|
state.counters.attachments,
|
|
boot.counters.attachments + switches,
|
|
);
|
|
assert.equal(changes.length, switches, JSON.stringify(changes));
|
|
assert.equal(changes.at(-1), 'cesium');
|
|
assert.deepEqual(errors, []);
|
|
results.push('PASS the voice session kept its microphone and transport');
|
|
|
|
await page.evaluate(() => window.__godsEyeView.voiceCommands.stop());
|
|
await page.waitForFunction(
|
|
() => window.__qaVoice.peerConnectionsClosed === 1,
|
|
);
|
|
results.push('PASS stopping voice closes the transport');
|
|
console.log(results.join('\n'));
|
|
} catch (error) {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await closeBrowser(browser);
|
|
}
|
|
await exitAfterFlush();
|