Updates
Rolling release / release (push) Successful in 7m21s

This commit is contained in:
2026-09-12 09:40:42 -04:00
parent a4073b9020
commit f26204505e
23 changed files with 531 additions and 114 deletions
+18 -2
View File
@@ -7,7 +7,7 @@ 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 { HarnessBridge, harnessRoots } from '../daemon/harness-bridge.js';
import { spokenReply } from '../skills/voice-prompt.js';
import { voiceSettings } from '../daemon/voice-settings.js';
@@ -67,11 +67,13 @@ test('ask extracts harness reply text instead of stringifying the object', async
});
test('harness bridge caps voice shell chaining', () => {
const bridge = new HarnessBridge();
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.deepEqual(bridge.options.builtinTools, [
'read_file',
'list_dir',
@@ -150,6 +152,20 @@ test('QVAC scheduler prioritizes voice and keeps one active job', async () => {
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'); };
+15
View File
@@ -255,6 +255,21 @@ test('session panel stays closed while Jarvis speaks unless expanded', () => {
assert.match(session.view.transcript.children[0].text, /still talking/);
});
test('lazy ASR still shows the microphone as available when capture is up', () => {
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
popup.setVoiceStatus({ tts: true, input: true, wake: true });
assert.match(popup.status.text, /SPEECH ON/);
assert.match(popup.status.text, /WAKE ON/);
assert.doesNotMatch(popup.status.text, /MIC UNAVAILABLE/);
assert.equal(popup.talk.label, 'Hold to talk');
popup.setVoiceStatus({ tts: false, input: false, wake: false });
assert.match(popup.status.text, /MIC UNAVAILABLE/);
assert.equal(popup.talk.label, 'Mic unavailable');
assert.match(extensionSource, /Boolean\(voice\.capture\)/);
assert.doesNotMatch(extensionSource, /voice\.asr && voice\.capture/);
});
test('voice status lives in the panel chip, not a floating overlay', () => {
const { JarvisOsd, SessionPanel, timers, chrome } = harness();
const osd = new JarvisOsd();
+3
View File
@@ -167,8 +167,11 @@ test('install.sh seeds default config when missing and does not require first-ru
const config = JSON.parse(await readFile(path.join(home, '.config/jarvis/config.json'), 'utf8'));
assert.equal(config.wakePhrase, 'hey jarvis');
assert.equal(config.wakeCommand, 'jarvis-wake-bridge');
assert.equal(config.modelProfile, 'laptop-16gb');
assert.equal(config.ttsEnabled, true);
assert.equal(config.fsAccess, 'workspace');
assert.equal(config.freeVramOnIdle, true);
assert.match(result.stdout, /Jarvis is ready/);
assert.doesNotMatch(result.stdout, /first-run/);
const installSource = readFileSync(new URL('../packaging/install.sh', import.meta.url), 'utf8');
+26 -3
View File
@@ -1,19 +1,20 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { mkdtemp, mkdir, writeFile, readFile, symlink } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { mkdtemp, mkdir, writeFile, readFile, symlink, rm } from 'node:fs/promises';
import os, { tmpdir } from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';
import { ComputerUseSession } from '../computer-use/session.js';
import { ComputerActuator } from '../computer-use/actuator.js';
import { PortalInputBackend } from '../computer-use/portal-input.js';
import { createComputerObserveTools } from '../skills/computer-observe.js';
import { createPhase2Tools } from '../skills/phase2-tools.js';
import { createPhase2Tools, filesystemRoots } from '../skills/phase2-tools.js';
import { VoiceStateMachine } from '../daemon/voice-state.js';
import { assertLocalEndpoint } from '../daemon/network-policy.js';
const require = createRequire(import.meta.url);
const custom = require('../vendor/agent-harness/agent/custom-tools.js');
const sandbox = require('../vendor/agent-harness/agent/sandbox.js');
test('custom tool permissions survive registration and default to confirmation', () => {
const id = 'review-permissions';
@@ -29,6 +30,28 @@ test('custom tool permissions survive registration and default to confirmation',
} finally { custom.clear(id); }
});
test('filesystem access lets path tools reach home while workspace stays jailed', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'jarvis-fs-access-'));
const workspace = path.join(dir, 'workspace'); await mkdir(workspace);
const homeProbe = await mkdtemp(path.join(os.homedir(), '.jarvis-fs-test-'));
try {
await writeFile(path.join(homeProbe, 'note.txt'), 'from-home');
const jailed = Object.fromEntries(createPhase2Tools({ cwd: workspace, roots: filesystemRoots('workspace', workspace) }).map(t => [t.name, t]));
await assert.rejects(jailed.fs_read.execute({ file: path.join(homeProbe, 'note.txt') }), /outside/);
const opened = Object.fromEntries(createPhase2Tools({ cwd: workspace, roots: filesystemRoots('filesystem', workspace) }).map(t => [t.name, t]));
assert.equal(await opened.fs_read.execute({ file: path.join(homeProbe, 'note.txt') }), 'from-home');
const home = Object.fromEntries(createPhase2Tools({ cwd: workspace, roots: filesystemRoots('home', workspace) }).map(t => [t.name, t]));
assert.equal(await home.fs_read.execute({ file: path.join(homeProbe, 'note.txt') }), 'from-home');
sandbox.setGrants({ roots: [path.resolve(workspace)] });
assert.equal(sandbox.isAllowed('jarvis-qvac', path.join(homeProbe, 'note.txt')), false);
sandbox.setGrants({ roots: [path.resolve(workspace), '/'] });
assert.equal(sandbox.isAllowed('jarvis-qvac', path.join(homeProbe, 'note.txt')), true);
} finally {
await rm(homeProbe, { recursive: true, force: true });
await rm(dir, { recursive: true, force: true });
}
});
test('workspace tools reject outside roots and symlink escapes and count UTF-8 bytes', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'jarvis-path-test-'));
const root = path.join(dir, 'workspace'); await mkdir(root);
+9 -1
View File
@@ -171,5 +171,13 @@ test('a disabled microphone never starts capture while speech output remains ava
const capture = new EventEmitter(); capture.start = () => assert.fail('microphone should be disabled');
const loop = new VoiceLoop({ capture, asr: null, tts: { start: async () => {} } });
await loop.start();
assert.equal(loop.status.capture, false); assert.equal(loop.status.asr, false); assert.equal(loop.status.tts, true);
assert.equal(loop.status.capture, false); assert.equal(loop.status.asr, false); assert.equal(loop.status.tts, false);
assert.equal(await loop.ensureTts(), true); assert.equal(loop.status.tts, true);
});
test('catalog defaults enable CPU wake, workspace files, and idle VRAM unload', () => {
const settings = voiceSettings({});
assert.equal(settings.fsAccess, 'workspace');
assert.equal(settings.freeVramOnIdle, true);
assert.equal(settings.wakeCommand, 'jarvis-wake-bridge');
});
+76 -10
View File
@@ -117,7 +117,7 @@ test('PCM packing uses even s16le sample pairs', () => {
assert.equal(fromBytes.samples[0], 256);
});
test('ASR failure preserves speech output and reports unavailable microphone', async () => {
test('ASR failure keeps the microphone hot and still allows speech output', async () => {
const capture = new EventEmitter(); let captured = false;
capture.start = () => { captured = true; }; capture.stop = () => {};
let spoken = '';
@@ -127,12 +127,14 @@ test('ASR failure preserves speech output and reports unavailable microphone', a
playback: { play: async () => {}, stop() {} },
});
loop.on('error', () => {});
await loop.start(); await loop.speak('Speech still works.');
assert.equal(spoken, 'Speech still works.'); assert.equal(captured, false);
assert.equal(loop.status.tts, true); assert.equal(loop.status.asr, false);
spoken = '';
await loop.speak('OK.');
assert.equal(spoken, 'OK.');
await loop.start();
assert.equal(captured, true);
assert.equal(loop.status.asr, false);
assert.equal(loop.status.tts, false);
await loop.speak('Speech still works.');
assert.equal(spoken, 'Speech still works.');
assert.equal(loop.status.tts, true);
assert.equal(await loop.ensureAsr(), false);
assert.match(loop.status.errors.asr, /Whisper/); await loop.stop();
});
@@ -142,8 +144,11 @@ test('TTS failure preserves ASR and push to talk without wake command', async ()
asr: { start: async () => {} }, tts: { start: async () => { throw new Error('TTS failed'); } },
playback: { stop() {} },
});
loop.on('error', () => {}); await loop.start(); loop.setPushToTalk(true);
loop.on('error', () => {}); await loop.start();
assert.equal(loop.status.asr, false);
await loop.setPushToTalk(true);
assert.equal(loop.status.asr, true); assert.equal(loop.ptt, true);
assert.equal(await loop.ensureTts(), false);
assert.equal(loop.status.tts, false); assert.equal(loop.status.wake, false); await loop.stop();
});
@@ -169,7 +174,7 @@ test('voice loop retries microphone capture after PipeWire comes up', async () =
});
loop.on('error', () => {});
await loop.start();
assert.equal(loop.status.asr, true);
assert.equal(loop.status.asr, false);
await new Promise((resolve) => setTimeout(resolve, 5));
assert.equal(loop.status.capture, false);
await new Promise((resolve) => setTimeout(resolve, 40));
@@ -194,7 +199,10 @@ test('voice loop retries ASR after a first-login GPU miss', async () => {
loop.on('error', () => {});
await loop.start();
assert.equal(loop.status.asr, false);
assert.equal(capture.started, undefined);
assert.equal(capture.started, true);
assert.equal(attempts, 0);
assert.equal(await loop.ensureAsr(), false);
assert.equal(attempts, 1);
await new Promise((resolve) => setTimeout(resolve, 50));
assert.equal(attempts, 2);
assert.equal(loop.status.asr, true);
@@ -207,3 +215,61 @@ test('voice adapters load canonical ASR and TTS plugins', () => {
assert.match(source, /whispercpp-transcription/);
assert.match(source, /tts-ggml/);
});
function tonePcm(ms, amplitude = 12_000, hz = 220) {
const samples = Math.floor(16_000 * ms / 1000);
const buf = Buffer.alloc(samples * 2);
for (let i = 0; i < samples; i++) buf.writeInt16LE(Math.floor(amplitude * Math.sin(2 * Math.PI * hz * i / 16_000)), i * 2);
return buf;
}
test('built-in CPU wake detector fires on a two-peak hey-jarvis cadence', async () => {
const { KeywordWakeEngine, createWakeEngine } = await import('../daemon/wake-engine.js');
const engine = createWakeEngine({ command: 'jarvis-wake-bridge', phrases: ['hey jarvis'] });
assert.equal(engine instanceof KeywordWakeEngine, true);
const heard = [];
engine.on('wake', (phrase) => heard.push(phrase));
engine.push(Buffer.concat([tonePcm(80, 0), tonePcm(220), tonePcm(90, 0), tonePcm(280), tonePcm(200, 0)]));
assert.deepEqual(heard, ['hey jarvis']);
engine.push(tonePcm(800));
assert.equal(heard.length, 1);
});
test('sleeping voice loop still feeds the wake detector and skips VAD', () => {
const capture = new EventEmitter(); capture.start = () => {}; capture.stop = () => {};
let wakeFrames = 0; let vadFrames = 0;
const wake = new WakeEngine({ detect: () => null });
wake.push = () => { wakeFrames += 1; };
const vad = new VadSegmenter();
vad.push = () => { vadFrames += 1; };
const daemon = new EventEmitter(); daemon.state = 'SLEEPING';
const loop = new VoiceLoop({ daemon, capture, wake, vad });
loop.running = true;
loop.pushAudio(Buffer.alloc(640));
assert.equal(wakeFrames, 1);
assert.equal(vadFrames, 0);
});
test('voice start keeps the microphone hot without loading ASR or TTS', async () => {
const capture = new EventEmitter(); let captured = false;
capture.start = () => { captured = true; }; capture.stop = () => {};
let asrStarts = 0; let ttsStarts = 0;
const loop = new VoiceLoop({
capture,
asr: { start: async () => { asrStarts += 1; } },
tts: { start: async () => { ttsStarts += 1; } },
wake: new WakeEngine({ detect: () => null }),
});
await loop.start();
assert.equal(captured, true);
assert.equal(loop.status.capture, true);
assert.equal(loop.status.asr, false);
assert.equal(loop.status.tts, false);
assert.equal(asrStarts, 0);
assert.equal(ttsStarts, 0);
assert.equal(loop.status.wake, true);
await loop.parkModels();
assert.equal(loop.status.asr, false);
assert.equal(loop.status.tts, false);
await loop.stop();
});