feat(mobile): correlate voice tasks and recover results on reconnect

This commit is contained in:
yilsem committed 2026-09-08 21:00:28 +03:00
1 parent 9f9f3226a1
commit b66daed07a
17 files changed
+308 -48

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { VoiceDispatchGate } from '../src/behavior.mjs'
import { VoiceSession } from '../src/voice.mjs'
test('IDE result cannot resurrect pending audio consent', async () => {
const voice = new VoiceSession({ onText() {} }); voice.ready = true; voice.send = () => {}
voice.audioTranscript = 'Bunu yap'; voice.dispatchGate.input('Bunu yap')
voice.result({ taskId: 'done', status: 'complete', text: 'Bunu yap' })
assert.equal(voice.audioTranscript, '')
assert.throws(() => voice.dispatchGate.claim('Bunu yap', 'execute'))
})
for (const phrase of ['Tamam, bunu yap.', 'Bu görevi gönder', 'Lütfen araştır', 'Şimdi başlat', 'Evet uygula']) {
test(`direct current instruction: ${phrase}`, () => {
const gate = new VoiceDispatchGate(); gate.input(phrase)
assert.equal(gate.claim(phrase, 'research'), 1)
assert.throws(() => gate.claim(phrase, 'research'))
})
}
for (const phrase of ['Bunu yapma', 'Bunu yap mı', 'Bunu yapabilir miyiz?', '“Bunu yap” dedi', 'Yapsak nasıl olur', 'Hayır, bekle', 'Tamam']) {
test(`discussion is not dispatch permission: ${phrase}`, () => {
const gate = new VoiceDispatchGate(); gate.input(phrase)
assert.throws(() => gate.claim(phrase, 'execute'))
})
}
test('old consent, unfinished speech, research escalation and stopped session fail closed', () => {
const gate = new VoiceDispatchGate(); gate.input('Bunu yap'); gate.input('Hayır bekle')
assert.throws(() => gate.claim('Bunu yap', 'execute'))
gate.input('Araştır'); assert.throws(() => gate.claim('Araştır', 'execute'))
gate.input('Bunu ', false); assert.throws(() => gate.claim('Araştır', 'research'))
gate.input('yap', true); assert.equal(gate.claim('Bunu yap', 'execute'), 4)
gate.invalidate(); assert.throws(() => gate.claim('Bunu yap', 'execute'))
})
test('voice dispatch uses actual user text once; no model-generated permission or success', async () => {
const sends = [], responses = []
const voice = new VoiceSession({ onText() {}, onDraft() {}, onSubmit: async (...args) => { sends.push(args); return { accepted: true, completed: false } } })
voice.send = frame => responses.push(frame)
const args = { intent: 'research', objective: 'İncele', scope: 'Android', context: 'Yeşil seçildi', constraints: 'Değiştirme', output: 'Rapor', completion: 'Kanıt', userInstruction: 'Araştır' }
const frame = { data: JSON.stringify({ toolCall: { functionCalls: [{ id: '1', name: 'submit_task', args }] } }) }
await voice.receive(frame); assert.equal(sends.length, 0)
await voice.receive({ data: JSON.stringify({ toolCall: { functionCalls: [{ id: 'draft', name: 'prepare_task', args }] } }) })
responses.length = 0
await voice.receive({ data: JSON.stringify({ serverContent: { inputTranscription: { text: 'Araştır' } } }) })
await voice.receive(frame); await voice.receive(frame)
assert.equal(sends.length, 1)
assert.deepEqual(responses[0].toolResponse.functionResponses[0].response, { accepted: true, completed: false })
assert.ok(responses[1].toolResponse.functionResponses[0].response.error)
})