| 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 |