Files
gods-eye-view/src/voice/pointerReticle.test.mjs
T
Bilawal Sidhu a19897ad15 feat(voice): voice core — honest layer reach, spoken summary with on-screen card, narration, point-and-ask
- Voice layer manifest (src/voice/layerManifest.js) generates the layer enums,
  aliases and analyst field hints; a test fails when a catalog layer has no entry.
- analyst_query validates fields, operators, units, values and scopes, counts up
  to 250,000 loaded records per layer, reports complete/partial/unanswered, tells
  LAYER_OFF / NOT_READY / FEED_UNAVAILABLE apart from zero, refuses mismatched
  follow-ups and never lets a superseded query replace follow-up memory. Seven
  more layers expose analyst records.
- Tool results carry a code-built say line and a display card; the voice card in
  the mic control shows captions, plan steps and the result
  (src/voice/resultDisplay.js is the one adapter). Slow tools narrate their
  current step out of band; holding Space while the assistant talks cuts it off.
  Turn spans go to the debug log; caption transcription is metered; the log
  omits what was said unless GEV_VOICE_LOG_CONTENT=1.
- Point and ask: the Space-down pointer snapshot resolves "this", "that one" and
  "around here" through 'pointer' targets, and "the second one" through
  referent:n from the last numbered list.
- Prompt states each policy once (instructions + tools 48.5k -> 46.7k chars).
- Voice benchmark harness (scripts/qa-voice-bench.mjs, scripts/voice-bench/)
  shares its phrase table and grading with scripts/qa-voice-routing.mjs.
2026-09-28 21:19:23 -05:00

104 lines
3.6 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { readStylesheet } from '../testSupport/readStylesheet.mjs';
import { PointerReticle, RETICLE_LINGER_MS, RETICLE_MAX_MS } from './pointerReticle.js';
const css = readStylesheet(new URL('../../style.css', import.meta.url));
function fakeDoc() {
const appended = [];
return {
appended,
body: { appendChild: (node) => appended.push(node) },
createElement: (tag) => ({
tag,
hidden: false,
style: {},
dataset: {},
attributes: {},
setAttribute(name, value) {
this.attributes[name] = value;
},
removed: false,
remove() {
this.removed = true;
},
}),
};
}
function harness() {
const doc = fakeDoc();
const timers = new Map();
let next = 1;
const reticle = new PointerReticle({
doc,
rectOf: () => ({ left: 10, top: 20 }),
setTimer: (fn, ms) => {
const id = next++;
timers.set(id, { fn, ms });
return id;
},
clearTimer: (id) => timers.delete(id),
});
const fire = () => {
const [[id, timer]] = [...timers];
timers.delete(id);
timer.fn();
};
return { doc, timers, reticle, fire };
}
const pointer = { kind: 'this', label: 'UPS793', screenPx: { x: 600, y: 200 } };
test('reticle: shows at the pointed pixel and caps its lifetime', () => {
const { doc, timers, reticle, fire } = harness();
reticle.handle({ type: 'pointer', pointer });
const node = doc.appended[0];
assert.equal(node.className, 'gev-pointer-reticle');
assert.equal(node.attributes['aria-hidden'], 'true');
assert.equal(node.hidden, false);
assert.equal(node.style.left, '610px');
assert.equal(node.style.top, '220px');
assert.equal(node.dataset.target, 'entity');
assert.equal([...timers.values()][0].ms, RETICLE_MAX_MS);
fire();
assert.equal(node.hidden, true);
});
test('reticle: leaves when a tool moves the camera, lingers after a reply', () => {
const { doc, timers, reticle, fire } = harness();
reticle.handle({ type: 'pointer', pointer: { ...pointer, kind: 'here' } });
const node = doc.appended[0];
assert.equal(node.dataset.target, 'ground');
reticle.handle({ type: 'action-call', name: 'analyst_query', callId: 'c1' });
reticle.handle({ type: 'action-call', name: 'analyst_query', callId: 'c2' });
reticle.handle({ type: 'action-result', name: 'analyst_query', callId: 'c1' });
reticle.handle({ type: 'completion' });
assert.equal([...timers.values()][0].ms, RETICLE_MAX_MS, 'a sibling call still running keeps it');
reticle.handle({ type: 'action-result', name: 'analyst_query', callId: 'c2' });
reticle.handle({ type: 'completion' });
assert.equal([...timers.values()][0].ms, RETICLE_LINGER_MS);
fire();
assert.equal(node.hidden, true);
reticle.handle({ type: 'pointer', pointer });
reticle.handle({ type: 'action-call', name: 'track_entity' });
assert.equal(node.hidden, true, 'camera tools hide it at once');
reticle.handle({ type: 'pointer', pointer });
reticle.handle({ type: 'state', state: 'idle' });
assert.equal(node.hidden, true, 'session end hides it');
assert.equal(timers.size, 0);
reticle.destroy();
reticle.destroy();
assert.equal(node.removed, true);
reticle.handle({ type: 'pointer', pointer });
assert.equal(doc.appended.length, 1, 'destroyed reticle ignores events');
});
test('reticle css: overlay is inert and hidden in Clean-UI and recording', () => {
assert.match(css, /\.gev-pointer-reticle\s*\{[^}]*pointer-events:\s*none/);
assert.match(css, /body\.ui-clean-view \.gev-pointer-reticle/);
assert.match(css, /body\.recording-mode \.gev-pointer-reticle/);
assert.match(css, /\.gev-pointer-reticle\[hidden\]/);
});