Procházet zdrojové kódy

补充子账号机器人创建成员的权限回归测试

Samien před 1 měsícem
rodič
revize
911d4af698
1 změnil soubory, kde provedl 45 přidání a 0 odebrání
  1. 45 0
      tests/server/subaccountPermissions.test.ts

+ 45 - 0
tests/server/subaccountPermissions.test.ts

@@ -8,6 +8,7 @@ import request from 'supertest'
 import { createApplication, type ApplicationRuntime } from '../../src/server/app'
 import { loadServerConfig } from '../../src/server/config'
 import { LocalAuthStore } from '../../src/server/localAuth'
+import type { Work } from '../../src/server/workspaceScheduler'
 
 let runtime: ApplicationRuntime, home: string, admin: ReturnType<typeof request.agent>, child: ReturnType<typeof request.agent>
 let adminCSRF: string, childCSRF: string, childID: string, upstreamPaths: string[]
@@ -44,6 +45,50 @@ beforeEach(async () => {
 afterEach(() => { runtime.close(); rmSync(home, { recursive: true, force: true }); vi.restoreAllMocks() })
 
 describe('subaccount Bot-only security boundary with real login sessions', () => {
+  async function startBotTurn() {
+    const agent = (await call(child, 'post', '/api/app/agents', childCSRF).send({ name: '子账号负责人', profile: 'allowed' }).expect(201)).body.agent
+    const conversation = (await call(child, 'get', '/api/app/conversations').expect(200)).body.conversations[0]
+    await call(child, 'post', `/api/app/conversations/${conversation.id}/messages`, childCSRF)
+      .send({ requestId: randomUUID(), content: '帮我创建一个长期研究助手' }).expect(202)
+    const work = runtime.workspace.list<Work>(childID, 'turn')[0]!
+    // Simulate the grant normally issued after a healthy Hermes bridge handshake.
+    runtime.workspace.put(childID, 'turn', work.id, { ...work, status: 'running', teamManagementRevision: agent.revision })
+    return { agent, work }
+  }
+
+  it('lets a subaccount Bot create persistent Bots from its assigned source with owner isolation and idempotency', async () => {
+    const { agent, work } = await startBotTurn()
+    const tools = runtime.workspaceRuntime.teamTools
+    expect(tools.catalog(childID, work.id).map(tool => tool.name)).toContain('workspace_create_agent')
+    const sources = await tools.call(childID, work.id, 'workspace_list_sources', {})
+    expect(sources).toMatchObject({ sources: [{ nodeId: 'local', profile: 'allowed' }] })
+    const input = { requestId: randomUUID(), name: '研究助手', profile: 'allowed', instructions: '核对资料并提供来源' }
+    const result = await tools.call(childID, work.id, 'workspace_create_agent', input)
+    expect(await tools.call(childID, work.id, 'workspace_create_agent', input)).toEqual(result)
+    const agents = (await call(child, 'get', '/api/app/agents').expect(200)).body.agents
+    expect(agents).toHaveLength(2)
+    expect(agents.find((item: any) => item.name === input.name)).toMatchObject({
+      profile: 'allowed', createdByAgentId: agent.id, createdFromRunId: work.runId, canManageTeam: true,
+    })
+    expect((await call(child, 'get', '/api/app/conversations').expect(200)).body.conversations).toHaveLength(2)
+    expect((await call(admin, 'get', '/api/app/agents').expect(200)).body.agents).toEqual([])
+  })
+
+  it('rejects unassigned sources and revokes a subaccount Bot creation grant after reassignment', async () => {
+    const { work } = await startBotTurn()
+    const tools = runtime.workspaceRuntime.teamTools
+    for (const source of [{ profile: 'private' }, { profile: 'allowed', nodeId: randomUUID() }]) {
+      await expect(tools.call(childID, work.id, 'workspace_create_agent', {
+        requestId: randomUUID(), name: '越权助手', ...source,
+      })).rejects.toMatchObject({ status: 403, code: 'agent_source_forbidden' })
+    }
+    await call(admin, 'patch', `/api/app/admin/users/${childID}`, adminCSRF).send({ assignedProfiles: [] }).expect(200)
+    await expect(tools.call(childID, work.id, 'workspace_create_agent', {
+      requestId: randomUUID(), name: '撤权后助手', profile: 'allowed',
+    })).rejects.toMatchObject({ status: 403 })
+    expect(runtime.workspace.list(childID, 'agent')).toHaveLength(1)
+  })
+
   it('persists assignment, filters sources, hides native profiles and rejects unassigned/local-node spoofing', async () => {
     expect((await call(child, 'get', '/api/app/bootstrap')).body.profiles).toEqual([])
     expect((await call(child, 'get', '/api/profiles')).body.profiles).toEqual([])