Rolling release / release (push) Canceled after 3m29s
The compaction turn was trying to load a fake model, and the desktop grant assertion still looked for the old Settings button. Co-authored-by: Cursor <[email protected]>
674 lines
30 KiB
JavaScript
674 lines
30 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import vm from 'node:vm';
|
|
|
|
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 = [];
|
|
let nextTimer = 1;
|
|
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, width: 0, text: this.text };
|
|
this.allocation = { x1: 0, x2: 320, get_width() { return this.x2 - this.x1; } };
|
|
this.vadjustment = {
|
|
value: 0, upper: 240, page_size: 80, handlers: {},
|
|
connect(name, handler) { this.handlers[name] = handler; },
|
|
emit(name) { this.handlers[name]?.(); },
|
|
};
|
|
}
|
|
get_vadjustment() { return this.vadjustment; }
|
|
get_allocation_box() { return this.allocation; }
|
|
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; }
|
|
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; this.visible = false; }
|
|
set_position() {}
|
|
set_width() {}
|
|
set_height() {}
|
|
grab_key_focus() {}
|
|
set_style() {}
|
|
add_style_class_name() {}
|
|
remove_style_class_name() {}
|
|
get_text() { return this.text || ''; }
|
|
set_text(value) { this.text = value; }
|
|
}
|
|
class BoxLayout extends Actor {}
|
|
class Label extends Actor {}
|
|
class ScrollView extends Actor {
|
|
set_child(child) {
|
|
if (child instanceof Label) throw new TypeError('Object is of type St.Label - cannot convert to StScrollable');
|
|
this.destroy_all_children();
|
|
this.add_child(child);
|
|
}
|
|
}
|
|
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, Label, Icon: Actor, Widget: Actor, Button: Actor, Entry: Actor, ScrollView, PolicyType: { NEVER: 0, AUTOMATIC: 1, ALWAYS: 2 } },
|
|
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 } },
|
|
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; },
|
|
timeout_add(_priority, _delay, callback) { const id = nextTimer++; timers.set(id, callback); return id; },
|
|
Source: { remove(id) { timers.delete(id); } },
|
|
},
|
|
log() {},
|
|
});
|
|
vm.runInContext(
|
|
`${stripModules(uiSource)}\n${stripModules(extensionSource)}\nglobalThis.classes = { ConversationView, JarvisOsd, ComputerUseChrome, SessionPanel, JarvisExtension, JarvisProxy };`,
|
|
context,
|
|
);
|
|
return { ...context.classes, timers, chrome, menu };
|
|
}
|
|
|
|
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, undefined);
|
|
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 { ConversationView, ComputerUseChrome } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
const cu = new ComputerUseChrome();
|
|
assert.equal(popup.confirm.visible, false);
|
|
assert.equal(popup.thinkingScroll.visible, false);
|
|
assert.equal(popup.thinkingPane.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('final reply replaces mashed streaming tokens', () => {
|
|
const { ConversationView } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
popup.token('Greetings,');
|
|
popup.token('young');
|
|
popup.token('Rebel.');
|
|
popup.finalizeReply('Greetings, young Rebel.');
|
|
assert.equal(popup.transcript.children.length, 1);
|
|
assert.match(popup.transcript.children[0].text, /Greetings, young Rebel/);
|
|
assert.doesNotMatch(popup.transcript.children[0].text, /Greetings,youngRebel/);
|
|
});
|
|
|
|
test('Reply finalizes a streaming row instead of duplicating [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 { 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, 2);
|
|
assert.match(popup.transcript.children[0].style_class, /jarvis-row-tool/);
|
|
assert.match(popup.transcript.children[1].text, /Your computer is ready/);
|
|
});
|
|
|
|
test('tokens after a tool result start a new spoken Jarvis row', () => {
|
|
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, 2);
|
|
assert.match(popup.transcript.children[1].text, /Your computer is ready/);
|
|
assert.doesNotMatch(popup.transcript.children[1].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 { 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]) {
|
|
test('daemon connection finishing after disable is ignored: ' + (fails ? 'failure' : 'success'), async () => {
|
|
const { JarvisExtension } = harness();
|
|
const extension = new JarvisExtension();
|
|
let finish;
|
|
extension.proxy = { connect: () => new Promise((resolve, reject) => { finish = fails ? reject : resolve; }) };
|
|
const pending = extension._connectDaemon();
|
|
extension.proxy = 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(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.doesNotMatch(extensionSource, /DO_NOT_AUTO_START/);
|
|
assert.match(extensionSource, /DBusProxyFlags\.NONE/);
|
|
assert.match(extensionSource, /_keepSyncing/);
|
|
assert.match(extensionSource, /is starting/);
|
|
assert.match(extensionSource, /\['Thinking'/);
|
|
assert.match(extensionSource, /\['ToolCall'/);
|
|
assert.match(extensionSource, /_openPopup/);
|
|
assert.doesNotMatch(uiSource, /Open conversation/);
|
|
assert.doesNotMatch(extensionSource, /PopupMenuItem\('Grant desktop'\)/);
|
|
assert.doesNotMatch(extensionSource, /PopupMenuItem\('Revoke desktop'\)/);
|
|
assert.doesNotMatch(extensionSource, /PopupMenuItem\('Settings'\)/);
|
|
assert.doesNotMatch(uiSource, /label: 'Open'/);
|
|
assert.match(extensionSource, /PopupMenuSection/);
|
|
assert.match(extensionSource, /import Pango from 'gi:\/\/Pango'/);
|
|
assert.match(extensionSource, /ellipsize = Pango\.EllipsizeMode\.NONE/);
|
|
assert.match(extensionSource, /osd\.attach\(this\._panelBox, this\._glyph\)/);
|
|
assert.match(extensionSource, /jarvis-panel-mark/);
|
|
assert.match(extensionSource, /brand\/icons\/jarvis-mark\.svg/);
|
|
assert.match(uiSource, /jarvis-panel-state/);
|
|
assert.doesNotMatch(uiSource, /style_class: 'jarvis-osd'/);
|
|
assert.match(uiSource, /setAssistantName/);
|
|
assert.match(extensionSource, /_applyAssistantName/);
|
|
assert.doesNotMatch(uiSource, /button-press-event', \(\) => Clutter\.EVENT_STOP/);
|
|
assert.match(uiSource, /onListen/);
|
|
assert.match(uiSource, /jarvis-mute/);
|
|
assert.match(extensionSource, /SetMuted/);
|
|
assert.match(extensionSource, /SetListening/);
|
|
assert.doesNotMatch(extensionSource, /this\._call\('Arm'\)/);
|
|
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', () => {
|
|
const dbusSource = readFileSync(new URL('../daemon/dbus-service.js', import.meta.url), 'utf8');
|
|
assert.match(dbusSource, /ConfirmationRequired\(tool, args, pattern\) \{ return \[String\(tool\)/);
|
|
assert.doesNotMatch(dbusSource, /ConfirmationRequired\(tool, args, pattern\) \{ this\.emit/);
|
|
assert.match(dbusSource, /ContextReset\(\) \{ return; \}/);
|
|
assert.match(dbusSource, /Confirm: \{ inSignature: 'sss'/);
|
|
});
|
|
|
|
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/);
|
|
session.view.scroll.vadjustment.value = 0;
|
|
session.show(true);
|
|
assert.equal(session.view.scroll.vadjustment.value, 160);
|
|
});
|
|
|
|
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.equal(popup.status.text, 'Local');
|
|
assert.match(popup.statusLine.text, /speech on/);
|
|
assert.match(popup.statusLine.text, /wake on/);
|
|
assert.doesNotMatch(popup.status.style_class, /jarvis-state-/);
|
|
assert.equal(popup.talk.label, 'Listening');
|
|
popup.setVoiceStatus({ tts: false, input: false, wake: false });
|
|
assert.match(popup.status.text, /Mic off/);
|
|
assert.match(popup.statusLine.text, /mic off/);
|
|
assert.equal(popup.talk.label, 'Listening');
|
|
assert.equal(popup.talk.reactive, false);
|
|
popup.setVoiceStatus({ tts: false, input: false, wake: false, muted: true });
|
|
assert.equal(popup.status.text, 'Muted');
|
|
assert.match(popup.statusLine.text, /muted/);
|
|
assert.equal(popup.talk.reactive, false);
|
|
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();
|
|
const session = new SessionPanel();
|
|
const name = { visible: true };
|
|
const bar = { children: [], add_child(child) { this.children.push(child); } };
|
|
osd.attach(bar, name);
|
|
session.attach();
|
|
assert.equal(bar.children[0], osd.root);
|
|
osd.setState('LISTENING');
|
|
assert.equal(osd.root.visible, true);
|
|
assert.equal(name.visible, false);
|
|
assert.match(osd.label.text, /Listening/);
|
|
assert.equal(session.root.visible, false);
|
|
osd.setState('ARMED');
|
|
assert.equal(osd.root.visible, false);
|
|
assert.equal(name.visible, true);
|
|
osd.setState('SPEAKING');
|
|
assert.equal(osd.root.visible, true);
|
|
assert.equal(name.visible, false);
|
|
assert.match(osd.label.text, /Speaking/);
|
|
assert.equal(timers.size, 0);
|
|
osd.setState('THINKING');
|
|
assert.match(osd.label.text, /Thinking/);
|
|
assert.equal(name.visible, false);
|
|
osd.setState('SLEEPING');
|
|
assert.match(osd.label.text, /Privacy/);
|
|
osd.showWake();
|
|
assert.match(osd.label.text, /Wake/);
|
|
assert.equal(timers.size, 1);
|
|
osd.destroy();
|
|
assert.equal(timers.size, 0);
|
|
assert.equal(chrome.includes(osd.root), false);
|
|
});
|
|
|
|
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.addStep('{"action":"revoke"}');
|
|
assert.equal(cu.root.visible, false);
|
|
cu.setTarget('{"rect":[1,2,3,4]}');
|
|
cu.addStep('{"action":"grant"}');
|
|
assert.equal(cu.step.visible, false);
|
|
assert.equal(cu.root.visible, true);
|
|
cu.hide();
|
|
assert.equal(cu.root.visible, false);
|
|
});
|
|
|
|
test('ConfirmationRequired chips answer Confirm with job and tool ids', () => {
|
|
const { ConversationView } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
const answers = [];
|
|
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('truncated fetch JSON previews status and URL, not HTML', () => {
|
|
const { ConversationView } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
popup.addToolCall('{"name":"web_fetch"}');
|
|
popup.addToolResult(JSON.stringify({
|
|
name: 'web_fetch',
|
|
result: '{"status":200,"url":"https://cage.report/CAGE/8GPQ2","text":"<!DOCTYPE htmlhtml lang=\\"en\\"><headmeta',
|
|
}));
|
|
assert.match(popup.transcript.children[0].text, /200 https:\/\/cage\.report/);
|
|
assert.doesNotMatch(popup.transcript.children[0].text, /DOCTYPE|headmeta/);
|
|
});
|
|
|
|
test('search results preview titles instead of raw JSON', () => {
|
|
const { ConversationView } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
popup.addToolCall('{"name":"web_search"}');
|
|
popup.addToolResult(JSON.stringify({
|
|
name: 'web_search',
|
|
result: JSON.stringify([{ url: 'https://whatismyipaddress.com/ip-lookup', title: 'IP Lookup' }]),
|
|
}));
|
|
assert.match(popup.transcript.children[0].text, /IP Lookup/);
|
|
assert.doesNotMatch(popup.transcript.children[0].text, /uddg|whatismyipaddress/);
|
|
});
|
|
|
|
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[0].text, /Linux 6\.8/);
|
|
});
|
|
|
|
test('errors and reset failures are one-line notices, not chat rows', () => {
|
|
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('desktop grant and extra Settings stay in preferences, not the tray menu', () => {
|
|
const settings = readFileSync(new URL('../apps/gnome-extension/[email protected]/settings-window.js', import.meta.url), 'utf8');
|
|
const access = readFileSync(new URL('../apps/gnome-extension/[email protected]/access-settings.js', import.meta.url), 'utf8');
|
|
const extension = readFileSync(new URL('../apps/gnome-extension/[email protected]/extension.js', import.meta.url), 'utf8');
|
|
assert.match(settings, /accessSettingsPage/);
|
|
assert.match(access, /Save & Allow/);
|
|
assert.match(access, /ComputerGrant', '\(b\)', \[false\]/);
|
|
assert.match(access, /ComputerRevoke/);
|
|
assert.match(access, /WebcamGrant/);
|
|
assert.match(access, /WebcamRevoke/);
|
|
assert.match(access, /Complete any GNOME prompt/);
|
|
assert.doesNotMatch(extension, /ComputerGrant|WebcamGrant|Save & Allow/);
|
|
const editor = readFileSync(new URL('../apps/gnome-extension/[email protected]/settings-editor.js', import.meta.url), 'utf8');
|
|
assert.match(editor, /Video\/Source/);
|
|
const { ConversationView } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
assert.equal(popup.settings.label, 'Settings');
|
|
});
|
|
|
|
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, listening, and mute 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.onSettings = () => calls.push('settings');
|
|
popup.onListen = () => calls.push('listen');
|
|
popup.onMute = (muted) => calls.push(muted ? 'mute' : 'unmute');
|
|
popup.stop.handlers.clicked();
|
|
popup.reset.handlers.clicked();
|
|
popup.settings.handlers.clicked();
|
|
popup.talk.handlers.clicked();
|
|
popup.mute.handlers.clicked();
|
|
assert.deepEqual(calls, ['stop', 'reset', 'settings', 'listen', 'mute']);
|
|
assert.equal(popup.stop.handlers['button-press-event'], undefined);
|
|
assert.equal(popup.settings.handlers['button-press-event'], undefined);
|
|
assert.equal(popup.talk.handlers['notify::pressed'], undefined);
|
|
assert.equal(popup.mute.label, 'Mute');
|
|
popup.setMuted(true);
|
|
assert.equal(popup.mute.label, 'Muted');
|
|
assert.match(popup.statusLine.text, /muted/);
|
|
popup.setState('LISTENING');
|
|
assert.equal(popup.talk.reactive, false);
|
|
assert.equal(popup.talk.visible, true);
|
|
popup.showTab('thinking');
|
|
assert.equal(popup.mute.visible, true);
|
|
assert.equal(popup.talk.visible, true);
|
|
});
|
|
|
|
test('closing the popup does not stop listening', () => {
|
|
const { ConversationView } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
const talks = [];
|
|
popup.onTalk = (pressed) => talks.push(pressed);
|
|
popup.onListen = () => talks.push('listen');
|
|
popup.endTalk();
|
|
assert.deepEqual(talks, []);
|
|
});
|
|
|
|
test('Chat and Thinking tabs exist and thinking auto-follows', () => {
|
|
const { ConversationView } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
const session = new ConversationView({ compact: false });
|
|
assert.equal(popup.thinkingPane.clip_to_allocation, true);
|
|
assert.equal(popup.thinkingPane.height, 228);
|
|
assert.equal(popup.thinkingPane.children[0], popup.thinkingScroll);
|
|
assert.equal(popup.thinkingScroll.overlay_scrollbars, false);
|
|
assert.equal(popup.thinkingScroll.vscrollbar_policy, 1);
|
|
assert.equal(popup.thinkingScroll.clip_to_allocation, true);
|
|
assert.equal(popup.thinkingScroll.enable_mouse_scrolling, true);
|
|
assert.equal(session.thinkingPane.height, 268);
|
|
for (const view of [popup, session]) {
|
|
assert.equal(view.chatTab.label, 'Chat');
|
|
assert.equal(view.thinkTab.label, 'Thinking');
|
|
assert.equal(view.thinkingBox.children[0], view.thinking);
|
|
assert.equal(view.thinkingScroll.children[0], view.thinkingBox);
|
|
assert.equal(view.thinkingPane.visible, false);
|
|
assert.equal(view.thinkingScroll.visible, false);
|
|
assert.equal(view.scroll.visible, true);
|
|
view.setState('THINKING');
|
|
view.updateThinking('Considering the lookup. ');
|
|
assert.equal(view._tab, 'thinking');
|
|
assert.equal(view.thinkingPane.visible, true);
|
|
assert.equal(view.thinkingScroll.visible, true);
|
|
assert.equal(view.scroll.visible, false);
|
|
assert.match(view.thinking.text, /Considering the lookup/);
|
|
assert.equal(view.thinking.clutter_text.ellipsize, 0);
|
|
assert.equal(view.thinking.clutter_text.line_wrap, true);
|
|
assert.ok(view.thinking.clutter_text.width >= 240);
|
|
assert.equal(view.thinkingScroll.vadjustment.value, 160);
|
|
view.token('Latest reply token');
|
|
view.scroll.vadjustment.value = 0;
|
|
view.setState('SPEAKING');
|
|
assert.equal(view._tab, 'chat');
|
|
assert.equal(view.scroll.visible, true);
|
|
assert.equal(view.thinkingPane.visible, false);
|
|
assert.equal(view.thinkingScroll.visible, false);
|
|
assert.equal(view.scroll.vadjustment.value, 160);
|
|
view.showTab('chat', { user: true });
|
|
view.updateThinking('still thinking');
|
|
assert.equal(view._tab, 'thinking');
|
|
assert.equal(view.thinkingPane.visible, true);
|
|
assert.equal(view.thinkingScroll.visible, true);
|
|
assert.match(view.thinking.text, /still thinking/);
|
|
view.thinkingScroll.vadjustment.value = 0;
|
|
view.showTab('thinking', { user: true });
|
|
assert.equal(view._tab, 'thinking');
|
|
assert.equal(view.thinkingPane.visible, true);
|
|
assert.equal(view.thinkingScroll.visible, true);
|
|
assert.equal(view.thinkingScroll.vadjustment.value, 160);
|
|
}
|
|
});
|
|
|
|
test('tool calls are activity rows, not spoken Jarvis replies', () => {
|
|
const { ConversationView } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
popup.addToolCall('{"name":"web_fetch"}');
|
|
assert.equal(popup.transcript.children.length, 1);
|
|
assert.match(popup.transcript.children[0].style_class, /jarvis-row-tool/);
|
|
assert.equal(popup.transcript.children[0].text, 'web fetch');
|
|
assert.doesNotMatch(popup.transcript.children[0].text, /^J\s/);
|
|
assert.doesNotMatch(popup.transcript.children[0].style_class, /jarvis-row-jarvis/);
|
|
popup.addToolResult(JSON.stringify({ name: 'web_fetch', result: { error: 'timed out after 12000ms', url: 'https://ifconfig.me/ip' } }));
|
|
assert.equal(popup.transcript.children.length, 1);
|
|
assert.match(popup.transcript.children[0].text, /web fetch · timed out/);
|
|
});
|
|
|
|
test('status line describes the live THINKING step', () => {
|
|
const { ConversationView } = harness();
|
|
const popup = new ConversationView({ compact: true });
|
|
popup.setState('THINKING');
|
|
assert.equal(popup.statusLine.text, 'Thinking');
|
|
popup.addToolCall('{"name":"web_fetch"}');
|
|
assert.equal(popup.statusLine.text, 'Using web fetch');
|
|
popup.setState('LISTENING');
|
|
assert.equal(popup.statusLine.text, 'Listening');
|
|
});
|
|
|
|
test('both settings entry points use the shared preferences window', () => {
|
|
const prefs = readFileSync(new URL('../apps/gnome-extension/[email protected]/prefs.js', import.meta.url), 'utf8');
|
|
const control = readFileSync(new URL('../apps/control-center/main.js', import.meta.url), 'utf8');
|
|
assert.match(prefs, /fillSettingsWindow/);
|
|
assert.match(control, /fillSettingsWindow/);
|
|
});
|
|
|
|
test('the Shell helper can capture a screenshot from inside GNOME', () => {
|
|
const source = readFileSync(new URL('../apps/gnome-extension/[email protected]/shell-dbus.js', import.meta.url), 'utf8');
|
|
assert.match(source, /method name="Screenshot"/);
|
|
assert.match(source, /new Shell\.Screenshot/);
|
|
assert.match(source, /screenshot\(false, stream\)/);
|
|
});
|
|
|
|
for (const compact of [true, false]) {
|
|
test(`inference follows alternating content despite polling and manual navigation (${compact})`, () => {
|
|
const { ConversationView } = harness();
|
|
const view = new ConversationView({ compact });
|
|
view.setState('THINKING');
|
|
for (let i = 0; i < 12; i++) {
|
|
view.updateThinking(`reason ${i}. `);
|
|
assert.equal(view._tab, 'thinking');
|
|
view.token(`answer ${i}. `);
|
|
assert.equal(view._tab, 'chat');
|
|
view.setState('THINKING');
|
|
assert.equal(view._tab, 'chat');
|
|
view.showTab('thinking', { user: true });
|
|
view.addToolCall('{"name":"lookup"}');
|
|
assert.equal(view._tab, 'chat');
|
|
view.updateThinking('checking. ');
|
|
view.addToolResult('{"name":"lookup","result":"done"}');
|
|
assert.equal(view._tab, 'chat');
|
|
}
|
|
assert.match(view.thinking.text, /reason 0.*reason 11/);
|
|
view.updateThinking('finished');
|
|
view.finalizeReply('Final answer');
|
|
assert.equal(view._tab, 'chat');
|
|
assert.match(view.transcript.get_last_child().text, /Final answer/);
|
|
});
|
|
|
|
test(`follow survives delayed layout, scrolling, remapping and teardown (${compact})`, () => {
|
|
const { ConversationView, timers } = harness();
|
|
const view = new ConversationView({ compact });
|
|
view.setState('THINKING');
|
|
for (const pane of [view.scroll, view.thinkingScroll]) {
|
|
const adjustment = pane.vadjustment;
|
|
adjustment.value = 0;
|
|
adjustment.emit('notify::value');
|
|
view.token('reply');
|
|
view.updateThinking('reason');
|
|
adjustment.upper = 1000;
|
|
adjustment.emit('notify::upper');
|
|
assert.equal(adjustment.value, 920);
|
|
adjustment.page_size = 200;
|
|
adjustment.emit('notify::page-size');
|
|
assert.equal(adjustment.value, 800);
|
|
adjustment.value = 0;
|
|
pane.mapped = true;
|
|
pane.handlers['notify::mapped']();
|
|
assert.equal(adjustment.value, 800);
|
|
}
|
|
for (let i = 0; i < 100; i++) view.token(' more');
|
|
assert.equal(timers.size, 2);
|
|
for (const [id, callback] of [...timers]) { timers.delete(id); callback(); }
|
|
assert.equal(timers.size, 0);
|
|
view.updateThinking('again');
|
|
view.token('done');
|
|
view.destroy();
|
|
assert.equal(timers.size, 0);
|
|
});
|
|
}
|