agentworkshop 0.7.37 → 0.7.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/builds/latest.json +1 -1
  3. package/.output/public/_nuxt/builds/meta/44b66249-54d0-4c8a-9057-9755a32329e2.json +1 -0
  4. package/.output/server/chunks/_/nitro.mjs +798 -799
  5. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  6. package/.output/server/package.json +1 -1
  7. package/README-zh.md +2 -2
  8. package/README.md +2 -2
  9. package/docs/cli.en.md +2 -2
  10. package/docs/cli.md +2 -2
  11. package/docs/full-test-plan.md +1 -1
  12. package/docs/plugins.en.md +1 -1
  13. package/docs/plugins.md +1 -1
  14. package/docs/sdk.en.md +1 -1
  15. package/docs/sdk.md +1 -1
  16. package/package.json +1 -1
  17. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +1 -0
  18. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +1 -0
  19. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +153 -0
  20. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +153 -0
  21. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +431 -0
  22. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +180 -0
  23. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +112 -0
  24. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +227 -0
  25. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +131 -0
  26. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +160 -0
  27. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +157 -0
  28. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +348 -0
  29. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +296 -0
  30. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +285 -0
  31. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +217 -0
  32. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +301 -0
  33. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +151 -0
  34. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +135 -0
  35. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +156 -0
  36. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +72 -0
  37. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +469 -0
  38. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +259 -0
  39. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +97 -0
  40. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +277 -0
  41. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +171 -0
  42. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +425 -0
  43. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +99 -0
  44. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +325 -0
  45. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +332 -0
  46. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +43 -0
  47. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +154 -0
  48. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +531 -0
  49. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +439 -0
  50. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +323 -0
  51. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +433 -0
  52. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +280 -0
  53. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +117 -0
  54. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +160 -0
  55. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +101 -0
  56. package/scripts/test-plugin-hardening.mjs +32 -0
  57. package/server/plugins/dev-stability-guard.ts +40 -63
  58. package/server/utils/stability-severity.ts +66 -0
  59. package/.output/public/_nuxt/builds/meta/a66fdef7-dcfd-453b-b1f6-543c7dfd98d5.json +0 -1
