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/jarvis@qvac.local/ui.js', import.meta.url), 'utf8'); const extensionSource = readFileSync(new URL('../apps/gnome-extension/jarvis@qvac.local/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":" { 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/jarvis@qvac.local/settings-window.js', import.meta.url), 'utf8'); assert.match(settings, /Allow now/); assert.match(settings, /ComputerGrant', '\(b\)', \[false\]/); assert.match(settings, /ComputerRevoke/); assert.match(settings, /WebcamGrant/); assert.match(settings, /WebcamRevoke/); assert.match(settings, /Grant requested. Jarvis can use the camera/); assert.match(settings, /Choose a screen in the GNOME prompt/); const editor = readFileSync(new URL('../apps/gnome-extension/jarvis@qvac.local/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/jarvis@qvac.local/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/jarvis@qvac.local/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); }); }