test(voice): classify current-frame send and cancellation outcomes

Keep viewport and pointer-crop capture unchanged, but expose static reason codes for unavailable frames, invalidated turn captures and retained image sends. No image or user text in diagnostics.
This commit is contained in:
Kevin Rajan committed 2026-10-16 23:37:22 -05:00
1 parent 8c02f5852f
commit 050f68ac03
2 files changed
+75 -2

No files matched your search

+14 -2
View File
@@ -437,15 +437,24 @@ export class RealtimeViewport {
const captured = await captureImage();
const imageUrl =
typeof captured === 'string' ? captured : captured?.dataUrl || null;
if (!imageUrl) {
this.debugLog?.('viewport_context.skipped', {
reason: 'capture-unavailable',
});
return false;
}
if (
!imageUrl ||
generation !== this.generation ||
request !== this.captureSeq ||
this.dc !== channel ||
channel.readyState !== 'open' ||
(typeof frame.valid === 'function' && !frame.valid())
)
) {
this.debugLog?.('viewport_context.skipped', {
reason: 'capture-invalidated',
});
return false;
}
// Keep at most one viewport screenshot in context. Images are the single
// most expensive item (re-billed every turn they linger), so we proactively
@@ -506,6 +515,9 @@ export class RealtimeViewport {
cameraKey: this.readCameraKey(),
}
: null;
this.debugLog?.('viewport_context.result', {
reason: sent ? 'sent' : 'data-channel-send-failed',
});
return sent;
}
+61
View File
@@ -179,3 +179,64 @@ test('closed channel never captures pointer or full viewport', async () => {
);
assert.equal(await viewport.sendVisualContextIfUseful(localContext()), false);
});
test('capture failures and invalidated turns are reason-coded without sending', async () => {
const channel = { readyState: 'open' };
const diagnostics = [];
const viewport = new RealtimeViewport({
readChannel: () => channel,
operations: {
sendRealtimeEvent: () => {
throw new Error('should not send unavailable image');
},
debugLog: (event, detail) => diagnostics.push({ event, detail }),
},
capture: async () => null,
});
assert.equal(await viewport.sendVisualContextIfUseful(localContext()), false);
assert.ok(
diagnostics.some(
(item) =>
item.event === 'viewport_context.skipped' &&
item.detail.reason === 'capture-unavailable',
),
);
let release;
viewport.capture = () =>
new Promise((resolve) => {
release = resolve;
});
const pending = viewport.sendVisualContextIfUseful(localContext());
viewport.reset();
release('data:image/jpeg;base64,AA==');
assert.equal(await pending, false);
assert.ok(
diagnostics.some(
(item) =>
item.event === 'viewport_context.skipped' &&
item.detail.reason === 'capture-invalidated',
),
);
});
test('successful retained send is logged without recording image contents', async () => {
const channel = { readyState: 'open' };
const diagnostics = [];
const viewport = new RealtimeViewport({
readChannel: () => channel,
readCameraKey: () => 'camera:fixture',
operations: {
sendRealtimeEvent: () => true,
debugLog: (event, detail) => diagnostics.push({ event, detail }),
},
capture: async () => 'data:image/jpeg;base64,AA==',
});
assert.equal(await viewport.sendVisualContextIfUseful(localContext()), true);
assert.ok(diagnostics.some((item) =>
item.event === 'viewport_context.result' && item.detail.reason === 'sent'
));
assert.equal(
JSON.stringify(diagnostics).includes('data:image/jpeg'),
false,
);
});