Updates
Rolling release / release (push) Successful in 6m40s

This commit is contained in:
2026-09-12 07:22:47 -04:00
parent e4546f8e95
commit e9040d110a
30 changed files with 1688 additions and 444 deletions
+88
View File
@@ -0,0 +1,88 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { VOICE_SYSTEM_PROMPT } from '../skills/voice-prompt.js';
const require = createRequire(import.meta.url);
const compaction = require('../vendor/agent-harness/agent/compaction.js');
const prompts = require('../vendor/agent-harness/agent/prompts.js');
function hugeTools() {
return Array.from({ length: 24 }, (_, i) => ({
name: 'tool_' + i,
description: 'schema '.repeat(80),
parameters: { type: 'object', properties: { q: { type: 'string' } } },
}));
}
test('short voice chats do not compact just because tool schemas are large', () => {
const tools = hugeTools();
const hist = [
{ role: 'system', content: VOICE_SYSTEM_PROMPT },
{ role: 'assistant', content: 'Hello! How can I help you today?' },
{ role: 'user', content: 'Hi, please tell me about my computer.' },
];
assert.equal(compaction.shouldCompact(hist, tools, 8192), false);
const out = compaction.compact(hist, {
budgetTokens: compaction.historyBudget(8192, tools, 0),
tools,
voice: true,
});
assert.equal(out.length, hist.length);
assert.equal(out[2].content, hist[2].content);
assert.equal(JSON.stringify(out).includes('Earlier turns were compacted'), false);
});
test('heuristic compact does not double-count tools against the history budget', () => {
const tools = hugeTools();
const hist = [
{ role: 'system', content: 'You are Jarvis' },
{ role: 'assistant', content: 'Hello! How can I help you today?' },
{ role: 'user', content: 'Hi, please tell me about my computer.' },
{ role: 'assistant', content: 'Let me check that.' },
{ role: 'user', content: 'Please continue.' },
];
const budget = compaction.historyBudget(8192, tools, 0);
assert.ok(budget <= 240 || compaction.toolTokens(tools) > 1000);
const out = compaction.heuristicCompact(hist, { budgetTokens: budget, tools });
assert.ok(out.some((m) => String(m.content).includes('tell me about my computer')));
assert.equal(JSON.stringify(out).includes('Earlier turns were compacted'), false);
});
test('voice compaction does not auto-continue a new greeting', () => {
const cont = compaction.autoContinue([{ role: 'assistant', content: 'Hello!' }], { voice: true });
assert.equal(cont, null);
assert.match(compaction.compactReminder({ voice: true }), /Do not greet again/);
});
test('voice assemble uses only the Jarvis prompt', () => {
const sys = prompts.assemble({
personality: 'voice',
extra: VOICE_SYSTEM_PROMPT,
cwd: '/home/raven/.local/share/jarvis-qvac',
hostWorkspace: true,
fsRead: () => 'You are a local coding agent. Read AGENTS.md.',
});
assert.match(sys, /You are Jarvis/);
assert.doesNotMatch(sys, /You are a local coding agent/);
assert.doesNotMatch(sys, /AGENTS.md/);
});
test('LLM compact skips a two-turn voice chat', async () => {
let called = false;
const hist = [
{ role: 'system', content: 'You are Jarvis' },
{ role: 'assistant', content: 'Hello! How can I help you today?' },
{ role: 'user', content: 'Hi, please tell me about my computer.' },
];
const out = await compaction.compactWithLlm(hist, {
voice: true,
complete: async () => {
called = true;
return { text: '1. Latest user request\n2. Facts\n3. Answered\n4. Follow-ups\n' };
},
});
assert.equal(called, false);
assert.equal(out.length, hist.length);
assert.equal(out[2].content, hist[2].content);
});
+20 -2
View File
@@ -62,6 +62,23 @@ test('ask extracts harness reply text instead of stringifying the object', async
}
});
test('harness bridge caps voice shell chaining', () => {
const bridge = new HarnessBridge();
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.builtinTools, [
'read_file',
'list_dir',
'grep',
'run_terminal_cmd',
'web_fetch',
'web_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 () => {
@@ -92,13 +109,14 @@ test('ask with no spoken text after tools returns to listening without a Reply',
}
});
test('confirmPermission forwards Allow/Deny to the harness session', async () => {
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');
assert.deepEqual(calls, [['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();
}
+289 -97
View File
@@ -3,41 +3,93 @@ import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import vm from 'node:vm';
const source = readFileSync(new URL('../apps/gnome-extension/[email protected]/extension.js', import.meta.url), 'utf8');
const uiSource = readFileSync(new URL('../apps/gnome-extension/[email protected]/ui.js', import.meta.url), 'utf8');
const extensionSource = readFileSync(new URL('../apps/gnome-extension/[email protected]/extension.js', import.meta.url), 'utf8');
function stripModules(source) {
return source
.replace(/^import(?:\s+type)?\s+[\s\S]*?from\s+['"][^'"]+['"];\s*$/gm, '')
.replace(/^export default class/gm, 'class')
.replace(/^export class/gm, 'class')
.replace(/^export const /gm, 'const ')
.replace(/^export function /gm, 'function ')
.replace(/^export \{[\s\S]*?\};$/gm, '');
}
function harness() {
const timers = new Map();
const chrome = [];
class Actor {
constructor(props = {}) {
if ('hexpand' in props) throw new Error('No property hexpand on StWidget');
Object.assign(this, props);
this.children = [];
this.visible = props.visible !== false;
this.text = props.text || props.hint_text || '';
this.clutter_text = { connect() {}, ellipsize: null };
}
add_child(child) { this.children.push(child); }
add_child(child) { this.children.push(child); child.get_parent = () => this; }
remove_child(child) { this.children = this.children.filter((item) => item !== child); }
destroy_all_children() { this.children = []; }
get_n_children() { return this.children.length; }
contains() { return false; }
get_last_child() { return this.children[this.children.length - 1] || null; }
connect() { return 1; }
get_first_child() { return this.children[0] || null; }
connect(name, handler) {
this.handlers = this.handlers || {};
this.handlers[name] = handler;
return 1;
}
hide() { this.visible = false; }
show() { this.visible = true; }
destroy() { this.destroyed = true; }
destroy() { this.destroyed = true; this.visible = false; }
set_position() {}
set_width() {}
set_height() {}
grab_key_focus() {}
set_style() {}
add_style_class_name() {}
get_first_child() { return this.children[0] || null; }
get_text() { return this.text || ''; }
set_text(value) { this.text = value; }
}
const menu = {
box: new Actor(),
actor: new Actor(),
opened: false,
connect(_name, handler) { this._handler = handler; return 2; },
disconnect() {},
addMenuItem(item) { this.box.add_child(item.actor || item); },
open() { this.opened = true; this._handler?.(this, true); },
close() { this.opened = false; this._handler?.(this, false); },
removeAll() { this.box.destroy_all_children(); },
};
const context = vm.createContext({
Extension: class {},
global: { stage: { get_key_focus() { return null; } } },
St: { BoxLayout: Actor, Label: Actor, Widget: Actor, Button: Actor, Entry: Actor, ScrollView: Actor, PolicyType: { NEVER: 0, AUTOMATIC: 1 } },
Clutter: { ActorAlign: { CENTER: 0, START: 1 }, EVENT_STOP: 1, EVENT_PROPAGATE: 0 },
Clutter: { ActorAlign: { CENTER: 0, START: 1 }, EVENT_STOP: 1, EVENT_PROPAGATE: 0, KEY_space: 32, KEY_Return: 65293, KEY_Escape: 65307 },
Pango: { WrapMode: { WORD_CHAR: 2 }, EllipsizeMode: { NONE: 0, END: 3 } },
Main: { layoutManager: { addChrome() {}, primaryMonitor: { x: 0, y: 0, width: 1920, height: 1080 } } },
PopupMenu: {
PopupBaseMenuItem: class extends Actor {
constructor(props) { super(props); this.actor = this; }
},
PopupMenuItem: class extends Actor {
constructor(props) { super(props); this.actor = this; this.label = typeof props === 'string' ? props : props?.label; }
},
PopupSeparatorMenuItem: class extends Actor { constructor() { super(); this.actor = this; } },
PopupMenuSection: class extends Actor {
constructor() { super(); this.actor = this; this.box = this; }
},
},
PanelMenu: { Button: class extends Actor {
constructor() { super(); this.menu = menu; }
} },
Main: {
layoutManager: { addChrome(actor) { chrome.push(actor); }, primaryMonitor: { x: 0, y: 0, width: 1920, height: 1080 } },
panel: { addToStatusArea() {} },
wm: { addKeybinding() {}, removeKeybinding() {} },
screenShield: { connect() { return 1; }, disconnect() {}, locked: false },
},
GLib: {
PRIORITY_DEFAULT: 0, PRIORITY_DEFAULT_IDLE: 200, SOURCE_REMOVE: false,
idle_add(_priority, callback) { callback(); return 1; },
@@ -46,75 +98,98 @@ function harness() {
},
log() {},
});
vm.runInContext(source.replace(/^import .*;\n/gm, '').replace('export default class', 'class') +
'\nglobalThis.classes = { ArcOverlay, JarvisExtension, JarvisProxy };', context);
return { ...context.classes, timers };
vm.runInContext(
`${stripModules(uiSource)}\n${stripModules(extensionSource)}\nglobalThis.classes = { ConversationView, JarvisOsd, ComputerUseChrome, SessionPanel, JarvisExtension, JarvisProxy };`,
context,
);
return { ...context.classes, timers, chrome, menu };
}
test('overlay constructs and destroys with pending halo animation', () => {
const { ArcOverlay, timers } = harness();
const overlay = new ArcOverlay();
assert.equal(overlay.header.children[1].x_expand, true);
overlay.attach();
overlay.show(true);
overlay.showHalo();
overlay.showHalo();
assert.equal(timers.size, 1);
overlay.destroy();
assert.equal(timers.size, 0);
test('compact popup constructs without a floating 720px overlay', () => {
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
assert.equal(popup.compact, true);
assert.match(popup.root.style_class, /jarvis-popup/);
assert.equal(popup.header.children[1].x_expand, true);
assert.equal(popup.expand.label, 'Open');
assert.equal(popup.expand.accessible_name, 'Open conversation');
assert.equal(popup.settings.label, 'Settings');
assert.equal(popup.header.children.includes(popup.settings), true);
assert.doesNotMatch(popup.root.style_class, /jarvis-arc/);
});
test('empty chrome widgets start hidden', () => {
const { ArcOverlay } = harness();
const overlay = new ArcOverlay();
assert.equal(overlay.job.visible, false);
assert.equal(overlay.target.visible, false);
assert.equal(overlay.cursor.visible, false);
assert.equal(overlay.wave.visible, false);
const { ConversationView, ComputerUseChrome } = harness();
const popup = new ConversationView({ compact: true });
const cu = new ComputerUseChrome();
assert.equal(popup.confirm.visible, false);
assert.equal(popup.thinking.visible, false);
assert.equal(popup.chipScroll.visible, false);
assert.equal(cu.root.visible, false);
assert.equal(cu.target.visible, false);
assert.equal(cu.cursor.visible, false);
});
test('Reply finalizes a streaming row instead of duplicating [object Object]', () => {
const { ArcOverlay } = harness();
const overlay = new ArcOverlay();
overlay.token('Hello! I am ready.');
overlay.finalizeReply('[object Object]');
overlay.finalizeReply({ text: 'ignored duplicate' });
assert.equal(overlay.transcript.children.length, 1);
assert.match(overlay.transcript.children[0].text, /Hello! I am ready/);
assert.doesNotMatch(overlay.transcript.children[0].text, /object Object/);
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
popup.token('Hello! I am ready.');
popup.finalizeReply('[object Object]');
popup.finalizeReply({ text: 'ignored duplicate' });
assert.equal(popup.transcript.children.length, 1);
assert.match(popup.transcript.children[0].text, /Hello! I am ready/);
assert.doesNotMatch(popup.transcript.children[0].text, /object Object/);
});
test('final reply is shown after tool result rows', () => {
const { ArcOverlay } = harness();
const overlay = new ArcOverlay();
overlay.addToolCall('{"name":"runtime_status"}');
overlay.addToolResult('{"name":"runtime_status"}');
overlay.finalizeReply('Your computer is ready.');
assert.equal(overlay.transcript.children.length, 3);
assert.match(overlay.transcript.children[2].text, /Your computer is ready/);
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
popup.addToolCall('{"name":"runtime_status"}');
popup.addToolResult('{"name":"runtime_status"}');
popup.finalizeReply('Your computer is ready.');
assert.equal(popup.transcript.children.length, 3);
assert.match(popup.transcript.children[2].text, /Your computer is ready/);
});
test('tokens after a tool result start a new spoken Jarvis row', () => {
const { ArcOverlay } = harness();
const overlay = new ArcOverlay();
overlay.addToolCall('{"name":"capability_status"}');
overlay.addToolResult('{"name":"capability_status"}');
overlay.token('Your computer is ready.');
overlay.finalizeReply('Your computer is ready.');
assert.equal(overlay.transcript.children.length, 3);
assert.match(overlay.transcript.children[2].text, /Your computer is ready/);
assert.doesNotMatch(overlay.transcript.children[2].style_class, /jarvis-row-tool/);
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
popup.addToolCall('{"name":"capability_status"}');
popup.addToolResult('{"name":"capability_status"}');
popup.token('Your computer is ready.');
popup.finalizeReply('Your computer is ready.');
assert.equal(popup.transcript.children.length, 3);
assert.match(popup.transcript.children[2].text, /Your computer is ready/);
assert.doesNotMatch(popup.transcript.children[2].style_class, /jarvis-row-tool/);
});
test('whitespace-only tokens do not open an empty Jarvis row', () => {
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
popup.token(' \n');
assert.equal(popup.transcript.children.length, 0);
popup.token('Hostname is nest.');
assert.equal(popup.transcript.children.length, 1);
popup.token(' ');
assert.match(popup.transcript.children[0].text, /Hostname is nest/);
});
test('addRow and token coerce objects to readable text', () => {
const { ArcOverlay } = harness();
const overlay = new ArcOverlay();
overlay.addRow('U', { text: 'Hi there' });
overlay.token({ text: 'Hello from Jarvis' });
assert.equal(overlay.transcript.children.length, 2);
assert.match(overlay.transcript.children[0].text, /Hi there/);
assert.match(overlay.transcript.children[1].text, /Hello from Jarvis/);
assert.doesNotMatch(overlay.transcript.children.map((row) => row.text).join('\n'), /object Object/);
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
popup.addRow('U', { text: 'Hi there' });
popup.token({ text: 'Hello from Jarvis' });
assert.equal(popup.transcript.children.length, 2);
assert.match(popup.transcript.children[0].text, /Hi there/);
assert.match(popup.transcript.children[1].text, /Hello from Jarvis/);
assert.doesNotMatch(popup.transcript.children.map((row) => row.text).join('\n'), /object Object/);
});
test('popup transcript stays short', () => {
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
for (let i = 0; i < 12; i++) popup.addRow('U', `line ${i}`);
assert.equal(popup.transcript.children.length, 8);
});
for (const fails of [false, true]) {
@@ -125,22 +200,33 @@ for (const fails of [false, true]) {
extension.proxy = { connect: () => new Promise((resolve, reject) => { finish = fails ? reject : resolve; }) };
const pending = extension._connectDaemon();
extension.proxy = null;
extension.overlay = null;
extension.popup = null;
extension.session = null;
finish(fails ? new Error('Disconnected') : undefined);
await pending;
});
}
test('GNOME GI imports expose default namespaces and panel has a real menu', () => {
assert.match(source, /import Shell from 'gi:\/\/Shell'/);
assert.match(source, /import Meta from 'gi:\/\/Meta'/);
assert.match(source, /import Pango from 'gi:\/\/Pango'/);
assert.match(source, /new PanelMenu.Button\(0.0, 'Jarvis QVAC', false\)/);
assert.match(source, /PushToTalk/);
assert.match(source, /finalizeReply/);
assert.match(source, /\['Thinking'/);
assert.match(source, /\['ToolCall'/);
assert.match(source, /Minimize/);
assert.match(extensionSource, /import Shell from 'gi:\/\/Shell'/);
assert.match(extensionSource, /import Meta from 'gi:\/\/Meta'/);
assert.match(extensionSource, /new PanelMenu.Button\(0.0, 'Jarvis QVAC', false\)/);
assert.match(extensionSource, /PushToTalk/);
assert.match(extensionSource, /\['Thinking'/);
assert.match(extensionSource, /\['ToolCall'/);
assert.match(extensionSource, /_openPopup/);
assert.match(uiSource, /Open conversation/);
assert.match(extensionSource, /PopupMenuSection/);
assert.match(extensionSource, /_openSettings/);
assert.match(extensionSource, /PopupMenuItem\('Settings'\)/);
assert.match(extensionSource, /PopupMenuItem\('Grant desktop'\)/);
assert.doesNotMatch(uiSource, /button-press-event', \(\) => Clutter\.EVENT_STOP/);
assert.match(uiSource, /notify::pressed/);
assert.match(extensionSource, /ComputerGrant/);
assert.match(extensionSource, /OpenExtensionPrefs/);
assert.match(uiSource, /finalizeReply/);
assert.doesNotMatch(extensionSource, /this\.overlay\.show\(true\)/);
assert.doesNotMatch(extensionSource, /Minimize/);
});
test('D-Bus confirmation signal does not call missing Interface.emit', () => {
@@ -151,42 +237,143 @@ test('D-Bus confirmation signal does not call missing Interface.emit', () => {
assert.match(dbusSource, /Confirm: \{ inSignature: 'sss'/);
});
test('minimize stays closed while Jarvis speaks, then restore shows the transcript', () => {
const { ArcOverlay } = harness();
const overlay = new ArcOverlay();
overlay.attach();
overlay.show(true);
overlay.minimize();
overlay.setState('SPEAKING');
overlay.finalizeReply('I am still talking in the background.');
assert.equal(overlay.root.visible, false);
overlay.show(true);
assert.equal(overlay.root.visible, true);
assert.match(overlay.transcript.children[0].text, /still talking/);
test('session panel stays closed while Jarvis speaks unless expanded', () => {
const { SessionPanel } = harness();
const session = new SessionPanel();
session.attach();
session.view.setState('SPEAKING');
session.view.finalizeReply('I am still talking in the background.');
assert.equal(session.root.visible, false);
session.show(true);
assert.equal(session.root.visible, true);
assert.match(session.view.transcript.children[0].text, /still talking/);
});
test('OSD shows listening without opening a conversation panel', () => {
const { JarvisOsd, SessionPanel, timers } = harness();
const osd = new JarvisOsd();
const session = new SessionPanel();
osd.attach();
session.attach();
osd.setState('LISTENING');
assert.equal(osd.root.visible, true);
assert.match(osd.label.text, /Listening/);
assert.equal(session.root.visible, false);
osd.setState('ARMED');
assert.equal(osd.root.visible, false);
osd.setState('SPEAKING');
assert.equal(osd.root.visible, true);
assert.ok(timers.size >= 1);
osd.showWake();
assert.equal(timers.size, 1);
osd.destroy();
assert.equal(timers.size, 0);
});
test('computer-use chrome shows a target without a transcript', () => {
const { ComputerUseChrome } = harness();
const cu = new ComputerUseChrome();
cu.attach();
assert.equal(cu.root.visible, false);
cu.setTarget('{"rect":[1,2,3,4]}');
assert.equal(cu.root.visible, true);
assert.equal(cu.target.visible, true);
assert.equal(cu.cursor.visible, true);
assert.ok(!cu.transcript);
cu.hide();
assert.equal(cu.root.visible, false);
});
test('ConfirmationRequired chips answer Confirm with job and tool ids', () => {
const { ArcOverlay } = harness();
const overlay = new ArcOverlay();
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
const answers = [];
overlay.onConfirm = (...args) => answers.push(args);
overlay.offerConfirm('write_file', JSON.stringify({ jobId: 'job-1', toolCallId: 'call-9', args: { path: '~/notes' } }), 'destructive');
assert.equal(overlay.confirm.visible, true);
overlay._answerConfirm('allow');
assert.equal(overlay.confirm.visible, false);
const shown = [];
popup.onConfirm = (...args) => answers.push(args);
popup.onConfirmShown = () => shown.push(true);
popup.offerConfirm('write_file', JSON.stringify({ jobId: 'job-1', toolCallId: 'call-9', args: { path: '~/notes' } }), 'destructive');
assert.equal(popup.confirm.visible, true);
assert.equal(popup.confirmButtons.children.length, 3);
assert.equal(popup.confirmButtons.children[1].label, 'Always allow');
assert.equal(shown.length, 1);
popup._answerConfirm('allow');
assert.equal(popup.confirm.visible, false);
assert.deepEqual(answers, [['job-1', 'call-9', 'allow']]);
});
test('Always allow Confirm remembers the decision string', () => {
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
const answers = [];
popup.onConfirm = (...args) => answers.push(args);
popup.offerConfirm('run_terminal_cmd', JSON.stringify({ jobId: 'job-2', toolCallId: 'call-3', args: { command: 'uname -a' } }), 'uname -a');
assert.match(popup.confirmLabel.text, /uname -a/);
popup._answerConfirm('always');
assert.deepEqual(answers, [['job-2', 'call-3', 'always']]);
});
test('tool results show a stdout preview instead of only complete', () => {
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
popup.addToolCall('{"name":"run_terminal_cmd"}');
popup.addToolResult(JSON.stringify({ name: 'run_terminal_cmd', result: 'Linux 6.8\nexit 0' }));
assert.match(popup.transcript.children[1].text, /Linux 6\.8/);
});
test('errors and reset failures are one-line notices, not chat rows', () => {
const { ArcOverlay } = harness();
const overlay = new ArcOverlay();
overlay.setNotice('ResetContext failed:\nTypeError: Cannot read properties of undefined (reading \'apply\')');
assert.equal(overlay.transcript.children.length, 0);
assert.equal(overlay.notice.visible, true);
assert.doesNotMatch(overlay.notice.text, /\n/);
overlay.clear();
overlay.setNotice('New conversation');
assert.equal(overlay.notice.text, 'New conversation');
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
popup.setNotice('ResetContext failed:\nTypeError: Cannot read properties of undefined (reading \'apply\')');
assert.equal(popup.transcript.children.length, 0);
assert.equal(popup.notice.visible, true);
assert.doesNotMatch(popup.notice.text, /\n/);
popup.clear();
popup.setNotice('New conversation');
assert.equal(popup.notice.text, 'New conversation');
});
test('Settings chip is in the header and opens preferences', () => {
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
const calls = [];
popup.onSettings = () => calls.push('settings');
popup.settings.handlers.clicked();
assert.deepEqual(calls, ['settings']);
const session = new ConversationView({ compact: false });
session.onSettings = () => calls.push('session');
session.settings.handlers.clicked();
assert.deepEqual(calls, ['settings', 'session']);
});
test('HUD chips and hold-to-talk receive clicks instead of swallowing them', () => {
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
const calls = [];
popup.onStop = () => calls.push('stop');
popup.onReset = () => calls.push('reset');
popup.onExpand = () => calls.push('open');
popup.onSettings = () => calls.push('settings');
popup.onTalk = (pressed) => calls.push(pressed ? 'talk-down' : 'talk-up');
popup.stop.handlers.clicked();
popup.reset.handlers.clicked();
popup.expand.handlers.clicked();
popup.settings.handlers.clicked();
popup.talk.pressed = true;
popup.talk.handlers['notify::pressed']();
popup.talk.pressed = false;
popup.talk.handlers['notify::pressed']();
assert.deepEqual(calls, ['stop', 'reset', 'open', 'settings', 'talk-down', 'talk-up']);
assert.equal(popup.stop.handlers['button-press-event'], undefined);
assert.equal(popup.settings.handlers['button-press-event'], undefined);
});
test('closing the popup ends hold-to-talk', () => {
const { ConversationView } = harness();
const popup = new ConversationView({ compact: true });
const talks = [];
popup.onTalk = (pressed) => talks.push(pressed);
popup.endTalk();
assert.deepEqual(talks, [false]);
});
test('prefs bind every GSettings schema key', () => {
@@ -198,4 +385,9 @@ test('prefs bind every GSettings schema key', () => {
assert.match(prefs, /wakePhrase/);
assert.match(prefs, /ttsEnabled/);
assert.match(prefs, /modelProfile/);
assert.match(prefs, /'tray'/);
assert.match(prefs, /'expanded'/);
assert.match(prefs, /ComputerGrant/);
assert.match(prefs, /Allow now/);
assert.match(prefs, /ComputerRevoke/);
});
+10 -1
View File
@@ -2,7 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { chmod, cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -127,3 +127,12 @@ exit 0
await rm(work, { recursive: true, force: true });
}
});
test('first-run keeps TTS enabled even when the preview is skipped', () => {
const source = readFileSync(new URL('../packaging/first-run.sh', import.meta.url), 'utf8');
assert.match(source, /Play a TTS preview now\?/);
assert.match(source, /TTS_ENABLED=true/);
assert.doesNotMatch(source, /Enable TTS preview\?/);
assert.doesNotMatch(source, /TTS_ENABLED=false/);
assert.match(source, /spoken replies remain enabled/);
});
+16 -1
View File
@@ -2,7 +2,7 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { createRuntimeTools } from '../skills/runtime-tools.js';
import { assertSdkVersion } from '../daemon/qvac-master.js';
import { parseHudSidecar } from '../skills/voice-prompt.js';
import { parseHudSidecar, VOICE_SYSTEM_PROMPT } from '../skills/voice-prompt.js';
import { createPhase2Tools } from '../skills/phase2-tools.js';
import { createQvacTools } from '../skills/qvac-tools.js';
import { profile } from '../daemon/model-profiles.js';
@@ -26,6 +26,21 @@ test('voice sidecars are removed from speech and retained for the HUD', () => {
assert.equal(parsed.hud.title, 'Done');
});
test('voice prompt tells the model not to chain extra terminal commands', () => {
assert.match(VOICE_SYSTEM_PROMPT, /call run_terminal_cmd\nonce/s);
assert.match(VOICE_SYSTEM_PROMPT, /Do not chain extra commands/);
assert.match(VOICE_SYSTEM_PROMPT, /Reply in plain text only/);
assert.match(VOICE_SYSTEM_PROMPT, /Never use markdown/);
assert.match(VOICE_SYSTEM_PROMPT, /Quantum Verse Automatic Computer/);
assert.match(VOICE_SYSTEM_PROMPT, /spell it as Q V A C/);
assert.match(VOICE_SYSTEM_PROMPT, /Internet protocol addresses have no dots/);
assert.match(VOICE_SYSTEM_PROMPT, /Spell them as separate letters/);
assert.match(VOICE_SYSTEM_PROMPT, /call web_fetch/);
assert.match(VOICE_SYSTEM_PROMPT, /This computer can reach the internet/);
assert.match(VOICE_SYSTEM_PROMPT, /Allow now/);
assert.doesNotMatch(VOICE_SYSTEM_PROMPT, /Never claim cloud access/);
});
test('phase 2 registers safe local tools with permission metadata', async () => {
const tools = createPhase2Tools({ cwd: process.cwd() });
assert.deepEqual(tools.map((tool) => tool.name), ['app_list', 'fs_search', 'fs_read', 'fs_write', 'memory_recall', 'memory_remember', 'rag_workspaces', 'capability_status']);
+56
View File
@@ -0,0 +1,56 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import os from 'node:os';
const require = createRequire(import.meta.url);
const tools = require('../vendor/agent-harness/agent/tools.js');
const permRules = require('../vendor/agent-harness/agent/perm-rules.js');
const toolBudget = require('../vendor/agent-harness/agent/tool-budget.js');
test('runShell captures stdout after the process closes', async () => {
const result = await tools.runShell(os.tmpdir(), 'printf hello-jarvis');
assert.equal(result.exitCode, 0);
assert.match(result.stdout, /hello-jarvis/);
});
test('formatShellResult puts command output in front of the exit code', () => {
assert.equal(tools.formatShellResult({ exitCode: 0, stdout: 'Linux 6.8\n', stderr: '' }), 'Linux 6.8\nexit 0');
assert.equal(tools.formatShellResult({ exitCode: 0, stdout: '', stderr: '' }), '(no output)\nexit 0');
});
test('Always allow wildcard matches later shell commands', () => {
const rules = permRules.addRule([], 'run_terminal_cmd', { command: '*' }, 'allow');
assert.equal(permRules.resolve(rules, 'run_terminal_cmd', { command: 'uname -a' }), 'allow');
assert.equal(permRules.resolve(rules, 'run_terminal_cmd', { command: 'ls -la /tmp' }), 'allow');
});
test('run_terminal_cmd description still exists for the model', () => {
const def = tools.SCHEMAS.find((item) => item.name === 'run_terminal_cmd');
assert.ok(def);
assert.match(def.description, /shell command/i);
});
test('Jarvis voice budget runs one shell then forces an answer', () => {
const budget = toolBudget.fromPayload({}, 'jarvis-qvac');
assert.equal(budget.maxShellCalls, 1);
assert.equal(budget.maxTurns, 6);
assert.equal(toolBudget.shouldSkipShell(budget), false);
toolBudget.markShell(budget);
assert.equal(budget.answerOnly, true);
assert.equal(toolBudget.shouldSkipShell(budget), true);
assert.match(toolBudget.skipShellMessage(), /already ran/i);
assert.equal(
toolBudget.lastToolText([{ role: 'tool', content: 'Static hostname: nest\nexit 0' }], 80),
'Static hostname: nest exit 0'
);
});
test('coding-agent origin does not cap shell chaining', () => {
const budget = toolBudget.fromPayload({}, 'local');
assert.equal(budget.maxShellCalls, 0);
toolBudget.markShell(budget);
toolBudget.markShell(budget);
assert.equal(toolBudget.shouldSkipShell(budget), false);
assert.equal(budget.answerOnly, false);
});
+34 -1
View File
@@ -7,13 +7,19 @@ import { pcmS16le } from '../daemon/voice-adapters.js';
import { WakeEngine } from '../daemon/wake-engine.js';
import { VadSegmenter } from '../daemon/vad.js';
import { PipeWireCapture, pcmRms } from '../daemon/audio-pipewire.js';
import { SentenceBuffer, isMeaningfulTranscript, isSpeakable } from '../daemon/transcript.js';
import { SentenceBuffer, isMeaningfulTranscript, isSpeakable, speakableForTts } from '../daemon/transcript.js';
test('Phase 4 transcript filtering and sentence buffering are deterministic', () => {
assert.equal(isMeaningfulTranscript('[BLANK_AUDIO]'), false);
assert.equal(isMeaningfulTranscript('hi'), false);
assert.equal(isSpeakable('OK.'), true);
assert.equal(isSpeakable('[object Object]'), false);
assert.equal(speakableForTts('Using `run_terminal_cmd`'), "Using 'run terminal cmd'");
assert.equal(speakableForTts('Your IP is 192.168.0.1'), 'Your I P is 192 168 0 1');
assert.equal(
speakableForTts('QVAC fetched https://example.com/ip'),
'Quantum Verse Automatic Computer fetched example dot com slash I P'
);
assert.equal(isMeaningfulTranscript('what time is it'), true);
const out = []; const buffer = new SentenceBuffer({ onSentence: (s) => out.push(s) });
buffer.push('First sentence. Second'); buffer.push(' sentence!'); buffer.flush();
@@ -71,6 +77,33 @@ test('empty TTS still leaves the daemon listening', async () => {
assert.equal(daemon.finished, true);
});
test('multi-sentence speech returns to listening once', async () => {
const daemon = new EventEmitter();
const states = [];
daemon.state = 'SPEAKING';
daemon.voice = {
finishSpeaking() {
daemon.finished = (daemon.finished || 0) + 1;
},
};
daemon.setState = (state) => { daemon.state = state; states.push(state); };
const capture = new EventEmitter(); capture.start = () => {}; capture.stop = () => {};
const wake = new WakeEngine({ detect: () => null });
const loop = new VoiceLoop({
daemon,
capture,
wake,
vad: new VadSegmenter(),
tts: { speak: async () => ({ samples: new Int16Array(2) }) },
playback: { play: async () => {}, stop() {} },
});
loop.status.tts = true;
await loop.speak('Hello there. How are you today?');
assert.equal(daemon.state, 'LISTENING');
assert.equal(daemon.finished, 1);
assert.deepEqual(states, ['LISTENING']);
});
test('PCM packing uses even s16le sample pairs', () => {
const buf = Buffer.alloc(4);
buf.writeInt16LE(256, 0);