Files
gnome-jarvis/test/daemon.test.js
T
snxraven 5317576087
Rolling release / release (push) Successful in 8m2s
Allow to change the agents name + Workspace files
2026-09-12 15:30:11 -04:00

200 lines
8.2 KiB
JavaScript

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-'));
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. <jarvis_hud>{"title":"x","chips":[]}</jarvis_hud>' }), 'Spoken.');
assert.equal(spokenReply({}), '');
assert.equal(spokenReply('[object Object]'), '');
});
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('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.deepEqual(bridge.options.builtinTools, [
'read_file',
'write_file',
'search_replace',
'list_dir',
'grep',
'run_terminal_cmd',
'web_fetch',
'fetch_page',
'google_search',
'web_search',
'wiki_search',
'hn_search',
'code_search',
]);
assert.equal(bridge.options.webFetch, true);
});
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('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(); }
});