agentworkshop 0.7.37 → 0.7.39

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 (91) hide show
  1. package/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
  2. package/.AgentWorkShop/prompts/aml-playbook.md +49 -49
  3. package/.AgentWorkShop/prompts/host-tools.json +1238 -1238
  4. package/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
  5. package/.output/.AgentWorkShop/prompts/aml-mission-default.md +20 -20
  6. package/.output/.AgentWorkShop/prompts/aml-playbook.md +49 -49
  7. package/.output/.AgentWorkShop/prompts/host-tools.json +1238 -1238
  8. package/.output/.AgentWorkShop/prompts/peer-reply-cross-channel.md +10 -10
  9. package/.output/nitro.json +1 -1
  10. package/.output/public/_nuxt/{BG8sCcwx.js → CUDwzgLV.js} +4 -4
  11. package/.output/public/_nuxt/CmsOtOAA2.js +1 -0
  12. package/.output/public/_nuxt/builds/latest.json +1 -1
  13. package/.output/public/_nuxt/builds/meta/4457d63f-0866-4cea-b320-04a321f24795.json +1 -0
  14. package/.output/public/_nuxt/default.BXI_fZUi.css +1 -0
  15. package/.output/server/chunks/_/nitro.mjs +703 -704
  16. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  17. package/.output/server/chunks/build/{default-hseyOqBT.mjs → default-CZmZFpqH.mjs} +27 -42
  18. package/.output/server/chunks/build/{default-hseyOqBT.mjs.map → default-CZmZFpqH.mjs.map} +1 -1
  19. package/.output/server/chunks/routes/renderer.mjs +1 -1
  20. package/.output/server/chunks/virtual/entry.mjs +3 -1
  21. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  22. package/.output/server/package.json +1 -1
  23. package/README-zh.md +6 -6
  24. package/README.md +6 -6
  25. package/app/components/AppHeader.vue +0 -17
  26. package/app/components/AppSidebar.vue +1 -0
  27. package/docs/cli.en.md +2 -2
  28. package/docs/cli.md +2 -2
  29. package/docs/full-test-plan.md +5 -1
  30. package/docs/plugins.en.md +2 -1
  31. package/docs/plugins.md +2 -1
  32. package/docs/sdk.en.md +1 -1
  33. package/docs/sdk.md +1 -1
  34. package/i18n/locales/en.ts +1 -0
  35. package/i18n/locales/zh-CN.ts +1 -0
  36. package/package.json +1 -1
  37. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +1 -0
  38. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +1 -0
  39. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +153 -0
  40. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +153 -0
  41. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +431 -0
  42. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +180 -0
  43. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +112 -0
  44. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +227 -0
  45. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +131 -0
  46. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +160 -0
  47. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +157 -0
  48. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +348 -0
  49. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +296 -0
  50. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +285 -0
  51. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +217 -0
  52. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +301 -0
  53. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +151 -0
  54. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +135 -0
  55. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +156 -0
  56. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +72 -0
  57. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +469 -0
  58. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +259 -0
  59. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +97 -0
  60. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +277 -0
  61. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +171 -0
  62. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +425 -0
  63. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +99 -0
  64. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +325 -0
  65. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +332 -0
  66. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +43 -0
  67. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +154 -0
  68. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +531 -0
  69. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +439 -0
  70. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +323 -0
  71. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +433 -0
  72. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +280 -0
  73. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +117 -0
  74. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +160 -0
  75. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +101 -0
  76. package/scripts/_audit/detached-start.mjs +103 -0
  77. package/scripts/_audit/guard-upstream-liveness.mjs +104 -0
  78. package/scripts/_dbg-perms-e2e.mjs +10 -3
  79. package/scripts/_dbg-protocol-matrix.mjs +453 -441
  80. package/scripts/_dbg-render-regression.mjs +151 -130
  81. package/scripts/_dbg-town-agents-probe.mjs +60 -0
  82. package/scripts/_dbg-visual-acceptance.mjs +63 -0
  83. package/scripts/e2e-agent-team.mjs +5 -3
  84. package/scripts/test-plugin-hardening.mjs +32 -0
  85. package/scripts/three-system-e2e.mjs +6 -6
  86. package/server/plugins/dev-stability-guard.ts +40 -63
  87. package/server/plugins-builtin/rag-bridge/index.mjs +9 -2
  88. package/server/utils/stability-severity.ts +66 -0
  89. package/.output/public/_nuxt/DVsjtsvZ2.js +0 -1
  90. package/.output/public/_nuxt/builds/meta/a66fdef7-dcfd-453b-b1f6-543c7dfd98d5.json +0 -1
  91. package/.output/public/_nuxt/default.DE_vT238.css +0 -1
