workspaceTeamTools.live.test.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  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 { createApplication } from '../../src/server/app'
  9. import { loadServerConfig } from '../../src/server/config'
  10. import type { WorkspaceAgent, WorkspaceConversation, WorkspaceMessage, WorkspaceRun } from '../../src/shared/workspace'
  11. // This opt-in test only accepts the isolated real-Hermes wire fixture, never a user service.
  12. const url = process.env.HERMES_TEAM_WIRE_URL
  13. it.skipIf(!url)('uses actual Hermes auth, sessions and native plugin tools to create a member, team and task', async () => {
  14. const base = new URL(url!)
  15. expect(base.hostname).toBe('127.0.0.1')
  16. const marker = await fetch(new URL('/api/yaoyao-verification-fixture',base))
  17. expect(marker.ok).toBe(true)
  18. const proof = await marker.json()
  19. expect(proof).toMatchObject({fixture:true,kind:'hermes-tool-bridge'})
  20. const home=mkdtempSync(join(tmpdir(),'yaoyao-team-wire-client-'))
  21. 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'})})
  22. try {
  23. const bootstrap=await request(app.app.callback()).get('/api/app/bootstrap').set('Host','127.0.0.1:15300').expect(200)
  24. 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')
  25. .set('Cookie',(bootstrap.headers['set-cookie'] as unknown as string[]).map(c=>c.split(';')[0]).join('; '))
  26. .set('X-CSRF-Token',bootstrap.body.csrfToken).send({username:'team-fixture',password:'team-fixture-password'}).expect(200)
  27. const owner=setup.body.user.id as string
  28. const manager=app.workspace.createAgent(owner,{name:'组队老板',profile:'default',canManageTeam:true})
  29. await app.workspaceRuntime.teamTools.requireAvailable(owner,manager)
  30. const direct=app.workspace.list<WorkspaceConversation>(owner,'conversation').find(c=>c.memberIds[0]===manager.id)!
  31. const invoke=async(name:string,args:unknown,allowed=true,conversation=direct)=>{
  32. const run=app.workspaceRuntime.send(owner,conversation.id,{requestId:randomUUID(),content:'wire_call:'+JSON.stringify({name,arguments:args})})
  33. await expect.poll(()=>app.workspace.require<WorkspaceRun>(owner,'run',run.id).status,{timeout:30_000}).toMatch(/complete|failed/)
  34. const current=app.workspace.require<WorkspaceRun>(owner,'run',run.id)
  35. const messages=app.workspace.messages(owner,conversation.id).filter(m=>m.runId===run.id && m.role==='assistant')
  36. expect(current.status,JSON.stringify(messages.map(m=>({status:m.status,content:m.content,error:m.error})))).toBe('complete')
  37. const report=JSON.parse(messages.at(-1)!.content)
  38. expect(report.actual_plugin_registry).toBe(true)
  39. expect(report.ok,report.error).toBe(allowed)
  40. if(allowed)expect(report.calls[0].native_name).toMatch(/^yaoyao_workspace_/)
  41. return messages
  42. }
  43. const requestId=randomUUID()
  44. await invoke('workspace_create_agent',{requestId,name:'实际工具成员',profile:'default',instructions:'执行分配任务并提交结果'})
  45. const member=app.workspace.list<WorkspaceAgent>(owner,'agent').find(a=>a.name==='实际工具成员')!
  46. expect(member).toBeDefined();expect(member.canManageTeam).toBe(false)
  47. await invoke('workspace_create_agent',{requestId,name:'实际工具成员',profile:'default',instructions:'执行分配任务并提交结果'})
  48. expect(app.workspace.list(owner,'agent')).toHaveLength(2)
  49. await invoke('workspace_create_team',{requestId:randomUUID(),name:'真实工具团队',memberIds:[member.id]})
  50. const team=app.workspace.list<WorkspaceConversation>(owner,'conversation').find(c=>c.kind==='group')!
  51. expect(team).toMatchObject({administratorId:manager.id,memberIds:[manager.id,member.id]})
  52. await invoke('workspace_start_team_task',{requestId:randomUUID(),teamId:team.id,title:'实际启动验证',content:'核验团队可用工具。\ncatalog'})
  53. await expect.poll(()=>app.workspace.list<WorkspaceRun>(owner,'run').filter(r=>r.conversationId===team.id).map(r=>r.status),{timeout:30_000}).toEqual(['complete'])
  54. const task=app.workspace.tasks(owner,team.id)[0]!
  55. expect(task.title).toBe('实际启动验证')
  56. expect(app.workspace.list<WorkspaceMessage>(owner,'message').some(m=>m.conversationId===team.id && m.role==='assistant' && m.content)).toBe(true)
  57. await invoke('workspace_get_team_task',{teamId:team.id,taskId:task.id})
  58. app.workspace.updateAgent(owner,manager.id,{canManageTeam:false})
  59. await invoke('workspace_create_agent',{requestId:randomUUID(),name:'关闭后不应创建',profile:'default'},false)
  60. expect(app.workspace.list(owner,'agent')).toHaveLength(2)
  61. // Hold both real native invocations open together to catch shared-registry
  62. // name collisions between simultaneous leases of the same logical tools.
  63. app.workspace.updateAgent(owner,manager.id,{canManageTeam:true})
  64. const peer=app.workspace.createAgent(owner,{name:'第二位老板',profile:'default',canManageTeam:true})
  65. const peerDirect=app.workspace.list<WorkspaceConversation>(owner,'conversation').find(c=>c.kind==='direct' && c.memberIds[0]===peer.id)!
  66. const execute=app.workspaceRuntime.teamTools.call.bind(app.workspaceRuntime.teamTools)
  67. let entered=0, release!:()=>void
  68. const gate=new Promise<void>(resolve=>{release=resolve})
  69. const deadline=setTimeout(()=>release(),5000)
  70. const spy=vi.spyOn(app.workspaceRuntime.teamTools,'call').mockImplementation(async(...args)=>{
  71. if(args[2]==='workspace_list_agents'){entered++;if(entered===2)release();await gate}
  72. return execute(...args)
  73. })
  74. try {
  75. const [a,b]=await Promise.all([invoke('workspace_list_agents',{}),invoke('workspace_list_agents',{},true,peerDirect)])
  76. expect(entered).toBe(2)
  77. expect(JSON.parse(a.at(-1)!.content).calls[0].native_name).not.toBe(JSON.parse(b.at(-1)!.content).calls[0].native_name)
  78. } finally { clearTimeout(deadline);spy.mockRestore() }
  79. } finally {
  80. app.close()
  81. // Let asynchronous loopback lease unbinds finish before removing the fixture store.
  82. await new Promise(resolve=>setTimeout(resolve,100))
  83. rmSync(home,{recursive:true,force:true})
  84. }
  85. },120_000)