返回 CodeWhale
app-targeting.test.mjs
根目录 / crates / tui / plugins / computer-use / tests / app-targeting.test.mjs
1 import {test,after} from 'node:test';
2 import assert from 'node:assert/strict';
3 import fs from 'node:fs';
4 import os from 'node:os';
5 import path from 'node:path';
6 import {fileURLToPath} from 'node:url';
7 import {spawnSync} from 'node:child_process';
8 import {handle,closeAllSessions} from '../src/app-handler.mjs';
9
10 const root=fileURLToPath(new URL('..',import.meta.url));
11 const dir=fs.mkdtempSync(path.join(os.tmpdir(),'cu-app-targeting-'));
12 const binary=path.join(dir,'Contents','MacOS','accessibility');
13 const log=path.join(dir,'native.jsonl');
14 const saved={};
15 for(const key of ['CODEWHALE_CU_TEST_BACKEND','CODEWHALE_CU_APP_BUNDLE','CODEWHALE_CU_RECORDINGS_DIR','CU_TARGETING_CALLS','CU_TARGETING_NATIVE']) saved[key]=process.env[key];
16 process.env.CODEWHALE_CU_TEST_BACKEND=path.join(root,'tests/fixtures/darwin-targeting-backend.mjs');
17 process.env.CODEWHALE_CU_APP_BUNDLE=dir;
18 process.env.CODEWHALE_CU_RECORDINGS_DIR=path.join(dir,'recordings');
19 process.env.CU_TARGETING_CALLS=log;
20 delete process.env.CU_TARGETING_NATIVE;
21 fs.mkdirSync(path.dirname(binary),{recursive:true});fs.writeFileSync(binary,'');
22 after(async()=>{
23 await closeAllSessions();
24 for(const [key,value] of Object.entries(saved)) if(value===undefined) delete process.env[key]; else process.env[key]=value;
25 fs.rmSync(dir,{recursive:true,force:true});
26 });
27 const call=(tool,args)=>handle({tool,args},{sessionId:'targeting-fixture'});
28 const calls=()=>fs.readFileSync(log,'utf8').trim().split('\n').map(JSON.parse);
29
30 test('app-handler forwards public list_windows arguments through the real Darwin backend',async()=>{
31 const ref={pid:123,name:'Fixture',bundle_id:'test.fixture'};
32 const reply=await call('list_windows',{app_ref:ref});
33 assert.equal(reply.ok,true,JSON.stringify(reply));
34 assert.deepEqual(calls().at(-1).args.app_ref,ref);
35 await call('list_windows',{});
36 assert.equal(Object.hasOwn(calls().at(-1).args,'app_ref'),false,'omission must reach the resolver as omission');
37 for(const tool of ['get_app_state','resolve_element']) {
38 await call(tool,{});
39 assert.equal(Object.hasOwn(calls().at(-1).args,'app_ref'),false,`${tool} must not synthesize an empty reference`);
40 }
41 });
42
43 test('explicit null screenshot target reaches app resolution before capture',async()=>{
44 const reply=await call('screenshot',{app_ref:null});
45 assert.equal(reply.ok,false,'the fixture refuses any attempt to run a capture command');
46 assert.equal(calls().at(-1).tool,'window_info');
47 assert.equal(calls().at(-1).args.app_ref,null);
48 });
49
50 test('real handler/backend/native resolver never redirects an explicit app reference to frontmost',{skip:process.platform!=='darwin'},async()=>{
51 const build=spawnSync('clang',['-fobjc-arc','-Os','-framework','Cocoa','-framework','ApplicationServices','-framework','ScreenCaptureKit','-framework','AVFoundation','-framework','CoreMedia','-framework','Vision',path.join(root,'tests/fixtures/darwin-targeting.m'),'-o',binary],{encoding:'utf8'});
52 assert.equal(build.status,0,build.stderr);
53 const guarded=spawnSync(binary,[JSON.stringify({tool:'inspect_pointer_guard',args:{lock_dir:dir}})],{encoding:'utf8'});
54 assert.equal(guarded.status,0,guarded.stderr);
55 const guard=JSON.parse(guarded.stdout);
56 assert.match(guard.refusal,/foreground changed to Other/);
57 assert.equal(guard.posts,0,'a stale foreground binding cannot post a global mouse gesture');
58 assert.equal(guard.activations,0,'a pointer gesture cannot reclaim the user foreground');
59 process.env.CU_TARGETING_NATIVE='1';
60 try {
61 const implicit=await call('list_windows',{});
62 assert.equal(implicit.ok,true,JSON.stringify(implicit));assert.equal(implicit.data.pid,999);
63 for(const app_ref of [{pid:123},{name:'fixture'},{bundle_id:'test.fixture'},{pid:123,name:'Fixture',bundle_id:'test.fixture'}]) {
64 const reply=await call('list_windows',{app_ref});
65 assert.equal(reply.ok,true,JSON.stringify(reply));assert.equal(reply.data.pid,123);
66 }
67 const invalid=[null,{},[],false,'Fixture',123,{app_ref:{pid:123}},{unexpected:123},{pid:123,unexpected:true},{pid:null},{pid:true},{pid:'123'},{pid:0},{pid:-1},{pid:1.5},{pid:4294967419},{name:''},{name:' \n'},{name:123},{bundle_id:[]},{bundle_id:''},{pid:123,name:null},{pid:321},{name:'missing'},{bundle_id:'missing'},{pid:123,bundle_id:'test.other'}];
68 for(const tool of ['list_windows','get_app_state','resolve_element','screenshot']) {
69 for(const app_ref of invalid) {
70 const reply=await call(tool,{app_ref});
71 assert.equal(reply.ok,false,`${tool} must reject ${JSON.stringify(app_ref)}`);
72 assert.match(reply.error.message,/application not found/);
73 }
74 }
75 } finally { delete process.env.CU_TARGETING_NATIVE; }
76 });
77
77 lines Plain Text