Files
gnome-jarvis/test/computer-functional.test.js
T
snxraven a097adf4eb
Rolling release / release (push) Successful in 8m31s
Updates
2026-09-13 22:24:14 -04:00

103 lines
6.1 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { EventEmitter } from 'node:events';
import { PortalInputBackend } from '../computer-use/portal-input.js';
import { ComputerUseSession } from '../computer-use/session.js';
import { ComputerActuator } from '../computer-use/actuator.js';
import { DesktopObserver } from '../computer-use/observer.js';
function setup(options = {}) {
const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.stdin = new EventEmitter(); child.stdin.writable = true;
const sent = []; child.stdin.write = s => sent.push(JSON.parse(s)); child.kill = () => {};
const input = new PortalInputBackend({ spawnImpl: () => child, ...options });
const grant = input.grant(); child.stdout.emit('data', '{"type":"ready","screen":true,"backend":"portal-ei"}\n');
return { child, input, sent, grant, event: value => child.stdout.emit('data', JSON.stringify(value) + '\n') };
}
test('input awaits a matching ack, propagates errors, and rejects pending work on revoke', async () => {
const {input,grant,event,sent} = setup(); await grant;
const first = input.send({type:'keyboard',action:'key',combo:'home'});
event({type:'ack',id:sent[0].id+100});
assert.equal(input._pending.size,1);
event({type:'error',id:sent[0].id,reason:'device paused'});
await assert.rejects(first,/device paused/);
const second=input.send({type:'pointer',action:'move',x:1,y:1});
event({type:'ack',id:sent[1].id,ok:true}); assert.equal((await second).ok,true);
const third=input.send({type:'keyboard',action:'key',combo:'end'});
input.revoke(); await assert.rejects(third,/revoked/);
});
test('missing input acknowledgment revokes the helper instead of claiming success', async () => {
const {input,grant}=setup({actionTimeoutMs:20});await grant;
await assert.rejects(input.send({}),/timed out/);assert.equal(input.available,false);
});
test('old helper exit cannot cancel a newer grant or its commands', async () => {
const children=[];
const spawnImpl=()=>{const c=new EventEmitter();c.stdout=new EventEmitter();c.stderr=new EventEmitter();c.stdin=new EventEmitter();c.stdin.writable=true;c.stdin.write=()=>{};c.kill=()=>{};children.push(c);return c;};
const input=new PortalInputBackend({spawnImpl});
let g=input.grant();children[0].stdout.emit('data','{"type":"ready"}\n');await g;input.revoke();
g=input.grant();children[1].stdout.emit('data','{"type":"ready"}\n');await g;
children[0].emit('close');assert.equal(input.available,true);input.revoke();
});
test('actuator waits for actual input completion and refuses a revoke during readiness', async () => {
const session=new ComputerUseSession();session.grant();let sent=false;
const actuator=new ComputerActuator({session,input:{ready:async()=>session.revoke(),send:()=>{sent=true;}},sleep:async()=>{}});
await assert.rejects(actuator.click({x:1,y:1}),/inactive/);assert.equal(sent,false);
session.grant();actuator.input={send:async()=>{throw Error('injection failed');}};
await assert.rejects(actuator.click({x:1,y:1}),/injection failed/);
await assert.rejects(actuator.click({x:NaN,y:1}),/finite/);
});
test('observer maps Wayland surface bounds to desktop and never reuses old refs',async()=>{
const observer=new DesktopObserver({atspi:{tree:async()=>[{name:'Target',role:'button',pid:2,rect:[46,113,640,34],window_rect:[0,0,732,649]}]},shell:{windows:async()=>({windows:[{pid:2,focused:true,rect:[2540,242,680,597],buffer_rect:[2514,219,732,649]}]})}});
const first=await observer.tree();assert.deepEqual(first[0].rect,[2560,332,640,34]);
const second=await observer.tree();assert.notEqual(second[0].ref,first[0].ref);
});
test('native fallback preserves shortcuts, Unicode composition, stream ids and both scroll axes', async () => {
const {spawnSync}=await import('node:child_process');
const run=spawnSync('python3',['-c',`
import sys
sys.path.insert(0, 'computer-use/py')
import portal_remote_desktop as p
calls=[]
p.notify=lambda method, signature, values: calls.append((method,values))
n=p.PortalNotify('/session',94,(1920,0))
n.handle({'type':'pointer','action':'click','x':2000,'y':300})
assert calls[0] == ('NotifyPointerMotionAbsolute',('/session',{},94,80.0,300.0))
calls.clear()
n.handle({'type':'keyboard','action':'key','combo':'ctrl+l'})
assert [x[1][-2:] for x in calls] == [(29,1),(38,1),(38,0),(29,0)]
calls.clear()
n.handle({'type':'keyboard','action':'type','text':'✓'})
assert calls[0][0] == 'NotifyKeyboardKeycode' and calls[0][1][-2:] == (29,1)
assert any(c[0] == 'NotifyKeyboardKeysym' and c[1][-2] == ord('7') for c in calls)
calls.clear()
n.handle({'type':'pointer','action':'scroll','x':2000,'y':300,'dx':1,'dy':2})
assert calls[-2][1][-2:] == (0,2) and calls[-1][1][-2:] == (1,1)
from libei_sender import LibeiSender, _lib
sender=object.__new__(LibeiSender)
taps=[]
sender._tap=lambda key,mods=(): taps.append((key,mods))
sender.key_combo('ctrl+left')
assert taps == [(105,(29,))]
try: sender.key_combo('invalid')
except ValueError: pass
else: raise AssertionError('invalid key silently accepted')
assert _lib().ei_seat_bind_capabilities.argtypes
`],{encoding:'utf8'});
assert.equal(run.status,0,run.stderr);
});
test('frame normalization reports native and scaled dimensions', async () => {
const {spawnSync}=await import('node:child_process');
const {mkdtemp,rm}=await import('node:fs/promises');
const {FrameNormalizer}=await import('../computer-use/frame.js');
const dir=await mkdtemp('/tmp/jarvis-frame-test-');
try {
const made=spawnSync('python3',['-c','from PIL import Image; import sys; Image.new("RGB",(1600,900),"red").save(sys.argv[1])',dir+'/input.png'],{encoding:'utf8'});
assert.equal(made.status,0,made.error?.message || made.stderr);
const frame=await new FrameNormalizer().normalize(dir+'/input.png',dir+'/output.webp');
assert.equal(frame.source_width,1600);assert.equal(frame.width,1280);assert.equal(frame.height,720);
assert.equal(frame.mime,'image/webp');
const jpeg=await new FrameNormalizer().normalize(dir+'/input.png',dir+'/output.jpg');
assert.equal(jpeg.mime,'image/jpeg');assert.equal(jpeg.width,1280);
} finally {await rm(dir,{recursive:true,force:true});}
});