import test from 'node:test'; import assert from 'node:assert/strict'; import { VoiceStateMachine } from '../daemon/voice-state.js'; import { QvacScheduler } from '../daemon/qvac-scheduler.js'; import { JarvisDaemon } from '../daemon/index.js'; import { HarnessBridge } from '../daemon/harness-bridge.js'; import { spokenReply } from '../skills/voice-prompt.js'; test('voice state machine handles wake, reply, cancel, and idle sleep', () => { let now = 0; const voice = new VoiceStateMachine({ now: () => now, idleMs: 100 }); voice.wake(); voice.utterance(); voice.speak(); voice.finishSpeaking(); assert.equal(voice.state, 'LISTENING'); assert.equal(voice.command('hands off'), 'cancel'); assert.equal(voice.state, 'ARMED'); voice.wake(); now = 101; voice.expireIdle(); assert.equal(voice.state, 'SLEEPING'); }); test('typed questions wake an armed voice session before thinking', () => { const voice = new VoiceStateMachine(); voice.typedUtterance(); assert.equal(voice.state, 'THINKING'); }); test('typed questions barge in from SPEAKING', () => { const voice = new VoiceStateMachine(); voice.wake(); voice.utterance(); voice.speak(); voice.typedUtterance(); assert.equal(voice.state, 'THINKING'); }); test('spoken replies use harness text and strip HUD sidecars', () => { assert.equal(spokenReply({ ok: true, text: 'Hello there.', reason: 'stop' }), 'Hello there.'); assert.equal(spokenReply({ text: 'Spoken. {"title":"x","chips":[]}' }), 'Spoken.'); assert.equal(spokenReply({}), ''); assert.equal(spokenReply('[object Object]'), ''); }); test('ask extracts harness reply text instead of stringifying the object', async () => { const daemon = new JarvisDaemon(); daemon.harness = { ask: async () => ({ ok: true, text: 'Hello there.', reason: 'stop' }), cancel() {}, close: async () => {} }; daemon.voiceLoop = null; const replies = []; daemon.on('Reply', (text) => replies.push(text)); try { const result = await daemon.ask('Hi'); assert.equal(result, 'Hello there.'); assert.deepEqual(replies, ['Hello there.']); assert.equal(daemon.lastReply, 'Hello there.'); assert.equal(daemon.state, 'LISTENING'); } finally { await daemon.close(); } }); test('harness bridge recovers streamed text when final envelope is empty after a tool call', async () => { const session = new (await import('node:events')).EventEmitter(); session.prompt = async () => { session.emit('agent_message_chunk', { text: 'I will check that. ' }); session.emit('tool_call', { call: { name: 'runtime_status' } }); session.emit('agent_message_chunk', { text: 'Your computer is ready.' }); return { ok: true, text: '', reason: 'stop' }; }; const bridge = Object.create(HarnessBridge.prototype); bridge.session = session; const reply = await bridge.ask('How is my computer?'); assert.equal(reply.text, 'Your computer is ready.'); }); test('ask with no spoken text after tools returns to listening without a Reply', async () => { const daemon = new JarvisDaemon(); daemon.harness = { ask: async () => ({ ok: true, text: '', reason: 'stop' }), cancel() {}, close: async () => {} }; daemon.voiceLoop = null; const replies = []; daemon.on('Reply', (text) => replies.push(text)); try { const result = await daemon.ask('status'); assert.equal(result, ''); assert.deepEqual(replies, []); assert.equal(daemon.state, 'LISTENING'); } finally { await daemon.close(); } }); test('confirmPermission forwards Allow/Deny to the harness session', async () => { const daemon = new JarvisDaemon(); const calls = []; daemon.harness = { session: { permit(...args) { calls.push(args); } }, cancel() {}, close: async () => {} }; try { daemon.confirmPermission('job-1', 'call-9', 'allow'); assert.deepEqual(calls, [['job-1', 'call-9', 'allow']]); } finally { await daemon.close(); } }); test('harness bridge resetContext disposes the current conversation', async () => { let cancelled = false; let disposed = false; const bridge = Object.create(HarnessBridge.prototype); bridge.session = { cancel() { cancelled = true; }, async dispose() { disposed = true; }, }; await bridge.resetContext(); assert.equal(cancelled, true); assert.equal(disposed, true); assert.equal(bridge.session, null); }); test('QVAC scheduler prioritizes voice and keeps one active job', async () => { const scheduler = new QvacScheduler(); const order = []; const first = scheduler.run(async () => { order.push('first'); await new Promise((r) => setTimeout(r, 5)); }, { lane: 'background' }); const media = scheduler.run(async () => order.push('media'), { lane: 'background' }); const voice = scheduler.run(async () => order.push('voice'), { lane: 'voice' }); await Promise.all([first, media, voice]); assert.deepEqual(order, ['first', 'voice', 'media']); assert.deepEqual(scheduler.status(), { running: 0, queued: [] }); });