feat: voice command mapping — context-gated approve/reject over voice
While a tool-permission gate is held on the active tab, spoken confirm phrases resolve it via the existing approve/reject WS channel — no server change (byte-shuttle intact). Everything else stays ordinary dictation. Safety: whole-utterance-exact matching + leading-negation guard + reject precedence + confidence gate (approve only) + a cancellable 1.5s confirm window whose commit re-validates gate identity/epoch/connectivity (TOCTOU guard) + stale-gate epoch bound at PTT-start. New pure modules public/voice-commands.ts and public/voice-confirm.ts at 100% coverage; wiring in voice/terminal-session/tabs/main. 1307 tests pass, typecheck clean, build:web OK. Independent code + security reviews flagged a confirm-window TOCTOU and an unwired cancel path — both fixed and covered. Plan: docs/PLAN_VOICE_COMMANDS.md.
This commit is contained in:
@@ -200,7 +200,7 @@ describe('createVoiceInput', () => {
|
||||
const mock = MockRecognition.last!
|
||||
voice?.start()
|
||||
mock.onresult?.(makeResultEvent('hello world', true))
|
||||
expect(onTranscript).toHaveBeenCalledWith('hello world')
|
||||
expect(onTranscript).toHaveBeenCalledWith('hello world', expect.any(Number))
|
||||
})
|
||||
|
||||
it('autoSend=true appends \\r to the transcript (AC-A2.4)', () => {
|
||||
@@ -209,7 +209,7 @@ describe('createVoiceInput', () => {
|
||||
const mock = MockRecognition.last!
|
||||
voice?.start()
|
||||
mock.onresult?.(makeResultEvent('hello', true))
|
||||
expect(onTranscript).toHaveBeenCalledWith('hello\r')
|
||||
expect(onTranscript).toHaveBeenCalledWith('hello\r', expect.any(Number))
|
||||
})
|
||||
|
||||
it('autoSend=false does not append \\r (AC-A2.4)', () => {
|
||||
@@ -218,7 +218,7 @@ describe('createVoiceInput', () => {
|
||||
const mock = MockRecognition.last!
|
||||
voice?.start()
|
||||
mock.onresult?.(makeResultEvent('hello', true))
|
||||
expect(onTranscript).toHaveBeenCalledWith('hello')
|
||||
expect(onTranscript).toHaveBeenCalledWith('hello', expect.any(Number))
|
||||
})
|
||||
|
||||
it('omitting autoSend does not append \\r', () => {
|
||||
@@ -227,7 +227,16 @@ describe('createVoiceInput', () => {
|
||||
const mock = MockRecognition.last!
|
||||
voice?.start()
|
||||
mock.onresult?.(makeResultEvent('hello', true))
|
||||
expect(onTranscript).toHaveBeenCalledWith('hello')
|
||||
expect(onTranscript).toHaveBeenCalledWith('hello', expect.any(Number))
|
||||
})
|
||||
|
||||
it('forwards the top alternative ASR confidence as a 2nd arg (VC decision D)', () => {
|
||||
const onTranscript = vi.fn()
|
||||
const voice = createVoiceInput(onTranscript)
|
||||
const mock = MockRecognition.last!
|
||||
voice?.start()
|
||||
mock.onresult?.(makeResultEvent('hello', true))
|
||||
expect(onTranscript).toHaveBeenCalledWith('hello', 1.0)
|
||||
})
|
||||
|
||||
it('calls onInterim with interim transcript text', () => {
|
||||
|
||||
Reference in New Issue
Block a user