Files
gnome-jarvis/test/review-regressions.test.js
T
snxraven 0e52942b8b
Rolling release / release (push) Successful in 8m58s
Better Search
2026-09-12 12:33:52 -04:00

171 lines
9.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { mkdtemp, mkdir, writeFile, readFile, symlink, rm } from 'node:fs/promises';
import os, { tmpdir } from 'node:os';
import path from 'node:path';
import { createRequire } from 'node:module';
import { ComputerUseSession } from '../computer-use/session.js';
import { ComputerActuator } from '../computer-use/actuator.js';
import { PortalInputBackend } from '../computer-use/portal-input.js';
import { createComputerObserveTools } from '../skills/computer-observe.js';
import { createPhase2Tools, filesystemRoots } from '../skills/phase2-tools.js';
import { VoiceStateMachine } from '../daemon/voice-state.js';
import { assertLocalEndpoint } from '../daemon/network-policy.js';
const require = createRequire(import.meta.url);
const custom = require('../vendor/agent-harness/agent/custom-tools.js');
const sandbox = require('../vendor/agent-harness/agent/sandbox.js');
test('custom tool permissions survive registration and default to confirmation', () => {
const id = 'review-permissions';
try {
custom.register(id, ['read', 'write', 'dangerous', 'computer-use', undefined].map((permission, i) => ({ name: `review_${i}`, permission })));
assert.equal(custom.needsPermission(id, 'review_0', 'ask'), false);
for (let i = 1; i < 5; i++) {
assert.equal(custom.needsPermission(id, `review_${i}`, 'ask'), true);
assert.equal(custom.needsPermission(id, `review_${i}`, 'always-approve'), false);
}
custom.unregister(id, 'review_1');
assert.equal(custom.needsPermission(id, 'review_1', 'ask'), false);
} finally { custom.clear(id); }
});
test('filesystem access lets path tools reach home while workspace stays jailed', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'jarvis-fs-access-'));
const workspace = path.join(dir, 'workspace'); await mkdir(workspace);
const homeProbe = await mkdtemp(path.join(os.homedir(), '.jarvis-fs-test-'));
try {
await writeFile(path.join(homeProbe, 'note.txt'), 'from-home');
const jailed = Object.fromEntries(createPhase2Tools({ cwd: workspace, roots: filesystemRoots('workspace', workspace) }).map(t => [t.name, t]));
await assert.rejects(jailed.fs_read.execute({ file: path.join(homeProbe, 'note.txt') }), /outside/);
const opened = Object.fromEntries(createPhase2Tools({ cwd: workspace, roots: filesystemRoots('filesystem', workspace) }).map(t => [t.name, t]));
assert.equal(await opened.fs_read.execute({ file: path.join(homeProbe, 'note.txt') }), 'from-home');
const home = Object.fromEntries(createPhase2Tools({ cwd: workspace, roots: filesystemRoots('home', workspace) }).map(t => [t.name, t]));
assert.equal(await home.fs_read.execute({ file: path.join(homeProbe, 'note.txt') }), 'from-home');
sandbox.setGrants({ roots: [path.resolve(workspace)] });
assert.equal(sandbox.isAllowed('jarvis-qvac', path.join(homeProbe, 'note.txt')), false);
sandbox.setGrants({ roots: [path.resolve(workspace), '/'] });
assert.equal(sandbox.isAllowed('jarvis-qvac', path.join(homeProbe, 'note.txt')), true);
} finally {
await rm(homeProbe, { recursive: true, force: true });
await rm(dir, { recursive: true, force: true });
}
});
test('workspace tools reject outside roots and symlink escapes and count UTF-8 bytes', async () => {
const dir = await mkdtemp(path.join(tmpdir(), 'jarvis-path-test-'));
const root = path.join(dir, 'workspace'); await mkdir(root);
const outside = path.join(dir, 'outside'); await mkdir(outside);
await writeFile(path.join(outside, 'secret'), 'private');
await symlink(outside, path.join(root, 'link'));
await symlink(path.join(outside, 'missing'), path.join(root, 'dangling'));
const tools = Object.fromEntries(createPhase2Tools({ cwd: root }).map(t => [t.name, t]));
await assert.rejects(tools.fs_read.execute({ file: 'link/secret' }), /outside/);
await assert.rejects(tools.fs_write.execute({ file: 'link/new', contents: 'x', confirmed: true }), /outside/);
await assert.rejects(tools.fs_write.execute({ file: 'dangling', contents: 'x', confirmed: true }), /symlink/);
await assert.rejects(tools.fs_search.execute({ root: outside, query: 'secret' }), /outside/);
const result = await tools.fs_write.execute({ file: 'new', contents: 'é', confirmed: true });
assert.equal(result.bytes, 2);
assert.equal(await readFile(path.join(root, 'new'), 'utf8'), 'é');
});
test('coordinate click uses portal input even when accessibility actions exist', async () => {
const session = new ComputerUseSession(); session.grant(); const sent = [];
const actuator = new ComputerActuator({ session, input: { send: a => sent.push(a) }, atspiAction: () => assert.fail('unexpected semantic click'), sleep: async () => {} });
await actuator.click({ x: 20, y: 30 });
assert.equal(sent[0].x, 20);
});
test('stale semantic refs fail before input and revocation during preview prevents action', async () => {
const session = new ComputerUseSession(); session.grant();
const actuator = new ComputerActuator({ session, find: async () => [], input: { send: () => assert.fail('unexpected input') }, highlight: async () => session.revoke() });
await assert.rejects(actuator.type({ ref: 'missing', text: 'secret' }), /stale/);
await assert.rejects(actuator.click({ x: 1, y: 1 }), /inactive/);
});
test('desktop observation does not wait for a second Allow after Grant desktop', () => {
const id = 'cu-observe-permission';
try {
custom.register(id, createComputerObserveTools({ computer: { status: () => ({ active: true }) }, observer: {} }));
assert.equal(custom.needsPermission(id, 'cu_observe', 'ask'), false);
} finally { custom.clear(id); }
});
test('expired grants prevent desktop observation', async () => {
let now = 0; const computer = new ComputerUseSession({ clock: () => now }); computer.grant(); now = 180001;
const [observe] = createComputerObserveTools({ computer, observer: { observe: () => assert.fail('expired observation') } });
await assert.rejects(observe.execute(), /grant/);
});
function helper() {
const child = new EventEmitter();
child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.stdin = new EventEmitter(); child.stdin.writable = true; child.stdin.write = () => {};
child.kill = () => { child.killed = true; };
return child;
}
test('portal helper reports a notify fallback as ready', async () => {
const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child });
const grant = input.grant();
child.stdout.emit('data', '{"type":"ready","restore_token_present":true,"backend":"portal-notify"}\n');
assert.equal((await grant).backend, 'portal-notify');
});
test('portal helper returns a PipeWire frame without ending the grant', async () => {
const child = helper(); let written = '';
child.stdin.write = (chunk) => { written += String(chunk); };
const input = new PortalInputBackend({ spawnImpl: () => child });
const grant = input.grant();
child.stdout.emit('data', '{"type":"ready","backend":"portal-ei","screen":true}\n');
assert.equal((await grant).screen, true);
const frame = input.captureFrame('/tmp/pw.png');
assert.match(written, /"type":"frame"/);
child.stdout.emit('data', '{"type":"frame","path":"/tmp/pw.png","source":"pipewire"}\n');
assert.equal(await frame, '/tmp/pw.png');
assert.equal(input.available, true);
});
test('a PipeWire frame error does not revoke desktop access', async () => {
const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child });
const grant = input.grant();
child.stdout.emit('data', '{"type":"ready","backend":"portal-notify","screen":false}\n');
await grant;
const frame = input.captureFrame('/tmp/pw.png');
child.stdout.emit('data', '{"type":"error","reason":"no PipeWire frame yet"}\n');
await assert.rejects(frame, /no PipeWire frame/);
assert.equal(input.available, true);
});
test('portal grants wait for a complete readiness message and clear on exit', async () => {
const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child });
const grant = input.grant();
child.stdout.emit('data', '{"type":"rea'); assert.equal(input.available, false);
child.stdout.emit('data', 'dy","restore_token_present":false}\n');
assert.equal((await grant).backend, 'portal-ei');
child.emit('close', 0); assert.equal(input.available, false);
assert.throws(() => input.send({}), /unavailable/);
});
test('revoked portal grants cannot become ready later', async () => {
const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child });
const grant = input.grant(); input.revoke();
child.stdout.emit('data', '{"type":"ready"}\n');
await assert.rejects(grant, /revoked/); assert.equal(input.available, false); assert.equal(child.killed, true);
});
test('portal helper errors reject and allow a new grant', async () => {
const child = helper(); const input = new PortalInputBackend({ spawnImpl: () => child });
const grant = input.grant(); child.stdout.emit('data', '{"type":"error","reason":"denied"}\n');
await assert.rejects(grant, /denied/); assert.equal(input.process, null);
});
test('idle timer never attempts to sleep a thinking or speaking voice', () => {
let now = 0; const voice = new VoiceStateMachine({ now: () => now, idleMs: 10 });
voice.typedUtterance(); now = 100; assert.equal(voice.expireIdle(), 'THINKING');
voice.speak(); now = 200; assert.equal(voice.expireIdle(), 'SPEAKING');
});
test('local endpoint policy accepts IPv6 loopback', () => {
assert.equal(assertLocalEndpoint('http://[::1]:8080').hostname, '[::1]');
});