workspace.test.ts 86 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417
  1. // @vitest-environment node
  2. import { afterEach, beforeEach, describe, 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 { WebSocketServer, type WebSocket } from 'ws'
  8. import { defaultAgentIdentity, encodeAgentAvatar, decodeAgentAvatar } from '../../src/shared/agentIdentity'
  9. import {HttpError} from '../../src/server/errors'
  10. import {WorkspaceAssets} from '../../src/server/workspaceAssets'
  11. import { WorkspaceStore } from '../../src/server/workspaceStore'
  12. import { WorkspaceRuntime, mentionedAgents } from '../../src/server/workspaceRuntime'
  13. import type { Work } from '../../src/server/workspaceScheduler'
  14. import { REPEATED_RELAY_NOTICE } from '../../src/server/workspaceRelay'
  15. import { WorkspaceNodes } from '../../src/server/workspaceGateway'
  16. import { UploadStore } from '../../src/server/uploads'
  17. import type {
  18. WorkspaceConversation,
  19. WorkspaceRun,
  20. WorkspaceMessage,
  21. WorkspaceInteraction,
  22. } from '../../src/shared/workspace'
  23. let home: string,
  24. store: WorkspaceStore,
  25. uploads: UploadStore,
  26. runtime: WorkspaceRuntime,
  27. server: WebSocketServer
  28. let requests: Array<{ method: string; params: Record<string, any> }>,
  29. reply: (socket: WebSocket, params: Record<string, any>) => void
  30. let nodes: WorkspaceNodes
  31. let configuredCwds: Map<string, string>
  32. let rejectPrompt: string | undefined
  33. let rejectedInterrupts: number
  34. let recoveryHistory: Array<{ role: string; content: string; tool_calls?: unknown }>
  35. let storedByRuntime: Map<string, string>, recoveryByStored: Map<string, Array<{ role: string; content: string }>>
  36. const owner = 'first-user',
  37. other = 'second-user'
  38. beforeEach(async () => {
  39. home = mkdtempSync(join(tmpdir(), 'yaoyao-workspace-test-'))
  40. store = new WorkspaceStore(home)
  41. uploads = new UploadStore(home)
  42. requests = []
  43. configuredCwds = new Map()
  44. recoveryHistory = []
  45. storedByRuntime = new Map(); recoveryByStored = new Map()
  46. rejectPrompt = undefined
  47. rejectedInterrupts = 0
  48. server = new WebSocketServer({ port: 0, host: '127.0.0.1' })
  49. await new Promise<void>((resolve) => server.once('listening', resolve))
  50. const address = server.address() as { port: number }
  51. const target = {
  52. url: new URL(`http://127.0.0.1:${address.port}`),
  53. client: { directAgent: undefined },
  54. session: {
  55. webSocketCredential: async () => ({ name: 'ticket', value: 'test' }),
  56. request: async (path: string, options?: {search?: URLSearchParams}) => ({
  57. status: 200,
  58. body: Buffer.from(JSON.stringify(path === '/api/config'
  59. ? { terminal: { cwd: configuredCwds.get(options?.search?.get('profile') ?? '') } }
  60. : { messages: (() => { const all = recoveryByStored.get(decodeURIComponent(path.split('/')[3]!)) ?? recoveryHistory; const offset = Number(options?.search?.get('offset') ?? 0); return all.slice(Math.max(0,all.length-offset-500), all.length-offset) })() })),
  61. }),
  62. },
  63. }
  64. nodes = { requireSource: () => {}, target: () => target } as unknown as WorkspaceNodes
  65. runtime = new WorkspaceRuntime(store, nodes, uploads)
  66. reply = (socket, p) =>
  67. setTimeout(
  68. () =>
  69. socket.send(
  70. JSON.stringify({
  71. method: 'event',
  72. params: {
  73. type: 'message.complete',
  74. session_id: p.session_id,
  75. payload: { text: '完成', status: 'complete' },
  76. },
  77. }),
  78. ),
  79. 5,
  80. )
  81. server.on('connection', (socket) => {
  82. socket.send(JSON.stringify({ method: 'event', params: { type: 'gateway.ready' } }))
  83. socket.on('message', (data) => {
  84. const frame = JSON.parse(String(data))
  85. requests.push(frame)
  86. const respond = (result: unknown) => socket.send(JSON.stringify({ id: frame.id, result }))
  87. if (frame.method === 'session.create' || frame.method === 'session.resume') {
  88. const runtimeId = randomUUID(), storedId = frame.method === 'session.resume' ? frame.params.session_id : randomUUID()
  89. storedByRuntime.set(runtimeId, storedId)
  90. respond({ session_id: runtimeId, stored_session_id: storedId, running: false, info: { profile_name: frame.params.profile } })
  91. }
  92. else if (frame.method === 'session.cwd.set') respond({ cwd: frame.params.cwd })
  93. else if (frame.method === 'prompt.submit') {
  94. if (rejectPrompt) { socket.send(JSON.stringify({id:frame.id,error:{code:4006,message:rejectPrompt}})); return }
  95. respond({ status: 'streaming' })
  96. reply(socket, frame.params)
  97. } else if (frame.method === 'session.usage') respond({ context_used: 400, context_max: 1000 })
  98. else if (frame.method === 'session.interrupt' && rejectedInterrupts-- > 0) socket.send(JSON.stringify({id:frame.id,error:{code:500,message:'暂时不能中断'}}))
  99. else respond({ ok: true, status: 'interrupted' })
  100. })
  101. })
  102. })
  103. afterEach(async () => {
  104. runtime.close()
  105. await new Promise((resolve) => setTimeout(resolve, 10))
  106. for (const client of server.clients) client.terminate()
  107. await new Promise<void>((resolve) => server.close(() => resolve()))
  108. uploads.close()
  109. store.close()
  110. rmSync(home, { recursive: true, force: true })
  111. })
  112. function agent(name: string, user = owner) {
  113. return store.createAgent(user, { name, profile: 'default', instructions: `规则:你是${name}` })
  114. }
  115. it('executes a structured assignment on its actual worker and leaves acceptance to its coordinator', async () => {
  116. const lead = store.updateAgent(owner, agent('负责人').id, { canManageTeam: true })
  117. const worker = agent('研究成员')
  118. const source = direct(lead.id)
  119. const original = runtime.send(owner, source.id, { requestId: randomUUID(), content: '完成研究' })
  120. for (const work of store.list<any>(owner, 'turn').filter(w => w.runId === original.id))
  121. store.put(owner, 'turn', work.id, { ...work, status: 'complete', planned: true })
  122. original.status = 'complete';store.saveRun(owner, original)
  123. const team = store.createGroup(owner, { name: '研究团队', memberIds: [lead.id,worker.id], administratorId: lead.id })
  124. const goal = runtime.tasks.begin(owner, store.tasks(owner, team.id)[0]!, lead, '研究事实', { conversationId: source.id, runId: original.id, agentId: lead.id })
  125. const assignment = runtime.tasks.createAssignment(owner, lead.id, { requestId: randomUUID(), goalId: goal.id, agentId: worker.id, title: '核对事实', brief: '提交核对结果' })
  126. await vi.waitFor(() => expect(store.get<any>(owner,'assignment',assignment.id)?.status).toBe('review'), { timeout: 5000 })
  127. const submitted = requests.find(r => r.method === 'prompt.submit' && String(r.params.text).includes('核对事实'))!
  128. expect(submitted.params.text).toContain('你是 研究成员')
  129. expect(submitted.params.text).toContain('执行子任务')
  130. expect(store.get<any>(owner,'goal',goal.id)?.status).not.toBe('complete')
  131. })
  132. it('keeps structured goal coordination on the lead and does not relay mention text as duplicate work', async () => {
  133. const lead=store.updateAgent(owner,agent('交付负责人').id,{canManageTeam:true}),worker=agent('研究成员')
  134. const target=nodes.target(owner,'local'), request=target.session.request.bind(target.session)
  135. vi.spyOn(target.session,'request').mockImplementation(async(path,options)=>path.startsWith('/api/plugins/yaoyao-bot-bridge/')
  136. ? {status:200,body:Buffer.from(JSON.stringify({ok:true,version:1,ready:true,in_process:true,native_tools:true})),headers:new Headers()}
  137. : request(path,options))
  138. const team=store.createGroup(owner,{name:'自由协作交付',memberIds:[lead.id,worker.id],administratorId:lead.id,mode:'free',autoReplyIds:[worker.id]})
  139. reply=(socket,p)=>socket.send(JSON.stringify({method:'event',params:{type:'message.complete',session_id:p.session_id,payload:{text:'@研究成员 请核对资料',status:'complete'}}}))
  140. const run=runtime.send(owner,team.id,{requestId:randomUUID(),content:'交付研究报告',mode:'goal'})
  141. await vi.waitFor(()=>expect(['complete','failed']).toContain(store.require<WorkspaceRun>(owner,'run',run.id).status))
  142. const settled=store.require<WorkspaceRun>(owner,'run',run.id)
  143. expect(settled.status,settled.error).toBe('complete')
  144. const work=store.list<Work>(owner,'turn').filter(w=>w.runId===run.id)
  145. expect(work.map(w=>w.agentId)).toEqual([lead.id])
  146. expect(runtime.tasks.assignments(owner,run.goalId!)).toEqual([])
  147. expect(requests.filter(r=>r.method==='prompt.submit').every(r=>!String(r.params.text).startsWith('你是 研究成员'))).toBe(true)
  148. })
  149. function direct(id: string, user = owner) {
  150. return store
  151. .list<WorkspaceConversation>(user, 'conversation')
  152. .find((c) => c.kind === 'direct' && c.memberIds[0] === id)!
  153. }
  154. function defaultTask(conversationId: string, user = owner) {
  155. return store.tasks(user, conversationId)[0]!
  156. }
  157. async function finished(id: string) {
  158. await vi.waitFor(() =>
  159. expect(store.require<WorkspaceRun>(owner, 'run', id).status).toBe('complete'),
  160. )
  161. return store.require<WorkspaceRun>(owner, 'run', id)
  162. }
  163. describe('Web-owned workspace', () => {
  164. it('restores recent user and assistant context when moving a completed conversation to another runner',async()=>{
  165. const a=agent('移动成员'),c=direct(a.id)
  166. await finished(runtime.send(owner,c.id,{requestId:randomUUID(),content:'保存研究结论'}).id)
  167. const binding=store.list<any>(owner,'binding')[0]!
  168. store.put(owner,'binding',binding.id,{...binding,runnerId:'previous-runner'})
  169. const before=requests.length
  170. await finished(runtime.send(owner,c.id,{requestId:randomUUID(),content:'继续后续分析'}).id)
  171. const next=requests.slice(before)
  172. expect(next.some(r=>r.method==='session.resume')).toBe(false)
  173. const text=next.find(r=>r.method==='prompt.submit')!.params.text
  174. expect(text).toContain('执行节点已切换')
  175. expect(text).toContain('保存研究结论')
  176. expect(text).toContain('移动成员(complete):完成')
  177. expect(text).toContain('继续后续分析')
  178. })
  179. it('uses each base profile cwd and replaces historical cwd before the next Bot turn', async () => {
  180. configuredCwds.set('default', '/profile/first')
  181. configuredCwds.set('other', '/profile/other')
  182. const a = agent('主助手'), c = direct(a.id)
  183. await finished(runtime.send(owner, c.id, { requestId: randomUUID(), content: 'first' }).id)
  184. expect(requests.find(r => r.method === 'session.create')?.params.cwd).toBe('/profile/first')
  185. expect(requests.find(r => r.method === 'session.cwd.set')?.params.cwd).toBe('/profile/first')
  186. configuredCwds.set('default', '/profile/changed')
  187. const before = requests.length
  188. await finished(runtime.send(owner, c.id, { requestId: randomUUID(), content: 'second' }).id)
  189. const next = requests.slice(before)
  190. expect(next[0].method).toBe('session.resume')
  191. expect(next.find(r => r.method === 'session.cwd.set')?.params.cwd).toBe('/profile/changed')
  192. expect(next.findIndex(r => r.method === 'session.cwd.set')).toBeLessThan(next.findIndex(r => r.method === 'prompt.submit'))
  193. const b = store.createAgent(owner, { name: '其他 Profile', profile: 'other' })
  194. await finished(runtime.send(owner, direct(b.id).id, { requestId: randomUUID(), content: 'third' }).id)
  195. expect(requests.filter(r => r.method === 'session.create').at(-1)?.params.cwd).toBe('/profile/other')
  196. })
  197. it('flushes a paused delta before completion and cancels pending flush on finish', async () => {
  198. let upstream: { socket: WebSocket; params: Record<string, any> } | undefined
  199. reply = (socket, params) => {
  200. upstream = { socket, params }
  201. const event = (type: string, payload: Record<string, unknown>) => socket.send(JSON.stringify({
  202. method: 'event', params: { type, session_id: params.session_id, payload },
  203. }))
  204. event('message.start', {})
  205. event('message.delta', { text: '# 流式标题' })
  206. }
  207. const bot = agent('流式助手'), conversation = direct(bot.id)
  208. const run = runtime.send(owner, conversation.id, { requestId: randomUUID(), content: '验证暂停分片' })
  209. const assistant = () => store.list<WorkspaceMessage>(owner, 'message').find(m => m.runId === run.id && m.role === 'assistant')
  210. await vi.waitFor(() => expect(assistant()?.content).toBe('# 流式标题'))
  211. expect(assistant()?.status).toBe('streaming')
  212. expect(store.require<WorkspaceRun>(owner, 'run', run.id).status).not.toBe('complete')
  213. const { socket, params } = upstream!
  214. socket.send(JSON.stringify({ method: 'event', params: { type: 'message.delta', session_id: params.session_id, payload: { text: '\n\n最后一段' } } }))
  215. completeReply(socket, params, '# 最终正文')
  216. await finished(run.id)
  217. await new Promise(resolve => setTimeout(resolve, 150))
  218. expect(assistant()?.content).toBe('# 最终正文')
  219. expect(assistant()?.status).toBe('complete')
  220. })
  221. it('backfills legacy group history into the initial task instead of leaving a new empty task', () => {
  222. const legacyHome = mkdtempSync(join(tmpdir(), 'yaoyao-workspace-task-migration-'))
  223. let seededStore: WorkspaceStore | undefined = new WorkspaceStore(legacyHome)
  224. let migratedStore: WorkspaceStore | undefined
  225. try {
  226. const administrator = seededStore.createAgent(owner, { name: '历史管理员', profile: 'default' }),
  227. member = seededStore.createAgent(owner, { name: '历史成员', profile: 'default' }),
  228. conversation = seededStore.createGroup(owner, {
  229. name: '公众号文案团队',
  230. memberIds: [administrator.id, member.id],
  231. administratorId: administrator.id,
  232. }),
  233. initialTask = seededStore.tasks(owner, conversation.id)[0]!,
  234. userAt = 1_788_000_000_000,
  235. assistantAt = userAt + 60_000
  236. seededStore.put(owner, 'message', 'legacy-user', {
  237. id: 'legacy-user', conversationId: conversation.id, seq: 1, role: 'user',
  238. content: '请继续完成公众号文案', reasoning: '', status: 'complete',
  239. attachments: [], tools: [], createdAt: userAt,
  240. } satisfies WorkspaceMessage)
  241. seededStore.put(owner, 'message', 'legacy-assistant', {
  242. id: 'legacy-assistant', conversationId: conversation.id, seq: 2, role: 'assistant',
  243. agentId: administrator.id, agentName: '历史管理员', content: '旧内容已经完成。',
  244. reasoning: '', status: 'complete', attachments: [], tools: [], createdAt: assistantAt,
  245. } satisfies WorkspaceMessage)
  246. seededStore.put(owner, 'conversation', conversation.id, {
  247. ...conversation,
  248. readSeq: 1,
  249. lastSeq: 2,
  250. preview: '旧内容已经完成。',
  251. lastMessageAt: assistantAt,
  252. updatedAt: assistantAt,
  253. })
  254. seededStore.put(owner, 'run', 'legacy-run', {
  255. id: 'legacy-run', conversationId: conversation.id, messageId: 'legacy-user',
  256. mentionIds: [], status: 'complete', round: 1, createdAt: userAt, updatedAt: assistantAt,
  257. } satisfies WorkspaceRun)
  258. seededStore.put(owner, 'turn', 'legacy-turn', {
  259. id: 'legacy-turn', runId: 'legacy-run', conversationId: conversation.id,
  260. agentId: administrator.id, messageId: 'legacy-user', status: 'complete', createdAt: userAt,
  261. })
  262. seededStore.put(owner, 'interaction', 'legacy-interaction', {
  263. id: 'legacy-interaction', conversationId: conversation.id, runId: 'legacy-run',
  264. agentId: administrator.id, kind: 'clarification', message: '补充说明',
  265. choices: [], resolved: true,
  266. } satisfies WorkspaceInteraction)
  267. seededStore.put(owner, 'interaction-binding', 'legacy-interaction', {
  268. key: `${conversation.id}:${administrator.id}`,
  269. upstreamId: 'upstream-interaction', taskId: 'legacy-turn',
  270. })
  271. seededStore.put(owner, 'binding', `${conversation.id}:${administrator.id}`, {
  272. nodeId: 'local', profile: 'default', storedId: 'stored-legacy',
  273. runtimeId: 'runtime-legacy', aliases: [], runId: 'legacy-run',
  274. messageId: 'legacy-user', taskId: 'legacy-turn',
  275. })
  276. seededStore.put(owner, 'context', conversation.id, {
  277. usedTokens: 100, limitTokens: 1_000, observedAt: assistantAt,
  278. })
  279. seededStore.db.prepare('DELETE FROM workspace_migrations WHERE id=?')
  280. .run('conversation-task-history-v1')
  281. seededStore.close()
  282. seededStore = undefined
  283. migratedStore = new WorkspaceStore(legacyHome)
  284. const tasks = migratedStore.tasks(owner, conversation.id)
  285. expect(tasks).toHaveLength(1)
  286. expect(tasks[0]).toMatchObject({
  287. id: initialTask.id,
  288. title: '请继续完成公众号文案',
  289. titleSource: 'automatic',
  290. messageCount: 2,
  291. readSeq: 1,
  292. lastSeq: 2,
  293. unreadCount: 1,
  294. lastMessageAt: assistantAt,
  295. createdAt: userAt,
  296. updatedAt: assistantAt,
  297. })
  298. expect(
  299. migratedStore.messages(
  300. owner, conversation.id, Number.MAX_SAFE_INTEGER, 100, false, initialTask.id,
  301. ).map(message => message.id),
  302. ).toEqual(['legacy-user', 'legacy-assistant'])
  303. expect(migratedStore.require<WorkspaceRun>(owner, 'run', 'legacy-run').conversationTaskId)
  304. .toBe(initialTask.id)
  305. expect(migratedStore.require<{ conversationTaskId: string }>(owner, 'turn', 'legacy-turn').conversationTaskId)
  306. .toBe(initialTask.id)
  307. expect(migratedStore.require<WorkspaceInteraction>(owner, 'interaction', 'legacy-interaction').conversationTaskId)
  308. .toBe(initialTask.id)
  309. expect(migratedStore.get(owner, 'binding', `${conversation.id}:${administrator.id}`)).toBeUndefined()
  310. expect(migratedStore.get(owner, 'binding', `${conversation.id}:${initialTask.id}:${administrator.id}`))
  311. .toMatchObject({ conversationTaskId: initialTask.id })
  312. expect(migratedStore.get(owner, 'context', conversation.id)).toBeUndefined()
  313. expect(migratedStore.get(owner, 'context', initialTask.id))
  314. .toMatchObject({ conversationTaskId: initialTask.id })
  315. } finally {
  316. seededStore?.close()
  317. migratedStore?.close()
  318. rmSync(legacyHome, { recursive: true, force: true })
  319. }
  320. })
  321. it('serializes one Agent across group tasks while keeping messages, sessions and context isolated', async () => {
  322. const administrator = store.createAgent(owner, { name: '多任务远程管理员', profile: 'remote-profile', nodeId: 'remote-node' }),
  323. member = agent('多任务成员'),
  324. conversation = store.createGroup(owner, {
  325. name: '多任务群聊',
  326. memberIds: [administrator.id, member.id],
  327. administratorId: administrator.id,
  328. }),
  329. firstTask = defaultTask(conversation.id),
  330. secondTask = store.createTask(owner, conversation.id, { title: '显式标题' })
  331. const pending: Array<{ socket: WebSocket; params: Record<string, any> }> = []
  332. reply = (socket, params) => {
  333. pending.push({ socket, params })
  334. }
  335. const complete = (item: typeof pending[number]) => {
  336. const text = item.params.text.includes('第一项工作') ? '第一项结果' : '第二项结果'
  337. setTimeout(() => item.socket.send(JSON.stringify({
  338. method: 'event',
  339. params: { type: 'message.complete', session_id: item.params.session_id, payload: { text, status: 'complete' } },
  340. })), 5)
  341. }
  342. const firstRun = runtime.send(owner, conversation.id, { requestId: randomUUID(), taskId: firstTask.id, content: '第一项工作' }),
  343. secondRun = runtime.send(owner, conversation.id, { requestId: randomUUID(), taskId: secondTask.id, content: '第二项工作' })
  344. await vi.waitFor(()=>expect(pending).toHaveLength(1))
  345. expect(store.require<WorkspaceRun>(owner,'run',secondRun.id).status).toBe('queued')
  346. complete(pending[0]!)
  347. await finished(firstRun.id)
  348. await vi.waitFor(()=>expect(pending).toHaveLength(2))
  349. complete(pending[1]!)
  350. await finished(secondRun.id)
  351. const firstMessages = store.messages(owner, conversation.id, Number.MAX_SAFE_INTEGER, 100, false, firstTask.id),
  352. secondMessages = store.messages(owner, conversation.id, Number.MAX_SAFE_INTEGER, 100, false, secondTask.id)
  353. expect(firstMessages.map(message => message.content)).toEqual(['第一项工作', '第一项结果'])
  354. expect(secondMessages.map(message => message.content)).toEqual(['第二项工作', '第二项结果'])
  355. expect(firstMessages.every(message => message.conversationTaskId === firstTask.id)).toBe(true)
  356. expect(secondMessages.every(message => message.conversationTaskId === secondTask.id)).toBe(true)
  357. const prompts = requests.filter(request => request.method === 'prompt.submit').map(request => String(request.params.text))
  358. expect(prompts.find(prompt => prompt.includes('第一项工作'))).not.toContain('第二项工作')
  359. expect(prompts.find(prompt => prompt.includes('第二项工作'))).not.toContain('第一项工作')
  360. const bindings = store.list<any>(owner, 'binding')
  361. expect(bindings.map(binding => binding.conversationTaskId).sort()).toEqual([firstTask.id, secondTask.id].sort())
  362. expect(bindings.every(binding => binding.nodeId === 'remote-node' && binding.profile === 'remote-profile')).toBe(true)
  363. expect(new Set(bindings.map(binding => binding.storedId)).size).toBe(2)
  364. expect(store.get<any>(owner, 'context', firstTask.id)).toMatchObject({ conversationTaskId: firstTask.id, percent: 40 })
  365. expect(store.get<any>(owner, 'context', secondTask.id)).toMatchObject({ conversationTaskId: secondTask.id, percent: 40 })
  366. const firstSummary = store.require<any>(owner, 'conversation-task', firstTask.id)
  367. expect(firstSummary).toMatchObject({ title: '第一项工作', titleSource: 'automatic', messageCount: 2, unreadCount: 2 })
  368. expect(store.require<any>(owner, 'conversation-task', secondTask.id)).toMatchObject({ title: '显式标题', titleSource: 'user', messageCount: 2 })
  369. expect(store.markTaskRead(owner, conversation.id, firstTask.id, firstSummary.lastSeq).unreadCount).toBe(0)
  370. store.deleteTask(owner, conversation.id, firstTask.id)
  371. expect(store.list<WorkspaceMessage>(owner, 'message').some(message => message.conversationTaskId === firstTask.id)).toBe(false)
  372. expect(store.list<any>(owner, 'binding').some(binding => binding.conversationTaskId === firstTask.id)).toBe(false)
  373. expect(store.get(owner, 'context', firstTask.id)).toBeUndefined()
  374. expect(store.messages(owner, conversation.id, Number.MAX_SAFE_INTEGER, 100, false, secondTask.id).map(message => message.content))
  375. .toEqual(['第二项工作', '第二项结果'])
  376. })
  377. it('limits a group to four active conversation tasks while retaining the global scheduler limit', async () => {
  378. const administrator = agent('并发任务管理员'),
  379. member = agent('并发任务成员'),
  380. conversation = store.createGroup(owner, {
  381. name: '并发任务群聊',
  382. memberIds: [administrator.id, member.id],
  383. administratorId: administrator.id,
  384. }),
  385. tasks = [defaultTask(conversation.id), ...Array.from({ length: 4 }, (_, index) => store.createTask(owner, conversation.id, { title: `任务 ${index + 2}` }))]
  386. reply = () => {}
  387. for (const [index, task] of tasks.slice(0, 4).entries())
  388. runtime.send(owner, conversation.id, { requestId: randomUUID(), taskId: task.id, content: `执行 ${index + 1}` })
  389. expect(store.activeTaskCount(owner, conversation.id)).toBe(4)
  390. expect(() => runtime.send(owner, conversation.id, { requestId: randomUUID(), taskId: tasks[4]!.id, content: '第五项' }))
  391. .toThrow('最多同时运行 4 个任务')
  392. await Promise.all(tasks.slice(0, 4).map(task => runtime.stopTask(owner, conversation.id, task.id)))
  393. })
  394. it('keeps same-named upstream interactions scoped to their conversation task', async () => {
  395. const administrator = agent('交互隔离管理员'),
  396. member = agent('交互隔离成员'),
  397. conversation = store.createGroup(owner, {
  398. name: '交互隔离群聊',
  399. memberIds: [administrator.id, member.id],
  400. administratorId: administrator.id,
  401. }),
  402. firstTask = defaultTask(conversation.id),
  403. secondTask = store.createTask(owner, conversation.id, {})
  404. const pending: Array<{socket:WebSocket;params:Record<string,any>}> = []
  405. reply = (socket, params) => { pending.push({socket,params});setTimeout(() => socket.send(JSON.stringify({
  406. method: 'event',
  407. params: {
  408. type: 'approval.request',
  409. session_id: params.session_id,
  410. payload: { request_id: 'same-upstream-id', message: '允许执行吗?' },
  411. },
  412. })), 5) }
  413. const firstRun=runtime.send(owner, conversation.id, { requestId: randomUUID(), taskId: firstTask.id, content: '任务甲' })
  414. // Different tasks admitted in the same millisecond need not win a lane in
  415. // insertion order. Establish the active task before testing its isolation.
  416. await vi.waitFor(() => expect(pending).toHaveLength(1))
  417. const secondRun=runtime.send(owner, conversation.id, { requestId: randomUUID(), taskId: secondTask.id, content: '任务乙' })
  418. await vi.waitFor(() => expect(store.list<WorkspaceInteraction>(owner, 'interaction').filter(interaction => !interaction.resolved)).toHaveLength(1))
  419. const first=store.list<WorkspaceInteraction>(owner,'interaction').find(i=>i.conversationTaskId===firstTask.id)!
  420. expect(store.require<any>(owner, 'conversation-task', firstTask.id).activeRunStatus).toBe('waiting')
  421. expect(store.require<WorkspaceRun>(owner,'run',secondRun.id).status).toBe('queued')
  422. await runtime.respond(owner, first.id, 'once')
  423. pending[0]!.socket.send(JSON.stringify({method:'event',params:{type:'message.complete',session_id:pending[0]!.params.session_id,payload:{text:'任务甲完成',status:'complete'}}}))
  424. await finished(firstRun.id)
  425. await vi.waitFor(()=>expect(store.list<WorkspaceInteraction>(owner,'interaction')).toHaveLength(2))
  426. const second=store.list<WorkspaceInteraction>(owner,'interaction').find(i=>i.conversationTaskId===secondTask.id)!
  427. expect(second.id).not.toBe(first.id)
  428. expect(store.require<any>(owner, 'conversation-task', secondTask.id).activeRunStatus).toBe('waiting')
  429. await runtime.respond(owner,first.id,'once')
  430. expect(store.require<WorkspaceInteraction>(owner, 'interaction', first.id).resolved).toBe(true)
  431. expect(store.require<WorkspaceInteraction>(owner, 'interaction', second.id).resolved).toBe(false)
  432. await Promise.all([runtime.stopTask(owner, conversation.id, firstTask.id), runtime.stopTask(owner, conversation.id, secondTask.id)])
  433. })
  434. it('persists a visible failure when a fixed member loses its source node',async()=>{
  435. const a=agent('不可用成员'),c=direct(a.id)
  436. runtime.close()
  437. runtime=new WorkspaceRuntime(store,{requireSource: () => {},target:()=>{throw new Error('基础节点不可用')}} as unknown as WorkspaceNodes,uploads)
  438. const run=runtime.send(owner,c.id,{requestId:randomUUID(),content:'执行任务'})
  439. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('failed'))
  440. expect(store.messages(owner,c.id).at(-1)).toMatchObject({role:'system',status:'failed',content:'执行失败:基础节点不可用'})
  441. expect(store.require<WorkspaceConversation>(owner,'conversation',c.id).activeRunId).toBeUndefined()
  442. })
  443. it('interprets a reply against its launch-time roster when names or mode change mid-turn', async () => {
  444. const a = agent('管理员'),
  445. b = agent('执行者')
  446. const c = store.createGroup(owner, {
  447. name: '配置更新',
  448. memberIds: [a.id, b.id],
  449. administratorId: a.id,
  450. maxReplyRounds: 3,
  451. })
  452. let count = 0
  453. reply = (socket, p) => {
  454. let text = '已完成'
  455. if (count++ === 0) {
  456. store.updateAgent(owner, b.id, { name: '新名字', instructions: '新规则' })
  457. store.updateConversation(owner, c.id, { mode: 'free', administratorId: b.id })
  458. text = '请继续 @执行者'
  459. }
  460. setTimeout(
  461. () =>
  462. socket.send(
  463. JSON.stringify({
  464. method: 'event',
  465. params: { type: 'message.complete', session_id: p.session_id, payload: { text } },
  466. }),
  467. ),
  468. 5,
  469. )
  470. }
  471. await finished(runtime.send(owner, c.id, { requestId: randomUUID(), content: '开始' }).id)
  472. expect(
  473. store
  474. .messages(owner, c.id)
  475. .filter((m) => m.role === 'assistant')
  476. .map((m) => m.agentId),
  477. ).toEqual([a.id, b.id])
  478. expect(requests.filter((r) => r.method === 'prompt.submit').at(-1)!.params.text).toContain(
  479. '新规则',
  480. )
  481. })
  482. it('rejects unscoped and foreign-session completion events', async () => {
  483. const a = agent('会话隔离'),
  484. c = direct(a.id)
  485. reply = (socket, p) => {
  486. for (const session_id of [undefined, 'another-session'])
  487. socket.send(
  488. JSON.stringify({
  489. method: 'event',
  490. params: { type: 'message.complete', session_id, payload: { text: '别人的结果' } },
  491. }),
  492. )
  493. setTimeout(
  494. () =>
  495. socket.send(
  496. JSON.stringify({
  497. method: 'event',
  498. params: {
  499. type: 'message.complete',
  500. session_id: p.session_id,
  501. payload: { text: '本轮结果' },
  502. },
  503. }),
  504. ),
  505. 15,
  506. )
  507. }
  508. const run = runtime.send(owner, c.id, { requestId: randomUUID(), content: '执行' })
  509. await finished(run.id)
  510. expect(store.messages(owner, c.id).at(-1)?.content).toBe('本轮结果')
  511. expect(store.get<{ percent: number }>(owner, 'context', c.id)?.percent).toBe(40)
  512. })
  513. it('resumes a persisted uncertain run by inspecting history, without resending the prompt', async () => {
  514. const a = agent('恢复测试'),
  515. c = direct(a.id)
  516. reply = (socket, p) => {
  517. recoveryHistory = [
  518. { role: 'user', content: p.text },
  519. { role: 'assistant', content: '已经执行完成' },
  520. ]
  521. socket.terminate()
  522. }
  523. const run = runtime.send(owner, c.id, { requestId: randomUUID(), content: '修改一次' })
  524. await vi.waitFor(() =>
  525. expect(store.require<WorkspaceRun>(owner, 'run', run.id).status).toBe('uncertain'),
  526. )
  527. runtime.close()
  528. runtime = new WorkspaceRuntime(store, nodes, uploads)
  529. await runtime.reconcile(owner, run.id)
  530. await finished(run.id)
  531. expect(requests.filter((r) => r.method === 'prompt.submit')).toHaveLength(1)
  532. expect(store.messages(owner, c.id).map((m) => m.content)).toEqual(['修改一次', '已经执行完成'])
  533. })
  534. it('routes approvals and clarifications to the owning run and ignores optional push failures', async () => {
  535. const a = agent('交互测试'),
  536. c = direct(a.id)
  537. runtime.onNotify = () => {
  538. throw new Error('push storage unavailable')
  539. }
  540. let kind = 'approval'
  541. server.on('connection', (socket) =>
  542. socket.on('message', (data) => {
  543. const f = JSON.parse(String(data))
  544. if (['approval.respond', 'clarify.respond'].includes(f.method))
  545. socket.send(
  546. JSON.stringify({
  547. method: 'event',
  548. params: {
  549. type: 'message.complete',
  550. session_id: f.params.session_id,
  551. payload: { text: '确认后完成' },
  552. },
  553. }),
  554. )
  555. }),
  556. )
  557. reply = (socket, p) =>
  558. socket.send(
  559. JSON.stringify({
  560. method: 'event',
  561. params: {
  562. type: `${kind}.request`,
  563. session_id: p.session_id,
  564. payload: { request_id: 'upstream-interaction', message: '请确认' },
  565. },
  566. }),
  567. )
  568. for (kind of ['approval', 'clarify']) {
  569. const run = runtime.send(owner, c.id, { requestId: randomUUID(), content: '继续' })
  570. await vi.waitFor(() =>
  571. expect(store.require<WorkspaceRun>(owner, 'run', run.id).status).toBe('waiting'),
  572. )
  573. const interaction = store
  574. .list<WorkspaceInteraction>(owner, 'interaction')
  575. .find((i) => !i.resolved)!
  576. await expect(runtime.respond(other, interaction.id, 'once')).rejects.toThrow('记录不存在')
  577. await runtime.respond(owner, interaction.id, kind === 'approval' ? 'once' : '补充说明')
  578. await finished(run.id)
  579. expect(
  580. store.require<WorkspaceInteraction>(owner, 'interaction', interaction.id).resolved,
  581. ).toBe(true)
  582. }
  583. expect(
  584. requests
  585. .filter((r) => r.method === 'approval.respond' || r.method === 'clarify.respond')
  586. .map((r) => r.params.request_id),
  587. ).toEqual(['upstream-interaction', 'upstream-interaction'])
  588. })
  589. it('creates one direct chat without importing a native profile session', () => {
  590. const a = agent('编辑')
  591. expect(direct(a.id).kind).toBe('direct')
  592. expect(store.list(other, 'agent')).toEqual([])
  593. expect(() => store.require(other, 'conversation', direct(a.id).id)).toThrow('记录不存在')
  594. expect(requests).toEqual([])
  595. })
  596. it('protects the current administrator and validates member ownership', () => {
  597. const a = agent('甲'),
  598. b = agent('乙'),
  599. foreign = agent('外部', other)
  600. const c = store.createGroup(owner, {
  601. name: '团队',
  602. memberIds: [a.id, b.id],
  603. administratorId: a.id,
  604. })
  605. expect(() => store.updateConversation(owner, c.id, { memberIds: [b.id] })).toThrow('当前管理员不能移除')
  606. expect(() => store.updateConversation(owner, c.id, { memberIds: [b.id], administratorId: b.id })).toThrow('当前管理员不能移除')
  607. expect(() => store.updateConversation(owner, c.id, { memberIds: [a.id, foreign.id] })).toThrow('记录不存在')
  608. expect(() => store.updateConversation(owner, c.id, { memberIds: [a.id, a.id] })).toThrow('群成员或管理员无效')
  609. expect(() =>
  610. store.createGroup(owner, {
  611. name: '越权',
  612. memberIds: [a.id, foreign.id],
  613. administratorId: a.id,
  614. }),
  615. ).toThrow()
  616. store.updateAgent(owner, b.id, { archived: true })
  617. expect(store.require<WorkspaceConversation>(owner, 'conversation', c.id).memberIds).toEqual([
  618. a.id,
  619. b.id,
  620. ])
  621. expect(() =>
  622. store.createGroup(owner, { name: '新群', memberIds: [a.id, b.id], administratorId: a.id }),
  623. ).toThrow('已归档')
  624. expect(store.updateConversation(owner, c.id, { memberIds: [a.id] }).memberIds).toEqual([a.id])
  625. expect(() => store.updateConversation(owner, c.id, { memberIds: [a.id, b.id] })).toThrow('已归档')
  626. })
  627. it('adds and removes members, cleans automatic replies, and permits removal after administrator transfer', async () => {
  628. const a = agent('管理员'), b = agent('旧成员'), added = agent('新成员')
  629. const c = store.createGroup(owner, { name: '成员维护', memberIds: [a.id, b.id], administratorId: a.id, autoReplyIds: [b.id] })
  630. await finished(runtime.send(owner, c.id, { requestId: randomUUID(), content: '保留这条历史' }).id)
  631. const before = store.require<WorkspaceConversation>(owner, 'conversation', c.id)
  632. const history = store.messages(owner, c.id)
  633. const changed = store.updateConversation(owner, c.id, { memberIds: [a.id, added.id] })
  634. expect(changed.memberIds).toEqual([a.id, added.id])
  635. expect(changed.autoReplyIds).toEqual([])
  636. expect(changed.lastMessageAt).toBe(before.lastMessageAt)
  637. expect(store.messages(owner, c.id)).toEqual(history)
  638. expect(store.events(owner, 0).at(-1)).toMatchObject({ type: 'conversation.changed', data: { memberIds: [a.id, added.id] } })
  639. store.updateConversation(owner, c.id, { administratorId: added.id })
  640. expect(store.updateConversation(owner, c.id, { memberIds: [added.id] }).memberIds).toEqual([added.id])
  641. expect(() => store.updateConversation(owner, c.id, { memberIds: [a.id] })).toThrow('当前管理员不能移除')
  642. })
  643. it('skips removed queued members and accepts a newly added member on the next request', async () => {
  644. const a = agent('管理员'), b = agent('待回复'), added = agent('新成员')
  645. const c = store.createGroup(owner, { name: '队列维护', memberIds: [a.id, b.id], administratorId: a.id, mode: 'free', autoReplyIds: [a.id, b.id] })
  646. reply = (socket, p) => {
  647. socket.send(JSON.stringify({ method: 'event', params: { type: 'message.complete', session_id: p.session_id, payload: { text: '请@待回复继续' } } }))
  648. }
  649. const queued = runtime.send(owner, c.id, { requestId: randomUUID(), content: '开始' })
  650. store.updateConversation(owner, c.id, { memberIds: [a.id, added.id] })
  651. await finished(queued.id)
  652. expect(store.messages(owner, c.id).filter(m => m.role === 'assistant').map(m => m.agentId)).toEqual([a.id])
  653. await finished(runtime.send(owner, c.id, { requestId: randomUUID(), content: '请@新成员继续' }).id)
  654. expect(store.messages(owner, c.id).filter(m => m.role === 'assistant').map(m => m.agentId)).toEqual([a.id, added.id, a.id])
  655. })
  656. it('finishes an in-flight removed member reply without scheduling it again', async () => {
  657. const a = agent('管理员'), b = agent('正在回复')
  658. const c = store.createGroup(owner, { name: '回复中移除', memberIds: [a.id, b.id], administratorId: a.id })
  659. let turns = 0
  660. reply = (socket, p) => {
  661. if (turns++ === 1) store.updateConversation(owner, c.id, { memberIds: [a.id] })
  662. socket.send(JSON.stringify({ method: 'event', params: { type: 'message.complete', session_id: p.session_id, payload: { text: turns === 1 ? '请@正在回复继续' : '已完成' } } }))
  663. }
  664. await finished(runtime.send(owner, c.id, { requestId: randomUUID(), content: '@正在回复 开始' }).id)
  665. expect(store.messages(owner, c.id).filter(m => m.role === 'assistant').map(m => m.agentId)).toEqual([a.id, b.id, a.id])
  666. expect(() => runtime.send(owner, c.id, { requestId: randomUUID(), content: '继续', mentionIds: [b.id] })).toThrow('只能 @ 群内成员')
  667. })
  668. it('dispatches a request exactly once across retries and rejects payload reuse', async () => {
  669. const a = agent('编辑'),
  670. c = direct(a.id),
  671. requestId = randomUUID(),
  672. payload = { requestId, content: '检查代码' }
  673. const run = runtime.send(owner, c.id, payload)
  674. expect(runtime.send(owner, c.id, payload).id).toBe(run.id)
  675. expect(() => runtime.send(owner, c.id, { ...payload, content: '另一件事' })).toThrow('请求编号')
  676. await finished(run.id)
  677. expect(requests.filter((r) => r.method === 'prompt.submit')).toHaveLength(1)
  678. expect(store.messages(owner, c.id).map((m) => m.role)).toEqual(['user', 'assistant'])
  679. })
  680. it('keeps template responsibilities inside their group and follows Agent identity after rename', async () => {
  681. const a = agent('现有甲'), b = agent('现有乙')
  682. const before = store.require<WorkspaceAgent>(owner, 'agent', a.id)
  683. const c = store.createGroup(owner, { name: '模板群', memberIds: [a.id, b.id], administratorId: a.id, memberRoles: {
  684. [a.id]: { name: '调研负责人', description: '汇总可信结论' },
  685. [b.id]: { name: '事实核验', description: '交叉验证来源' },
  686. } })
  687. expect(store.require<WorkspaceAgent>(owner, 'agent', a.id)).toEqual(before)
  688. store.updateAgent(owner, a.id, { name: '改名后的甲' })
  689. await finished(runtime.send(owner, c.id, { requestId: randomUUID(), content: '开始调研' }).id)
  690. const groupPrompt = requests.filter(r => r.method === 'prompt.submit').at(-1)!.params.text
  691. expect(groupPrompt).toContain('@改名后的甲:调研负责人;汇总可信结论')
  692. expect(groupPrompt).toContain('@现有乙:事实核验;交叉验证来源')
  693. await finished(runtime.send(owner, direct(a.id).id, { requestId: randomUUID(), content: '单聊' }).id)
  694. expect(requests.filter(r => r.method === 'prompt.submit').at(-1)!.params.text).not.toContain('汇总可信结论')
  695. const changed = store.updateConversation(owner, c.id, { memberIds: [a.id] })
  696. expect(changed.memberRoles).toEqual({ [a.id]: { name: '调研负责人', description: '汇总可信结论' } })
  697. expect(() => store.updateConversation(owner, c.id, { memberRoles: { [b.id]: { name: '已移除', description: '' } } })).toThrow('群成员或管理员无效')
  698. })
  699. it('isolates roles over the same profile and applies edited rules on the next turn', async () => {
  700. const a = agent('编辑'),
  701. b = agent('审查'),
  702. ca = direct(a.id),
  703. cb = direct(b.id)
  704. const first = runtime.send(owner, ca.id, { requestId: randomUUID(), content: '编辑任务' })
  705. const second = runtime.send(owner, cb.id, { requestId: randomUUID(), content: '审查任务' })
  706. await Promise.all([finished(first.id), finished(second.id)])
  707. expect(requests.filter((r) => r.method === 'session.create')).toHaveLength(2)
  708. expect(store.messages(owner, ca.id).some((m) => m.content === '审查任务')).toBe(false)
  709. store.updateAgent(owner, a.id, { instructions: '最新规则:先给证据' })
  710. await finished(runtime.send(owner, ca.id, { requestId: randomUUID(), content: '继续' }).id)
  711. expect(requests.filter((r) => r.method === 'prompt.submit').at(-1)!.params.text).toContain(
  712. '最新规则:先给证据',
  713. )
  714. expect(requests.some((r) => r.method === 'profiles.configure')).toBe(false)
  715. expect(
  716. requests
  717. .filter((r) => r.method === 'session.create')
  718. .every((r) => r.params.hidden && r.params.source === 'yaoyao_workspace'),
  719. ).toBe(true)
  720. })
  721. it('executes bounded administrator delegation and keeps group sessions separate from direct chat', async () => {
  722. const a = agent('管理员'),
  723. b = agent('开发者'),
  724. c = store.createGroup(owner, {
  725. name: '开发',
  726. memberIds: [a.id, b.id],
  727. administratorId: a.id,
  728. maxReplyRounds: 3,
  729. })
  730. let turns = 0
  731. reply = (socket, p) => {
  732. const text = turns++ === 0 ? '我已整理需求,现在请@开发者继续实现。' : '任务完成'
  733. setTimeout(
  734. () =>
  735. socket.send(
  736. JSON.stringify({
  737. method: 'event',
  738. params: { type: 'message.complete', session_id: p.session_id, payload: { text } },
  739. }),
  740. ),
  741. 5,
  742. )
  743. }
  744. await finished(runtime.send(owner, c.id, { requestId: randomUUID(), content: '实现功能' }).id)
  745. expect(
  746. store
  747. .messages(owner, c.id)
  748. .filter((m) => m.role === 'assistant')
  749. .map((m) => m.agentId),
  750. ).toEqual([a.id, b.id, a.id])
  751. expect(store.messages(owner, direct(a.id).id)).toHaveLength(0)
  752. expect(turns).toBe(3)
  753. })
  754. it('does not trigger delegation from quoted text or code blocks', () => {
  755. const a = agent('甲'),
  756. b = agent('乙')
  757. expect(mentionedAgents('> @乙\n```\n@甲\n```\n`@all`', [a, b])).toEqual([])
  758. expect(mentionedAgents('请处理 @乙。', [a, b])).toEqual([b.id])
  759. })
  760. it('keeps uncertain submissions blocked instead of executing tools twice', async () => {
  761. const a = agent('执行者'),
  762. c = direct(a.id)
  763. reply = (socket) => socket.terminate()
  764. const run = runtime.send(owner, c.id, { requestId: randomUUID(), content: '修改文件' })
  765. await vi.waitFor(() =>
  766. expect(store.require<WorkspaceRun>(owner, 'run', run.id).status).toBe('uncertain'),
  767. )
  768. const queued = runtime.send(owner, c.id, { requestId: randomUUID(), content: '后续请求' })
  769. expect(queued.status).toBe('queued')
  770. expect(requests.filter((r) => r.method === 'prompt.submit')).toHaveLength(1)
  771. await runtime.stopConversation(owner, c.id)
  772. expect(
  773. store.require<WorkspaceConversation>(owner, 'conversation', c.id).activeRunId,
  774. ).toBeUndefined()
  775. })
  776. it('publishes durable ordered events only after commit and rolls back failed transactions', () => {
  777. const a = agent('甲'),
  778. c = direct(a.id),
  779. cursor = store.cursor(owner)
  780. expect(() =>
  781. store.atomic(() => {
  782. store.event(owner, 'test', {})
  783. throw new Error('rollback')
  784. }),
  785. ).toThrow('rollback')
  786. expect(store.cursor(owner)).toBe(cursor)
  787. store.updateAgent(owner, a.id, { name: '乙' })
  788. expect(store.events(owner, cursor).map((e) => e.type)).toEqual([
  789. 'agent.changed',
  790. 'conversation.changed',
  791. ])
  792. expect(store.events(other, 0)).toEqual([])
  793. expect(store.require<WorkspaceConversation>(owner, 'conversation', c.id).name).toBe('乙')
  794. })
  795. })
  796. it('pins a direct chat without injecting or clearing optional profile fields', () => {
  797. const avatar = encodeAgentAvatar({...defaultAgentIdentity('reviewer'),shape:'triangle',color:'#00b9ac',expression:'curious'})
  798. const agent = store.createAgent(owner, { name: '审查员', profile: 'default', avatar })
  799. const chat = store.list<WorkspaceConversation>(owner, 'conversation')[0]!
  800. expect(store.updateConversation(owner, chat.id, { pinned: true }).pinned).toBe(true)
  801. expect(store.updateConversation(owner, chat.id, { pinned: false }).pinned).toBe(false)
  802. expect(store.require<WorkspaceConversation>(owner, 'conversation', chat.id).avatar).toBe(avatar)
  803. expect(store.updateAgent(owner, agent.id, { instructions: '请检查边界' }).avatar).toBe(avatar)
  804. expect(decodeAgentAvatar(store.updateAgent(owner, agent.id, { avatar: '' }).avatar)).toMatchObject({shape:'circle',color:'#00c875'})
  805. })
  806. it('accepts the existing cross-client mascot and team avatar formats', () => {
  807. const a = store.createAgent(owner, { name: '设计', profile: 'default', avatar: 'yaoyao-mascot:v1:square:377fe6:friendly' })
  808. const b = store.createAgent(owner, { name: '开发', profile: 'default' })
  809. const group = store.createGroup(owner, { name: '产品团队', memberIds: [a.id, b.id], administratorId: a.id, avatar: 'builtin:team-animal:fox' })
  810. expect(group.avatar).toBe('builtin:team-animal:fox')
  811. expect(() => store.updateAgent(owner, a.id, { avatar: 'javascript:alert(1)' })).toThrow()
  812. })
  813. it('assigns a random v2 avatar only when a new Agent omits its avatar', () => {
  814. const random = vi.spyOn(Math, 'random').mockReturnValue(0)
  815. const created = store.createAgent(owner, { name: '随机头像', profile: 'default' })
  816. expect(decodeAgentAvatar(created.avatar)).toMatchObject({ shape: 'circle', color: '#000000', expression: 'idle' })
  817. expect(created.avatar).not.toBe(encodeAgentAvatar(defaultAgentIdentity(created.id, created.name)))
  818. random.mockRestore()
  819. })
  820. it('keeps the last message time independent of pinning and metadata edits', () => {
  821. const agent=store.createAgent(owner,{name:'时间验收',profile:'default'})
  822. const c=store.list<WorkspaceConversation>(owner,'conversation')[0]!
  823. const message: WorkspaceMessage={id:randomUUID(),conversationId:c.id,seq:0,role:'user',content:'历史消息',reasoning:'',status:'complete',attachments:[],tools:[],createdAt:Date.now()-600_000}
  824. store.saveMessage(owner,message)
  825. const pinned=store.updateConversation(owner,c.id,{pinned:true})
  826. expect(store.conversationSummary(owner,pinned).lastMessageAt).toBe(message.createdAt)
  827. store.updateAgent(owner,agent.id,{name:'改名之后'})
  828. expect(store.conversationSummary(owner,store.require(owner,'conversation',c.id)).lastMessageAt).toBe(message.createdAt)
  829. // Existing records without the summary field still use their transcript time.
  830. const previous={...pinned}; delete previous.lastMessageAt
  831. expect(store.conversationSummary(owner,previous).lastMessageAt).toBe(message.createdAt)
  832. })
  833. it('publishes only the executing member and clears avatar activity when stopped', async () => {
  834. const a=store.createAgent(owner,{name:'管理员头像',profile:'default'})
  835. const b=store.createAgent(owner,{name:'执行者头像',profile:'default'})
  836. const c=store.createGroup(owner,{name:'头像状态',memberIds:[a.id,b.id],administratorId:a.id})
  837. reply=(socket,p)=>socket.send(JSON.stringify({method:'event',params:{type:'approval.request',session_id:p.session_id,payload:{request_id:'approval-avatar',message:'需要确认'}}}))
  838. const run=runtime.send(owner,c.id,{requestId:randomUUID(),content:'处理任务',mentionIds:[b.id]})
  839. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('waiting'))
  840. expect(store.require<WorkspaceConversation>(owner,'conversation',c.id)).toMatchObject({activeAgentId:a.id,activeRunStatus:'waiting'})
  841. await runtime.stop(owner,run.id)
  842. const stopped=store.require<WorkspaceConversation>(owner,'conversation',c.id)
  843. expect(stopped.activeAgentId).toBeUndefined()
  844. expect(stopped.activeRunStatus).toBeUndefined()
  845. })
  846. it('stores the expanded avatar shapes for cross-client role settings', () => {
  847. const agent=store.createAgent(owner,{name:'扩展头像验收',profile:'default'})
  848. for(const shape of ['ellipse','capsule','hexagon','cloud','droplet'] as const) {
  849. const avatar=encodeAgentAvatar({...defaultAgentIdentity('bot'),shape,color:'#f52ba5',expression:'curious'})
  850. expect(store.updateAgent(owner,agent.id,{avatar}).avatar).toBe(avatar)
  851. }
  852. })
  853. it('automatically reconciles lost completion without submitting the prompt again', async () => {
  854. const a=agent('自动恢复'),c=direct(a.id)
  855. reply=(socket,p)=>{recoveryHistory=[{role:'user',content:p.text},{role:'assistant',content:'已完成一次'}];socket.terminate()}
  856. const run=runtime.send(owner,c.id,{requestId:randomUUID(),content:'执行一次'})
  857. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('complete'),{timeout:5000})
  858. expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(1)
  859. expect(store.messages(owner,c.id).at(-1)).toMatchObject({content:'已完成一次',status:'complete'})
  860. })
  861. it('reports a rejected prompt as failed instead of leaving it uncertain', async () => {
  862. rejectPrompt='本轮请求被明确拒绝'
  863. const a=agent('拒绝测试'),c=direct(a.id)
  864. const run=runtime.send(owner,c.id,{requestId:randomUUID(),content:'执行'})
  865. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('failed'))
  866. expect(store.require<WorkspaceConversation>(owner,'conversation',c.id).activeRunId).toBeUndefined()
  867. expect(store.messages(owner,c.id).some(m=>m.status==='uncertain')).toBe(false)
  868. })
  869. it('keeps an unconfirmed turn separate from a later reply and retries history without resubmitting', async () => {
  870. const a = agent('恢复边界'), c = direct(a.id)
  871. let prompt = ''
  872. reply = (socket, p) => {
  873. prompt = p.text
  874. recoveryHistory = [{ role: 'user', content: prompt }, { role: 'user', content: '另一个请求' }, { role: 'assistant', content: '另一轮回复' }]
  875. socket.terminate()
  876. }
  877. const run = runtime.send(owner, c.id, { requestId: randomUUID(), content: '只执行一次' })
  878. await vi.waitFor(() => expect(requests.filter(r => r.method === 'session.resume').length).toBeGreaterThanOrEqual(1), { timeout: 3000 })
  879. expect(store.require<WorkspaceRun>(owner, 'run', run.id).status).toBe('uncertain')
  880. expect(store.messages(owner, c.id).at(-1)?.content).not.toBe('另一轮回复')
  881. recoveryHistory = [{ role: 'user', content: prompt }, { role: 'assistant', content: '本轮结果' }]
  882. await vi.waitFor(() => expect(store.require<WorkspaceRun>(owner, 'run', run.id).status).toBe('complete'), { timeout: 4000 })
  883. expect(requests.filter(r => r.method === 'prompt.submit')).toHaveLength(1)
  884. expect(store.messages(owner, c.id).at(-1)).toMatchObject({ content: '本轮结果', status: 'complete' })
  885. })
  886. it('uses one generated avatar for the agent and its direct conversation',()=>{
  887. const a=agent('默认头像'),c=direct(a.id)
  888. expect(decodeAgentAvatar(a.avatar)).toMatchObject({version:2,avatarMode:'mascot'})
  889. expect(store.agentSummary(a).avatar).toBe(store.conversationSummary(owner,c).avatar)
  890. const changed=store.updateAgent(owner,a.id,{name:'换个名称'})
  891. expect(store.agentSummary(changed).avatar).toBe(store.agentSummary(a).avatar)
  892. })
  893. it('recognizes mentions inside Chinese sentences and ignores quoted code and emails',()=>{
  894. const a=agent('瑶儿'),b=agent('竹儿'),c=agent('瑶儿助手'),d=agent('Ann')
  895. const roster=[a,b,c,d]
  896. expect(mentionedAgents('先整理,再请@瑶儿帮忙,最后由(@竹儿)复核。',roster)).toEqual([a.id,b.id])
  897. expect(mentionedAgents('请**@瑶儿助手**继续',roster)).toEqual([c.id])
  898. expect(mentionedAgents('mail@Ann.com https://site/@竹儿 `@瑶儿` <quoted_message>@瑶儿</quoted_message>',roster)).toEqual([])
  899. expect(mentionedAgents('@Anna',roster)).toEqual([])
  900. })
  901. function completeReply(socket: WebSocket, p: Record<string, any>, text: string, status = 'complete') {
  902. socket.send(JSON.stringify({ method: 'event', params: { type: 'message.complete', session_id: p.session_id, payload: { text, status, ...(status === 'failed' ? { error: text } : {}) } } }))
  903. }
  904. const speaker = (p: Record<string, any>) => /^你是 (.*?)。/.exec(p.text)?.[1]
  905. describe('automatic relay termination', () => {
  906. const receipt = '收到 @审核 终审维持通过。\n\n交付状态(不变):入草稿箱 Media ID `draft-123`,发布目录 `/work/final/`。\n等民哥群发指令。'
  907. const review = '@竹儿 维持通过,等民哥群发指令。无新事实、无新派工,本轮不重复核验。'
  908. const promptCount = () => requests.filter(r => r.method === 'prompt.submit').length
  909. async function idle(runId: string, conversationId: string) {
  910. await finished(runId)
  911. const count = promptCount()
  912. await new Promise(resolve => setTimeout(resolve, 80))
  913. expect(promptCount()).toBe(count)
  914. expect(store.list<Work>(owner, 'turn').filter(w => w.runId === runId && (!w.planned || !['complete', 'failed', 'interrupted'].includes(w.status)))).toEqual([])
  915. const summary = store.conversationSummary(owner, store.require(owner, 'conversation', conversationId))
  916. expect(summary.activeRunId).toBeUndefined()
  917. expect(summary.queuedMessageCount ?? 0).toBe(0)
  918. }
  919. it('finishes the observed host receipt cycle in exactly three calls and accepts a new user message', async () => {
  920. const a = agent('竹儿'), b = agent('审核')
  921. const g = store.createGroup(owner, { name: '收尾验收', memberIds: [a.id, b.id], administratorId: a.id, maxReplyRounds: -1 })
  922. let admin = 0
  923. reply = (socket, p) => completeReply(socket, p, speaker(p) === a.name ? admin++ === 0 ? '@审核 请审核文案' : receipt : review)
  924. const root = runtime.send(owner, g.id, { requestId: randomUUID(), content: '请完成文案' })
  925. await idle(root.id, g.id)
  926. expect(promptCount()).toBe(3)
  927. expect(store.messages(owner, g.id).map(m => m.content)).toEqual(['请完成文案', '@审核 请审核文案', review, receipt])
  928. expect(store.list<Work>(owner, 'turn').find(w => w.reviewOf)?.relay?.suppressed[b.id]).toBe('acknowledgement')
  929. const next = runtime.send(owner, g.id, { requestId: randomUUID(), content: '好的。' })
  930. await idle(next.id, g.id)
  931. expect(promptCount()).toBe(4)
  932. })
  933. it.each(['感谢 @审核,等待用户指令。', '收到,等待用户指令。'])('does not turn a free-mode receipt into an automatic broadcast: %s', content => {
  934. const a = agent('竹儿'), b = agent('审核')
  935. const g = store.createGroup(owner, { name: '自动收尾', memberIds: [a.id, b.id], administratorId: a.id, mode: 'free', autoReplyIds: [b.id], maxReplyRounds: -1 })
  936. reply = (socket, p) => completeReply(socket, p, content)
  937. return (async () => {
  938. const root = runtime.send(owner, g.id, { requestId: randomUUID(), content: '@竹儿 请处理' })
  939. await idle(root.id, g.id)
  940. expect(promptCount()).toBe(1)
  941. const next = runtime.send(owner, g.id, { requestId: randomUUID(), content: '感谢 @审核' })
  942. await idle(next.id, g.id)
  943. expect(speaker(requests.filter(r => r.method === 'prompt.submit').at(-1)!.params)).toBe(b.name)
  944. })()
  945. })
  946. it.each([
  947. ['host', 2, 5], ['host', 3, 7], ['free', 2, 4], ['free', 3, 5],
  948. ] as const)('bounds unchanged %s cooperation with %i members at %i calls', async (mode, size, expected) => {
  949. const members = [agent('甲'), agent('乙'), ...(size === 3 ? [agent('丙')] : [])]
  950. const g = store.createGroup(owner, { name: '重复往返', memberIds: members.map(a => a.id), administratorId: members[0]!.id, mode, maxReplyRounds: -1 })
  951. let count = 0
  952. reply = (socket, p) => {
  953. const index = members.findIndex(a => a.name === speaker(p)), whitespace = count++ % 2 ? ' ' : ' '
  954. const text = mode === 'host' ? index === 0 ? members.slice(1).map(a => `@${a.name}${whitespace}再检查一次`).join(',') : '检查结果相同'
  955. : `@${members[(index + 1) % members.length]!.name}${whitespace}再检查一次`
  956. completeReply(socket, p, count > 20 ? '完成' : text)
  957. completeReply(socket, p, count > 20 ? '完成' : text) // Duplicate completion must not double-plan.
  958. }
  959. const root = runtime.send(owner, g.id, { requestId: randomUUID(), content: '@甲 开始' })
  960. await idle(root.id, g.id)
  961. expect(count).toBe(expected)
  962. expect(store.messages(owner, g.id).filter(m => m.content === REPEATED_RELAY_NOTICE)).toHaveLength(1)
  963. expect(store.list<Work>(owner, 'turn').some(w => Object.values(w.relay?.suppressed ?? {}).includes('repeated'))).toBe(true)
  964. })
  965. it.each(['result', 'tool', 'attachment', 'interaction'] as const)('allows repeated instructions with new %s evidence beyond thirteen calls', async kind => {
  966. const a = agent('负责人'), b = agent('审核')
  967. const g = store.createGroup(owner, { name: '持续推进', memberIds: [a.id, b.id], administratorId: a.id, maxReplyRounds: -1 })
  968. let admin = 0, worker = 0
  969. reply = (socket, p) => {
  970. if (speaker(p) === a.name) completeReply(socket, p, admin++ < 7 ? '感谢 @审核,请重新检查第二段' : '完成')
  971. else { worker++; completeReply(socket, p, kind === 'result' ? `第 ${worker} 版结果` : '本次检查结果') }
  972. }
  973. runtime.onMessage = async (_owner, message) => {
  974. if (message.agentId !== b.id) return
  975. if (kind === 'tool') message.tools = [{ id: randomUUID(), name: 'verify', status: 'tool.complete', result: { revision: worker } }]
  976. if (kind === 'attachment') message.attachments = [{ id: randomUUID(), name: 'revision.txt', mimeType: 'text/plain', size: 10, createdAt: Date.now() }]
  977. if (kind === 'interaction') {
  978. const id = randomUUID()
  979. store.put(owner, 'interaction', id, { id, runId: message.runId, conversationId: g.id, conversationTaskId: message.conversationTaskId, agentId: b.id, kind: 'clarification', message: '继续重试?', answer: '继续', responseState: 'sent', resolved: true })
  980. store.put(owner, 'interaction-binding', id, { taskId: message.taskId })
  981. }
  982. store.saveMessage(owner, message)
  983. }
  984. const root = runtime.send(owner, g.id, { requestId: randomUUID(), content: '反复改进' })
  985. await idle(root.id, g.id)
  986. expect(promptCount()).toBe(15)
  987. expect(store.messages(owner, g.id).some(m => m.content === REPEATED_RELAY_NOTICE)).toBe(false)
  988. })
  989. it('ends only the repeating free-mode branch while an independent member finishes', async () => {
  990. const a = agent('甲'), b = agent('乙'), c = agent('独立成员')
  991. const g = store.createGroup(owner, { name: '分支隔离', memberIds: [a.id, b.id, c.id], administratorId: a.id, mode: 'free', maxReplyRounds: -1 })
  992. let pending: { socket: WebSocket; p: Record<string, any> } | undefined, loops = 0
  993. reply = (socket, p) => {
  994. if (speaker(p) === c.name) { pending = { socket, p }; return }
  995. completeReply(socket, p, ++loops > 15 ? '完成' : speaker(p) === a.name ? '@乙 再检查' : '@甲 再检查')
  996. }
  997. const root = runtime.send(owner, g.id, { requestId: randomUUID(), content: '@甲 @独立成员 请分别处理' })
  998. await vi.waitFor(() => expect(store.messages(owner, g.id).some(m => m.content === REPEATED_RELAY_NOTICE)).toBe(true))
  999. expect(store.require<WorkspaceRun>(owner, 'run', root.id).status).toBe('running')
  1000. expect(store.list<Work>(owner, 'turn').filter(w => w.cancelRequested)).toEqual([])
  1001. expect(loops).toBe(4)
  1002. completeReply(pending!.socket, pending!.p, '完成')
  1003. await idle(root.id, g.id)
  1004. expect(store.messages(owner, g.id).find(m => m.agentId === c.id)).toMatchObject({ content: '完成', status: 'complete' })
  1005. })
  1006. it('checks each target independently when a new automatic member joins the same repeated reply', async () => {
  1007. const a = agent('甲'), b = agent('乙'), c = agent('丙')
  1008. const g = store.createGroup(owner, { name: '目标隔离', memberIds: [a.id, b.id, c.id], administratorId: a.id, mode: 'free', autoReplyIds: [a.id], maxReplyRounds: -1 })
  1009. let calls = 0, admin = 0
  1010. reply = (socket, p) => { calls++; completeReply(socket, p, calls > 15 ? '完成' : speaker(p) === a.name ? '@乙 再检查' : speaker(p) === b.name ? '继续检查' : '完成') }
  1011. runtime.onMessage = async (_owner, message) => {
  1012. if (message.agentId === b.id && ++admin === 2) store.updateConversation(owner, g.id, { autoReplyIds: [a.id, c.id] })
  1013. }
  1014. const root = runtime.send(owner, g.id, { requestId: randomUUID(), content: '@甲 开始' })
  1015. await idle(root.id, g.id)
  1016. expect(calls).toBe(5)
  1017. const blocked = store.list<Work>(owner, 'turn').find(w => w.relay?.suppressed[a.id] === 'repeated')!
  1018. expect(blocked.relay?.suppressed[c.id]).toBeUndefined()
  1019. expect(store.list<Work>(owner, 'turn').find(w => w.batchId === blocked.id && w.agentId === c.id)?.status).toBe('complete')
  1020. })
  1021. it('starts fresh for identical new requests in the same task and a different task', async () => {
  1022. const a = agent('甲'), b = agent('乙')
  1023. const g = store.createGroup(owner, { name: '任务隔离', memberIds: [a.id, b.id], administratorId: a.id, mode: 'free', maxReplyRounds: -1 })
  1024. let calls = 0
  1025. reply = (socket, p) => { calls++; completeReply(socket, p, calls > 20 ? '完成' : speaker(p) === a.name ? '@乙 再检查' : '@甲 再检查') }
  1026. const originalTask = defaultTask(g.id), secondTask = store.createTask(owner, g.id, { title: '另一个任务' })
  1027. for (const [index, task] of [originalTask, originalTask, secondTask].entries()) {
  1028. const root = runtime.send(owner, g.id, { requestId: randomUUID(), taskId: task.id, content: '@甲 开始' })
  1029. await idle(root.id, g.id)
  1030. expect(calls).toBe((index + 1) * 4)
  1031. expect(store.messages(owner, g.id, Number.MAX_SAFE_INTEGER, 100, false, task.id).filter(m => m.runId === root.id && m.content === REPEATED_RELAY_NOTICE)).toHaveLength(1)
  1032. }
  1033. })
  1034. it.each([false, true])('recovers repeat suppression across restart before planning (legacy=%s)', async legacy => {
  1035. const a = agent('负责人'), b = agent('成员')
  1036. const g = store.createGroup(owner, { name: '循环恢复', memberIds: [a.id, b.id], administratorId: a.id, maxReplyRounds: -1 })
  1037. let calls = 0
  1038. reply = (socket, p) => { calls++; completeReply(socket, p, calls > 15 ? '完成' : speaker(p) === a.name ? '@成员 再检查' : '结果相同') }
  1039. runtime.onMessage = async () => { if (calls === 5) runtime.close() }
  1040. const root = runtime.send(owner, g.id, { requestId: randomUUID(), content: '开始' })
  1041. await vi.waitFor(() => expect(store.list<Work>(owner, 'turn').filter(w => w.status === 'complete')).toHaveLength(5))
  1042. if (legacy) for (const work of store.list<Work>(owner, 'turn')) { delete work.relay; store.put(owner, 'turn', work.id, work) }
  1043. runtime = new WorkspaceRuntime(store, nodes, uploads); runtime.start()
  1044. await idle(root.id, g.id)
  1045. expect(calls).toBe(5)
  1046. expect(store.messages(owner, g.id).filter(m => m.content === REPEATED_RELAY_NOTICE)).toHaveLength(1)
  1047. runtime.close(); runtime = new WorkspaceRuntime(store, nodes, uploads); runtime.start()
  1048. await idle(root.id, g.id)
  1049. expect(calls).toBe(5)
  1050. expect(store.messages(owner, g.id).filter(m => m.content === REPEATED_RELAY_NOTICE)).toHaveLength(1)
  1051. })
  1052. })
  1053. it('starts with the administrator, executes a batch concurrently, and reviews failures once without spending a round', async () => {
  1054. const a = agent('管理员'), b = agent('成员乙'), c = agent('成员丙')
  1055. const group = store.createGroup(owner, { name: '并行验收', memberIds: [a.id,b.id,c.id], administratorId: a.id, maxReplyRounds: 2 })
  1056. const pending = new Map<string, { socket: WebSocket; p: Record<string, any> }>()
  1057. let admin = 0, review = ''
  1058. reply = (socket,p) => {
  1059. if (speaker(p) === a.name) { if (admin++ === 0) completeReply(socket,p,'请@成员乙检查接口,@成员丙检查数据'); else { review=p.text; completeReply(socket,p,'汇总完成') } }
  1060. else pending.set(speaker(p)!, {socket,p})
  1061. }
  1062. const root = runtime.send(owner,group.id,{requestId:randomUUID(),content:'@成员乙 请处理'})
  1063. await vi.waitFor(()=>expect(pending.size).toBe(2))
  1064. expect(admin).toBe(1)
  1065. expect(store.require<WorkspaceConversation>(owner,'conversation',group.id).activeAgentStates).toEqual({[b.id]:'running',[c.id]:'running'})
  1066. completeReply(pending.get(c.name)!.socket,pending.get(c.name)!.p,'数据检查失败','failed')
  1067. await vi.waitFor(()=>expect(store.messages(owner,group.id).some(m=>m.status==='failed')).toBe(true))
  1068. expect(admin).toBe(1)
  1069. completeReply(pending.get(b.name)!.socket,pending.get(b.name)!.p,'接口正常')
  1070. await finished(root.id)
  1071. expect(admin).toBe(2)
  1072. expect(review).toContain('数据检查失败')
  1073. expect(review).toContain('接口正常')
  1074. expect(store.require<WorkspaceRun>(owner,'run',root.id).round).toBe(1)
  1075. })
  1076. it('allows successive dependent delegations and does not charge administrator reviews against the limit', async () => {
  1077. const a=agent('负责人'),b=agent('先做'),c=agent('后做')
  1078. const g=store.createGroup(owner,{name:'串行依赖',memberIds:[a.id,b.id,c.id],administratorId:a.id,maxReplyRounds:3})
  1079. const order:string[]=[];let admin=0
  1080. reply=(socket,p)=>{const name=speaker(p)!;order.push(name);completeReply(socket,p,name===a.name ? ['@先做 第一步','@后做 第二步','完成'][admin++]! : '结果已提交')}
  1081. await finished(runtime.send(owner,g.id,{requestId:randomUUID(),content:'执行'}).id)
  1082. expect(order).toEqual([a.name,b.name,a.name,c.name,a.name])
  1083. })
  1084. it('continues free discussion automatically and hides irrelevant automatic replies', async () => {
  1085. const a=agent('公开管理员'),b=agent('自动成员')
  1086. const g=store.createGroup(owner,{name:'自由讨论',memberIds:[a.id,b.id],administratorId:a.id,mode:'free',autoReplyIds:[b.id],maxReplyRounds:3})
  1087. reply=(socket,p)=>completeReply(socket,p,speaker(p)===a.name?'我来回答':'[[YAOYAO_NO_REPLY_V1]]')
  1088. const root=runtime.send(owner,g.id,{requestId:randomUUID(),content:'一个问题'})
  1089. await finished(root.id)
  1090. expect(requests.filter(r=>r.method==='prompt.submit'&&speaker(r.params)===b.name)).toHaveLength(2)
  1091. expect(store.messages(owner,g.id).map(m=>m.content)).toEqual(['一个问题','我来回答'])
  1092. expect(store.conversationSummary(owner,store.require(owner,'conversation',g.id)).unreadCount).toBe(2)
  1093. expect(store.hiddenMessageIds(owner,g.id)).toHaveLength(2)
  1094. })
  1095. it('requires a public administrator reply when the model tries to remain silent', async () => {
  1096. const a=agent('必须答复'),b=agent('旁观者'),g=store.createGroup(owner,{name:'公开答复',memberIds:[a.id,b.id],administratorId:a.id})
  1097. reply=(socket,p)=>completeReply(socket,p,'[[YAOYAO_NO_REPLY_V1]]')
  1098. await finished(runtime.send(owner,g.id,{requestId:randomUUID(),content:'帮助我'}).id)
  1099. const answer=store.messages(owner,g.id).at(-1)!
  1100. expect(answer.content).toContain('请补充具体目标')
  1101. expect(answer.content).not.toContain('YAOYAO_')
  1102. })
  1103. it('queues later user requests durably without leaking them into the preceding collaboration', async () => {
  1104. const a=agent('排队管理员'),b=agent('排队执行者'),g=store.createGroup(owner,{name:'消息排队',memberIds:[a.id,b.id],administratorId:a.id})
  1105. let first: {socket:WebSocket;p:Record<string,any>}|undefined
  1106. let admin=0;const prompts:string[]=[]
  1107. reply=(socket,p)=>{prompts.push(p.text);if(speaker(p)===a.name&&admin++===0)first={socket,p};else completeReply(socket,p,'已完成')}
  1108. const one=runtime.send(owner,g.id,{requestId:randomUUID(),content:'第一条任务'})
  1109. await vi.waitFor(()=>expect(first).toBeDefined())
  1110. const two=runtime.send(owner,g.id,{requestId:randomUUID(),content:'之后才处理的秘密标记'})
  1111. expect(two.status).toBe('queued')
  1112. expect(store.require<WorkspaceConversation>(owner,'conversation',g.id).activeRunId).toBe(one.id)
  1113. completeReply(first!.socket,first!.p,'请@排队执行者继续')
  1114. await finished(one.id);await finished(two.id)
  1115. expect(prompts).toHaveLength(4)
  1116. expect(prompts.slice(0,3).every(p=>!p.includes('之后才处理的秘密标记'))).toBe(true)
  1117. expect(prompts[3]).toContain('之后才处理的秘密标记')
  1118. })
  1119. it('stops one member while keeping its sibling running and returns the interrupted result for review', async () => {
  1120. const a=agent('停止管理员'),b=agent('停止乙'),c=agent('继续丙'),g=store.createGroup(owner,{name:'独立停止',memberIds:[a.id,b.id,c.id],administratorId:a.id})
  1121. const pending=new Map<string,{socket:WebSocket;p:Record<string,any>}>();let admin=0,review=''
  1122. reply=(socket,p)=>{if(speaker(p)===a.name){if(admin++===0)completeReply(socket,p,'@停止乙 @继续丙');else {review=p.text;completeReply(socket,p,'已汇总')}}else pending.set(speaker(p)!,{socket,p})}
  1123. const root=runtime.send(owner,g.id,{requestId:randomUUID(),content:'开始'})
  1124. await vi.waitFor(()=>expect(pending.size).toBe(2))
  1125. await runtime.stopAgent(owner,g.id,b.id)
  1126. expect(admin).toBe(1)
  1127. expect(store.require<WorkspaceConversation>(owner,'conversation',g.id).activeAgentStates?.[c.id]).toBe('running')
  1128. completeReply(pending.get(c.name)!.socket,pending.get(c.name)!.p,'丙完成')
  1129. await finished(root.id)
  1130. expect(review).toContain('interrupted')
  1131. expect(review).toContain('丙完成')
  1132. })
  1133. it('recovers a partially completed parallel batch after restart without resubmitting members or duplicating review', async () => {
  1134. const a=agent('恢复管理员'),b=agent('恢复乙'),c=agent('恢复丙'),g=store.createGroup(owner,{name:'批次恢复',memberIds:[a.id,b.id,c.id],administratorId:a.id})
  1135. let admin=0;const pending=new Map<string,{socket:WebSocket;p:Record<string,any>}>()
  1136. reply=(socket,p)=>{if(speaker(p)===a.name)completeReply(socket,p,admin++===0?'@恢复乙 @恢复丙':'复核完成');else pending.set(speaker(p)!,{socket,p})}
  1137. const root=runtime.send(owner,g.id,{requestId:randomUUID(),content:'并行执行'})
  1138. await vi.waitFor(()=>expect(pending.size).toBe(2))
  1139. completeReply(pending.get(b.name)!.socket,pending.get(b.name)!.p,'乙已完成')
  1140. const last=pending.get(c.name)!
  1141. recoveryByStored.set(storedByRuntime.get(last.p.session_id)!,[{role:'user',content:last.p.text},{role:'assistant',content:'丙已完成'}])
  1142. last.socket.terminate()
  1143. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',root.id).status).toBe('uncertain'))
  1144. runtime.close();runtime=new WorkspaceRuntime(store,nodes,uploads);runtime.start()
  1145. await finished(root.id)
  1146. expect(admin).toBe(2)
  1147. expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(4)
  1148. expect(store.list<any>(owner,'turn').filter(t=>t.reviewOf)).toHaveLength(1)
  1149. })
  1150. it('persists the next delegation when restarted between reply completion and cascade planning', async () => {
  1151. const a=agent('记录管理员'),b=agent('记录成员'),g=store.createGroup(owner,{name:'计划恢复',memberIds:[a.id,b.id],administratorId:a.id})
  1152. let admin=0
  1153. reply=(socket,p)=>completeReply(socket,p,speaker(p)===a.name&&admin++===0?'@记录成员 执行':'完成')
  1154. runtime.onMessage=async()=>{runtime.close()}
  1155. const root=runtime.send(owner,g.id,{requestId:randomUUID(),content:'执行一次'})
  1156. await vi.waitFor(()=>expect(store.list<any>(owner,'turn')[0]?.status).toBe('complete'))
  1157. runtime=new WorkspaceRuntime(store,nodes,uploads);runtime.start()
  1158. await finished(root.id)
  1159. expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(3)
  1160. })
  1161. it('bounds long context while preserving the task beginning and ending', async () => {
  1162. const a=agent('长上下文'),b=agent('辅助'),g=store.createGroup(owner,{name:'上下文边界',memberIds:[a.id,b.id],administratorId:a.id})
  1163. const text='任务开始标记'+ '文'.repeat(60_000)+'任务结束标记'
  1164. await finished(runtime.send(owner,g.id,{requestId:randomUUID(),content:text}).id)
  1165. const prompt=requests.find(r=>r.method==='prompt.submit')!.params.text
  1166. expect(prompt).toContain('任务开始标记');expect(prompt).toContain('任务结束标记')
  1167. expect(prompt).toContain('保留首尾片段');expect(prompt.length).toBeLessThan(32_000)
  1168. })
  1169. it('enforces three tasks per group and four globally while preserving queued work', async () => {
  1170. const agents=['并发甲','并发乙','并发丙','并发丁','其他群'].map(n=>agent(n))
  1171. const group=store.createGroup(owner,{name:'并发限制',memberIds:agents.slice(0,4).map(a=>a.id),administratorId:agents[0]!.id,mode:'free',autoReplyIds:agents.slice(0,4).map(a=>a.id),maxReplyRounds:1})
  1172. const pending=new Map<string,{socket:WebSocket;p:Record<string,any>}>()
  1173. reply=(socket,p)=>pending.set(speaker(p)!,{socket,p})
  1174. const one=runtime.send(owner,group.id,{requestId:randomUUID(),content:'并发测试'})
  1175. const two=runtime.send(owner,direct(agents[4]!.id).id,{requestId:randomUUID(),content:'另一群'})
  1176. await vi.waitFor(()=>expect(pending.size).toBe(4))
  1177. expect(Object.keys(store.require<WorkspaceConversation>(owner,'conversation',group.id).activeAgentStates!)).toHaveLength(3)
  1178. expect(store.list<any>(owner,'turn').filter(t=>t.status==='running')).toHaveLength(4)
  1179. const released=[...pending.values()].find(v=>speaker(v.p)!==agents[4]!.name)!
  1180. completeReply(released.socket,released.p,'完成')
  1181. await vi.waitFor(()=>expect(pending.size).toBe(5))
  1182. expect(store.list<any>(owner,'turn').filter(t=>t.status==='running').length).toBeLessThanOrEqual(4)
  1183. for(const v of pending.values()) if(v!==released) completeReply(v.socket,v.p,'完成')
  1184. await finished(one.id);await finished(two.id)
  1185. })
  1186. it('supports unlimited rounds beyond the finite limit and still permits stopping', async () => {
  1187. const a=agent('不限甲'),b=agent('不限乙'),g=store.createGroup(owner,{name:'不限轮次',memberIds:[a.id,b.id],administratorId:a.id,mode:'free',autoReplyIds:[a.id],maxReplyRounds:-1})
  1188. let count=0
  1189. reply=(socket,p)=>{
  1190. if(count++===0){const work=store.list<any>(owner,'turn').find(t=>t.status==='running')!;work.depth=100;store.put(owner,'turn',work.id,work);completeReply(socket,p,'继续讨论')}
  1191. else completeReply(socket,p,'[[YAOYAO_NO_REPLY_V1]]')
  1192. }
  1193. const root=runtime.send(owner,g.id,{requestId:randomUUID(),content:'@不限乙 开始'})
  1194. await finished(root.id)
  1195. expect(count).toBe(2)
  1196. expect(store.require<WorkspaceRun>(owner,'run',root.id).round).toBe(101)
  1197. expect(()=>store.updateConversation(owner,g.id,{maxReplyRounds:0})).toThrow()
  1198. })
  1199. it('keeps parallel approval cards scoped to their member and deduplicates replayed requests and answers', async () => {
  1200. const a=agent('审批管理员'),b=agent('审批乙'),c=agent('审批丙'),g=store.createGroup(owner,{name:'并发审批',memberIds:[a.id,b.id,c.id],administratorId:a.id})
  1201. let admin=0
  1202. server.on('connection',socket=>socket.on('message',raw=>{const f=JSON.parse(String(raw));if(f.method==='approval.respond')completeReply(socket,f.params,'已确认')}))
  1203. reply=(socket,p)=>{
  1204. if(speaker(p)===a.name){completeReply(socket,p,admin++===0?'@审批乙 @审批丙':'汇总');return}
  1205. const frame=JSON.stringify({method:'event',params:{type:'approval.request',session_id:p.session_id,payload:{request_id:'same-request',message:'是否继续'}}})
  1206. socket.send(frame);socket.send(frame)
  1207. }
  1208. const root=runtime.send(owner,g.id,{requestId:randomUUID(),content:'开始'})
  1209. await vi.waitFor(()=>expect(store.list<WorkspaceInteraction>(owner,'interaction').filter(i=>!i.resolved)).toHaveLength(2))
  1210. const interactions=store.list<WorkspaceInteraction>(owner,'interaction')
  1211. expect(store.require<WorkspaceConversation>(owner,'conversation',g.id).activeAgentStates).toEqual({[b.id]:'waiting',[c.id]:'waiting'})
  1212. await runtime.respond(owner,interactions[0]!.id,'once')
  1213. await runtime.respond(owner,interactions[0]!.id,'once')
  1214. await expect(runtime.respond(owner,interactions[0]!.id,'deny')).rejects.toThrow('其他答复')
  1215. expect(admin).toBe(1)
  1216. await runtime.respond(owner,interactions[1]!.id,'once')
  1217. await finished(root.id)
  1218. expect(requests.filter(r=>r.method==='approval.respond')).toHaveLength(2)
  1219. expect(admin).toBe(2)
  1220. })
  1221. it('retries a rolled-back cascade plan without losing or duplicating delegation', async () => {
  1222. const a=agent('事务管理员'),b=agent('事务成员'),g=store.createGroup(owner,{name:'调度事务',memberIds:[a.id,b.id],administratorId:a.id})
  1223. const original=store.put.bind(store);let failed=false,admin=0
  1224. store.put=(user,kind,id,value)=>{if(!failed&&kind==='turn'&&(value as any).agentId===b.id){failed=true;throw new Error('模拟一次写入失败')}original(user,kind,id,value)}
  1225. reply=(socket,p)=>completeReply(socket,p,speaker(p)===a.name&&admin++===0?'@事务成员 继续':'完成')
  1226. const root=runtime.send(owner,g.id,{requestId:randomUUID(),content:'开始'})
  1227. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',root.id).status).toBe('complete'),{timeout:4000})
  1228. expect(failed).toBe(true)
  1229. expect(store.list<any>(owner,'turn').filter(t=>t.agentId===b.id)).toHaveLength(1)
  1230. expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(3)
  1231. })
  1232. it('upgrades an admitted v0.3.7 execution without submitting its prompt again', async () => {
  1233. const a=agent('升级执行'),c=direct(a.id)
  1234. reply=(socket,p)=>{recoveryHistory=[{role:'user',content:p.text},{role:'assistant',content:'升级前已完成'}];socket.terminate()}
  1235. const root=runtime.send(owner,c.id,{requestId:randomUUID(),content:'只执行一次'})
  1236. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',root.id).status).toBe('uncertain'))
  1237. runtime.close()
  1238. const old=store.list<any>(owner,'turn')[0]!
  1239. for(const task of store.list<any>(owner,'turn'))store.remove(owner,'turn',task.id)
  1240. const legacy=store.require<any>(owner,'run',root.id)
  1241. Object.assign(legacy,{queue:[a.id],next:[],hostReturn:false,currentMessageId:old.currentMessageId,turnConfiguration:old.turnConfiguration})
  1242. store.put(owner,'run',root.id,legacy)
  1243. const binding=store.get<any>(owner,'binding',`${c.id}:${a.id}`)!
  1244. delete binding.taskId;store.put(owner,'binding',binding.id,binding)
  1245. runtime=new WorkspaceRuntime(store,nodes,uploads);runtime.start()
  1246. await finished(root.id)
  1247. expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(1)
  1248. expect(store.messages(owner,c.id).at(-1)?.content).toBe('升级前已完成')
  1249. })
  1250. it('does not read unrelated or orphaned historical runs when projecting a group prompt', async () => {
  1251. const a=agent('上下文隔离'),b=agent('组内成员'),g=store.createGroup(owner,{name:'干净群',memberIds:[a.id,b.id],administratorId:a.id})
  1252. store.put(owner,'run','historical-orphan',{id:'historical-orphan',conversationId:'unrelated-conversation',messageId:'missing-history',mentionIds:[],status:'complete',round:0,createdAt:1,updatedAt:1})
  1253. await finished(runtime.send(owner,g.id,{requestId:randomUUID(),content:'当前群任务'}).id)
  1254. expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(1)
  1255. })
  1256. it('pages backwards to find a long-running turn marker and never mistakes a tool call for a final answer', async () => {
  1257. const a=agent('分页恢复'),c=direct(a.id)
  1258. reply=(socket,p)=>{
  1259. recoveryHistory=[{role:'user',content:p.text},...Array.from({length:600},()=>({role:'tool',content:'工具结果'})),{role:'assistant',content:'最终结果'}]
  1260. socket.terminate()
  1261. }
  1262. await finished(runtime.send(owner,c.id,{requestId:randomUUID(),content:'长时间执行'}).id)
  1263. expect(store.messages(owner,c.id).at(-1)?.content).toBe('最终结果')
  1264. let submitted=''
  1265. reply=(socket,p)=>{submitted=p.text;recoveryHistory=[{role:'user',content:submitted},{role:'assistant',content:'准备调用工具',tool_calls:[{id:'tool'}]},{role:'tool',content:'工具输出'}];socket.terminate()}
  1266. const root=runtime.send(owner,c.id,{requestId:randomUUID(),content:'不要把过程当结论'})
  1267. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',root.id).status).toBe('uncertain'))
  1268. await runtime.reconcile(owner,root.id)
  1269. await new Promise(resolve=>setTimeout(resolve,100))
  1270. expect(store.require<WorkspaceRun>(owner,'run',root.id).status).toBe('uncertain')
  1271. recoveryHistory.push({role:'assistant',content:'真正完成'})
  1272. await runtime.reconcile(owner,root.id)
  1273. await finished(root.id)
  1274. expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(2)
  1275. })
  1276. it('keeps explicit replies visible and emits one notice when the configured cascade limit is reached', async () => {
  1277. const a=agent('轮数管理员'),b=agent('轮数成员'),g=store.createGroup(owner,{name:'有限轮次',memberIds:[a.id,b.id],administratorId:a.id,maxReplyRounds:2})
  1278. reply=(socket,p)=>completeReply(socket,p,speaker(p)===a.name?'@轮数成员 继续':'已完成本步')
  1279. const root=runtime.send(owner,g.id,{requestId:randomUUID(),content:'开始'})
  1280. await finished(root.id)
  1281. expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(3)
  1282. expect(store.messages(owner,g.id).filter(m=>m.content.includes('轮数上限'))).toHaveLength(1)
  1283. store.updateConversation(owner,g.id,{mode:'free',autoReplyIds:[]})
  1284. reply=(socket,p)=>completeReply(socket,p,'[[YAOYAO_NO_REPLY_V1]]')
  1285. await finished(runtime.send(owner,g.id,{requestId:randomUUID(),content:'@轮数成员 显示这个字符串'}).id)
  1286. expect(store.messages(owner,g.id).at(-1)?.content).toBe('[[YAOYAO_NO_REPLY_V1]]')
  1287. })
  1288. it('retries a failed stop of a live task without falsely completing it or resubmitting work', async () => {
  1289. const a=agent('停止恢复'),c=direct(a.id)
  1290. reply=()=>{}
  1291. const root=runtime.send(owner,c.id,{requestId:randomUUID(),content:'等待停止'})
  1292. await vi.waitFor(()=>expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(1))
  1293. rejectedInterrupts=1
  1294. await runtime.stop(owner,root.id)
  1295. expect(store.require<WorkspaceRun>(owner,'run',root.id).status).toBe('uncertain')
  1296. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',root.id).status).toBe('interrupted'),{timeout:3000})
  1297. expect(requests.filter(r=>r.method==='session.interrupt')).toHaveLength(2)
  1298. expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(1)
  1299. })
  1300. it('marks a stopped administrator as interrupted without sending a false completion notification', async () => {
  1301. const a=agent('管理员停止'),b=agent('普通成员'),g=store.createGroup(owner,{name:'停止管理员',memberIds:[a.id,b.id],administratorId:a.id})
  1302. reply=()=>{}
  1303. const notifications: string[]=[]
  1304. runtime.onNotify=(_owner,_c,run)=>notifications.push(run.status)
  1305. const root=runtime.send(owner,g.id,{requestId:randomUUID(),content:'等待'})
  1306. await vi.waitFor(()=>expect(requests.filter(r=>r.method==='prompt.submit')).toHaveLength(1))
  1307. await runtime.stopAgent(owner,g.id,a.id)
  1308. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',root.id).status).toBe('interrupted'))
  1309. expect(notifications).toEqual([])
  1310. })
  1311. it('randomizes old default avatars once while preserving photos, custom v2 settings and direct snapshots',()=>{
  1312. const a=store.createAgent(owner,{name:'旧头像',profile:'default',instructions:'保留规则'})
  1313. const photo='data:image/png;base64,aGVsbG8='
  1314. const b=store.createAgent(owner,{name:'照片',profile:'default',avatar:photo})
  1315. store.put(owner,'agent',a.id,{...a,avatar:'yaoyao-mascot:v1:triangle:ff0000:friendly'})
  1316. store.put(owner,'agent',b.id,{...b,avatar:photo})
  1317. store.db.prepare("DELETE FROM workspace_migrations WHERE id IN ('avatar-v2','avatar-random-default-v1')").run()
  1318. const random=vi.spyOn(Math,'random').mockReturnValue(0)
  1319. const migrated=new WorkspaceStore(home)
  1320. random.mockRestore()
  1321. const next=migrated.require<any>(owner,'agent',a.id)
  1322. expect(next.instructions).toBe('保留规则')
  1323. expect(decodeAgentAvatar(next.avatar)).toMatchObject({avatarMode:'mascot'})
  1324. expect(next.avatar).not.toBe(encodeAgentAvatar(defaultAgentIdentity(a.id,a.name)))
  1325. const image=migrated.require<any>(owner,'agent',b.id)
  1326. expect(decodeAgentAvatar(image.avatar)).toMatchObject({avatarMode:'image',imageDataURL:photo,imageCrop:'rounded'})
  1327. for(const c of migrated.list<WorkspaceConversation>(owner,'conversation')) expect(c.avatar).toBe(migrated.require<any>(owner,'agent',c.memberIds[0]!).avatar)
  1328. const customized=encodeAgentAvatar({...defaultAgentIdentity('bot'),bodyId:'star',expression:'proud',color:'#1488ff'})
  1329. migrated.updateAgent(owner,a.id,{avatar:customized})
  1330. migrated.close()
  1331. const reopened=new WorkspaceStore(home)
  1332. expect(reopened.require<any>(owner,'agent',a.id).avatar).toBe(customized)
  1333. expect(reopened.db.prepare("SELECT count(*) AS count FROM workspace_migrations WHERE id='avatar-random-default-v1'").get()!.count).toBe(1)
  1334. reopened.close()
  1335. })
  1336. // A member can have a queued follow-up while their current task needs approval.
  1337. it('keeps active avatar state ahead of queued work and publishes only recent outcomes', () => {
  1338. const a=agent('状态优先级'), c=direct(a.id), now=Date.now()
  1339. const run: WorkspaceRun={id:'avatar-run',conversationId:c.id,messageId:'unused',mentionIds:[],status:'complete',round:1,createdAt:now,updatedAt:now}
  1340. for (const [id,status] of [['current','waiting'],['next','queued']] as const)
  1341. store.put(owner,'turn',id,{id,conversationId:c.id,agentId:a.id,status,planned:true,updatedAt:now})
  1342. store.put(owner,'turn','result',{id:'result',conversationId:c.id,agentId:a.id,status:'failed',updatedAt:now})
  1343. store.saveRun(owner,run)
  1344. let summary=store.require<WorkspaceConversation>(owner,'conversation',c.id)
  1345. expect(summary.activeAgentStates).toEqual({[a.id]:'waiting'})
  1346. expect(summary.avatarSignals?.[a.id]).toMatchObject({id:'result',state:'failure'})
  1347. store.put(owner,'turn','result',{id:'result',conversationId:c.id,agentId:a.id,status:'complete',updatedAt:now-3000})
  1348. store.saveRun(owner,run)
  1349. expect(store.require<WorkspaceConversation>(owner,'conversation',c.id).avatarSignals).toEqual({})
  1350. })
  1351. it('queues an unsubmitted turn while computer capacity is unavailable and submits it once after recovery',async()=>{
  1352. const a=agent('等候电脑'),c=direct(a.id),target=nodes.target(owner,'local')
  1353. vi.spyOn(target.session,'request').mockRejectedValueOnce(new HttpError(429,'资源不足','computer_quota'))
  1354. const run=runtime.send(owner,c.id,{requestId:randomUUID(),content:'等待资源后执行'})
  1355. await vi.waitFor(()=>expect(store.list<any>(owner,'turn').find(work=>work.runId===run.id)?.resourceWait).toBe(true))
  1356. expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('queued')
  1357. await vi.waitFor(()=>expect(store.require<WorkspaceRun>(owner,'run',run.id).status).toBe('complete'),{timeout:5000})
  1358. expect(requests.filter(request=>request.method==='prompt.submit')).toHaveLength(1)
  1359. })
  1360. it('forwards a generated artifact to a new task without changing its original provenance',async()=>{
  1361. const a=agent('产物接收者'),c=direct(a.id)
  1362. const first=runtime.send(owner,c.id,{requestId:randomUUID(),content:'first'})
  1363. await finished(first.id)
  1364. const message=store.messages(owner,c.id).find(message=>message.role==='assistant')!
  1365. const assets=new WorkspaceAssets(store,nodes,home)
  1366. const file=assets.publish(owner,message,a,'review.txt',Buffer.from('verified artifact'))
  1367. const sent=runtime.dispatch(owner,c.id,{requestId:randomUUID(),content:'复核附件',fileIds:[file.id]},{agentId:a.id,kind:'task_review'})
  1368. await finished(sent.id)
  1369. const attached=requests.find(request=>request.method==='file.attach')!
  1370. expect(Buffer.from(attached.params.data_url.split(',')[1],'base64').toString()).toBe('verified artifact')
  1371. expect(store.require<any>(owner,'file',file.id).messageId).toBe(message.id)
  1372. expect(store.require<WorkspaceMessage>(owner,'message',sent.messageId).attachments.map(file=>file.id)).toEqual([file.id])
  1373. assets.close()
  1374. })