service-manager.test.mjs 5.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. import {test} from 'node:test'
  2. import assert from 'node:assert/strict'
  3. import {createServer} from 'node:http'
  4. import {dataKey,DesktopServiceManager} from './service-manager.mjs'
  5. import {mkdtemp,realpath,writeFile,rm} from 'node:fs/promises'
  6. import {tmpdir} from 'node:os'
  7. import {join} from 'node:path'
  8. test('checks recorded and bundled versions while permitting a separately owned older service',async()=>{
  9. const root='/fixture/desktop',record={protocol:1,instanceId:'fixture',pid:1234,dataKey:dataKey(root),version:'0.3.32',token:'fixture'}
  10. let actual={...record};delete actual.token
  11. const server=createServer((request,response)=>{response.setHeader('Content-Type','application/json');response.end(JSON.stringify(actual))})
  12. await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve))
  13. record.url=`http://127.0.0.1:${server.address().port}`
  14. const manager=new DesktopServiceManager({version:'0.3.33'});manager.root=root
  15. try{
  16. assert.equal((await manager.verify(record)).version,'0.3.32')
  17. actual={...actual,version:'0.3.31'}
  18. await assert.rejects(manager.verify(record),/身份/)
  19. actual={...actual,version:'0.3.32'};manager.child={pid:record.pid}
  20. await assert.rejects(manager.verify(record),/版本不匹配/)
  21. }finally{await new Promise(resolve=>server.close(resolve))}
  22. })
  23. test('busy synchronization connects the verified existing Web and remains retryable without accepting other errors',async()=>{
  24. const home=await realpath(await mkdtemp(join(tmpdir(),'yaoyao-startup-busy-')))
  25. const record={protocol:1,instanceId:'busy-fixture',pid:process.pid,dataKey:dataKey(home),version:'0.3.31',token:'fixture'}
  26. const server=createServer((request,response)=>{const {token,...identity}=record;response.setHeader('Content-Type','application/json');response.end(JSON.stringify(identity))})
  27. await new Promise(done=>server.listen(0,'127.0.0.1',done))
  28. record.url=`http://127.0.0.1:${server.address().port}`
  29. await writeFile(join(home,'service-instance.json'),JSON.stringify(record))
  30. let attempts=0
  31. const options={home,version:'0.3.32',fork:()=>{throw new Error('must not fork')},synchronize:async()=>{if(++attempts===1)throw Object.assign(new Error('已有任务,同步待完成'),{code:'service_busy'})}}
  32. const manager=new DesktopServiceManager(options)
  33. try{
  34. const state=await manager.start()
  35. assert.equal(state.phase,'ready');assert.equal(state.external,true);assert.match(state.updateNotice,/同步待完成/)
  36. assert.equal((await manager.retrySynchronization()).updateNotice,undefined);assert.equal(attempts,2)
  37. await manager.stop()
  38. assert.equal(manager.state.phase,'disconnected')
  39. assert.doesNotMatch(manager.state.message,/服务已停止/)
  40. assert.equal((await manager.reconnect()).phase,'ready')
  41. assert.equal(manager.state.pid,process.pid)
  42. await assert.rejects(manager.stopBackground(),/不由本 App 管理/)
  43. assert.equal(manager.state.phase,'error')
  44. assert.equal((await manager.reconnect()).phase,'ready')
  45. const invalid=new DesktopServiceManager({...options,synchronize:async()=>{throw new Error('invalid package')}})
  46. await assert.rejects(invalid.start(),/invalid package/)
  47. assert.equal(invalid.state.phase,'error')
  48. }finally{await new Promise(done=>server.close(done));await rm(home,{recursive:true,force:true})}
  49. })
  50. test('publishes stopped only after the native service manager confirms shutdown',async()=>{
  51. let finish,fail=false
  52. const manager=new DesktopServiceManager({stopBackground:()=>fail?Promise.reject(new Error('still alive')):new Promise(resolve=>{finish=resolve})})
  53. const stopping=manager.stopBackground()
  54. await new Promise(resolve=>setImmediate(resolve))
  55. assert.equal(manager.state.phase,'stopping')
  56. finish();await stopping
  57. assert.equal(manager.state.phase,'stopped')
  58. fail=true;await assert.rejects(manager.stopBackground(),/still alive/)
  59. assert.equal(manager.state.phase,'error')
  60. assert.doesNotMatch(manager.state.message,/服务已停止/)
  61. })
  62. test('a late activate health check cannot reconnect after quit has begun',async()=>{
  63. const manager=new DesktopServiceManager({})
  64. manager.state={phase:'ready'}
  65. let finish,starts=0
  66. manager.check=()=>new Promise(resolve=>{finish=resolve})
  67. manager.start=async()=>{starts++}
  68. const reconnect=manager.reconnect()
  69. await manager.stop();finish();await reconnect
  70. assert.equal(starts,0);assert.equal(manager.state.phase,'disconnected')
  71. })
  72. test('unknown same-version builds offer an explicit one-shot overwrite without changing ordinary retries',async()=>{
  73. const home=await realpath(await mkdtemp(join(tmpdir(),'yaoyao-force-sync-'))),attempts=[]
  74. const manager=new DesktopServiceManager({home,version:'0.4.18',synchronize:async(_progress,options)=>{
  75. attempts.push(options.force)
  76. if(attempts.length===1)throw Object.assign(new Error('同版本构建需要选择'),{code:'service_build_unknown'})
  77. }})
  78. manager.readRecord=async()=>({url:'http://127.0.0.1:1'})
  79. manager.verify=async()=>({url:'http://127.0.0.1:1',version:'0.4.18',pid:process.pid})
  80. try{
  81. await assert.rejects(manager.start(),/需要选择/)
  82. assert.equal(manager.state.canForceSync,true);assert.equal(manager.canForceSynchronization,true)
  83. await manager.retrySynchronization({force:true});assert.equal(manager.state.phase,'ready')
  84. await manager.retrySynchronization();assert.deepEqual(attempts,[false,true,false])
  85. manager.state={phase:'error',message:'签名校验失败'}
  86. await assert.rejects(manager.retrySynchronization({force:true}),/当前状态不能覆盖/)
  87. }finally{await rm(home,{recursive:true,force:true})}
  88. })