@@ -0,0 +1,296 @@
1
+ /**
2
+ * A2A 类型 + Agent 接口层测试(node + tsx 直跑,无浏览器)。
3
+ *
4
+ * 覆盖:
5
+ * 1. Part 四变体(text/data/url/raw)构造与判别
6
+ * 2. A2AMessage 往返 JSON 序列化(messageId/contextId/parts/metadata 保真)
7
+ * 3. WorkspaceTask 默认值与 JSON 序列化
8
+ * 4. AgentEvent 五变体(kind 判别 + 各字段类型收窄)
9
+ * 5. SupervisionDecision 五变体判别
10
+ * 6. AgentInterface 结构验证(mock 对象 run 返回 AsyncIterable 可迭代)
11
+ */
12
+ import type { Part, A2AMessage, A2AArtifact } from '../server/services/workshop/types/a2a'
13
+ import type { TaskState, WorkspaceTask } from '../server/services/workshop/types/task'
14
+ import type {
15
+ AgentEvent,
16
+ AgentInfo,
17
+ AgentInterface,
18
+ AgentRunContext,
19
+ AgentRunRequest,
20
+ AgentWorkspace,
21
+ SupervisionDecision,
22
+ SupervisionSnapshot,
23
+ } from '../server/services/workshop/agents/agent-interface'
24
+
25
+ let failures = 0
26
+ function check(name: string, ok: boolean, detail = ''): void {
27
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
28
+ if (!ok) {
29
+ failures += 1
30
+ }
31
+ }
32
+
33
+ /** 判别 Part 变体,返回其判别键(用于类型收窄验证) */
34
+ function partKind(part: Part): 'text' | 'data' | 'url' | 'raw' {
35
+ if ('text' in part) return 'text'
36
+ if ('data' in part) return 'data'
37
+ if ('url' in part) return 'url'
38
+ return 'raw'
39
+ }
40
+
41
+ function testPart(): void {
42
+ console.log('\n--- 1. Part 四变体 ---')
43
+
44
+ const text: Part = { text: 'hello', mediaType: 'text/plain', metadata: { k: 'v' } }
45
+ const data: Part = { data: { n: 1 }, mediaType: 'application/json' }
46
+ const url: Part = { url: 'https://example.com/a.png', mediaType: 'image/png', filename: 'a.png' }
47
+ const raw: Part = { raw: 'base64==', mediaType: 'application/octet-stream', filename: 'b.bin' }
48
+
49
+ check('text 变体判别', partKind(text) === 'text')
50
+ check('text 变体字段收窄(string)', typeof text.text === 'string')
51
+ check('text 变体 mediaType', text.mediaType === 'text/plain')
52
+ check('text 变体 metadata', text.metadata?.k === 'v')
53
+
54
+ check('data 变体判别', partKind(data) === 'data')
55
+ check('data 变体字段收窄(unknown 承载任意值)', (data.data as { n: number }).n === 1)
56
+ check('data 变体 mediaType', data.mediaType === 'application/json')
57
+
58
+ check('url 变体判别', partKind(url) === 'url')
59
+ check('url 变体字段收窄(string + filename)', typeof url.url === 'string' && url.filename === 'a.png')
60
+
61
+ check('raw 变体判别', partKind(raw) === 'raw')
62
+ check('raw 变体字段收窄(string + filename)', typeof raw.raw === 'string' && raw.filename === 'b.bin')
63
+ }
64
+
65
+ function testA2AMessage(): void {
66
+ console.log('\n--- 2. A2AMessage 往返 JSON 序列化 ---')
67
+
68
+ const msg: A2AMessage = {
69
+ messageId: 'm-1',
70
+ contextId: 'ch-1',
71
+ taskId: 't-1',
72
+ role: 'ROLE_AGENT',
73
+ parts: [
74
+ { text: 'hi' },
75
+ { data: { x: 42 }, mediaType: 'application/json' },
76
+ { url: 'https://x/y', filename: 'y' },
77
+ { raw: 'AAAA', mediaType: 'application/octet-stream' },
78
+ ],
79
+ metadata: { 'x-aw-task-kind': 'assign', 'x-aw-task-id': 't-1' },
80
+ extensions: ['ext-a'],
81
+ referenceTaskIds: ['t-0'],
82
+ }
83
+
84
+ const roundtrip: A2AMessage = JSON.parse(JSON.stringify(msg))
85
+ check('messageId 保真', roundtrip.messageId === 'm-1')
86
+ check('contextId 保真', roundtrip.contextId === 'ch-1')
87
+ check('taskId 保真', roundtrip.taskId === 't-1')
88
+ check('role 保真', roundtrip.role === 'ROLE_AGENT')
89
+ check('parts 长度保真', roundtrip.parts.length === 4)
90
+ check('parts 内容保真(text)', (roundtrip.parts[0] as { text: string }).text === 'hi')
91
+ check('parts 内容保真(data)', (roundtrip.parts[1] as { data: unknown }).data !== undefined)
92
+ check('metadata 保真', roundtrip.metadata?.['x-aw-task-kind'] === 'assign')
93
+ check('extensions 保真', roundtrip.extensions?.[0] === 'ext-a')
94
+ check('referenceTaskIds 保真', roundtrip.referenceTaskIds?.[0] === 't-0')
95
+ }
96
+
97
+ function testWorkspaceTask(): void {
98
+ console.log('\n--- 3. WorkspaceTask 默认值与 JSON 序列化 ---')
99
+
100
+ const artifact: A2AArtifact = { artifactId: 'art-1', name: '报告', parts: [{ text: '成果' }] }
101
+ const history: A2AMessage = { messageId: 'm-h', contextId: 'ch-1', role: 'ROLE_USER', parts: [{ text: '开始' }] }
102
+
103
+ const task: WorkspaceTask = {
104
+ id: 'task-1',
105
+ channelId: 'ch-1',
106
+ assigneeId: 'agent-a',
107
+ creatorId: 'lead-1',
108
+ title: '写周报',
109
+ state: 'WORKING',
110
+ progress: 0,
111
+ retryCount: 0,
112
+ artifacts: [artifact],
113
+ history: [history],
114
+ createdAt: '2026-08-13T00:00:00.000Z',
115
+ updatedAt: '2026-08-13T00:00:00.000Z',
116
+ }
117
+
118
+ // 可选字段(parentId/description)缺省为 undefined
119
+ check('parentId 缺省为 undefined', task.parentId === undefined)
120
+ check('description 缺省为 undefined', task.description === undefined)
121
+
122
+ const roundtrip: WorkspaceTask = JSON.parse(JSON.stringify(task))
123
+ check('id 保真', roundtrip.id === 'task-1')
124
+ check('channelId 保真', roundtrip.channelId === 'ch-1')
125
+ check('state 保真', roundtrip.state === 'WORKING')
126
+ check('progress 保真', roundtrip.progress === 0)
127
+ check('retryCount 保真', roundtrip.retryCount === 0)
128
+ check('artifacts 保真', roundtrip.artifacts.length === 1 && roundtrip.artifacts[0].artifactId === 'art-1')
129
+ check('history 保真', roundtrip.history.length === 1 && roundtrip.history[0].messageId === 'm-h')
130
+ check('createdAt 保真', roundtrip.createdAt === '2026-08-13T00:00:00.000Z')
131
+
132
+ // 终态枚举:TaskState 各取值均可赋值
133
+ const states: TaskState[] = ['SUBMITTED', 'ASSIGNED', 'WORKING', 'WAITING', 'COMPLETED', 'FAILED', 'CANCELED']
134
+ check('TaskState 七个枚举值', states.length === 7 && states.includes('COMPLETED') && states.includes('CANCELED'))
135
+ }
136
+
137
+ function testAgentEvent(): void {
138
+ console.log('\n--- 4. AgentEvent 五变体 ---')
139
+
140
+ const statusEvt: AgentEvent = { kind: 'status', status: { state: 'working', timestamp: '2026-08-13T00:00:00.000Z' } }
141
+ const msgEvt: AgentEvent = { kind: 'message', message: { messageId: 'm-1', contextId: 'ch-1', role: 'ROLE_AGENT', parts: [{ text: 'hi' }] } }
142
+ const artEvt: AgentEvent = { kind: 'artifact', artifact: { artifactId: 'art-1', parts: [{ text: 'x' }] }, append: true, lastChunk: false, totalChunks: 3 }
143
+ const errEvt: AgentEvent = { kind: 'error', error: { code: 'X', message: 'boom' } }
144
+ const doneEvt: AgentEvent = { kind: 'done', final: { taskId: 't-1' } }
145
+
146
+ check('status 变体判别', statusEvt.kind === 'status')
147
+ check('status 变体字段收窄(state/timestamp)', typeof statusEvt.status.state === 'string' && typeof statusEvt.status.timestamp === 'string')
148
+
149
+ check('message 变体判别', msgEvt.kind === 'message')
150
+ check('message 变体字段收窄(A2AMessage)', msgEvt.message.messageId === 'm-1')
151
+
152
+ check('artifact 变体判别', artEvt.kind === 'artifact')
153
+ check('artifact 变体字段收窄(artifact + 分块)', artEvt.artifact.artifactId === 'art-1' && artEvt.append === true && artEvt.lastChunk === false && artEvt.totalChunks === 3)
154
+
155
+ check('error 变体判别', errEvt.kind === 'error')
156
+ check('error 变体字段收窄(A2AError)', errEvt.error.code === 'X' && errEvt.error.message === 'boom')
157
+
158
+ check('done 变体判别', doneEvt.kind === 'done')
159
+ check('done 变体字段收窄(final.taskId)', doneEvt.final?.taskId === 't-1')
160
+
161
+ // 泛化类型收窄:switch 后可安全访问各字段
162
+ const events: AgentEvent[] = [statusEvt, msgEvt, artEvt, errEvt, doneEvt]
163
+ let narrowed = true
164
+ for (const ev of events) {
165
+ switch (ev.kind) {
166
+ case 'status':
167
+ narrowed = narrowed && ev.status.state !== undefined
168
+ break
169
+ case 'message':
170
+ narrowed = narrowed && ev.message.messageId !== undefined
171
+ break
172
+ case 'artifact':
173
+ narrowed = narrowed && ev.artifact.artifactId !== undefined
174
+ break
175
+ case 'error':
176
+ narrowed = narrowed && ev.error.code !== undefined
177
+ break
178
+ case 'done':
179
+ narrowed = narrowed && ev.final !== undefined
180
+ break
181
+ default:
182
+ narrowed = false
183
+ }
184
+ }
185
+ check('五变体 switch 全收窄无遗漏', narrowed)
186
+ }
187
+
188
+ function testSupervisionDecision(): void {
189
+ console.log('\n--- 5. SupervisionDecision 五变体判别 ---')
190
+
191
+ const decisions: SupervisionDecision[] = [
192
+ { kind: 'dispatch', parentTaskId: 'p-1', assigneeId: 'w-1', title: '子任务', description: 'd', parts: [{ text: 'x' }] },
193
+ { kind: 'reassign', taskId: 't-1', toAgentId: 'w-2' },
194
+ { kind: 'cancel', taskId: 't-2' },
195
+ { kind: 'complete', taskId: 't-3', artifacts: [{ artifactId: 'a-1', parts: [{ text: 'r' }] }] },
196
+ { kind: 'notify', toAgentId: 'w-3', parts: [{ text: '通知' }] },
197
+ ]
198
+
199
+ check('dispatch 判别', decisions[0].kind === 'dispatch')
200
+ check('dispatch 字段(assigneeId/title/parts)', decisions[0].kind === 'dispatch' && decisions[0].assigneeId === 'w-1' && decisions[0].title === '子任务' && decisions[0].parts?.length === 1)
201
+
202
+ check('reassign 判别', decisions[1].kind === 'reassign')
203
+ check('reassign 字段(taskId/toAgentId)', decisions[1].kind === 'reassign' && decisions[1].taskId === 't-1' && decisions[1].toAgentId === 'w-2')
204
+
205
+ check('cancel 判别', decisions[2].kind === 'cancel')
206
+ check('cancel 字段(taskId)', decisions[2].kind === 'cancel' && decisions[2].taskId === 't-2')
207
+
208
+ check('complete 判别', decisions[3].kind === 'complete')
209
+ check('complete 字段(taskId/artifacts)', decisions[3].kind === 'complete' && decisions[3].taskId === 't-3' && decisions[3].artifacts?.length === 1)
210
+
211
+ check('notify 判别', decisions[4].kind === 'notify')
212
+ check('notify 字段(toAgentId/parts)', decisions[4].kind === 'notify' && decisions[4].toAgentId === 'w-3' && decisions[4].parts.length === 1)
213
+ }
214
+
215
+ async function testAgentInterface(): Promise<void> {
216
+ console.log('\n--- 6. AgentInterface 结构验证 ---')
217
+
218
+ const mockImpl: AgentInterface = {
219
+ async* run(request: AgentRunRequest, _ctx: AgentRunContext): AsyncIterable<AgentEvent> {
220
+ yield { kind: 'status', status: { state: 'running', timestamp: new Date().toISOString() } }
221
+ yield { kind: 'message', message: request.message }
222
+ yield { kind: 'done', final: { taskId: request.taskId } }
223
+ },
224
+ async supervise(snapshot: SupervisionSnapshot, _ctx: AgentRunContext): Promise<SupervisionDecision[]> {
225
+ return snapshot.tasks.length === 0 ? [] : [{ kind: 'cancel', taskId: snapshot.tasks[0].id }]
226
+ },
227
+ }
228
+
229
+ const agent: AgentInfo = { id: 'agent-a', channelId: 'ch-1', name: 'A', harness: 'mock', role: 'worker', config: {} }
230
+ const workspace: AgentWorkspace = {
231
+ listAgents: async () => [agent],
232
+ dispatchTask: async () => { throw new Error('not implemented') },
233
+ listTasks: async () => [],
234
+ getTask: async () => { throw new Error('not implemented') },
235
+ reportTask: async () => { throw new Error('not implemented') },
236
+ completeTask: async () => { throw new Error('not implemented') },
237
+ cancelTask: async () => { throw new Error('not implemented') },
238
+ myQueue: async () => ({ agentId: 'agent-a', channelId: 'ch-1', queued: [], completed: [] }),
239
+ queueOverview: async () => [],
240
+ updateTask: async () => { throw new Error('not implemented') },
241
+ reassignTask: async () => { throw new Error('not implemented') },
242
+ sendMessage: async () => { throw new Error('not implemented') },
243
+ pollMailbox: async () => [],
244
+ listMail: async () => [],
245
+ subscribe: async () => {},
246
+ recallMemory: async () => [],
247
+ saveMemory: async () => { throw new Error('not implemented') },
248
+ }
249
+ const ctx: AgentRunContext = { agentId: 'agent-a', channelId: 'ch-1', role: 'worker', workspace, signal: new AbortController().signal }
250
+
251
+ const req: AgentRunRequest = {
252
+ message: { messageId: 'm-1', contextId: 'ch-1', role: 'ROLE_USER', parts: [{ text: 'hi' }] },
253
+ contextId: 'ch-1',
254
+ fromAgentId: null,
255
+ toAgentId: 'agent-a',
256
+ }
257
+
258
+ check('run 返回 AsyncIterable 且可迭代', typeof mockImpl.run(req, ctx)[Symbol.asyncIterator] === 'function')
259
+
260
+ // 实际迭代收集事件
261
+ const collected: AgentEvent[] = []
262
+ for await (const ev of mockImpl.run(req, ctx)) {
263
+ collected.push(ev)
264
+ }
265
+ check('迭代产出 3 个事件', collected.length === 3)
266
+ check('事件顺序(status→message→done)', collected[0].kind === 'status' && collected[1].kind === 'message' && collected[2].kind === 'done')
267
+
268
+ // supervise 可选实现
269
+ check('supervise 可调用返回决策', mockImpl.supervise !== undefined)
270
+ const decisions = await mockImpl.supervise!(
271
+ { tick: 1, now: Date.now(), tasks: [], members: [], pendingChildren: {} },
272
+ ctx,
273
+ )
274
+ check('supervise 空快照返回空数组', Array.isArray(decisions) && decisions.length === 0)
275
+
276
+ // init/dispose 为可选方法
277
+ check('init 可选(未实现为 undefined)', mockImpl.init === undefined)
278
+ check('dispose 可选(未实现为 undefined)', mockImpl.dispose === undefined)
279
+ }
280
+
281
+ async function main(): Promise<void> {
282
+ testPart()
283
+ testA2AMessage()
284
+ testWorkspaceTask()
285
+ testAgentEvent()
286
+ testSupervisionDecision()
287
+ await testAgentInterface()
288
+
289
+ console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
290
+ process.exit(failures === 0 ? 0 : 1)
291
+ }
292
+
293
+ main().catch((e) => {
294
+ console.error(e)
295
+ process.exit(1)
296
+ })
@@ -0,0 +1,285 @@
1
+ /**
2
+ * MCP 工具层测试(node + tsx 直跑,无浏览器)。
3
+ *
4
+ * 覆盖:
5
+ * 1. tools/list 返回 §6.1 的 16 个工具(名逐字一致)
6
+ * 2. tools/call 参数校验(zod 错误返回)
7
+ * 3. 无 token → UNAUTHORIZED
8
+ * 4. 无效 token → UNAUTHORIZED
9
+ * 5. a2a.send 的 fromAgentId 由 token 决定(manager 收到 callerAgentId,而非请求体自报)
10
+ *
11
+ * 通过 SDK 的 Client + InMemoryTransport 协议层测试;token 经自定义 transport
12
+ * 注入(Authorization 头 / auth 中间件两种路径)。
13
+ */
14
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
15
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
16
+ import { createWorkshopMcpServer } from '../server/mcp/workshop-server'
17
+ import type { AgentChannelManager } from '../server/services/workshop/runtime/manager'
18
+ import type { AgentInfo } from '../server/services/workshop/agents/agent-interface'
19
+
20
+ let failures = 0
21
+ function check(name: string, ok: boolean, detail = ''): void {
22
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
23
+ if (!ok) {
24
+ failures += 1
25
+ }
26
+ }
27
+
28
+ /** 提取工具错误文案(isError 结果的第一个文本块) */
29
+ function toolErrorText(result: unknown): string {
30
+ const block = (result as { content?: Array<{ text?: string }> })?.content?.[0]
31
+ return typeof block?.text === 'string' ? block.text : ''
32
+ }
33
+
34
+ /** 可注入 caller token 的客户端 transport(InMemoryTransport 只原生转发 authInfo) */
35
+ class TokenClientTransport extends InMemoryTransport {
36
+ token: string | undefined
37
+ /** 'auth' = 经 authInfo.token 注入;'header' = 经 Authorization: Bearer 头(requestInfo)注入 */
38
+ mode: 'auth' | 'header' = 'auth'
39
+
40
+ override async send(message: unknown, options?: { authInfo?: unknown }): Promise<void> {
41
+ if (this.token === undefined) {
42
+ return super.send(message as never, options as never)
43
+ }
44
+ if (this.mode === 'auth') {
45
+ return super.send(message as never, {
46
+ ...options,
47
+ authInfo: { token: this.token, clientId: 'test-client', scopes: [] },
48
+ } as never)
49
+ }
50
+ // header 模式:复刻 InMemoryTransport.send 但注入 requestInfo(Authorization 头)
51
+ const self = this as unknown as { _otherTransport?: { onmessage?: (m: unknown, e: unknown) => void, _messageQueue: Array<{ message: unknown, extra: unknown }> } }
52
+ const other = self._otherTransport
53
+ if (!other) throw new Error('Not connected')
54
+ const extra = { requestInfo: { headers: { authorization: `Bearer ${this.token}` } } }
55
+ if (other.onmessage) other.onmessage(message, extra)
56
+ else other._messageQueue.push({ message, extra })
57
+ }
58
+ }
59
+
60
+ /** 手动链接一对 transport(client 侧为可注入 token 的 TokenClientTransport) */
61
+ function createLinkedPair(): [TokenClientTransport, InMemoryTransport] {
62
+ const clientTransport = new TokenClientTransport()
63
+ const serverTransport = new InMemoryTransport()
64
+ const c = clientTransport as unknown as { _otherTransport?: InMemoryTransport }
65
+ const s = serverTransport as unknown as { _otherTransport?: InMemoryTransport }
66
+ c._otherTransport = serverTransport
67
+ s._otherTransport = clientTransport
68
+ return [clientTransport, serverTransport]
69
+ }
70
+
71
+ /** MCP 工具目录(与 server/mcp/workshop-server.ts 逐字一致):16 基础 + 4 扩展 + 5 设备孪生 */
72
+ const EXPECTED_TOOLS = [
73
+ 'workshop.channel.create',
74
+ 'workshop.channel.list',
75
+ 'workshop.channel.remove',
76
+ 'workshop.agent.create',
77
+ 'workshop.agent.add',
78
+ 'workshop.agent.definitions',
79
+ 'workshop.agent.list',
80
+ 'workshop.agent.remove',
81
+ 'workshop.task.submit',
82
+ 'workshop.task.dispatch',
83
+ 'workshop.task.list',
84
+ 'workshop.task.get',
85
+ 'workshop.task.report',
86
+ 'workshop.task.complete',
87
+ 'workshop.task.cancel',
88
+ 'workshop.a2a.send',
89
+ 'workshop.a2a.poll',
90
+ 'workshop.a2a.subscribe',
91
+ 'workshop.mail.list',
92
+ 'workshop.queue.overview',
93
+ 'device.list',
94
+ 'device.read',
95
+ 'device.control',
96
+ 'device.push_telemetry',
97
+ 'device.bind_agent',
98
+ ]
99
+
100
+ /** fake manager stub:记录调用 + 返回预设值(与 AgentChannelManager 相同方法签名) */
101
+ function makeFakeManager() {
102
+ const calls: Array<{ method: string, args: unknown[] }> = []
103
+
104
+ const agentsByToken = new Map<string, AgentInfo>()
105
+ agentsByToken.set('token-lead-1', {
106
+ id: 'agent-lead-1',
107
+ channelId: 'ch-1',
108
+ name: 'Lead',
109
+ harness: 'mock',
110
+ role: 'lead',
111
+ config: {},
112
+ })
113
+ agentsByToken.set('token-worker-2', {
114
+ id: 'agent-worker-2',
115
+ channelId: 'ch-1',
116
+ name: 'Worker2',
117
+ harness: 'mock',
118
+ role: 'worker',
119
+ config: {},
120
+ })
121
+
122
+ const record
123
+ = (method: string, result: unknown) =>
124
+ (...args: unknown[]) => {
125
+ calls.push({ method, args })
126
+ return Promise.resolve(result)
127
+ }
128
+
129
+ const manager = {
130
+ createChannel: record('createChannel', { channelId: 'ch-1' }),
131
+ listChannels: record('listChannels', []),
132
+ removeChannel: record('removeChannel', undefined),
133
+ createAgent: record('createAgent', {
134
+ id: 'agent-tpl',
135
+ name: 'New',
136
+ harness: 'mock',
137
+ config: {},
138
+ enabled: 1,
139
+ instances: [],
140
+ }),
141
+ addAgentToChannel: record('addAgentToChannel', {
142
+ id: 'agent-inst',
143
+ channelId: 'ch-1',
144
+ name: 'New',
145
+ harness: 'mock',
146
+ role: 'worker',
147
+ config: {},
148
+ }),
149
+ listAgents: record('listAgents', []),
150
+ listChannelAgents: record('listChannelAgents', []),
151
+ submitChannelTask: record('submitChannelTask', { id: 'task-1' }),
152
+ dispatchTask: record('dispatchTask', { id: 'task-2' }),
153
+ reportTask: record('reportTask', { id: 'task-1' }),
154
+ completeTask: record('completeTask', { id: 'task-1' }),
155
+ cancelTask: record('cancelTask', { id: 'task-1' }),
156
+ listTasks: record('listTasks', []),
157
+ getTask: record('getTask', { id: 'task-1' }),
158
+ sendA2A: record('sendA2A', {
159
+ messageId: 'm-1',
160
+ contextId: 'ch-1',
161
+ role: 'ROLE_AGENT',
162
+ parts: [{ text: 'hi' }],
163
+ }),
164
+ pollMailbox: record('pollMailbox', []),
165
+ listChannelMail: record('listChannelMail', []),
166
+ queueOverview: record('queueOverview', []),
167
+ subscribe: record('subscribe', undefined),
168
+ findByToken: (token: string) => agentsByToken.get(token),
169
+ restore: record('restore', undefined),
170
+ }
171
+
172
+ return {
173
+ manager: manager as unknown as AgentChannelManager,
174
+ calls,
175
+ agentsByToken,
176
+ }
177
+ }
178
+
179
+ async function main(): Promise<void> {
180
+ const [clientTransport, serverTransport] = createLinkedPair()
181
+ const fake = makeFakeManager()
182
+ const server = createWorkshopMcpServer(fake.manager)
183
+
184
+ const client = new Client({ name: 'test-client', version: '1.0.0' })
185
+ await server.connect(serverTransport)
186
+ await client.connect(clientTransport)
187
+
188
+ // ===== 1. tools/list:20 个工具,名逐字一致 =====
189
+ const { tools } = await client.listTools()
190
+ const actualNames = tools.map(t => t.name).sort()
191
+ const expectedNames = [...EXPECTED_TOOLS].sort()
192
+ check('tools/list 返回 25 个工具', tools.length === 25, `got ${tools.length}`)
193
+ check(
194
+ '工具名与 §6.1 逐字一致',
195
+ JSON.stringify(actualNames) === JSON.stringify(expectedNames),
196
+ JSON.stringify(actualNames),
197
+ )
198
+
199
+ // ===== 2. 参数校验:zod 错误返回 =====
200
+ const missingName = await client.callTool({ name: 'workshop.channel.create', arguments: {} })
201
+ check(
202
+ '参数校验:channel.create 缺 name → 工具错误',
203
+ (missingName as { isError?: boolean }).isError === true,
204
+ toolErrorText(missingName),
205
+ )
206
+ check(
207
+ '参数校验:错误文案含 Input validation error',
208
+ toolErrorText(missingName).includes('Input validation error'),
209
+ toolErrorText(missingName),
210
+ )
211
+
212
+ const badRole = await client.callTool({
213
+ name: 'workshop.agent.add',
214
+ arguments: { channelId: 'ch-1', agentId: 'agent-tpl', role: 'admin' },
215
+ })
216
+ check(
217
+ '参数校验:agent.add role 非法枚举 → 工具错误',
218
+ (badRole as { isError?: boolean }).isError === true,
219
+ toolErrorText(badRole),
220
+ )
221
+
222
+ // ===== 3. 无 token → UNAUTHORIZED =====
223
+ clientTransport.token = undefined
224
+ const noToken = await client.callTool({ name: 'workshop.agent.list', arguments: {} })
225
+ check(
226
+ '无 token → UNAUTHORIZED',
227
+ (noToken as { isError?: boolean }).isError === true && toolErrorText(noToken) === 'UNAUTHORIZED',
228
+ toolErrorText(noToken),
229
+ )
230
+
231
+ // ===== 4. 无效 token → UNAUTHORIZED =====
232
+ clientTransport.token = 'bad-token'
233
+ const badToken = await client.callTool({ name: 'workshop.agent.list', arguments: {} })
234
+ check(
235
+ '无效 token → UNAUTHORIZED',
236
+ (badToken as { isError?: boolean }).isError === true && toolErrorText(badToken) === 'UNAUTHORIZED',
237
+ toolErrorText(badToken),
238
+ )
239
+
240
+ // ===== 5. a2a.send 的 fromAgentId 由 token 决定 =====
241
+ clientTransport.token = 'token-lead-1'
242
+ // 请求体故意自报 fromAgentId,应被忽略;caller 由 token 解析为 agent-lead-1
243
+ const sendResult = await client.callTool({
244
+ name: 'workshop.a2a.send',
245
+ arguments: { toAgentId: 'agent-worker-2', parts: [{ text: 'hello' }], fromAgentId: 'evil-agent' },
246
+ })
247
+ check('a2a.send(有效 token)→ 成功', (sendResult as { isError?: boolean }).isError !== true, toolErrorText(sendResult))
248
+
249
+ const sendCall = fake.calls.find(c => c.method === 'sendA2A')
250
+ const callerAgentId = sendCall?.args[1]
251
+ const sendInput = sendCall?.args[2] as { toAgentId?: string, fromAgentId?: string } | undefined
252
+ check(
253
+ 'a2a.send callerAgentId 由 token 决定(agent-lead-1)',
254
+ callerAgentId === 'agent-lead-1',
255
+ `got ${String(callerAgentId)}`,
256
+ )
257
+ check(
258
+ 'a2a.send 忽略请求体自报的 fromAgentId',
259
+ sendInput?.fromAgentId === undefined && sendInput?.toAgentId === 'agent-worker-2',
260
+ JSON.stringify(sendInput),
261
+ )
262
+
263
+ // ===== 6. Authorization 头路径同样解析 token =====
264
+ clientTransport.mode = 'header'
265
+ clientTransport.token = 'token-worker-2'
266
+ const headerResult = await client.callTool({ name: 'workshop.a2a.send', arguments: { toAgentId: 'agent-lead-1', parts: [{ text: 'hi' }] } })
267
+ check('Authorization 头解析 token → 成功', (headerResult as { isError?: boolean }).isError !== true, toolErrorText(headerResult))
268
+ const headerCall = fake.calls.filter(c => c.method === 'sendA2A').at(-1)
269
+ check(
270
+ 'Authorization 头路径 callerAgentId = agent-worker-2',
271
+ headerCall?.args[1] === 'agent-worker-2',
272
+ `got ${String(headerCall?.args[1])}`,
273
+ )
274
+
275
+ await client.close()
276
+ await server.close()
277
+
278
+ console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
279
+ process.exit(failures === 0 ? 0 : 1)
280
+ }
281
+
282
+ main().catch((e) => {
283
+ console.error(e)
284
+ process.exit(1)
285
+ })