browserConfigurationUpgrade.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
  1. // @vitest-environment node
  2. import {afterEach,expect,it,vi} from 'vitest'
  3. import {EventEmitter} from 'node:events'
  4. import {mkdtemp,readFile,writeFile,rm} from 'node:fs/promises'
  5. import {randomUUID,randomBytes,createCipheriv,createDecipheriv} from 'node:crypto'
  6. import {tmpdir} from 'node:os'
  7. import {join} from 'node:path'
  8. import {parseRunnerConfiguration} from '../../src/runner/config.js'
  9. import {RunnerAgent} from '../../src/runner/agent.js'
  10. import type {RunnerConfiguration} from '../../src/shared/runner.js'
  11. import {DesktopRunnerManager} from '../../desktop/runner-manager.mjs'
  12. const browserConstructed=vi.hoisted(()=>vi.fn())
  13. vi.mock('../../src/runner/managedBrowser.js',()=>({RunnerBrowser:class {
  14. available=false
  15. constructor(config:unknown){browserConstructed(config)}
  16. async disconnect(){}async shutdown(){}async sweep(){}
  17. }}))
  18. const base=()=>({protocol:1 as const,runnerId:randomUUID(),token:randomBytes(32).toString('base64url'),serverURL:'https://fixture.invalid',hermesURL:'http://127.0.0.1:9119',allowedProfiles:['default'],artifactRoots:[]})
  19. afterEach(()=>vi.clearAllMocks())
  20. it.each([undefined,{}, {enabled:false,maxSessions:2,idleTimeoutMs:120000}])('revalidates an old encrypted desktop configuration on restart and import (%j)',async browser=>{
  21. const home=await mkdtemp(join(tmpdir(),'yaoyao-browser-config-upgrade-')),key=randomBytes(32),children:any[]=[]
  22. const encrypt=(value:string)=>{const iv=randomBytes(12),cipher=createCipheriv('aes-256-gcm',key,iv),bytes=Buffer.concat([cipher.update(value),cipher.final()]);return Buffer.concat([iv,cipher.getAuthTag(),bytes])}
  23. const decrypt=(bytes:Buffer)=>{const cipher=createDecipheriv('aes-256-gcm',key,bytes.subarray(0,12));cipher.setAuthTag(bytes.subarray(12,28));return Buffer.concat([cipher.update(bytes.subarray(28)),cipher.final()]).toString()}
  24. const manager=new DesktopRunnerManager({home,encrypt,decrypt,validate:parseRunnerConfiguration,fork:()=>{
  25. const child=Object.assign(new EventEmitter(),{messages:[] as any[],postMessage(value:any){this.messages.push(value);if(value.type==='shutdown')queueMicrotask(()=>this.emit('exit',0))},kill(){queueMicrotask(()=>this.emit('exit',0));return true}})
  26. children.push(child);queueMicrotask(()=>child.emit('spawn'));return child
  27. }})
  28. const config={...base(),...(browser?{browser}:{})},encrypted=encrypt(JSON.stringify(config)),expected=browser?.enabled===false?browser:{enabled:true}
  29. try{
  30. // Existing bytes are the pre-upgrade settings; start must not rewrite them.
  31. await writeFile(manager.path,encrypted,{mode:0o600});await manager.start()
  32. expect(children[0].messages.find((item:any)=>item.type==='configure').config.browser).toEqual(expected)
  33. expect(await readFile(manager.path)).toEqual(encrypted)
  34. await manager.stop();await manager.start()
  35. expect(children[1].messages.find((item:any)=>item.type==='configure').config.browser).toEqual(expected)
  36. const file=join(home,'runner.json');await writeFile(file,JSON.stringify(config),{mode:0o600})
  37. await manager.importFile(file)
  38. expect(children[2].messages.find((item:any)=>item.type==='configure').config.browser).toEqual(expected)
  39. expect((await readFile(manager.path)).includes(config.token)).toBe(false)
  40. }finally{await manager.stop();await rm(home,{recursive:true,force:true})}
  41. })
  42. it.each([undefined,{}, {enabled:false}])('advertises setup for direct/embedded old configurations unless explicitly disabled (%j)',async browser=>{
  43. const home=await mkdtemp(join(tmpdir(),'yaoyao-browser-agent-upgrade-'))
  44. const fetchImpl=vi.fn(async()=>Response.json({commands:[],epoch:randomUUID()}))
  45. const runner=new RunnerAgent({...base(),...(browser?{browser}:{})} as RunnerConfiguration,home,fetchImpl as typeof fetch)
  46. try{
  47. await (runner as any).api('poll')
  48. const headers=new Headers(fetchImpl.mock.calls[0]![1]?.headers)
  49. const features=headers.get('x-runner-features')!.split(',')
  50. expect(features.includes('managed-browser-setup-v1')).toBe(browser?.enabled!==false)
  51. expect(features.includes('managed-browser-disabled-v1')).toBe(browser?.enabled===false)
  52. expect(features).not.toContain('managed-browser-v1')
  53. expect(features).not.toContain('local-vm-v1')
  54. if(browser?.enabled===false)expect(browserConstructed).not.toHaveBeenCalled()
  55. else expect(browserConstructed).toHaveBeenCalledWith({enabled:true})
  56. }finally{const stop=new AbortController();stop.abort();await runner.run(stop.signal);await rm(home,{recursive:true,force:true})}
  57. })