@@ -0,0 +1,217 @@
1
+ /**
2
+ * E2E:Lead 执行中自主团队管理(成员 创建/更新/删除 + 孤儿任务回收)。
3
+ *
4
+ * 覆盖面:
5
+ * 1. 工具注入按角色差异化:hostToolsForRole(worker 剔除调度/团队管理;lead 全量含新工具)
6
+ * 2. 决策路径:mock lead 首个 supervise tick 发 spawn_agent → SchedulerLoop 执行 → 新成员入册
7
+ * 3. 工具桥路径(与 omp host tool 同源):manager.createTeamMember/updateTeamMember/removeTeamMember
8
+ * - 权限:worker 调用 → 403;lead 改/删自己 → 400
9
+ * - 事件:AEP agent.member(op=added/updated/removed,by=lead:<id>)广播
10
+ * 4. 新成员可被调度:queueOverview 可见 → 接收 dispatch → 完成任务
11
+ * 5. 孤儿任务回收:
12
+ * - 排队中(ASSIGNED)成员被移除 → 任务重派给剩余最短队列成员
13
+ * - 执行中(WORKING)成员被移除 → 任务 FAILED → 调度循环重试重派 → 最终 COMPLETED
14
+ */
15
+ import { openWorkshopDb } from '../server/services/workshop/db/database'
16
+ import { createChannelRepo } from '../server/services/workshop/db/channel.repo'
17
+ import { createAgentRepo } from '../server/services/workshop/db/agent.repo'
18
+ import { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
19
+ import { createTaskRepo } from '../server/services/workshop/db/task.repo'
20
+ import { createMemoryRepo } from '../server/services/workshop/db/memory.repo'
21
+ import { createChannelEventRepo } from '../server/services/workshop/db/channel-event.repo'
22
+ import { createTeamRepo } from '../server/services/workshop/db/team.repo'
23
+ import { createTeamMemberRepo } from '../server/services/workshop/db/team-member.repo'
24
+ import { createMessageRepo } from '../server/services/workshop/db/message.repo'
25
+ import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
26
+ import { createUserRepo } from '../server/services/workshop/db/user.repo'
27
+ import { createAgentChannelManager } from '../server/services/workshop/runtime/manager'
28
+ import type { AgentChannelManager } from '../server/services/workshop/runtime/manager'
29
+ import type { MemberChangeEvent } from '../server/services/workshop/runtime/agent-runtime'
30
+ import { createAgentImpl } from '../server/services/workshop/agents/factory'
31
+ import { hostToolsForRole } from '../server/services/workshop/agents/omp-agent'
32
+ import { AppError } from '../server/utils/errors'
33
+
34
+ let failures = 0
35
+ function check(name: string, ok: boolean, detail = ''): void {
36
+ console.log(` ${ok ? '✅' : '❌'} ${name}${detail ? ` — ${detail}` : ''}`)
37
+ if (!ok) failures += 1
38
+ }
39
+
40
+ const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms))
41
+
42
+ async function waitUntil(cond: () => boolean | Promise<boolean>, timeoutMs: number): Promise<boolean> {
43
+ const deadline = Date.now() + timeoutMs
44
+ while (Date.now() < deadline) {
45
+ if (await cond()) return true
46
+ await sleep(50)
47
+ }
48
+ return false
49
+ }
50
+
51
+ function setup(): { manager: AgentChannelManager } {
52
+ const db = openWorkshopDb(':memory:')
53
+ const repos = {
54
+ users: createUserRepo(db),
55
+ channels: createChannelRepo(db),
56
+ agents: createAgentRepo(db),
57
+ channelAgents: createChannelAgentRepo(db),
58
+ messages: createMessageRepo(db),
59
+ subscriptions: createSubscriptionRepo(db),
60
+ tasks: createTaskRepo(db),
61
+ memories: createMemoryRepo(db),
62
+ channelEvents: createChannelEventRepo(db),
63
+ teams: createTeamRepo(db),
64
+ teamMembers: createTeamMemberRepo(db),
65
+ }
66
+ const manager = createAgentChannelManager({ repos, implFactory: createAgentImpl, db })
67
+ return { manager }
68
+ }
69
+
70
+ async function main(): Promise<void> {
71
+ console.log('== 1. 工具注入按角色差异化 ==')
72
+ {
73
+ const leadTools = hostToolsForRole('lead').map(t => t.name)
74
+ const workerTools = hostToolsForRole('worker').map(t => t.name)
75
+ check('lead 拥有团队管理三工具', ['create_team_agent', 'update_team_agent', 'remove_team_agent'].every(n => leadTools.includes(n)))
76
+ check('worker 无团队管理/派发工具', ['create_team_agent', 'update_team_agent', 'remove_team_agent', 'dispatch_task', 'reassign_task'].every(n => !workerTools.includes(n)))
77
+ check('worker 保留执行/通信/记忆面', ['complete_task', 'report_progress', 'send_message_to_agent', 'search_memory', 'save_memory'].every(n => workerTools.includes(n)))
78
+ check('lead 保留全量(worker 工具不缺)', workerTools.every(n => leadTools.includes(n)), `lead=${leadTools.length} worker=${workerTools.length}`)
79
+ }
80
+
81
+ console.log('== 2. 决策路径:lead spawn_agent → 新成员入册 ==')
82
+ const { manager } = setup()
83
+ const memberEvents: MemberChangeEvent[] = []
84
+ const leadConfig = {
85
+ delayMs: 100,
86
+ // mock lead 首个 supervise tick 的团队管理剧本
87
+ teamOps: [
88
+ { kind: 'spawn_agent' as const, name: 'test-writer', harness: 'mock', config: { delayMs: 120 }, reason: '扩容:任务需要专职测试编写' },
89
+ ],
90
+ }
91
+ const { channelId, leadAgentId } = await manager.createChannel({
92
+ name: 'team-mgmt',
93
+ leadAgent: { name: 'lead', harness: 'mock', config: leadConfig },
94
+ })
95
+ manager.subscribeMemberEvents(channelId, e => memberEvents.push(e))
96
+ const w1tpl = await manager.createAgent({ name: 'w1', harness: 'mock', config: { delayMs: 120 } })
97
+ await manager.addAgentToChannel({ channelId, agentId: w1tpl.id, role: 'worker' })
98
+
99
+ const task = await manager.submitChannelTask({ channelId, title: '交付一个功能', description: '分解并完成' })
100
+ const spawned = await waitUntil(async () => (await manager.listChannelAgents(channelId)).some(a => a.name === 'test-writer'), 10_000)
101
+ check('spawn_agent 决策落地:新成员入册', spawned)
102
+ const spawnEvt = memberEvents.find(e => e.op === 'added' && e.name === 'test-writer')
103
+ check('agent.member(added) 事件广播(by=lead:…)', !!spawnEvt && spawnEvt.by === `lead:${leadAgentId}`, JSON.stringify(spawnEvt ?? null))
104
+ check('新成员为 worker 角色', spawnEvt?.role === 'worker')
105
+
106
+ const completed = await waitUntil(async () =>
107
+ (await manager.listTasks(channelId, leadAgentId!)).find(t => t.id === task.id)?.state === 'COMPLETED', 15_000)
108
+ check('主任务在新团队配置下正常完成', completed)
109
+
110
+ console.log('== 3. 工具桥路径:createTeamMember/updateTeamMember ==')
111
+ const docWriter = await manager.createTeamMember(channelId, leadAgentId!, {
112
+ name: 'doc-writer',
113
+ harness: 'mock',
114
+ config: { systemPromptPrefix: '你是文档专家', delayMs: 100 },
115
+ reason: '需要文档专员',
116
+ })
117
+ check('createTeamMember 成功返回实例', !!docWriter.id && docWriter.role === 'worker' && docWriter.harness === 'mock')
118
+ const templates = await manager.listAgents()
119
+ check('按需落模板(可复用)', templates.some(t => t.name === 'doc-writer'))
120
+ const renamed = await manager.updateTeamMember(channelId, leadAgentId!, docWriter.id, { name: 'doc-writer-v2', reason: '更名' })
121
+ check('updateTeamMember 改名生效', renamed.name === 'doc-writer-v2')
122
+ const updEvt = memberEvents.find(e => e.op === 'updated' && e.agentId === docWriter.id)
123
+ check('agent.member(updated) 事件广播', !!updEvt && updEvt.name === 'doc-writer-v2')
124
+
125
+ console.log('== 4. 权限校验 ==')
126
+ const workers = (await manager.listChannelAgents(channelId)).filter(a => a.role === 'worker')
127
+ const anyWorker = workers[0]!
128
+ let saw403 = false
129
+ try {
130
+ await manager.createTeamMember(channelId, anyWorker.id, { name: 'rogue', harness: 'mock' })
131
+ }
132
+ catch (err) { saw403 = err instanceof AppError && err.status === 403 }
133
+ check('worker 调 createTeamMember → 403', saw403)
134
+ let saw400self = false
135
+ try {
136
+ await manager.removeTeamMember(channelId, leadAgentId!, leadAgentId!)
137
+ }
138
+ catch (err) { saw400self = err instanceof AppError && err.status === 400 }
139
+ check('lead 移除自己 → 400', saw400self)
140
+ let saw400selfUpd = false
141
+ try {
142
+ await manager.updateTeamMember(channelId, leadAgentId!, leadAgentId!, { name: 'x' })
143
+ }
144
+ catch (err) { saw400selfUpd = err instanceof AppError && err.status === 400 }
145
+ check('lead 更新自己 → 400', saw400selfUpd)
146
+
147
+ console.log('== 5. 新成员可被调度 ==')
148
+ const testWriter = (await manager.listChannelAgents(channelId)).find(a => a.name === 'test-writer')!
149
+ const overview = await manager.queueOverview(channelId, leadAgentId!)
150
+ check('queueOverview 可见新成员', overview.some(o => o.agentId === testWriter.id))
151
+ // 直接以 lead 身份 dispatch 给新成员(验证完整投递链)
152
+ const directTask = await manager.dispatchTask(channelId, leadAgentId!, {
153
+ assigneeId: testWriter.id,
154
+ title: '编写测试用例',
155
+ parentTaskId: undefined,
156
+ description: '为核心模块编写测试',
157
+ })
158
+ const directDone = await waitUntil(async () => (await manager.getTask(channelId, leadAgentId!, directTask.id)).state === 'COMPLETED', 10_000)
159
+ check('新成员接取并完成任务', directDone)
160
+
161
+ console.log('== 6. 孤儿任务回收:排队中(ASSIGNED)成员移除 → 重派 ==')
162
+ // 造一个"排队中"任务:给启用成员连发两个任务(FIFO)——第一个进入 WORKING,
163
+ // 第二个滞留其信箱 ASSIGNED(排队中,未消费)——随后移除成员经回收路径重派。
164
+ // (注:新版有"投递失败补偿",向已禁用成员派发会被 DELIVERY_FAILED 拒绝并回收任务,
165
+ // 因此排队任务只可能存在于"曾可投递但未及消费"的成员队列中)
166
+ const idleTpl = await manager.createAgent({ name: 'idle-queue', harness: 'mock', config: { delayMs: 100 } })
167
+ const idleMember = await manager.addAgentToChannel({ channelId, agentId: idleTpl.id, role: 'worker' })
168
+ await manager.dispatchTask(channelId, leadAgentId!, {
169
+ assigneeId: idleMember.id,
170
+ title: '队列首任务',
171
+ description: '先被消费',
172
+ })
173
+ const queuedTask = await manager.dispatchTask(channelId, leadAgentId!, {
174
+ assigneeId: idleMember.id,
175
+ title: '排队中的任务',
176
+ description: '等待回收',
177
+ })
178
+ await sleep(200)
179
+ const queuedState = (await manager.getTask(channelId, leadAgentId!, queuedTask.id)).state
180
+ check('第二个任务滞留排队(ASSIGNED)', queuedState === 'ASSIGNED', queuedState)
181
+ const recycle = await manager.removeTeamMember(channelId, leadAgentId!, idleMember.id, '清理闲置成员')
182
+ check('removeTeamMember 返回回收任务列表', recycle.recycledTasks.includes(queuedTask.id), JSON.stringify(recycle.recycledTasks))
183
+ const afterTask = await manager.getTask(channelId, leadAgentId!, queuedTask.id)
184
+ check('排队任务已重派给剩余成员', afterTask.assigneeId !== idleMember.id, `→ ${afterTask.assigneeId.slice(0, 8)}`)
185
+ const gone = !(await manager.listChannelAgents(channelId)).some(a => a.id === idleMember.id)
186
+ check('成员已从名册移除', gone)
187
+ const rmEvt = memberEvents.find(e => e.op === 'removed' && e.agentId === idleMember.id)
188
+ check('agent.member(removed) 事件广播(含 reason)', !!rmEvt && rmEvt.reason === '清理闲置成员')
189
+ const recycleDone = await waitUntil(async () => (await manager.getTask(channelId, leadAgentId!, queuedTask.id)).state === 'COMPLETED', 10_000)
190
+ check('重派任务最终完成(工作不丢失)', recycleDone)
191
+
192
+ console.log('== 7. 孤儿任务回收:执行中(WORKING)成员移除 → FAILED → 重试 ==')
193
+ const slowTpl = await manager.createAgent({ name: 'slow-worker', harness: 'mock', config: { delayMs: 60_000 } })
194
+ const slowMember = await manager.addAgentToChannel({ channelId, agentId: slowTpl.id, role: 'worker' })
195
+ const workingTask = await manager.dispatchTask(channelId, leadAgentId!, {
196
+ assigneeId: slowMember.id,
197
+ title: '长任务',
198
+ description: '会被中途裁撤',
199
+ })
200
+ const isWorking = await waitUntil(async () => (await manager.getTask(channelId, leadAgentId!, workingTask.id)).state === 'WORKING', 10_000)
201
+ check('慢 worker 进入 WORKING', isWorking)
202
+ const recycle2 = await manager.removeTeamMember(channelId, leadAgentId!, slowMember.id, '执行太慢,裁撤')
203
+ check('执行中任务被回收(转 FAILED 待重试)', recycle2.recycledTasks.includes(workingTask.id))
204
+ const retryDone = await waitUntil(async () =>
205
+ (await manager.getTask(channelId, leadAgentId!, workingTask.id)).state === 'COMPLETED', 15_000)
206
+ const afterRetry = await manager.getTask(channelId, leadAgentId!, workingTask.id)
207
+ check('调度循环重试重派并最终完成(工作不丢失)', retryDone && afterRetry.assigneeId !== slowMember.id, `state=${afterRetry.state}, retry=${afterRetry.retryCount}`)
208
+
209
+ await manager.shutdown()
210
+ console.log(failures === 0 ? '\n全部通过 🎉' : `\n${failures} 项失败`)
211
+ process.exit(failures === 0 ? 0 : 1)
212
+ }
213
+
214
+ main().catch((err) => {
215
+ console.error('e2e 异常:', err)
216
+ process.exit(1)
217
+ })
@@ -0,0 +1,301 @@
1
+ /**
2
+ * ChannelRuntime 路由测试(node + tsx 直跑,无浏览器)。
3
+ *
4
+ * 覆盖:
5
+ * 1. 点对点直投(x-aw-target-agent)
6
+ * 2. 跨 channel 目标忽略
7
+ * 3. 任务消息直投 assignee(x-aw-task-kind)
8
+ * 4. 广播只达订阅者
9
+ * 5. 无订阅者时广播无投递
10
+ * 6. 订阅后广播可达
11
+ *
12
+ * 使用真实 ChannelRuntime + 真实 Mailbox(:memory:) + 3 个 fake AgentRuntime(仅 getState/enqueue 记录)。
13
+ */
14
+ import { randomUUID } from 'node:crypto'
15
+ import type { DatabaseSync } from 'node:sqlite'
16
+ import { openWorkshopDb } from '../server/services/workshop/db/database'
17
+ import { createMessageRepo } from '../server/services/workshop/db/message.repo'
18
+ import type { MessageRepo } from '../server/services/workshop/db/message.repo'
19
+ import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
20
+ import { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
21
+ import { Mailbox } from '../server/services/workshop/runtime/mailbox'
22
+ import { ChannelRuntime } from '../server/services/workshop/runtime/channel-runtime'
23
+ import type { AgentRuntimeLike, TaskEngine } from '../server/services/workshop/runtime/agent-runtime'
24
+ import type { A2AMessage } from '../server/services/workshop/types/a2a'
25
+ import type { AgentStatusView, AgentTaskQueueView } from '../server/services/workshop/types/task'
26
+
27
+ let failures = 0
28
+ function check(name: string, ok: boolean, detail = ''): void {
29
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
30
+ if (!ok) {
31
+ failures += 1
32
+ }
33
+ }
34
+
35
+ /** 落库一个 channel(messages.channel_id / agents.channel_id 外键依赖) */
36
+ function seedChannel(db: DatabaseSync, id: string): void {
37
+ const now = new Date().toISOString()
38
+ db.prepare(
39
+ `INSERT INTO channels (id, name, description, lead_agent_id, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
40
+ ).run(id, id, '', null, 1, now, now)
41
+ }
42
+
43
+ /** 落库一个 Agent 实例(subscriptions.agent_id 外键依赖 channel_agents.id) */
44
+ function seedAgent(db: DatabaseSync, id: string, channelId: string, role: 'lead' | 'worker'): void {
45
+ const now = new Date().toISOString()
46
+ db.prepare(
47
+ `INSERT INTO channel_agents (id, channel_id, template_id, name, harness, config_json, role, token, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
48
+ ).run(id, channelId, null, id, 'mock', '{}', role, randomUUID(), 1, now, now)
49
+ }
50
+
51
+ function mkTask(channelId: string, assigneeId: string, title: string): WorkspaceTask {
52
+ return {
53
+ id: randomUUID(),
54
+ channelId,
55
+ assigneeId,
56
+ creatorId: '',
57
+ title,
58
+ state: 'ASSIGNED',
59
+ progress: 0,
60
+ retryCount: 0,
61
+ artifacts: [],
62
+ history: [],
63
+ createdAt: new Date().toISOString(),
64
+ updatedAt: new Date().toISOString(),
65
+ }
66
+ }
67
+
68
+ /** fake AgentRuntime:仅记录 enqueue,并透过真实 Mailbox 落库 */
69
+ class FakeAgentRuntime implements AgentRuntimeLike {
70
+ readonly agentId: string
71
+ readonly role: 'lead' | 'worker'
72
+ readonly channelId: string
73
+ received: A2AMessage[] = []
74
+ readonly mailbox: Mailbox
75
+
76
+ constructor(agentId: string, role: 'lead' | 'worker', channelId: string, messages: MessageRepo) {
77
+ this.agentId = agentId
78
+ this.role = role
79
+ this.channelId = channelId
80
+ this.mailbox = new Mailbox(messages, channelId, agentId, () => {})
81
+ }
82
+
83
+ enqueue(message: A2AMessage): void {
84
+ this.received.push(message)
85
+ this.mailbox.enqueue(message)
86
+ }
87
+
88
+ getState(): 'idle' | 'busy' | 'stopped' {
89
+ return 'idle'
90
+ }
91
+
92
+ wakeMailbox(): void {}
93
+
94
+ injectSteer(message: A2AMessage): void {
95
+ this.enqueue(message)
96
+ }
97
+
98
+ getStatus(): AgentStatusView {
99
+ return {
100
+ agentId: this.agentId,
101
+ channelId: this.channelId,
102
+ role: this.role,
103
+ name: this.agentId,
104
+ state: 'idle',
105
+ currentTaskId: null,
106
+ queuedCount: 0,
107
+ completedCount: 0,
108
+ }
109
+ }
110
+
111
+ getQueueView(): AgentTaskQueueView {
112
+ return { agentId: this.agentId, channelId: this.channelId, queued: [], completed: [] }
113
+ }
114
+
115
+ async stop(): Promise<void> {}
116
+
117
+ emitExternal(): void {}
118
+ }
119
+
120
+ function main(): void {
121
+ const db = openWorkshopDb(':memory:')
122
+ seedChannel(db, 'ch1')
123
+ seedAgent(db, 'agentA', 'ch1', 'lead')
124
+ seedAgent(db, 'agentB', 'ch1', 'worker')
125
+ seedAgent(db, 'agentC', 'ch1', 'worker')
126
+ const messages = createMessageRepo(db)
127
+ const subscriptions = createSubscriptionRepo(db)
128
+
129
+ // fake TaskEngine:route 仅调用 get
130
+ const tasks = new Map<string, WorkspaceTask>()
131
+ const engine = {
132
+ get: (taskId: string) => tasks.get(taskId),
133
+ create: () => {
134
+ throw new Error('未使用')
135
+ },
136
+ dispatch: () => {
137
+ throw new Error('未使用')
138
+ },
139
+ transition: () => {
140
+ throw new Error('未使用')
141
+ },
142
+ applyEvent: () => {},
143
+ list: () => [] as WorkspaceTask[],
144
+ complete: () => {
145
+ throw new Error('未使用')
146
+ },
147
+ reassign: () => {
148
+ throw new Error('未使用')
149
+ },
150
+ cancel: () => {
151
+ throw new Error('未使用')
152
+ },
153
+ onChildCompleted: () => {},
154
+ queueViewOf: () => ({ queued: [], completed: [] }),
155
+ } as TaskEngine
156
+
157
+ const channelId = 'ch1'
158
+ const cr = new ChannelRuntime(channelId, { taskEngine: engine, subscriptionRepo: subscriptions, channelAgents: createChannelAgentRepo(db) })
159
+ const a = new FakeAgentRuntime('agentA', 'lead', channelId, messages)
160
+ const b = new FakeAgentRuntime('agentB', 'worker', channelId, messages)
161
+ const c = new FakeAgentRuntime('agentC', 'worker', channelId, messages)
162
+ cr.addAgent(a)
163
+ cr.addAgent(b)
164
+ cr.addAgent(c)
165
+
166
+ function routeMsg(metadata: Record<string, unknown>): A2AMessage {
167
+ const msg: A2AMessage = { messageId: randomUUID(), contextId: channelId, role: 'ROLE_USER', parts: [], metadata }
168
+ cr.route(msg)
169
+ return msg
170
+ }
171
+
172
+ function clearReceived(): void {
173
+ a.received = []
174
+ b.received = []
175
+ c.received = []
176
+ }
177
+
178
+ console.log('\n--- 基础 ---')
179
+ check('getAgents 返回 3 个 agent', cr.getAgents().length === 3)
180
+ check('lead getter 返回 lead agent', cr.lead?.agentId === 'agentA')
181
+
182
+ console.log('\n--- 1. 点对点直投 ---')
183
+ clearReceived()
184
+ const p2p = routeMsg({ 'x-aw-target-agent': 'agentB', 'x-aw-from-agent': 'agentA' })
185
+ check('点对点直投:目标收到', b.received.includes(p2p))
186
+ check('点对点直投:非目标未收到', !a.received.includes(p2p) && !c.received.includes(p2p))
187
+
188
+ console.log('\n--- 2. 跨 channel 目标忽略 ---')
189
+ clearReceived()
190
+ routeMsg({ 'x-aw-target-agent': 'agentX', 'x-aw-from-agent': 'agentA' })
191
+ check('跨 channel 目标忽略:无投递', a.received.length === 0 && b.received.length === 0 && c.received.length === 0)
192
+
193
+ console.log('\n--- 3. 任务消息直投 assignee ---')
194
+ const task = mkTask(channelId, 'agentC', '任务C')
195
+ tasks.set(task.id, task)
196
+ clearReceived()
197
+ const assignMsg = routeMsg({ 'x-aw-task-kind': 'assign', 'x-aw-task-id': task.id })
198
+ check('任务消息直投 assignee', c.received.includes(assignMsg))
199
+ console.log('\n--- 4. 广播只达订阅者 ---')
200
+ subscriptions.add(channelId, 'agentA', 'agentB') // A 订阅 B
201
+ clearReceived()
202
+ const bcastB = routeMsg({ 'x-aw-from-agent': 'agentB' })
203
+ check('广播只达订阅者:A 收到', a.received.includes(bcastB))
204
+ check('广播只达订阅者:B/C 未收到', !b.received.includes(bcastB) && !c.received.includes(bcastB))
205
+
206
+ console.log('\n--- 5. 无订阅者时广播无投递 ---')
207
+ clearReceived()
208
+ routeMsg({ 'x-aw-from-agent': 'agentC' }) // C 无订阅者
209
+ console.log('\n--- 6. 订阅后广播可达 ---')
210
+ subscriptions.add(channelId, 'agentA', 'agentC') // A 订阅 C
211
+ clearReceived()
212
+ const bcastC = routeMsg({ 'x-aw-from-agent': 'agentC' })
213
+ check('订阅后广播可达:A 收到 C 的广播', a.received.includes(bcastC))
214
+ check('订阅后广播:C 自身未收到', !c.received.includes(bcastC))
215
+
216
+ console.log('\n--- 7. 垃圾拦截(无发送人/伪造发送人/无效任务) ---')
217
+ clearReceived()
218
+ const noSender = routeMsg({ 'x-aw-target-agent': 'agentB' })
219
+ check('无发送人消息被拦截', !b.received.includes(noSender))
220
+ const fakeSender = routeMsg({ 'x-aw-target-agent': 'agentB', 'x-aw-from-agent': 'agentZ' })
221
+ check('伪造发送人(非成员)消息被拦截', !b.received.includes(fakeSender))
222
+ routeMsg({ 'x-aw-task-kind': 'assign', 'x-aw-task-id': 'task-not-exist' })
223
+ check('无效任务消息被拦截', a.received.length === 0 && b.received.length === 0 && c.received.length === 0)
224
+ const humanMsg = routeMsg({ 'x-aw-target-agent': 'agentB', 'x-aw-from-label': 'tester' })
225
+ check('人类消息(from-label)放行', b.received.includes(humanMsg))
226
+
227
+ console.log('\n--- 真实 Mailbox 持久化 ---')
228
+ void b.mailbox.peek(10).then(async (peeked) => {
229
+ const persisted = peeked.find(m => m.metadata?.['x-aw-target-agent'] === 'agentB')
230
+ check('点对点直投:Mailbox 落库 pending', persisted !== undefined, `peeked ${peeked.length} 条`)
231
+
232
+ // ===== 恰好一次认领(steer 注入 / poll_messages 读即取 / 消费循环 dequeue 三方竞争) =====
233
+ console.log('\n--- 恰好一次认领(原子 claim) ---')
234
+ // 清空 B 信箱残留(FIFO 顺序干扰):全部认领并消费
235
+ for (const old of await b.mailbox.peek(50)) {
236
+ if (b.mailbox.claim(old.messageId)) b.mailbox.markConsumed(old.messageId)
237
+ }
238
+ const raceMsg: A2AMessage = {
239
+ messageId: randomUUID(),
240
+ contextId: channelId,
241
+ role: 'ROLE_AGENT',
242
+ parts: [{ text: 'race' }],
243
+ metadata: { 'x-aw-target-agent': 'agentB', 'x-aw-from-agent': 'agentA', 'x-aw-msg-priority': 'immediate' },
244
+ }
245
+ b.mailbox.enqueue(raceMsg)
246
+ // 三个竞争方依次认领:只有第一个成功(唯一所有权 → 消息绝不双投)
247
+ const steerGot = b.mailbox.claim(raceMsg.messageId)
248
+ const pollGot = b.mailbox.claim(raceMsg.messageId)
249
+ const dqGot = b.mailbox.claim(raceMsg.messageId)
250
+ check('首竞争者(steer)认领成功', steerGot === true)
251
+ check('后到竞争者(poll/dequeue)认领失败', pollGot === false && dqGot === false)
252
+ // 认领期间消息对 peek 不可见(poll_messages 不会重复取走)
253
+ const visible = await b.mailbox.peek(10)
254
+ check('认领期间 peek 不可见', !visible.some(x => x.messageId === raceMsg.messageId))
255
+ // steer 放弃(deferred)→ requeue 释放回 pending → dequeue 接管
256
+ check('requeue 释放认领', b.mailbox.requeue(raceMsg.messageId) === true)
257
+ const taken = await Promise.race([
258
+ b.mailbox.dequeue(),
259
+ new Promise<A2AMessage | null>(r => setTimeout(() => r(null), 1000)),
260
+ ])
261
+ check('释放后 dequeue 取到消息', taken?.messageId === raceMsg.messageId)
262
+ // 终态:markConsumed 后 claim 不复活消息
263
+ b.mailbox.markConsumed(raceMsg.messageId)
264
+ check('consumed 后 claim 不复活', b.mailbox.claim(raceMsg.messageId) === false)
265
+
266
+ // ===== 同毫秒突发 FIFO(rowid 决胜回归) =====
267
+ // 紧密循环同步入队:大概率全部落在同一毫秒内 → dequeue 顺序必须 === 入队顺序
268
+ console.log('\n--- 同毫秒突发 FIFO(rowid 决胜) ---')
269
+ const burst: A2AMessage[] = []
270
+ for (let i = 0; i < 30; i++) {
271
+ const m: A2AMessage = {
272
+ messageId: randomUUID(),
273
+ contextId: channelId,
274
+ role: 'ROLE_AGENT',
275
+ parts: [{ text: `burst-${i}` }],
276
+ metadata: { 'x-aw-target-agent': 'agentB', 'x-aw-from-agent': 'agentA' },
277
+ }
278
+ b.mailbox.enqueue(m)
279
+ burst.push(m)
280
+ }
281
+ const textOf = (m: A2AMessage): string =>
282
+ (m.parts[0] && 'text' in m.parts[0] ? m.parts[0].text : '') as string
283
+ const takenOrder: string[] = []
284
+ for (let i = 0; i < burst.length; i++) {
285
+ const got = await Promise.race([
286
+ b.mailbox.dequeue(),
287
+ new Promise<A2AMessage | null>(r => setTimeout(() => r(null), 1000)),
288
+ ])
289
+ if (!got) break
290
+ takenOrder.push(textOf(got))
291
+ }
292
+ const expectOrder = burst.map(textOf)
293
+ check('30 条同毫秒突发按入队顺序逐条出队(FIFO)', takenOrder.join(',') === expectOrder.join(','),
294
+ `${takenOrder.length} 条`)
295
+
296
+ console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILURE(S)`}`)
297
+ process.exit(failures === 0 ? 0 : 1)
298
+ })
299
+ }
300
+
301
+ main()
@@ -0,0 +1,151 @@
1
+ /**
2
+ * 端到端验证:Agent 模板 → Channel → 克隆实例 → 提交任务 → 监控执行过程。
3
+ *
4
+ * 流程:
5
+ * 1. createAgent(name, harness, config) → 创建 Agent 模板(可复用)
6
+ * 2. createChannel({ name, leadAgent }) → 创建 Channel(内部:lead 模板 + 克隆实例)
7
+ * 3. addAgentToChannel({ channelId, agentId }) → 把 Agent 模板克隆进 Channel(独立身份 id)
8
+ * 4. ensureChannelActive + submitChannelTask → 提交任务,lead 调度,worker 执行
9
+ * 5. monitorChannel → 实时观察 task.status / task.progress / agent.status / agent.event
10
+ *
11
+ * 运行: npx tsx scripts/e2e-agent-channel-task.ts
12
+ */
13
+ import type { DatabaseSync } from 'node:sqlite'
14
+ import { openWorkshopDb } from '../server/services/workshop/db/database'
15
+ import { createChannelRepo } from '../server/services/workshop/db/channel.repo'
16
+ import { createAgentRepo } from '../server/services/workshop/db/agent.repo'
17
+ import { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
18
+ import { createTaskRepo } from '../server/services/workshop/db/task.repo'
19
+ import { createMemoryRepo } from '../server/services/workshop/db/memory.repo'
20
+ import { createChannelEventRepo } from '../server/services/workshop/db/channel-event.repo'
21
+ import { createTeamRepo } from '../server/services/workshop/db/team.repo'
22
+ import { createTeamMemberRepo } from '../server/services/workshop/db/team-member.repo'
23
+ import { createMessageRepo } from '../server/services/workshop/db/message.repo'
24
+ import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
25
+ import { createAgentChannelManager } from '../server/services/workshop/runtime/manager'
26
+ import type { AgentChannelManager } from '../server/services/workshop/runtime/manager'
27
+ import { createAgentImpl } from '../server/services/workshop/agents/factory'
28
+ import { monitorChannel } from '../server/services/workshop/runtime/monitor'
29
+
30
+ let failures = 0
31
+
32
+ function check(name: string, ok: boolean, detail = ''): void {
33
+ console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
34
+ if (!ok) failures += 1
35
+ }
36
+
37
+ function makeManager(db: DatabaseSync): AgentChannelManager {
38
+ return createAgentChannelManager({
39
+ repos: {
40
+ channels: createChannelRepo(db),
41
+ agents: createAgentRepo(db),
42
+ channelAgents: createChannelAgentRepo(db),
43
+ messages: createMessageRepo(db),
44
+ subscriptions: createSubscriptionRepo(db),
45
+ tasks: createTaskRepo(db),
46
+ memories: createMemoryRepo(db),
47
+
48
+ channelEvents: createChannelEventRepo(db),
49
+
50
+ teams: createTeamRepo(db),
51
+
52
+ teamMembers: createTeamMemberRepo(db),
53
+ },
54
+ implFactory: createAgentImpl,
55
+ db,
56
+ })
57
+ }
58
+
59
+ async function main(): Promise<void> {
60
+ console.log('━━━ 端到端:Agent 模板 → Channel → 任务执行 → 监控 ━━━')
61
+ const db = openWorkshopDb(':memory:')
62
+ const manager = makeManager(db)
63
+ try {
64
+ // ---- 1. 创建 Agent 模板(可复用数据结构)----
65
+ console.log('\n--- 1. 创建 Agent 模板 ---')
66
+ const workerTpl = await manager.createAgent({
67
+ name: 'worker-执行者',
68
+ harness: 'mock',
69
+ config: { delayMs: 60 },
70
+ })
71
+ check('创建 Agent 模板成功', !!workerTpl.id)
72
+ check('模板无 channel 绑定(纯定义)', workerTpl.instances.length === 0)
73
+
74
+ // ---- 2. 创建 Channel(带 lead)----
75
+ console.log('\n--- 2. 创建 Channel ---')
76
+ const ch = await manager.createChannel({
77
+ name: '端到端演示频道',
78
+ description: 'Agent 模板复用 → Channel 克隆实例 → 任务监控',
79
+ leadAgent: { name: 'lead-主理人', harness: 'mock', config: { delayMs: 60 } },
80
+ })
81
+ check('创建 Channel 成功', !!ch.channelId && !!ch.leadAgentId)
82
+ check('lead 是独立实例(与 worker 模板 id 不同)', ch.leadAgentId !== workerTpl.id)
83
+
84
+ // ---- 3. 把 Agent 模板放入 Channel(克隆独立身份 id)----
85
+ console.log('\n--- 3. Agent 模板放入 Channel(克隆)---')
86
+ const worker = await manager.addAgentToChannel({
87
+ channelId: ch.channelId,
88
+ agentId: workerTpl.id,
89
+ role: 'worker',
90
+ })
91
+ check('克隆出独立实例 id(≠ 模板 id)', worker.id !== workerTpl.id, `inst=${worker.id.slice(0, 8)} tpl=${workerTpl.id.slice(0, 8)}`)
92
+ check('实例复制模板 name/harness', worker.name === 'worker-执行者' && worker.harness === 'mock')
93
+ const members = await manager.listChannelAgents(ch.channelId)
94
+ check('Channel 成员 = 1 lead + 1 worker', members.length === 2 && members.some(a => a.role === 'lead') && members.some(a => a.role === 'worker'))
95
+
96
+ // ---- 4. 激活 channel + 挂监控 ----
97
+ console.log('\n--- 4. 激活 channel + 挂监控 ---')
98
+ manager.ensureChannelActive(ch.channelId, { tickMs: 50 })
99
+ check('channel 已激活', manager.runtimeStatus().activeChannels.includes(ch.channelId))
100
+ const mon = monitorChannel(manager, ch.channelId)
101
+ check('监控器已启动', mon.events.some(e => e.kind === 'lifecycle'))
102
+
103
+ // ---- 5. 提交任务 ----
104
+ console.log('\n--- 5. 提交任务 ---')
105
+ const task = await manager.submitChannelTask({
106
+ channelId: ch.channelId,
107
+ title: '端到端验证任务',
108
+ description: '从模板创建 Agent → 克隆进 Channel → 执行任务',
109
+ })
110
+ check('任务已提交(SUBMITTED)', !!task.id && task.state === 'SUBMITTED', `state=${task.state}`)
111
+
112
+ // ---- 6. 监控执行过程(等待 COMPLETED)----
113
+ console.log('\n--- 6. 监控执行过程 ---')
114
+ const done = await mon.waitFor(e => e.kind === 'task.status' && e.state === 'COMPLETED', 10_000)
115
+ check('任务执行完成(COMPLETED)', done !== null)
116
+
117
+ const seenBusy = mon.events.some(e => e.kind === 'agent.status' && e.state === 'busy')
118
+ const seenIdle = mon.events.some(e => e.kind === 'agent.status' && e.state === 'idle')
119
+ const seenAssigned = mon.events.some(e => e.kind === 'task.status' && e.state === 'ASSIGNED')
120
+ const seenWorking = mon.events.some(e => e.kind === 'task.status' && e.state === 'WORKING')
121
+ const seenArtifact = mon.events.some(e => e.kind === 'agent.event' && e.event.kind === 'artifact')
122
+ const seenProgress = mon.events.some(e => e.kind === 'task.progress' && e.progress > 0)
123
+ check('监控到成员 busy → idle 状态流转', seenBusy && seenIdle)
124
+ check('监控到 lead dispatch(ASSIGNED 子任务)', seenAssigned)
125
+ check('监控到任务进入 WORKING', seenWorking)
126
+ check('监控到进度上报(progress > 0)', seenProgress)
127
+ check('监控到产物 artifact 产出', seenArtifact)
128
+
129
+ // 终态校验:主任务 COMPLETED + 有汇总成果
130
+ const finalTask = await manager.getTask(ch.channelId, worker.id, task.id)
131
+ check('主任务终态 COMPLETED', finalTask?.state === 'COMPLETED', `state=${finalTask?.state}`)
132
+ check('主任务有成果(artifacts)', (finalTask?.artifacts.length ?? 0) > 0, `n=${finalTask?.artifacts.length}`)
133
+
134
+ // ---- 7. 打印监控时间线 ----
135
+ console.log('\n--- 监控时间线 ---')
136
+ console.log(mon.summary())
137
+ mon.stop()
138
+ }
139
+ finally {
140
+ await manager.shutdown()
141
+ db.close()
142
+ }
143
+
144
+ console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
145
+ process.exit(failures === 0 ? 0 : 1)
146
+ }
147
+
148
+ main().catch((e) => {
149
+ console.error(e)
150
+ process.exit(1)
151
+ })