103 lines
6.1 KiB
JavaScript
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});}
|
|
});
|