|
@@ -95,6 +95,77 @@ afterEach(() => {
|
|
|
rmSync(home, { recursive: true, force: true })
|
|
rmSync(home, { recursive: true, force: true })
|
|
|
})
|
|
})
|
|
|
describe('application workspace HTTP contract', () => {
|
|
describe('application workspace HTTP contract', () => {
|
|
|
|
|
+ it('deletes only archived owned chats, clears their records, and preserves library files and members', async () => {
|
|
|
|
|
+ const store = runtime.workspace
|
|
|
|
|
+ const lead = store.createAgent('first', { name: '负责人', profile: 'default' })
|
|
|
|
|
+ const member = store.createAgent('first', { name: '成员', profile: 'default' })
|
|
|
|
|
+ const group = store.createGroup('first', { name: '待删除群聊', memberIds: [lead.id, member.id], administratorId: lead.id })
|
|
|
|
|
+ const task = store.tasks('first', group.id)[0]!
|
|
|
|
|
+ const path = `/api/app/conversations/${group.id}`
|
|
|
|
|
+ await req('delete', path).expect(409)
|
|
|
|
|
+ store.updateConversation('first', group.id, { archived: true })
|
|
|
|
|
+ store.put('second', 'conversation', group.id, group)
|
|
|
|
|
+ for (const kind of ['message', 'run', 'turn', 'interaction', 'interaction-binding', 'binding', 'assignment']) {
|
|
|
|
|
+ store.put('first', kind, kind, { id: kind, conversationId: group.id, conversationTaskId: task.id, status: 'complete' })
|
|
|
|
|
+ store.put('second', kind, kind, { id: kind, conversationId: group.id })
|
|
|
|
|
+ }
|
|
|
|
|
+ store.put('first', 'context', task.id, { used: 5 })
|
|
|
|
|
+ const bindingKey = `${group.id}:${task.id}:${lead.id}`
|
|
|
|
|
+ store.put('first', 'binding', bindingKey, { conversationTaskId: task.id, sessionId: 'session' })
|
|
|
|
|
+ store.put('first', 'interaction-binding', 'legacy-interaction', { key: bindingKey, taskId: 'turn' })
|
|
|
|
|
+ store.put('second', 'binding', bindingKey, { sessionId: 'other-session' })
|
|
|
|
|
+ store.put('first', 'file', 'file', { id: 'file', conversationId: group.id, messageId: 'message', name: '保留.txt' })
|
|
|
|
|
+ await req('delete', path, 'third').expect(404)
|
|
|
|
|
+ await req('delete', path).set('X-CSRF-Token', 'invalid').expect(403)
|
|
|
|
|
+ await req('delete', path).expect(200)
|
|
|
|
|
+ await req('get', path).expect(404)
|
|
|
|
|
+ expect(store.get('first', 'context', task.id)).toBeUndefined()
|
|
|
|
|
+ expect(store.get('first', 'conversation-task', task.id)).toBeUndefined()
|
|
|
|
|
+ expect(store.list('first', 'message')).toEqual([])
|
|
|
|
|
+ expect(store.list('first', 'run')).toEqual([])
|
|
|
|
|
+ expect(store.list('first', 'binding')).toEqual([])
|
|
|
|
|
+ expect(store.list('first', 'interaction-binding')).toEqual([])
|
|
|
|
|
+ expect(store.get('second', 'binding', bindingKey)).toEqual({ sessionId: 'other-session' })
|
|
|
|
|
+ expect(store.get('first', 'file', 'file')).toEqual({ id: 'file', name: '保留.txt' })
|
|
|
|
|
+ expect(store.list('first', 'agent')).toHaveLength(2)
|
|
|
|
|
+ expect(store.get('second', 'conversation', group.id)).toEqual(group)
|
|
|
|
|
+ expect(store.get('second', 'message', 'message')).toBeDefined()
|
|
|
|
|
+ expect((await req('get', '/api/app/events')).body.events.some((e: any) => e.type === 'conversation.deleted' && e.conversationId === group.id)).toBe(true)
|
|
|
|
|
+ })
|
|
|
|
|
+ it('deletes an archived Bot and its direct chat only after group references are removed', async () => {
|
|
|
|
|
+ const store = runtime.workspace
|
|
|
|
|
+ const bot = store.createAgent('first', { name: '归档 Bot', profile: 'default' })
|
|
|
|
|
+ const member = store.createAgent('first', { name: '成员', profile: 'default' })
|
|
|
|
|
+ const direct = store.list<any>('first', 'conversation').find(c => c.memberIds[0] === bot.id)!
|
|
|
|
|
+ const group = store.createGroup('first', { name: '关联群', memberIds: [bot.id, member.id], administratorId: member.id })
|
|
|
|
|
+ const path = `/api/app/agents/${bot.id}`
|
|
|
|
|
+ await req('delete', path).expect(409)
|
|
|
|
|
+ store.updateAgent('first', bot.id, { archived: true })
|
|
|
|
|
+ await req('delete', `/api/app/conversations/${direct.id}`).expect(400)
|
|
|
|
|
+ expect((await req('delete', path).expect(409)).body.code).toBe('agent_in_group')
|
|
|
|
|
+ expect(store.get('first', 'agent', bot.id)).toBeDefined()
|
|
|
|
|
+ store.updateConversation('first', group.id, { memberIds: [member.id] })
|
|
|
|
|
+ await req('delete', path, 'second').expect(404)
|
|
|
|
|
+ await req('delete', path).expect(200)
|
|
|
|
|
+ expect(store.get('first', 'agent', bot.id)).toBeUndefined()
|
|
|
|
|
+ expect(store.get('first', 'conversation', direct.id)).toBeUndefined()
|
|
|
|
|
+ expect(store.get('first', 'conversation', group.id)).toBeDefined()
|
|
|
|
|
+ expect(store.get('first', 'agent', member.id)).toBeDefined()
|
|
|
|
|
+ })
|
|
|
|
|
+ it('keeps archived chats intact while runs or delegated goals are unresolved', async () => {
|
|
|
|
|
+ const store = runtime.workspace
|
|
|
|
|
+ const bot = store.createAgent('first', { name: '运行中', profile: 'default' })
|
|
|
|
|
+ const direct = store.list<any>('first', 'conversation')[0]!
|
|
|
|
|
+ store.updateAgent('first', bot.id, { archived: true })
|
|
|
|
|
+ store.put('first', 'run', 'pending', { id: 'pending', conversationId: direct.id, status: 'uncertain' })
|
|
|
|
|
+ const path = `/api/app/agents/${bot.id}`
|
|
|
|
|
+ await req('delete', path).expect(409)
|
|
|
|
|
+ expect(store.get('first', 'conversation', direct.id)).toBeDefined()
|
|
|
|
|
+ store.remove('first', 'run', 'pending')
|
|
|
|
|
+ store.put('first', 'goal', 'delegated', { id: 'delegated', conversationId: 'another-chat', origin: { conversationId: direct.id }, status: 'waiting' })
|
|
|
|
|
+ await req('delete', path).expect(409)
|
|
|
|
|
+ expect(store.get('first', 'agent', bot.id)).toBeDefined()
|
|
|
|
|
+ })
|
|
|
it('resumes a stopped goal from a real user request without duplicating its run',async()=>{
|
|
it('resumes a stopped goal from a real user request without duplicating its run',async()=>{
|
|
|
vi.spyOn(runtime.workspaceRuntime,'wake').mockImplementation(()=>{})
|
|
vi.spyOn(runtime.workspaceRuntime,'wake').mockImplementation(()=>{})
|
|
|
vi.spyOn(runtime.workspaceRuntime.teamTools,'requireAvailable').mockResolvedValue()
|
|
vi.spyOn(runtime.workspaceRuntime.teamTools,'requireAvailable').mockResolvedValue()
|