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,171 @@
1
+ /**
2
+ * 审计实验 3 —— 「写入落到硬件但未入账」链路(只读审计;全部走本地假 PLC,不触真实数据)。
3
+ *
4
+ * 组成:
5
+ * A) 假 Modbus TCP PLC(FC16 写 / FC03 读;可切换「回读返回陈旧值」和「永不响应」)
6
+ * B) 真实 modbus-tcp 写驱动(server/services/workshop/dcw/drivers.ts)
7
+ * C) 真实 RecipeRollBackRepo(账本;cwd 已切到 scripts/_audit/.tmp,绝不碰 server/data/*)
8
+ * D) 忠实复刻 dcw-controller.ts:536 的入册闸门 `if (outcome.ok !== false)`
9
+ *
10
+ * 运行:node --experimental-transform-types scripts/_audit/run.mjs scripts/_audit/dcw-unrecorded-write.ts
11
+ */
12
+ import { mkdirSync } from 'node:fs'
13
+ import net from 'node:net'
14
+
15
+ const TMP_DIR = new URL('./.tmp/server/data/', import.meta.url)
16
+ mkdirSync(TMP_DIR, { recursive: true })
17
+ process.chdir(new URL('./.tmp/', import.meta.url).pathname.replace(/^\//, ''))
18
+
19
+ const { modbusTcpDcwDriver } = await import('../../server/services/workshop/dcw/drivers')
20
+ const { RecipeRollBackRepo } = await import('../../server/services/workshop/dcw/recipe-rollback.repo')
21
+ const { writeTolerance } = await import('../../server/services/workshop/dcw/dcw-runtime')
22
+
23
+ let failures = 0
24
+ function check(name: string, cond: boolean, extra = ''): void {
25
+ if (cond) {
26
+ console.log(` PASS ${name}${extra ? ` (${extra})` : ''}`)
27
+ }
28
+ else {
29
+ failures++
30
+ console.log(` FAIL ${name}${extra ? ` (${extra})` : ''}`)
31
+ }
32
+ }
33
+
34
+ interface FakePlc {
35
+ port: number
36
+ registers: Map<number, number>
37
+ writeCount: number
38
+ mode: 'stale-readback' | 'hang'
39
+ close: () => void
40
+ }
41
+
42
+ function startFakePlc(opts: { staleReadback?: number, mode?: 'stale-readback' | 'hang' }): Promise<FakePlc> {
43
+ const registers = new Map<number, number>()
44
+ const state = { writeCount: 0, mode: opts.mode ?? 'stale-readback' as 'stale-readback' | 'hang' }
45
+ const server = net.createServer((sock) => {
46
+ let buf = Buffer.alloc(0)
47
+ sock.on('data', (chunk) => {
48
+ buf = Buffer.concat([buf, chunk])
49
+ while (buf.length >= 6) {
50
+ const len = buf.readUInt16BE(4)
51
+ const total = 6 + len
52
+ if (buf.length < total) break
53
+ const unitId = buf.readUInt8(6)
54
+ const pdu = buf.subarray(7, total)
55
+ const fc = pdu.readUInt8(0)
56
+ const tid = buf.readUInt16BE(0)
57
+ if (state.mode === 'hang') {
58
+ buf = buf.subarray(total)
59
+ continue // 收到即吞:模拟半开链路(不响应)
60
+ }
61
+ let resp: Buffer
62
+ if (fc === 0x10) { // write multiple registers
63
+ const addr = pdu.readUInt16BE(1)
64
+ const qty = pdu.readUInt16BE(3)
65
+ for (let i = 0; i < qty; i++) registers.set(addr + i, pdu.readUInt16BE(6 + i * 2))
66
+ state.writeCount++
67
+ resp = Buffer.from([0x10, pdu[1]!, pdu[2]!, pdu[3]!, pdu[4]!])
68
+ }
69
+ else if (fc === 0x03) { // read holding registers
70
+ const addr = pdu.readUInt16BE(1)
71
+ const qty = pdu.readUInt16BE(3)
72
+ const data: number[] = []
73
+ for (let i = 0; i < qty; i++) {
74
+ // 关键:回读一律返回「陈旧值」——模拟 PLC 尚未生效/慢一拍
75
+ data.push(opts.staleReadback != null ? opts.staleReadback : (registers.get(addr + i) ?? 0))
76
+ }
77
+ resp = Buffer.from([0x03, data.length * 2, ...data.flatMap(v => [v >> 8 & 0xFF, v & 0xFF])])
78
+ }
79
+ else {
80
+ resp = Buffer.from([fc | 0x80, 0x01])
81
+ }
82
+ const head = Buffer.alloc(6)
83
+ head.writeUInt16BE(tid, 0)
84
+ head.writeUInt16BE(0, 2)
85
+ head.writeUInt16BE(resp.length + 1, 4)
86
+ sock.write(Buffer.concat([head, Buffer.from([unitId]), resp]))
87
+ buf = buf.subarray(total)
88
+ }
89
+ })
90
+ sock.on('error', () => {})
91
+ })
92
+ return new Promise((resolve) => {
93
+ server.listen(0, '127.0.0.1', () => {
94
+ resolve({
95
+ port: (server.address() as net.AddressInfo).port,
96
+ registers,
97
+ get writeCount() { return state.writeCount },
98
+ mode: state.mode,
99
+ close: () => server.close(),
100
+ } as FakePlc)
101
+ })
102
+ })
103
+ }
104
+
105
+ const nodeLike = { decimals: 2, min: 0, max: 100 } as never
106
+
107
+ console.log('\n=== A. 回读陈旧值:写已落到寄存器,但 ACK 判定失败(ok=false) ===')
108
+ {
109
+ const plc = await startFakePlc({ staleReadback: 5 })
110
+ const cfg = { host: '127.0.0.1', port: plc.port, unitId: 1, register: 0, dataType: 'int16', byteOrder: 'big' }
111
+ const res = await modbusTcpDcwDriver.write({
112
+ eng: 20,
113
+ tolerance: writeTolerance(nodeLike),
114
+ domain: { min: 0, max: 100 },
115
+ driverConfig: cfg,
116
+ })
117
+ console.log(` 驱动返回: ok=${res.ok} raw=${res.raw} readback=${res.readback} message=${res.message}`)
118
+ check('硬件寄存器已收到写入值 20', plc.registers.get(0) === 20, `reg0=${plc.registers.get(0)}`)
119
+ check('驱动判定为 ACK 失败(ok=false)', res.ok === false)
120
+ check('失败原因 = 回读偏差超容差', /回读偏差超容差/.test(res.message), res.message)
121
+ check('readback 为陈旧值 5', res.readback === 5)
122
+
123
+ // 账本语义复刻:dcw-controller.ts:536「仅成功写记锚」→ ok=false 时 afterWrite 不被调用
124
+ const repo = new RecipeRollBackRepo()
125
+ repo.appendAnchor({ lineId: 'ln', nodeId: 'dw-1', prevValue: 5, newValue: 10, source: 'manual', actor: 'user', recipeRunId: null })
126
+ const outcome = res
127
+ let ledgerAppends = 0
128
+ if (outcome.ok !== false) { // ← 与 dcw-controller.ts:532-536 同构
129
+ repo.appendAnchor({ lineId: 'ln', nodeId: 'dw-1', prevValue: 10, newValue: 20, source: 'manual', actor: 'user', recipeRunId: null })
130
+ ledgerAppends++
131
+ }
132
+ check('ok=false 时账本未入册(闸门同构复刻)', ledgerAppends === 0)
133
+ const stable = repo.lastStableAnchor('dw-1')
134
+ console.log(` 账本最新稳定锚 = {prev:${stable?.prevValue}, new:${stable?.newValue}};硬件寄存器 = ${plc.registers.get(0)}`)
135
+ check('入册锚不是本次写入(20 不在册)', stable?.newValue === 10, `new=${stable?.newValue}`)
136
+ check('硬件实际值(20)≠ 入册锚 newValue(10):账本与硬件漂移', plc.registers.get(0) !== stable?.newValue)
137
+ check('seeded 锚的 prevValue=5 仍是回退目标:回退将跳过真实的上一设定值 10', stable?.prevValue === 5)
138
+ plc.close()
139
+ }
140
+
141
+ console.log('\n=== B. 同理:另一个方向的漂移(node.value = 陈旧回读值 → 保写心跳将重下发陈旧值) ===')
142
+ {
143
+ const plc = await startFakePlc({ staleReadback: 5 })
144
+ const cfg = { host: '127.0.0.1', port: plc.port, unitId: 1, register: 0, dataType: 'int16', byteOrder: 'big' }
145
+ const res = await modbusTcpDcwDriver.write({ eng: 20, tolerance: writeTolerance(nodeLike), domain: { min: 0, max: 100 }, driverConfig: cfg })
146
+ // dcw-controller.ts:176 等价:node.applyWriteResult(outcome.readback ?? eng, ok, ...)
147
+ const DcwNodeMod = await import('../../server/services/workshop/dcw/dcw-node')
148
+ const node = DcwNodeMod.DcwNode.fromRow({ id: 'dw-1', templateRef: 'cw-temp', min: 0, max: 100, decimals: 2, holdIntervalMs: 1000, driver: 'modbus-tcp', driverConfig: cfg })
149
+ node.value = 10
150
+ node.applyWriteResult(res.readback ?? 20, res.ok, res.message, new Date().toISOString())
151
+ console.log(` 写入 20 / 回读 ${res.readback} → node.value=${node.value}(state=${node.state})`)
152
+ check('node.value 被写成陈旧回读值 5(既非设定值 20 也非真实上一设定 10)', node.value === 5, `value=${node.value}`)
153
+ plc.close()
154
+ }
155
+
156
+ console.log('\n=== C. 写超时(假 PLC 收到即吞):是否 bounded、是否误判 ===')
157
+ {
158
+ const plc = await startFakePlc({ mode: 'hang' })
159
+ const cfg = { host: '127.0.0.1', port: plc.port, unitId: 1, register: 0, dataType: 'int16', byteOrder: 'big' }
160
+ const t0 = Date.now()
161
+ const res = await modbusTcpDcwDriver.write({ eng: 30, tolerance: writeTolerance(nodeLike), domain: { min: 0, max: 100 }, driverConfig: cfg })
162
+ const dt = Date.now() - t0
163
+ console.log(` 返回: ok=${res.ok} message=${res.message} elapsed=${dt}ms`)
164
+ check('写有硬超时(<=4s 返回,实测 withModbusConn 3s)', dt < 4000, `${dt}ms`)
165
+ check('超时映射为 ACK 失败(ok=false)', res.ok === false)
166
+ check('超时时 readback=null(账本/节点值无法判定硬件真实状态)', res.readback === null, `readback=${res.readback}`)
167
+ plc.close()
168
+ }
169
+
170
+ console.log(`\nresult: ${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
171
+ process.exit(failures === 0 ? 0 : 1)
@@ -0,0 +1,425 @@
1
+ /**
2
+ * 端到端编排测试(T8,§10 test-orchestration)— 完整闭环。
3
+ *
4
+ * 流程:用户向 Channel 提交主任务 → 真实 SchedulerLoop(tickMs=10)驱动 lead 观察快照、
5
+ * 分解 dispatch 给 2 个 worker → worker(真实 MockAgentImpl,delayMs=0)自动接取执行上报 →
6
+ * lead 汇总子任务成果交付(主任务 COMPLETED + 成果可见 + 成员进度互见 + 跨 channel 作用域隔离)。
7
+ *
8
+ * 装配::memory: db + 真实 Manager(createAgentChannelManager,默认真实 TaskEngine)
9
+ * + 真实 SchedulerLoop(mock harness 渠道,tickMs=10)+ 真实 TaskEngine。
10
+ * lead 用测试内联的监督型 impl(SuperviseOnlyLead):run() 不执行任务(§2.1 lead 仅编排,
11
+ * 避免 mock 剧本把主任务当普通 assign 自行执行);supervise() 做分解/汇总决策。
12
+ * worker 用真实 MockAgentImpl(delayMs=0)。
13
+ *
14
+ * 运行:npx tsx scripts/test-orchestration.ts
15
+ */
16
+ import { randomUUID } from 'node:crypto'
17
+ import type { DatabaseSync } from 'node:sqlite'
18
+ import { openWorkshopDb, parseJson } from '../server/services/workshop/db/database'
19
+ import { createChannelRepo } from '../server/services/workshop/db/channel.repo'
20
+ import { createAgentRepo } from '../server/services/workshop/db/agent.repo'
21
+ import { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
22
+ import { createMessageRepo } from '../server/services/workshop/db/message.repo'
23
+ import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
24
+ import { createTaskRepo } from '../server/services/workshop/db/task.repo'
25
+ import { createMemoryRepo } from '../server/services/workshop/db/memory.repo'
26
+ import { createChannelEventRepo } from '../server/services/workshop/db/channel-event.repo'
27
+ import { createTeamRepo } from '../server/services/workshop/db/team.repo'
28
+ import { createTeamMemberRepo } from '../server/services/workshop/db/team-member.repo'
29
+ import {
30
+ createAgentChannelManager,
31
+ type AgentChannelManager,
32
+ type AllRepos,
33
+ } from '../server/services/workshop/runtime/manager'
34
+ import { SchedulerLoop } from '../server/services/workshop/runtime/scheduler-loop'
35
+ import type { ChannelRuntime } from '../server/services/workshop/runtime/channel-runtime'
36
+ import type { AgentRuntime } from '../server/services/workshop/runtime/agent-runtime'
37
+ import { MockAgentImpl } from '../server/services/workshop/agents/mock-agent'
38
+ import type {
39
+ AgentEvent,
40
+ AgentInfo,
41
+ AgentInterface,
42
+ AgentRunContext,
43
+ AgentRunRequest,
44
+ SupervisionDecision,
45
+ SupervisionSnapshot,
46
+ } from '../server/services/workshop/agents/agent-interface'
47
+ import type { A2AArtifact, Part } from '../server/services/workshop/types/a2a'
48
+ import type { WorkspaceTask } from '../server/services/workshop/types/task'
49
+ import { AppError } from '../server/utils/errors'
50
+
51
+ let failures = 0
52
+ function check(name: string, ok: boolean, detail = ''): void {
53
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
54
+ if (!ok) failures += 1
55
+ }
56
+
57
+ function sleep(ms: number): Promise<void> {
58
+ const { promise, resolve } = Promise.withResolvers<undefined>()
59
+ setTimeout(resolve, ms)
60
+ return promise
61
+ }
62
+
63
+ /** 轮询等待条件成立(上限 timeoutMs;sleep + 超时,与 §10 测试策略一致) */
64
+ async function waitUntil(
65
+ cond: () => boolean | Promise<boolean>,
66
+ timeoutMs: number,
67
+ ): Promise<boolean> {
68
+ const deadline = Date.now() + timeoutMs
69
+ for (;;) {
70
+ if (await cond()) return true
71
+ if (Date.now() >= deadline) return cond()
72
+ await sleep(20)
73
+ }
74
+ }
75
+
76
+ function isAppError(err: unknown): err is AppError {
77
+ return err instanceof AppError
78
+ }
79
+
80
+ /** 终态(与 mock-agent 同语义) */
81
+ const TERMINAL = new Set(['COMPLETED', 'FAILED', 'CANCELED'])
82
+
83
+ /** 汇总 artifact:列出每个子任务的成果文本(lead 交付时挂到父任务) */
84
+ function buildSummaryArtifact(
85
+ parent: { title: string },
86
+ children: { title: string, artifacts: A2AArtifact[] }[],
87
+ ): A2AArtifact {
88
+ const parts: Part[] = [{ text: `主任务「${parent.title}」子任务成果汇总:` }]
89
+ for (const child of children) {
90
+ for (const artifact of child.artifacts) {
91
+ for (const part of artifact.parts) {
92
+ const label = `[${child.title}] `
93
+ parts.push({ text: 'text' in part ? `${label}${part.text}` : `${label}${JSON.stringify(part)}` })
94
+ }
95
+ }
96
+ }
97
+ return { artifactId: randomUUID(), name: '汇总', description: '子任务成果汇总', parts }
98
+ }
99
+
100
+ /**
101
+ * 监督型 lead impl(测试内联,§2.1 lead=编排者语义):
102
+ * - run():no-op(lead 不直接执行任务;调度分解/汇总由 supervise 承担)
103
+ * - supervise():lead 自有且无子任务的任务 → dispatch 给全部空闲 worker(2 个 worker → 2 子任务);
104
+ * 子任务全部完成 → complete 父任务并附子任务成果汇总 artifact
105
+ */
106
+ class SuperviseOnlyLead implements AgentInterface {
107
+ async* run(_request: AgentRunRequest, _ctx: AgentRunContext): AsyncIterable<AgentEvent> {
108
+ // lead 不执行任务(空事件流;assign 消息由平台自动接取后经 supervise 分解)
109
+ }
110
+
111
+ async supervise(
112
+ snapshot: SupervisionSnapshot,
113
+ ctx: AgentRunContext,
114
+ ): Promise<SupervisionDecision[]> {
115
+ const decisions: SupervisionDecision[] = []
116
+ const idleWorkers = snapshot.members.filter(m => m.role === 'worker' && m.state === 'idle')
117
+ for (const task of snapshot.tasks) {
118
+ if (task.assigneeId !== ctx.agentId) continue
119
+ if (TERMINAL.has(task.state)) continue
120
+ const children = snapshot.tasks.filter(t => t.parentId === task.id)
121
+ if (children.length === 0) {
122
+ // 无子任务且任务可推进(SUBMITTED 待分解 / WORKING 已自动接取)→ 分解给每个空闲 worker
123
+ if (task.state === 'SUBMITTED' || task.state === 'WORKING') {
124
+ for (const worker of idleWorkers) {
125
+ decisions.push({
126
+ kind: 'dispatch',
127
+ parentTaskId: task.id,
128
+ assigneeId: worker.agentId,
129
+ title: task.title,
130
+ description: task.description,
131
+ })
132
+ }
133
+ }
134
+ }
135
+ else if (
136
+ (task.state === 'WAITING' || task.state === 'WORKING')
137
+ && children.every(c => c.state === 'COMPLETED')
138
+ ) {
139
+ // 子任务全部完成 → 汇总成果交付父任务
140
+ decisions.push({ kind: 'complete', taskId: task.id, artifacts: [buildSummaryArtifact(task, children)] })
141
+ }
142
+ }
143
+ return decisions
144
+ }
145
+ }
146
+
147
+ /** implFactory:lead → 监督型 impl(仅调度);worker → 真实 mock(delayMs=0) */
148
+ function buildImplFactory(): (agent: AgentInfo) => AgentInterface {
149
+ return agent => (agent.role === 'lead' ? new SuperviseOnlyLead() : new MockAgentImpl(agent.config))
150
+ }
151
+
152
+ /** implFactory:全部真实 MockAgentImpl(生产路径:mock lead 的 supervise 剧本做调度) */
153
+ function buildRealMockFactory(): (agent: AgentInfo) => AgentInterface {
154
+ return agent => new MockAgentImpl(agent.config)
155
+ }
156
+
157
+ interface Harness {
158
+ db: DatabaseSync
159
+ manager: AgentChannelManager
160
+ repos: AllRepos
161
+ }
162
+
163
+ /** :memory: db + 真实 Manager(默认真实 TaskEngine)+ 注入 implFactory */
164
+ function setup(): Harness {
165
+ const db = openWorkshopDb(':memory:')
166
+ const repos: AllRepos = {
167
+ channels: createChannelRepo(db),
168
+ agents: createAgentRepo(db),
169
+ channelAgents: createChannelAgentRepo(db),
170
+ messages: createMessageRepo(db),
171
+ subscriptions: createSubscriptionRepo(db),
172
+ tasks: createTaskRepo(db),
173
+ memories: createMemoryRepo(db),
174
+
175
+ channelEvents: createChannelEventRepo(db),
176
+
177
+ teams: createTeamRepo(db),
178
+
179
+ teamMembers: createTeamMemberRepo(db),
180
+ }
181
+ const manager = createAgentChannelManager({ repos, implFactory: buildImplFactory(), db })
182
+ return { db, manager, repos }
183
+ }
184
+
185
+ /** 白盒取 manager 内部 ChannelRuntime(manager 未暴露 accessor;测试装配 SchedulerLoop 用) */
186
+ function channelRuntimeOf(manager: AgentChannelManager, channelId: string): ChannelRuntime {
187
+ const internals = manager as unknown as { channels: Map<string, ChannelRuntime> }
188
+ const cr = internals.channels.get(channelId)
189
+ if (!cr) throw new AppError(404, 'NOT_FOUND', `channel runtime 不存在: ${channelId}`)
190
+ return cr
191
+ }
192
+
193
+ /** 给 channel 挂载真实 SchedulerLoop 并启动(真实 Manager + 真实 SchedulerLoop 装配) */
194
+ function attachScheduler(manager: AgentChannelManager, channelId: string, tickMs: number): SchedulerLoop {
195
+ // 懒加载时代:先激活 channel(装配 lead 运行时与默认循环),再换成测试配置的循环
196
+ manager.ensureChannelActive(channelId)
197
+ const cr = channelRuntimeOf(manager, channelId)
198
+ cr.scheduler?.stop()
199
+ // cr.lead 运行时即 manager 装配的真实 AgentRuntime(接口类型为 AgentRuntimeLike,此处收窄)
200
+ const lead = cr.lead as unknown as AgentRuntime
201
+ const loop = new SchedulerLoop(cr, lead, { tickMs })
202
+ cr.scheduler = loop
203
+ loop.start()
204
+ return loop
205
+ }
206
+
207
+ /** 创建带 lead + N 个 worker(mock harness)的 channel */
208
+ async function createTeamChannel(
209
+ manager: AgentChannelManager,
210
+ name: string,
211
+ leadName: string,
212
+ workerNames: string[],
213
+ ): Promise<{ channelId: string, leadAgentId: string, workers: AgentInfo[] }> {
214
+ const { channelId, leadAgentId } = await manager.createChannel({
215
+ name,
216
+ leadAgent: { name: leadName, harness: 'mock', config: { delayMs: 0 } },
217
+ })
218
+ if (!leadAgentId) throw new AppError(500, 'INTERNAL_ERROR', `channel ${channelId} 创建后无 lead`)
219
+ const workers: AgentInfo[] = []
220
+ for (const wn of workerNames) {
221
+ const tpl = await manager.createAgent({ name: wn, harness: 'mock', config: { delayMs: 0 } })
222
+ workers.push(await manager.addAgentToChannel({ channelId, agentId: tpl.id, role: 'worker' }))
223
+ }
224
+ return { channelId, leadAgentId, workers }
225
+ }
226
+
227
+ /** 场景 1:端到端闭环 — 主任务 → 2 子任务 → 汇总交付 → 进度互见 */
228
+ async function testOrchestrationFlow(h: Harness, loops: SchedulerLoop[], channels: string[]): Promise<void> {
229
+ console.log('\n--- 1. 端到端编排:lead 分解 → 2 worker 执行 → lead 汇总交付 ---')
230
+ const { manager } = h
231
+ const team = await createTeamChannel(manager, '频道A', 'leadA', ['workerA1', 'workerA2'])
232
+ channels.push(team.channelId)
233
+ loops.push(attachScheduler(manager, team.channelId, 10))
234
+
235
+ const main = await manager.submitChannelTask({
236
+ channelId: team.channelId,
237
+ title: '主任务',
238
+ description: '统筹交付',
239
+ })
240
+ check(
241
+ '主任务提交成功(SUBMITTED,assignee=lead)',
242
+ main.state === 'SUBMITTED' && main.assigneeId === team.leadAgentId,
243
+ )
244
+
245
+ // 轮询等待主任务 COMPLETED(上限 10s);期间记录子任务出现过的中间状态
246
+ const seenStates = new Map<string, Set<string>>()
247
+ const mainDone = await waitUntil(async () => {
248
+ const tasks = await manager.listTasks(team.channelId, team.leadAgentId)
249
+ for (const t of tasks) {
250
+ if (t.parentId !== main.id) continue
251
+ const states = seenStates.get(t.id) ?? new Set<string>()
252
+ states.add(t.state)
253
+ seenStates.set(t.id, states)
254
+ }
255
+ return tasks.some(t => t.id === main.id && t.state === 'COMPLETED')
256
+ }, 10_000)
257
+ check('主任务在 10s 内 COMPLETED', mainDone, `state=${mainDone ? 'COMPLETED' : '超时'}`)
258
+
259
+ const tasks = await manager.listTasks(team.channelId, team.leadAgentId)
260
+ const children = tasks.filter(t => t.parentId === main.id)
261
+
262
+ // ① 生成了 2 个子任务,且均 COMPLETED、进度 100
263
+ const observed = new Set<string>()
264
+ for (const states of seenStates.values()) {
265
+ for (const s of states) observed.add(s)
266
+ }
267
+ check(
268
+ '生成了 2 个子任务且均 COMPLETED/进度 100',
269
+ children.length === 2 && children.every(c => c.state === 'COMPLETED' && c.progress === 100),
270
+ `count=${children.length} 轮询观察到状态=[${[...observed].join('→') || '—'}]`,
271
+ )
272
+
273
+ // ② 每个子任务都有已消费的 assign 投递消息:证明 dispatch 落库投递 → mailbox 消费 →
274
+ // 自动接取 ASSIGNED→WORKING 路径(子任务创建即 ASSIGNED,状态机强制 ASSIGNED→WORKING→COMPLETED)
275
+ const recent = h.repos.messages.listRecentByChannel(team.channelId, 200)
276
+ const assignTrailComplete = children.every(c =>
277
+ recent.some(
278
+ m =>
279
+ m.taskId === c.id
280
+ && m.toAgentId === c.assigneeId
281
+ && m.state === 'consumed'
282
+ && parseJson<Record<string, unknown>>(m.metadataJson, {})['x-aw-task-kind'] === 'assign',
283
+ ),
284
+ )
285
+ check('每个子任务均有已消费的 assign 投递消息(ASSIGNED 后完成的证据)', assignTrailComplete)
286
+
287
+ // ③ 子任务成果 artifact 存在 + 成员进度互见(同 channel 同事 getTask 可见成果)
288
+ const [child1, child2] = children
289
+ const viewByAssignee = await manager.getTask(team.channelId, child1.assigneeId, child1.id)
290
+ const hasMockArtifact = (t: WorkspaceTask): boolean =>
291
+ t.artifacts.some(a => a.parts.some(p => 'text' in p && p.text.includes('mock 成果')))
292
+ check(
293
+ 'assignee getTask(自己子任务) 含成果 artifact',
294
+ viewByAssignee.artifacts.length >= 1 && hasMockArtifact(viewByAssignee),
295
+ `artifacts=${viewByAssignee.artifacts.length}`,
296
+ )
297
+ const viewByPeer = await manager.getTask(team.channelId, child2.assigneeId, child1.id)
298
+ check(
299
+ '同事 getTask(他人子任务) 进度成果互见',
300
+ viewByPeer.id === child1.id && viewByPeer.artifacts.length >= 1 && hasMockArtifact(viewByPeer),
301
+ )
302
+
303
+ // ④ lead 完成时父任务含子任务成果汇总
304
+ const mainTask = await manager.getTask(team.channelId, team.leadAgentId, main.id)
305
+ const summary = mainTask.artifacts.find(a => a.name === '汇总')
306
+ check(
307
+ '父任务 COMPLETED 且含子任务成果汇总 artifact',
308
+ mainTask.state === 'COMPLETED' && summary !== undefined,
309
+ `artifacts=${mainTask.artifacts.length}`,
310
+ )
311
+ check(
312
+ '汇总内容包含子任务成果文本',
313
+ summary !== undefined && summary.parts.some(p => 'text' in p && p.text.includes('mock 成果')),
314
+ )
315
+ }
316
+
317
+ /** 场景 2:作用域隔离 — 另一 channel 的 agent 看不到本 channel 任务 */
318
+ async function testScopeIsolation(h: Harness, channels: string[]): Promise<void> {
319
+ console.log('\n--- 2. 作用域隔离:跨 channel 不可见 ---')
320
+ const { manager } = h
321
+ const teamA = await createTeamChannel(manager, '频道A2', 'leadA2', ['workerA2-1', 'workerA2-2'])
322
+ const teamB = await createTeamChannel(manager, '频道B', 'leadB', ['workerB1'])
323
+ channels.push(teamA.channelId, teamB.channelId)
324
+ // B 频道不挂 SchedulerLoop:仅验证作用域,不发起任务流转
325
+
326
+ const mainA = await manager.submitChannelTask({ channelId: teamA.channelId, title: 'A频道任务' })
327
+ const tasksFromB = await manager.listTasks(teamB.channelId, teamB.workers[0].id)
328
+ check(
329
+ 'B 频道 agent listTasks 不含 A 频道任务',
330
+ !tasksFromB.some(t => t.id === mainA.id || t.channelId === teamA.channelId),
331
+ `B 频道可见任务数=${tasksFromB.length}`,
332
+ )
333
+
334
+ let scopeError: AppError | null = null
335
+ try {
336
+ await manager.getTask(teamB.channelId, teamB.workers[0].id, mainA.id)
337
+ }
338
+ catch (err) {
339
+ if (isAppError(err)) scopeError = err
340
+ }
341
+ check(
342
+ 'B 频道 agent getTask(A 任务) 抛 SCOPE_VIOLATION',
343
+ scopeError !== null && scopeError.code === 'SCOPE_VIOLATION' && scopeError.status === 403,
344
+ `code=${scopeError?.code ?? '无异常'} status=${scopeError?.status ?? '—'}`,
345
+ )
346
+ }
347
+
348
+ /** 场景 3:生产路径 — 真实 MockAgentImpl lead(角色感知 run + supervise 剧本)+ 真实 SchedulerLoop 全自动闭环 */
349
+ async function testRealMockLeadFlow(h: Harness, loops: SchedulerLoop[], channels: string[]): Promise<void> {
350
+ console.log('\n--- 3. 真实 Mock lead 全自动闭环(生产路径,无测试内联 impl) ---')
351
+ const { manager } = h
352
+ const team = await createTeamChannel(manager, '频道C', 'leadC', ['workerC1', 'workerC2'])
353
+ channels.push(team.channelId)
354
+ loops.push(attachScheduler(manager, team.channelId, 10))
355
+
356
+ const main = await manager.submitChannelTask({
357
+ channelId: team.channelId,
358
+ title: '生产路径主任务',
359
+ description: '真实 mock lead 自动调度',
360
+ })
361
+
362
+ const done = await waitUntil(async () => {
363
+ const tasks = await manager.listTasks(team.channelId, team.leadAgentId)
364
+ return tasks.some(t => t.id === main.id && t.state === 'COMPLETED')
365
+ }, 10_000)
366
+ check('真实 mock lead:主任务 10s 内 COMPLETED', done, done ? '' : '超时')
367
+
368
+ const tasks = await manager.listTasks(team.channelId, team.leadAgentId)
369
+ const children = tasks.filter(t => t.parentId === main.id)
370
+ check('真实 mock lead:生成子任务且全部完成', children.length >= 1 && children.every(c => c.state === 'COMPLETED'), `count=${children.length}`)
371
+
372
+ const parent = tasks.find(t => t.id === main.id)
373
+ check('真实 mock lead:父任务汇总 artifact 存在', (parent?.artifacts.length ?? 0) > 0, `artifacts=${parent?.artifacts.length ?? 0}`)
374
+
375
+ // 关键断言:lead 未自执行主任务(子任务 ≥1 即证明 dispatch 发生,而非 lead 自己 worker 剧本完成)
376
+ check('真实 mock lead:未自执行主任务(dispatch 路径)', children.length >= 1)
377
+ }
378
+
379
+ async function main(): Promise<void> {
380
+ const h = setup()
381
+ const loops: SchedulerLoop[] = []
382
+ const channels: string[] = []
383
+ try {
384
+ await testOrchestrationFlow(h, loops, channels)
385
+ await testScopeIsolation(h, channels)
386
+ // 场景 3 用独立 harness(真实 mock implFactory),避免污染场景 1/2 的监督型 lead 装配
387
+ const db2 = openWorkshopDb(':memory:')
388
+ const h2: Harness = {
389
+ db: db2,
390
+ manager: createAgentChannelManager({
391
+ repos: {
392
+ channels: createChannelRepo(db2),
393
+ agents: createAgentRepo(db2),
394
+ channelAgents: createChannelAgentRepo(db2),
395
+ messages: createMessageRepo(db2),
396
+ subscriptions: createSubscriptionRepo(db2),
397
+ tasks: createTaskRepo(db2),
398
+ memories: createMemoryRepo(db2),
399
+
400
+ teams: createTeamRepo(db2),
401
+
402
+ teamMembers: createTeamMemberRepo(db2),
403
+ },
404
+ implFactory: buildRealMockFactory(),
405
+ db: db2,
406
+ }),
407
+ repos: null as never,
408
+ }
409
+ await testRealMockLeadFlow(h2, loops, channels)
410
+ h2.db.close()
411
+ }
412
+ finally {
413
+ for (const loop of loops) loop.stop()
414
+ for (const cid of channels) await h.manager.removeChannel(cid)
415
+ h.db.close()
416
+ }
417
+
418
+ console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
419
+ process.exit(failures === 0 ? 0 : 1)
420
+ }
421
+
422
+ main().catch((e) => {
423
+ console.error('测试异常:', e)
424
+ process.exit(1)
425
+ })