|
|
@@ -8,7 +8,7 @@ import Koa from 'koa'
|
|
|
import {bodyParser} from '@koa/bodyparser'
|
|
|
import request from 'supertest'
|
|
|
import {WorkspaceStore} from '../../src/server/workspaceStore'
|
|
|
-import {DesktopEnvironments} from '../../src/server/desktopEnvironments'
|
|
|
+import {DesktopEnvironments,DESKTOP_ENVIRONMENT_TOOLS,DESKTOP_FILE_TOOL_IDS} from '../../src/server/desktopEnvironments'
|
|
|
import type {WorkspaceAgent} from '../../src/shared/workspace'
|
|
|
import {saveHostTools} from '../../src/server/hostToolSettings'
|
|
|
import {HttpError} from '../../src/server/errors'
|
|
|
@@ -132,6 +132,78 @@ it('passes the authorize scope through to the desktop host',async()=>{
|
|
|
await request(app()).post(base()+'/desktop-environment/authorize').send({scope:'full'}).expect(200)
|
|
|
expect(seen.find(c=>c.operation==='authorize')?.scope).toBe('full')
|
|
|
})
|
|
|
+it('copies files between the explicitly named hosts and server without chat attachments or model payloads',async()=>{
|
|
|
+ const bytes=Buffer.alloc(300_000);for(let i=0;i<bytes.length;i++)bytes[i]=i%256
|
|
|
+ const seen:Array<{host:string;action:any}>=[]
|
|
|
+ const handle=(id:string)=>(c:any)=>{seen.push({host:id,action:c.action});return c.action.op==='read'?{path:c.action.path,size:bytes.length,data:bytes.toString('base64')}:{path:c.action.path,size:Buffer.from(c.action.data,'base64').length,sha256:createHash('sha256').update(Buffer.from(c.action.data,'base64')).digest('hex')}}
|
|
|
+ host.full=[ownerKey('owner')]
|
|
|
+ drive(handle('server'))
|
|
|
+ driveRemote(HOST_A,{...host,id:randomUUID(),name:'mac1'},handle('mac1'))
|
|
|
+ driveRemote(HOST_B,{...host,id:randomUUID(),name:'mac2'},handle('mac2'))
|
|
|
+ for(const [sourceHost,targetHost,targetId] of [['mac1','mac2',HOST_B],['mac1','server','local'],['server','本机',HOST_A]]){
|
|
|
+ const result=await service.call('owner',agent.id,'desktop_file_copy',{sourceHost,sourcePath:'Desktop/a.txt',targetHost,targetPath:'Desktop/a.txt'},new AbortController().signal,()=>{},'local',service.hostEpochs('owner',agent),HOST_A)
|
|
|
+ expect(result).toMatchObject({copied:true,target:{host:targetId,path:'Desktop/a.txt'},size:bytes.length,sha256:createHash('sha256').update(bytes).digest('hex')})
|
|
|
+ expect(JSON.stringify(result).length).toBeLessThan(700)
|
|
|
+ }
|
|
|
+ expect(seen.map(({host,action})=>[host,action.op])).toEqual([['mac1','read'],['mac2','receive'],['mac1','read'],['server','receive'],['server','read'],['mac1','receive']])
|
|
|
+ for(const {action} of seen.filter(item=>item.action.op==='receive')){expect(action.overwrite).toBe(false);expect(Buffer.from(action.data,'base64')).toEqual(bytes)}
|
|
|
+ expect(store.list('owner','file')).toEqual([])
|
|
|
+ expect(DESKTOP_FILE_TOOL_IDS.has('desktop_file_copy')).toBe(true)
|
|
|
+ expect(DESKTOP_ENVIRONMENT_TOOLS.some(tool=>tool.id==='desktop_file_copy')).toBe(true)
|
|
|
+})
|
|
|
+it('serializes opposing transfers without deadlocking or changing the target',async()=>{
|
|
|
+ host.full=[ownerKey('owner')]
|
|
|
+ const handle=(c:any)=>c.action.op==='read'?{size:1,data:'YQ=='}:{path:c.action.path,size:1,sha256:c.action.sha256}
|
|
|
+ driveRemote(HOST_A,{...host,name:'mac1'},handle);driveRemote(HOST_B,{...host,id:randomUUID(),name:'mac2'},handle)
|
|
|
+ const copy=(sourceHost:string,targetHost:string)=>service.call('owner',agent.id,'desktop_file_copy',{sourceHost,sourcePath:'Desktop/a',targetHost,targetPath:'Desktop/b'},new AbortController().signal,()=>{},'local',service.hostEpochs('owner',agent))
|
|
|
+ const values=await Promise.all([copy('mac1','mac2'),copy('mac2','mac1'),copy('mac1','mac1')])
|
|
|
+ expect(values.map(value=>value.target.host)).toEqual([HOST_B,HOST_A,HOST_A])
|
|
|
+})
|
|
|
+it('requires both host grants and never falls back for missing or ambiguous hosts',async()=>{
|
|
|
+ host.full=[ownerKey('owner')];drive()
|
|
|
+ driveRemote(HOST_A,{...host,id:randomUUID(),name:'mac1',full:[]})
|
|
|
+ const copy=(sourceHost:string,targetHost:string)=>service.call('owner',agent.id,'desktop_file_copy',{sourceHost,sourcePath:'Desktop/a',targetHost,targetPath:'Desktop/a'},new AbortController().signal,()=>{},'local',service.hostEpochs('owner',agent))
|
|
|
+ await expect(copy('server','mac1')).rejects.toMatchObject({code:'desktop_full_required'})
|
|
|
+ await expect(copy('mac1','server')).rejects.toMatchObject({code:'desktop_full_required'})
|
|
|
+ await expect(copy('server','本机')).rejects.toMatchObject({code:'desktop_source_required'})
|
|
|
+ await expect(copy('missing','server')).rejects.toMatchObject({code:'desktop_host_unknown'})
|
|
|
+ driveRemote(HOST_B,{...host,id:randomUUID(),name:'mac1'})
|
|
|
+ await expect(copy('mac1','server')).rejects.toMatchObject({code:'desktop_host_ambiguous'})
|
|
|
+})
|
|
|
+it.each(['disconnect','cancel','revoke','corrupt','oversized'])('does not write the target when the source transfer is %s',async failure=>{
|
|
|
+ host.full=[ownerKey('owner')];const controller=new AbortController(),writes:any[]=[]
|
|
|
+ driveRemote(HOST_A,{...host,name:'mac1'},()=>{
|
|
|
+ if(failure==='disconnect')service.dropHost(HOST_B)
|
|
|
+ if(failure==='cancel')controller.abort()
|
|
|
+ if(failure==='revoke')version++
|
|
|
+ return {size:failure==='oversized'?10*1024*1024+1:failure==='corrupt'?2:1,data:'YQ=='}
|
|
|
+ })
|
|
|
+ driveRemote(HOST_B,{...host,id:randomUUID(),name:'mac2'},c=>{writes.push(c);return {}})
|
|
|
+ await expect(service.call('owner',agent.id,'desktop_file_copy',{sourceHost:'mac1',sourcePath:'Desktop/a',targetHost:'mac2',targetPath:'Desktop/a'},controller.signal,()=>{},'local',service.hostEpochs('owner',agent))).rejects.toThrow()
|
|
|
+ expect(writes).toEqual([])
|
|
|
+})
|
|
|
+it('reports a failed destination receipt without claiming success or retrying',async()=>{
|
|
|
+ host.full=[ownerKey('owner')];const receives:any[]=[]
|
|
|
+ driveRemote(HOST_A,{...host,name:'mac1'},()=>({size:0,data:''}))
|
|
|
+ driveRemote(HOST_B,{...host,id:randomUUID(),name:'mac2'},c=>{receives.push(c);return {path:c.action.path,size:0,sha256:'wrong'}})
|
|
|
+ await expect(service.call('owner',agent.id,'desktop_file_copy',{sourceHost:'mac1',sourcePath:'Desktop/a',targetHost:'mac2',targetPath:'Desktop/a',overwrite:true},new AbortController().signal,()=>{},'local',service.hostEpochs('owner',agent))).rejects.toMatchObject({code:'desktop_transfer_unconfirmed'})
|
|
|
+ expect(receives).toHaveLength(1);expect(receives[0].action.overwrite).toBe(true)
|
|
|
+})
|
|
|
+it('waits for a target taken over during the source read and resumes after giveback',async()=>{
|
|
|
+ host.full=[ownerKey('owner')]
|
|
|
+ const source={...host,name:'mac1'},target={...host,id:randomUUID(),name:'mac2'},writes:any[]=[]
|
|
|
+ service.remoteExchange(HOST_A,{host:source,results:[]})
|
|
|
+ driveRemote(HOST_B,target,c=>{if(c.operation!=='file')return {ok:true};writes.push(c);return {path:c.action.path,size:1,sha256:c.action.sha256}})
|
|
|
+ const copying=service.call('owner',agent.id,'desktop_file_copy',{sourceHost:'mac1',sourcePath:'Desktop/a',targetHost:'mac2',targetPath:'Desktop/a'},new AbortController().signal,()=>{},'local',service.hostEpochs('owner',agent))
|
|
|
+ let read:any
|
|
|
+ await vi.waitFor(()=>{read=service.remoteExchange(HOST_A,{host:source,results:[]}).commands[0];expect(read).toBeDefined()})
|
|
|
+ const query='?backend=desktop&host='+HOST_B
|
|
|
+ const ticket=(await request(app()).post(base()+'/computer/take'+query).send({requestId:randomUUID()}).expect(200)).body
|
|
|
+ service.remoteExchange(HOST_A,{host:source,results:[{id:read.id,value:{size:1,data:'YQ=='}}]})
|
|
|
+ await new Promise(resolve=>setTimeout(resolve,180));expect(writes).toEqual([])
|
|
|
+ await request(app()).post(base()+'/computer/giveback'+query).send({controlId:ticket.controlId,token:ticket.token,notes:'已交还 mac2'}).expect(200)
|
|
|
+ expect(await copying).toMatchObject({copied:true,handoffNotes:['mac2:已交还 mac2']});expect(writes).toHaveLength(1)
|
|
|
+})
|
|
|
it('routes parallel desktop tools to the host named in the call',async()=>{
|
|
|
agent=store.updateAgent('owner',agent.id,{computer:'local',envs:{desktop:true,cloud:true}})
|
|
|
const study={...host,name:'书房 iMac'},living={...host,id:randomUUID(),name:'客厅 Mac mini'}
|