|
|
@@ -19,6 +19,7 @@ let denied: Set<string>, target: GatewayTarget, binding: Record<string, any> | u
|
|
|
let leases: WorkspaceToolLease[], upstream: Array<{ path: string; body: any }>
|
|
|
const owner = 'account-one'
|
|
|
const call = (tool: string, args: unknown = {}) => runtime.teamTools.call(owner, work.id, `workspace_${tool}`, args) as Promise<any>
|
|
|
+const selfCall = (tool: 'get' | 'update', args: unknown = {}) => runtime.knowledgeTools.call(owner, work.id, `workspace_${tool}_self_rules`, args, false) as Promise<any>
|
|
|
|
|
|
beforeEach(() => {
|
|
|
home = mkdtempSync(join(tmpdir(), 'yaoyao-team-tools-'))
|
|
|
@@ -88,16 +89,21 @@ describe('Agent-owned team tools', () => {
|
|
|
await expect(call('start_team_task',{...input,requestId:randomUUID()})).rejects.toMatchObject({code:'goal_exists'})
|
|
|
})
|
|
|
it('records creator authority and archives only an unused member it created',async()=>{
|
|
|
- const child=(await call('create_agent',{requestId:randomUUID(),name:'临时项目成员',profile:'default'})).agent
|
|
|
+ const initialRules = { instructions:'核验任务结果', description:'负责事实核验', job:'核验资料', antiJobs:['不猜测结论'], voice:'rigorous', actBias:'ask_key_then_act' }
|
|
|
+ const child=(await call('create_agent',{requestId:randomUUID(),name:'临时项目成员',profile:'default',...initialRules})).agent
|
|
|
expect(child.createdByAgentId).toBe(manager.id)
|
|
|
- const edited=await call('update_created_agent',{requestId:randomUUID(),agentId:child.id,instructions:'核验任务结果'})
|
|
|
- expect(edited.agent.instructions).toBe('核验任务结果')
|
|
|
+ expect(store.require(owner,'agent',child.id)).toMatchObject(initialRules)
|
|
|
+ for (const [field, value] of Object.entries(initialRules)) await expect(call('update_created_agent',{requestId:randomUUID(),agentId:child.id,name:'改名成员',[field]:value})).rejects.toMatchObject({code:'invalid_workspace_request'})
|
|
|
+ expect(store.require(owner,'agent',child.id)).toMatchObject({...initialRules,revision:1})
|
|
|
+ const edited=await call('update_created_agent',{requestId:randomUUID(),agentId:child.id,name:'改名成员'})
|
|
|
+ expect(edited.agent.name).toBe('改名成员')
|
|
|
+ expect(store.require(owner,'agent',child.id)).toMatchObject(initialRules)
|
|
|
const other=store.createAgent(owner,{name:'用户成员',profile:'default'})
|
|
|
await expect(call('archive_created_agent',{requestId:randomUUID(),agentId:other.id,confirmName:other.name})).rejects.toMatchObject({status:403})
|
|
|
const team=(await call('create_team',{requestId:randomUUID(),name:'成员管理测试',memberIds:[child.id]})).team
|
|
|
- await expect(call('archive_created_agent',{requestId:randomUUID(),agentId:child.id,confirmName:child.name})).rejects.toMatchObject({code:'agent_in_use'})
|
|
|
+ await expect(call('archive_created_agent',{requestId:randomUUID(),agentId:child.id,confirmName:edited.agent.name})).rejects.toMatchObject({code:'agent_in_use'})
|
|
|
await call('update_team',{requestId:randomUUID(),teamId:team.id,archived:true})
|
|
|
- expect((await call('archive_created_agent',{requestId:randomUUID(),agentId:child.id,confirmName:child.name})).agent.archived).toBe(true)
|
|
|
+ expect((await call('archive_created_agent',{requestId:randomUUID(),agentId:child.id,confirmName:edited.agent.name})).agent.archived).toBe(true)
|
|
|
})
|
|
|
it('creates durable members, a managed team and one idempotent task with client-visible events', async () => {
|
|
|
const requestId = randomUUID(), args = {requestId,name:'研究员',profile:'writer',instructions:'研究事实并给出处'}
|
|
|
@@ -202,13 +208,72 @@ describe('Agent-owned team tools', () => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
+describe('Bot self rules', () => {
|
|
|
+ it('reads full rules, edits only itself and keeps its current tools usable across retries', async () => {
|
|
|
+ const instructions = '完整长期角色规则。'.repeat(400)
|
|
|
+ store.put(owner,'agent',manager.id,{...manager,instructions,voice:'custom',voiceCustom:'原先语气'})
|
|
|
+ const before = (await selfCall('get')).agent
|
|
|
+ expect(before).toMatchObject({id:manager.id,revision:1,instructions})
|
|
|
+ const request = {requestId:randomUUID(),expectedRevision:before.revision,name:'核验助手',instructions:'先核验再回答',description:'事实助手',job:'核对来源',antiJobs:['不编造来源'],voice:'rigorous',voiceCustom:null,actBias:'ask_key_then_act'}
|
|
|
+ const updated = await selfCall('update',request)
|
|
|
+ expect(updated.agent).toMatchObject({id:manager.id,name:request.name,instructions:request.instructions,description:request.description,job:request.job,antiJobs:request.antiJobs,voice:request.voice,actBias:request.actBias,revision:2})
|
|
|
+ expect(updated.agent.voiceCustom).toBeUndefined()
|
|
|
+ expect(await selfCall('update',request)).toEqual(updated)
|
|
|
+ expect((await selfCall('get')).agent).toEqual(updated.agent)
|
|
|
+ expect(store.require<WorkspaceAgent>(owner,'agent',manager.id)).toMatchObject({profile:'default',nodeId:'local',revision:2,memoryEnabled:true})
|
|
|
+ expect(store.require<WorkspaceConversation>(owner,'conversation',work.conversationId).name).toBe(request.name)
|
|
|
+ expect(store.require<Work>(owner,'turn',work.id).teamManagementRevision).toBe(2)
|
|
|
+ expect((await call('list_agents')).agents[0].name).toBe(request.name)
|
|
|
+ await expect(selfCall('update',{...request,instructions:'同一编号改写'})).rejects.toMatchObject({code:'idempotency_conflict'})
|
|
|
+ expect(upstream).toEqual([])
|
|
|
+ })
|
|
|
+
|
|
|
+ it('rejects stale revisions and empty edits without overwriting a user edit', async () => {
|
|
|
+ const before = (await selfCall('get')).agent
|
|
|
+ store.updateAgent(owner,manager.id,{instructions:'用户刚保存的规则'})
|
|
|
+ await expect(selfCall('update',{requestId:randomUUID(),expectedRevision:before.revision,instructions:'旧版本覆盖'})).rejects.toMatchObject({code:'agent_revision_conflict'})
|
|
|
+ await expect(selfCall('update',{requestId:randomUUID(),expectedRevision:2})).rejects.toMatchObject({code:'invalid_workspace_request'})
|
|
|
+ expect(store.require<WorkspaceAgent>(owner,'agent',manager.id)).toMatchObject({instructions:'用户刚保存的规则',revision:2})
|
|
|
+ expect(store.require<Work>(owner,'turn',work.id).teamManagementRevision).toBe(1)
|
|
|
+ await expect(call('list_agents')).rejects.toMatchObject({code:'team_tools_forbidden'})
|
|
|
+ })
|
|
|
+
|
|
|
+ it.each([
|
|
|
+ ['agentId',randomUUID()], ['profile','writer'], ['nodeId','other'], ['approvalPolicy','allow'],
|
|
|
+ ['canManageTeam',true], ['canCollaborate',true], ['memoryEnabled',false], ['envs',{vm:true}], ['modelSettings',null],
|
|
|
+ ])('rejects the protected %s field', async (field, value) => {
|
|
|
+ await expect(selfCall('update',{requestId:randomUUID(),expectedRevision:1,instructions:'新规则',[field as string]:value})).rejects.toMatchObject({code:'invalid_workspace_request'})
|
|
|
+ expect(store.require<WorkspaceAgent>(owner,'agent',manager.id)).toEqual(manager)
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps self edits available to a persistent assigned Bot without granting team management', async () => {
|
|
|
+ store.put(owner,'run',work.runId,{...store.require<WorkspaceRun>(owner,'run',work.runId),assignmentId:randomUUID()})
|
|
|
+ store.put(owner,'turn',work.id,{...work,teamManagementRevision:undefined})
|
|
|
+ const catalog = runtime.knowledgeTools.catalog(owner,work.id,false).map(tool => tool.name)
|
|
|
+ expect(catalog).toEqual(expect.arrayContaining(['workspace_get_self_rules','workspace_update_self_rules']))
|
|
|
+ expect(catalog).not.toContain('workspace_memory_write')
|
|
|
+ await selfCall('update',{requestId:randomUUID(),expectedRevision:1,instructions:'自己的后续规则'})
|
|
|
+ expect(store.require<Work>(owner,'turn',work.id).teamManagementRevision).toBeUndefined()
|
|
|
+ await expect(call('list_agents')).rejects.toMatchObject({code:'team_tools_forbidden'})
|
|
|
+ })
|
|
|
+
|
|
|
+ it('keeps temporary helpers and remote references from editing their rules', async () => {
|
|
|
+ for (const restricted of [{temporaryGoalId:randomUUID()},{remoteAgentId:randomUUID()}]) {
|
|
|
+ store.put(owner,'agent',manager.id,{...manager,...restricted})
|
|
|
+ if ('temporaryGoalId' in restricted) expect(()=>runtime.knowledgeTools.catalog(owner,work.id,false)).toThrow()
|
|
|
+ else expect(runtime.knowledgeTools.catalog(owner,work.id,false).map(tool => tool.name)).not.toContain('workspace_update_self_rules')
|
|
|
+ await expect(selfCall('update',{requestId:randomUUID(),expectedRevision:1,instructions:'不能修改'})).rejects.toMatchObject({code:'knowledge_tool_forbidden'})
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
async function mount(signal = new AbortController().signal) {
|
|
|
const lease = await createWorkspaceToolLease({
|
|
|
target, profile:manager.profile, workId:work.id,
|
|
|
session:()=>({runtimeId:'runtime',storedId:'stored'}),signal,
|
|
|
assertActive:()=>{runtime.teamTools.assertTurn(owner,work.id)},
|
|
|
- catalog:()=>runtime.teamTools.catalog(owner,work.id),
|
|
|
- call:(id,args)=>runtime.teamTools.call(owner,work.id,id,args),onFailure:()=>{},
|
|
|
+ catalog:()=>[...runtime.teamTools.catalog(owner,work.id),...runtime.knowledgeTools.catalog(owner,work.id,false)],
|
|
|
+ call:(id,args)=>runtime.knowledgeTools.handles(id)?runtime.knowledgeTools.call(owner,work.id,id,args,false):runtime.teamTools.call(owner,work.id,id,args),onFailure:()=>{},
|
|
|
})
|
|
|
leases.push(lease); await lease.bind()
|
|
|
return lease
|
|
|
@@ -218,6 +283,22 @@ function http(path:string, body:unknown, options:RequestInit={}) {
|
|
|
}
|
|
|
|
|
|
describe('loopback tool lease', () => {
|
|
|
+ it('updates self rules through the bridge without ending the current tool lease', async () => {
|
|
|
+ await mount()
|
|
|
+ const catalog = await (await http('/tools/list',{})).json()
|
|
|
+ const readId = catalog.tools.find((tool:any)=>tool.name==='workspace_get_self_rules').id
|
|
|
+ const writeId = catalog.tools.find((tool:any)=>tool.name==='workspace_update_self_rules').id
|
|
|
+ const current = await (await http('/tools/call',{callId:'read-self',toolId:readId,arguments:{}})).json()
|
|
|
+ const request = {callId:'update-self',toolId:writeId,arguments:{requestId:randomUUID(),expectedRevision:current.structuredContent.agent.revision,name:'自己的新名字',instructions:'自己的新规则'}}
|
|
|
+ const updated = await (await http('/tools/call',request)).json()
|
|
|
+ expect(updated.structuredContent.agent).toMatchObject({id:manager.id,name:'自己的新名字',instructions:'自己的新规则',revision:2})
|
|
|
+ expect(await (await http('/tools/call',request)).json()).toEqual(updated)
|
|
|
+ expect((await http('/tools/list',{})).status).toBe(200)
|
|
|
+ const after = await (await http('/tools/call',{callId:'read-updated',toolId:readId,arguments:{}})).json()
|
|
|
+ expect(after.structuredContent.agent).toEqual(updated.structuredContent.agent)
|
|
|
+ store.updateAgent(owner,manager.id,{instructions:'用户再修改'})
|
|
|
+ expect((await http('/tools/list',{})).status).toBe(403)
|
|
|
+ })
|
|
|
it.each(['initializing','agent_initialization_failed','binding_busy','bridge_catalog_unreachable'])('preserves the Profile and the %s binding failure without reflecting upstream secrets',async code=>{
|
|
|
manager.profile='yaoer'
|
|
|
vi.mocked(target.session.request).mockImplementation(async path=>({status:path.endsWith('/bind')?409:200,
|