workspaceTeamTools.live.test.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. // @vitest-environment node
  2. import { expect, it, vi } from 'vitest'
  3. import { mkdtempSync, rmSync } from 'node:fs'
  4. import { tmpdir } from 'node:os'
  5. import { join } from 'node:path'
  6. import { randomUUID } from 'node:crypto'
  7. import request from 'supertest'
  8. import {createServer, type Server} from 'node:http'
  9. import {RunnerAgent} from '../../src/runner/agent'
  10. import { createApplication } from '../../src/server/app'
  11. import { loadServerConfig } from '../../src/server/config'
  12. import type { WorkspaceAgent, WorkspaceConversation, WorkspaceMessage, WorkspaceRun } from '../../src/shared/workspace'
  13. // This opt-in test only accepts the isolated real-Hermes wire fixture, never a user service.
  14. const url = process.env.HERMES_TEAM_WIRE_URL
  15. it.skipIf(!url)('uses actual Hermes auth, sessions and native plugin tools to create a member, team and task', async () => {
  16. const base = new URL(url!)
  17. expect(base.hostname).toBe('127.0.0.1')
  18. const marker = await fetch(new URL('/api/yaoyao-verification-fixture',base))
  19. expect(marker.ok).toBe(true)
  20. const proof = await marker.json()
  21. expect(proof).toMatchObject({fixture:true,kind:'hermes-tool-bridge'})
  22. const home=mkdtempSync(join(tmpdir(),'yaoyao-team-wire-client-'))
  23. let runnerServer:Server|undefined,runnerDone:Promise<void>|undefined
  24. const runnerAbort=new AbortController()
  25. const app=createApplication({config:loadServerConfig({HERMES_YAOYAO_HOME:home,HERMES_YAOYAO_UPSTREAM:url!,HERMES_YAOYAO_UPSTREAM_USERNAME:'fixture',HERMES_YAOYAO_UPSTREAM_PASSWORD:'yaoyao-fixture-password'})})
  26. try {
  27. const bootstrap=await request(app.app.callback()).get('/api/app/bootstrap').set('Host','127.0.0.1:15300').expect(200)
  28. const setup=await request(app.app.callback()).post('/api/app/setup').set('Host','127.0.0.1:15300').set('Origin','http://127.0.0.1:15300')
  29. .set('Cookie',(bootstrap.headers['set-cookie'] as unknown as string[]).map(c=>c.split(';')[0]).join('; '))
  30. .set('X-CSRF-Token',bootstrap.body.csrfToken).send({username:'team-fixture',password:'team-fixture-password'}).expect(200)
  31. const owner=setup.body.user.id as string
  32. if(process.env.HERMES_TEAM_RUNNER==='1') {
  33. runnerServer=createServer(app.app.callback())
  34. await new Promise<void>(resolve=>runnerServer!.listen(0,'127.0.0.1',resolve))
  35. app.config.port=(runnerServer.address() as {port:number}).port
  36. const registered=app.runners.enroll(owner,{name:'真实协议 Runner',allowedProfiles:['default']})
  37. const runner=new RunnerAgent({protocol:1,serverURL:`http://127.0.0.1:${(runnerServer.address() as {port:number}).port}`,runnerId:registered.runner.id,token:registered.token,hermesURL:url!,allowedProfiles:['default'],artifactRoots:[],hermesCredentials:{username:'fixture',password:'yaoyao-fixture-password'}},home)
  38. runnerDone=runner.run(runnerAbort.signal);void runnerDone.catch(()=>{})
  39. await expect.poll(()=>app.runners.summary(app.runners.records()[0]!).online).toBe(true)
  40. }
  41. const manager=app.workspace.createAgent(owner,{name:'组队老板',profile:'default',canManageTeam:true})
  42. await app.workspaceRuntime.teamTools.requireAvailable(owner,manager)
  43. const direct=app.workspace.list<WorkspaceConversation>(owner,'conversation').find(c=>c.memberIds[0]===manager.id)!
  44. const invoke=async(name:string,args:unknown,allowed=true,conversation=direct)=>{
  45. const run=app.workspaceRuntime.send(owner,conversation.id,{requestId:randomUUID(),content:'wire_call:'+JSON.stringify({name,arguments:args})})
  46. await expect.poll(()=>app.workspace.require<WorkspaceRun>(owner,'run',run.id).status,{timeout:30_000}).toMatch(/complete|failed/)
  47. const current=app.workspace.require<WorkspaceRun>(owner,'run',run.id)
  48. const messages=app.workspace.messages(owner,conversation.id).filter(m=>m.runId===run.id && m.role==='assistant')
  49. expect(current.status,JSON.stringify(messages.map(m=>({status:m.status,content:m.content,error:m.error})))).toBe('complete')
  50. const report=JSON.parse(messages.at(-1)!.content)
  51. expect(report.actual_plugin_registry).toBe(true)
  52. expect(report.ok,report.error).toBe(allowed)
  53. if(allowed)expect(report.calls[0].native_name).toMatch(/^yaoyao_workspace_/)
  54. return messages
  55. }
  56. const requestId=randomUUID()
  57. await invoke('workspace_create_agent',{requestId,name:'实际工具成员',profile:'default',instructions:'执行分配任务并提交结果'})
  58. const member=app.workspace.list<WorkspaceAgent>(owner,'agent').find(a=>a.name==='实际工具成员')!
  59. expect(member).toBeDefined();expect(member.canManageTeam).toBe(false)
  60. await invoke('workspace_create_agent',{requestId,name:'实际工具成员',profile:'default',instructions:'执行分配任务并提交结果'})
  61. expect(app.workspace.list(owner,'agent')).toHaveLength(2)
  62. await invoke('workspace_create_team',{requestId:randomUUID(),name:'真实工具团队',memberIds:[member.id]})
  63. const team=app.workspace.list<WorkspaceConversation>(owner,'conversation').find(c=>c.kind==='group')!
  64. expect(team).toMatchObject({administratorId:manager.id,memberIds:[manager.id,member.id]})
  65. await invoke('workspace_start_team_task',{requestId:randomUUID(),teamId:team.id,title:'实际启动验证',content:'核验团队可用工具。\ncatalog'})
  66. await expect.poll(()=>app.workspace.list<WorkspaceRun>(owner,'run').filter(r=>r.conversationId===team.id).map(r=>r.status),{timeout:30_000}).toEqual(['complete'])
  67. const task=app.workspace.tasks(owner,team.id)[0]!
  68. expect(task.title).toBe('实际启动验证')
  69. expect(app.workspace.list<WorkspaceMessage>(owner,'message').some(m=>m.conversationId===team.id && m.role==='assistant' && m.content)).toBe(true)
  70. await invoke('workspace_get_team_task',{teamId:team.id,taskId:task.id})
  71. await invoke('workspace_assign_task',{requestId:randomUUID(),goalId:task.id,agentId:member.id,title:'核验执行权限',brief:'检查并报告当前执行环境。'})
  72. await expect.poll(()=>app.workspaceRuntime.tasks.assignments(owner,task.id)[0]?.status,{timeout:30_000}).toBe('review')
  73. const assignment=app.workspaceRuntime.tasks.assignments(owner,task.id)[0]!
  74. expect(JSON.parse(assignment.result!).tool_count).toBe(0)
  75. await invoke('workspace_review_assignment',{requestId:randomUUID(),assignmentId:assignment.id,decision:'accept',review:'已核对成员未继承组队工具。'})
  76. await invoke('workspace_finish_team_task',{requestId:randomUUID(),goalId:task.id,status:'complete',result:'团队执行权限核验完成。',checks:[{criterion:0,passed:true,evidence:'成员真实原生工具目录未获得组队权限,执行结果已经返回。'}]})
  77. await expect.poll(()=>app.workspace.tasks(owner,team.id)[0]?.goal?.status).toBe('complete')
  78. expect(app.workspace.messages(owner,direct.id).some(m=>m.taskReference?.taskId===task.id && m.role==='system')).toBe(true)
  79. app.workspace.updateAgent(owner,manager.id,{canManageTeam:false})
  80. await invoke('workspace_create_agent',{requestId:randomUUID(),name:'关闭后不应创建',profile:'default'},false)
  81. expect(app.workspace.list(owner,'agent')).toHaveLength(2)
  82. // Hold both real native invocations open together to catch shared-registry
  83. // name collisions between simultaneous leases of the same logical tools.
  84. app.workspace.updateAgent(owner,manager.id,{canManageTeam:true})
  85. const peer=app.workspace.createAgent(owner,{name:'第二位老板',profile:'default',canManageTeam:true})
  86. const peerDirect=app.workspace.list<WorkspaceConversation>(owner,'conversation').find(c=>c.kind==='direct' && c.memberIds[0]===peer.id)!
  87. const execute=app.workspaceRuntime.teamTools.call.bind(app.workspaceRuntime.teamTools)
  88. let entered=0, release!:()=>void
  89. const gate=new Promise<void>(resolve=>{release=resolve})
  90. const deadline=setTimeout(()=>release(),5000)
  91. const spy=vi.spyOn(app.workspaceRuntime.teamTools,'call').mockImplementation(async(...args)=>{
  92. if(args[2]==='workspace_list_agents'){entered++;if(entered===2)release();await gate}
  93. return execute(...args)
  94. })
  95. try {
  96. const [a,b]=await Promise.all([invoke('workspace_list_agents',{}),invoke('workspace_list_agents',{},true,peerDirect)])
  97. expect(entered).toBe(2)
  98. expect(JSON.parse(a.at(-1)!.content).calls[0].native_name).not.toBe(JSON.parse(b.at(-1)!.content).calls[0].native_name)
  99. } finally { clearTimeout(deadline);spy.mockRestore() }
  100. } finally {
  101. runnerAbort.abort();await runnerDone?.catch(()=>{})
  102. if(runnerServer)await new Promise<void>(resolve=>{runnerServer!.close(()=>resolve());runnerServer!.closeAllConnections()})
  103. app.close()
  104. // Let asynchronous loopback lease unbinds finish before removing the fixture store.
  105. await new Promise(resolve=>setTimeout(resolve,100))
  106. rmSync(home,{recursive:true,force:true})
  107. }
  108. },120_000)