import { mkdtempSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; process.env.XDG_STATE_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-daemon-test-')); process.env.XDG_DATA_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-daemon-data-')); process.env.XDG_CONFIG_HOME = mkdtempSync(path.join(tmpdir(), 'jarvis-daemon-config-')); 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, harnessRoots } from '../daemon/harness-bridge.js'; import { spokenReply } from '../skills/voice-prompt.js'; import { voiceSettings } from '../daemon/voice-settings.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({ text: 'I will look that up. weather', }), 'I will look that up.', ); assert.equal(spokenReply({}), ''); assert.equal(spokenReply('[object Object]'), ''); assert.equal(spokenReply({ text: '{"ok":true,"action":"type","name":"Discord"}' }), ''); assert.equal(spokenReply({ text: 'Done. {"ok":true,"action":"click","ref":"r1"}' }), 'Done.'); assert.equal( spokenReply({ text: '## Status\n- CPU is **fine**.\nUse `htop` if you want more.' }), 'Status\n- CPU is fine.\nUse htop if you want more.', ); }); test('voice settings default TTS on and honor an explicit disable', () => { assert.equal(voiceSettings({}).ttsEnabled, true); assert.equal(voiceSettings({ ttsEnabled: true }).ttsEnabled, true); assert.equal(voiceSettings({ ttsEnabled: false }).ttsEnabled, false); assert.equal(voiceSettings({ tts_enabled: false }).ttsEnabled, false); }); test('thought chunks forward SDK text onto Thinking', async () => { const daemon = new JarvisDaemon(); const thoughts = []; daemon.on('Thinking', (text) => thoughts.push(text)); try { daemon.harness.emit('agent_thought_chunk', { type: 'agent_thought_chunk', text: 'Considering the lookup.' }); daemon.harness.emit('agent_thought_chunk', 'plain thought'); daemon.harness.emit('agent_thought_chunk', { delta: ' via delta' }); daemon.harness.emit('agent_thought_chunk', { text: '' }); assert.deepEqual(thoughts, ['Considering the lookup.', 'plain thought', ' via delta']); } finally { await daemon.close(); } }); 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 caps voice shell chaining', () => { const bridge = new HarnessBridge({ fsAccess: 'workspace' }); assert.equal(bridge.options.origin, 'jarvis-qvac'); assert.equal(bridge.options.voice, true); assert.equal(bridge.options.maxShellCalls, 1); assert.equal(bridge.options.maxTurns, 6); assert.deepEqual(bridge.options.roots, []); assert.deepEqual(harnessRoots('filesystem'), ['/']); assert.match(bridge.options.cwd, /jarvis\/workspace$/); assert.match(bridge.options.system, /You are Jarvis/); assert.match(bridge.options.system, /SOUL\.md/); assert.match(bridge.options.system, /PERSONA\.md/); assert.match(bridge.options.system, /It is /); assert.doesNotMatch(bridge.options.system, /User personality notes/); assert.deepEqual(bridge.options.builtinTools, [ 'read_file', 'write_file', 'search_replace', 'list_dir', 'grep', 'run_terminal_cmd', 'todo_write', 'task', 'update_goal', 'memory_search', 'memory_get', 'memory_write', ]); assert.equal(bridge.options.webFetch, false); assert.equal(bridge.options.builtinTools.includes('web_search'), false); assert.equal(bridge.options.builtinTools.includes('web_fetch'), false); assert.ok(bridge.options.tools.some((tool) => tool.name === 'browser')); assert.equal(bridge.options.tools.find((tool) => tool.name === 'browser').permission, 'read'); assert.ok(bridge.options.tools.some((tool) => tool.name === 'webcam')); assert.equal(bridge.options.tools.find((tool) => tool.name === 'webcam').permission, 'read'); assert.ok(bridge.options.tools.length <= 32, 'custom tools exceed harness cap: ' + bridge.options.tools.length); }); test('ask rebuilds the system prompt instead of keeping a stale clock', async () => { const bridge = new HarnessBridge({ fsAccess: 'workspace' }); bridge.options.system = 'stale'; bridge.session = { async prompt() { return { ok: true, text: 'Hello there.' }; }, on() { return () => {}; }, }; try { await bridge.ask('hi'); assert.match(bridge.options.system, /You are Jarvis/); assert.match(bridge.options.system, /It is /); assert.doesNotMatch(bridge.options.system, /^stale$/); } finally { await bridge.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/Always 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'); daemon.confirmPermission('job-1', 'call-9', 'always'); assert.deepEqual(calls, [['job-1', 'call-9', 'allow'], ['job-1', 'call-9', 'always']]); } 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: [] }); }); test('idle sleep unloads GPU models instead of only suspending', async () => { const daemon = new JarvisDaemon(); const calls = []; daemon.settings = { ...daemon.settings, freeVramOnIdle: true }; daemon.parkModels = async () => { calls.push('park'); }; daemon.voiceLoop = { interrupt() {} }; daemon.harness = { cancel() {}, resetContext: async () => {}, close: async () => {} }; try { await daemon.sleep(); assert.equal(daemon.state, 'SLEEPING'); assert.deepEqual(calls, ['park']); } finally { await daemon.close(); } }); test('shutdown still closes the harness when recovery or voice cleanup fails', async () => { const daemon = new JarvisDaemon(); let closed = false; daemon.recovery.save = () => { throw new Error('disk full'); }; daemon.harness = { cancel() {}, close: async () => { closed = true; } }; daemon.voiceLoop = { stop: async () => { throw new Error('voice cleanup'); } }; await assert.rejects(daemon.close(), /voice cleanup/); assert.equal(closed, true); }); test('Grant desktop revokes any previous portal session before asking GNOME again', async () => { const daemon = new JarvisDaemon(); const order = []; daemon.input = { revoke: () => order.push('revoke'), grant: (opts) => { order.push(['grant', opts]); return Promise.resolve({ backend: 'portal-ei' }); }, }; daemon.computer = { revoke: () => order.push('computer-revoke'), grant: (opts) => { order.push(['computer-grant', opts]); return { session_id: 's' }; }, setBackend: (backend) => order.push(['backend', backend]), status: () => ({ active: true }), expiresAt: Date.now() + 180000, }; try { daemon.computerGrant(false); await Promise.resolve(); assert.equal(order[0], 'revoke'); assert.equal(order[1], 'computer-revoke'); assert.deepEqual(order[2], ['computer-grant', { persist: false }]); assert.deepEqual(order[3], ['grant', { persist: false, mode: daemon.settings.computerMode }]); assert.deepEqual(order[4], ['backend', 'portal-ei']); } finally { await daemon.close(); } }); test('camera Allow now starts a grant even when Camera access is off', async () => { const daemon = new JarvisDaemon(); const order = []; try { assert.equal(daemon.settings.webcamEnabled, false); daemon.webcam = { access: (opts) => { order.push(['access', opts]); return Promise.resolve({ ok: true, via: 'grant' }); }, }; daemon.webcamGrant(); await Promise.resolve(); assert.equal(daemon.camera.status().active, true); assert.equal(daemon.camera.status().enabled, true); assert.equal(daemon.camera.status().backend, 'grant'); assert.deepEqual(order[0], ['access', { device: '' }]); const status = JSON.parse(daemon.runtimeStatus()); assert.equal(status.camera.active, true); daemon.webcamRevoke(); assert.equal(daemon.camera.status().active, false); assert.equal(daemon.camera.status().enabled, false); } finally { await daemon.close(); } }); test('a failed camera permission request revokes the grant instead of showing active access', async () => { const daemon = new JarvisDaemon(); try { daemon.webcam = { access: async () => { throw new Error('Camera portal Access response 2'); } }; daemon.webcamGrant(); await Promise.resolve(); await Promise.resolve(); assert.equal(daemon.camera.status().active, false); assert.equal(daemon.camera.status().backend, 'none'); } finally { await daemon.close(); } }); test('cancel suppresses a late reply and revokes portal input', async () => { const daemon = new JarvisDaemon(); let resolve; let revoked = false; daemon.input = { revoke: () => { revoked = true; } }; daemon.harness = { ask: () => new Promise(r => { resolve = r; }), cancel() {}, close: async () => {} }; const replies = []; daemon.on('Reply', text => replies.push(text)); try { const ask = daemon.ask('hello'); await Promise.resolve(); daemon.cancel(); resolve({ text: 'late answer' }); assert.equal(await ask, ''); assert.deepEqual(replies, []); assert.equal(daemon.state, 'ARMED'); assert.equal(revoked, true); } finally { await daemon.close(); } }); test('mute ignores arm and does not return to listening after speech', async () => { const daemon = new JarvisDaemon(); const muted = []; daemon.voiceLoop = { setMuted(value) { muted.push(Boolean(value)); }, setPushToTalk(value) { this.ptt = value; }, interrupt() {}, status: { capture: false, wake: false, muted: false }, metrics: { snapshot() { return {}; } }, }; try { daemon.setState('LISTENING'); daemon.setMuted(true); assert.equal(daemon.muted, true); assert.equal(daemon.listenEnabled, false); assert.equal(daemon.state, 'ARMED'); assert.deepEqual(muted, [true]); await daemon.arm(); assert.equal(daemon.state, 'ARMED'); daemon.setListening(true); assert.equal(daemon.state, 'ARMED'); daemon.setState('SPEAKING'); daemon._finishSpeech(); assert.equal(daemon.state, 'ARMED'); const status = JSON.parse(daemon.runtimeStatus()); assert.equal(status.muted, true); assert.equal(status.voice.muted, true); daemon.setMuted(false); daemon.setListening(false); daemon.setState('SPEAKING'); daemon._finishSpeech(); assert.equal(daemon.state, 'ARMED'); } finally { await daemon.close(); } });