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.
- package/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/44b66249-54d0-4c8a-9057-9755a32329e2.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +798 -799
- package/.output/server/chunks/_/nitro.mjs.map +1 -1
- package/.output/server/package.json +1 -1
- package/README-zh.md +2 -2
- package/README.md +2 -2
- package/docs/cli.en.md +2 -2
- package/docs/cli.md +2 -2
- package/docs/full-test-plan.md +1 -1
- package/docs/plugins.en.md +1 -1
- package/docs/plugins.md +1 -1
- package/docs/sdk.en.md +1 -1
- package/docs/sdk.md +1 -1
- package/package.json +1 -1
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +1 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +1 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +153 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +153 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +431 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +180 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +112 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +227 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +131 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +160 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +157 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +348 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +296 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +285 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +217 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +301 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +151 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +135 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +156 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +72 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +469 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +259 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +97 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +277 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +171 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +425 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +99 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +325 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +332 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +43 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +154 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +531 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +439 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +323 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +433 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +280 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +117 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +160 -0
- package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +101 -0
- package/scripts/test-plugin-hardening.mjs +32 -0
- package/server/plugins/dev-stability-guard.ts +40 -63
- package/server/utils/stability-severity.ts +66 -0
- package/.output/public/_nuxt/builds/meta/a66fdef7-dcfd-453b-b1f6-543c7dfd98d5.json +0 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审计实验 5 —— 端到端:真实 DcwController + mock 驱动,验证
|
|
3
|
+
* ① 正常写:硬件写入 + 账本锚 + 写历史(正对照)
|
|
4
|
+
* ② PATCH decimals=101(无校验)→ 后续写「已落硬件但记账抛错」→ 无账本锚/无写历史/500
|
|
5
|
+
* ③ 不存在节点 id 的语义(服务层 404)
|
|
6
|
+
*
|
|
7
|
+
* cwd 已切到 scripts/_audit/.tmp:所有仓库文件写到临时目录,绝不碰 server/data/*。
|
|
8
|
+
* 运行:node --experimental-transform-types scripts/_audit/run.mjs scripts/_audit/dcw-controller-e2e.ts
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, rmSync } from 'node:fs'
|
|
11
|
+
|
|
12
|
+
// 清空本次实验的隔离数据目录(run.mjs 已把 AW_DATA_DIR 指向 scripts/_audit/.tmp/audit-data;
|
|
13
|
+
// 同目录其它实验残留的 20000 锚账本会污染断言,故先清空)
|
|
14
|
+
const ISOLATED = new URL('./.tmp/audit-data/', import.meta.url)
|
|
15
|
+
rmSync(ISOLATED, { recursive: true, force: true })
|
|
16
|
+
mkdirSync(ISOLATED, { recursive: true })
|
|
17
|
+
// 清空上次实验留下的临时库(benchmark 会写出 10MB+ 的 dcw-rollback.json)
|
|
18
|
+
rmSync(new URL('./.tmp/server/', import.meta.url), { recursive: true, force: true })
|
|
19
|
+
mkdirSync(new URL('./.tmp/server/data/', import.meta.url), { recursive: true })
|
|
20
|
+
process.chdir(new URL('./.tmp/', import.meta.url).pathname.replace(/^\//, ''))
|
|
21
|
+
|
|
22
|
+
const { getDcwController } = await import('../../server/services/workshop/dcw/dcw-controller')
|
|
23
|
+
const { getRecipeRollBackRepo } = await import('../../server/services/workshop/dcw/recipe-rollback.repo')
|
|
24
|
+
const { getDcwRecipeRepo } = await import('../../server/services/workshop/dcw/dcw-recipe.repo')
|
|
25
|
+
|
|
26
|
+
let failures = 0
|
|
27
|
+
function check(name: string, cond: boolean, extra = ''): void {
|
|
28
|
+
if (cond) {
|
|
29
|
+
console.log(` PASS ${name}${extra ? ` (${extra})` : ''}`)
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
failures++
|
|
33
|
+
console.log(` FAIL ${name}${extra ? ` (${extra})` : ''}`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const mockPlc = (globalThis as { __dcwMockPlc?: Map<string, number> }).__dcwMockPlc!
|
|
38
|
+
const plcValue = (): number | undefined => mockPlc.get('default')
|
|
39
|
+
|
|
40
|
+
const ctrl = getDcwController()
|
|
41
|
+
const ledger = getRecipeRollBackRepo()
|
|
42
|
+
const writes = getDcwRecipeRepo()
|
|
43
|
+
|
|
44
|
+
const node = ctrl.create({ templateRef: 'temp-sp', driver: 'mock', min: 150, max: 200, decimals: 1, enabled: true })
|
|
45
|
+
console.log(` 节点 ${node.id} 创建(mock 驱动,min150 max200 decimals1)`)
|
|
46
|
+
|
|
47
|
+
console.log('\n=== ① 正对照:正常写 ===')
|
|
48
|
+
{
|
|
49
|
+
const anchorsBefore = ledger.stats().anchors
|
|
50
|
+
const out = await ctrl.write(node.id, 160, null, { source: 'manual', actor: 'audit' })
|
|
51
|
+
check('写入返回 ok', out.ok === true, out.message)
|
|
52
|
+
check('mock PLC 记录写入值 160(硬件已写)', plcValue() === 160, `plc=${plcValue()}`)
|
|
53
|
+
check('账本新增 1 条锚', ledger.stats().anchors === anchorsBefore + 1, `anchors=${ledger.stats().anchors}`)
|
|
54
|
+
check('写历史新增 1 条', writes.historyList(5).length === 1, `n=${writes.historyList(5).length}`)
|
|
55
|
+
check('节点 value=160', node.value === 160, `value=${node.value}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log('\n=== ② PATCH decimals=101(接口无任何范围校验)→ 再写 ===')
|
|
59
|
+
{
|
|
60
|
+
const patched = ctrl.patch(node.id, { decimals: 101 })
|
|
61
|
+
check('PATCH decimals=101 被接受(无校验)', patched.decimals === 101, `decimals=${patched.decimals}`)
|
|
62
|
+
|
|
63
|
+
const anchorsBefore = ledger.stats().anchors
|
|
64
|
+
const historyBefore = writes.historyList(50).length
|
|
65
|
+
let err: unknown
|
|
66
|
+
try {
|
|
67
|
+
await ctrl.write(node.id, 170, null, { source: 'manual', actor: 'audit' })
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
err = e
|
|
71
|
+
}
|
|
72
|
+
console.log(` 写 170 抛错: ${(err as Error)?.name}: ${(err as Error)?.message}`)
|
|
73
|
+
check('写入抛出异常(调用方得到 500 而非 ACK)', err != null, String((err as Error)?.name))
|
|
74
|
+
check('异常来自 toFixed 的 RangeError 语义', /toFixed|digits|RangeError/i.test(`${(err as Error)?.name} ${(err as Error)?.message}`), `${(err as Error)?.name}`)
|
|
75
|
+
check('mock PLC 已记录 170(硬件写入已发生)', plcValue() === 170, `plc=${plcValue()}`)
|
|
76
|
+
check('账本未新增锚(这次硬件变更无法回退)', ledger.stats().anchors === anchorsBefore, `anchors=${ledger.stats().anchors}`)
|
|
77
|
+
check('写历史未新增条目(审计缺失)', writes.historyList(50).length === historyBefore, `n=${writes.historyList(50).length}`)
|
|
78
|
+
check('节点 state=error 且 value 仍是旧值 160', node.state === 'error' && node.value === 160, `state=${node.state} value=${node.value}`)
|
|
79
|
+
|
|
80
|
+
const stable = ledger.lastStableAnchor(node.id)
|
|
81
|
+
console.log(` 账本最新稳定锚={prev:${stable?.prevValue}, new:${stable?.newValue}};硬件实际=${plcValue()};节点显示=${node.value}`)
|
|
82
|
+
check('硬件(170)≠ 账本(160):账本与硬件已漂移', plcValue() !== stable?.newValue)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('\n=== ③ 不存在节点 id 的服务层语义 ===')
|
|
86
|
+
{
|
|
87
|
+
let e404: unknown
|
|
88
|
+
try {
|
|
89
|
+
await ctrl.write('dw-nonexistent', 160, null, { source: 'manual', actor: 'audit' })
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
e404 = e
|
|
93
|
+
}
|
|
94
|
+
const status = (e404 as { status?: number })?.status
|
|
95
|
+
check('不存在节点 → AppError 404(控制器层)', status === 404, `status=${status}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log(`\nresult: ${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
|
|
99
|
+
process.exit(failures === 0 ? 0 : 1)
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 双 Channel 隔离端到端(真实 omp harness):
|
|
3
|
+
* 两个 channel 各自独立 workspace,并行验证:
|
|
4
|
+
* S1 任务下发 + workspace 隔离:两 channel 同时下发"读 shared.txt 汇报内容"任务,
|
|
5
|
+
* A 的 worker 只能看到 marker-A,B 只能看到 marker-B(cwd 隔离经真实 omp 子进程验证)
|
|
6
|
+
* S2 点对点实时通信:A 内 lead↔worker / worker↔worker 触发对话;B 内 worker→lead 触发对话
|
|
7
|
+
* (真实 LLM 回执,in_reply_to 关联)
|
|
8
|
+
* S3 channel 隔离:任务不串channel、消息不串channel、monitor 事件流互不污染
|
|
9
|
+
* S4 监控 + 实时状态:两 channel 各自 monitor 独立事件流;全员最终 idle + 队列空;
|
|
10
|
+
* queueOverview 每 channel 只返回自己的成员
|
|
11
|
+
*
|
|
12
|
+
* 运行: npx tsx scripts/e2e-channel-isolation.ts(依赖 PATH 上的 omp 与已配置模型)
|
|
13
|
+
*/
|
|
14
|
+
import type { DatabaseSync } from 'node:sqlite'
|
|
15
|
+
import { mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
16
|
+
import { resolve } from 'node:path'
|
|
17
|
+
import { openWorkshopDb } from '../server/services/workshop/db/database'
|
|
18
|
+
import { createChannelRepo } from '../server/services/workshop/db/channel.repo'
|
|
19
|
+
import { createAgentRepo } from '../server/services/workshop/db/agent.repo'
|
|
20
|
+
import { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
|
|
21
|
+
import { createTaskRepo } from '../server/services/workshop/db/task.repo'
|
|
22
|
+
import { createMemoryRepo } from '../server/services/workshop/db/memory.repo'
|
|
23
|
+
import { createChannelEventRepo } from '../server/services/workshop/db/channel-event.repo'
|
|
24
|
+
import { createTeamRepo } from '../server/services/workshop/db/team.repo'
|
|
25
|
+
import { createTeamMemberRepo } from '../server/services/workshop/db/team-member.repo'
|
|
26
|
+
import { createMessageRepo } from '../server/services/workshop/db/message.repo'
|
|
27
|
+
import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
|
|
28
|
+
import { createAgentChannelManager } from '../server/services/workshop/runtime/manager'
|
|
29
|
+
import type { AgentChannelManager } from '../server/services/workshop/runtime/manager'
|
|
30
|
+
import { createAgentImpl } from '../server/services/workshop/agents/factory'
|
|
31
|
+
import { monitorChannel } from '../server/services/workshop/runtime/monitor'
|
|
32
|
+
import type { WorkshopMonitor, MonitorEvent } from '../server/services/workshop/runtime/monitor'
|
|
33
|
+
|
|
34
|
+
let failures = 0
|
|
35
|
+
const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms))
|
|
36
|
+
|
|
37
|
+
function check(name: string, ok: boolean, detail = ''): void {
|
|
38
|
+
console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
39
|
+
if (!ok) failures += 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeManager(db: DatabaseSync): AgentChannelManager {
|
|
43
|
+
return createAgentChannelManager({
|
|
44
|
+
repos: {
|
|
45
|
+
channels: createChannelRepo(db),
|
|
46
|
+
agents: createAgentRepo(db),
|
|
47
|
+
channelAgents: createChannelAgentRepo(db),
|
|
48
|
+
messages: createMessageRepo(db),
|
|
49
|
+
subscriptions: createSubscriptionRepo(db),
|
|
50
|
+
tasks: createTaskRepo(db),
|
|
51
|
+
memories: createMemoryRepo(db),
|
|
52
|
+
|
|
53
|
+
channelEvents: createChannelEventRepo(db),
|
|
54
|
+
teams: createTeamRepo(db),
|
|
55
|
+
teamMembers: createTeamMemberRepo(db),
|
|
56
|
+
},
|
|
57
|
+
implFactory: createAgentImpl,
|
|
58
|
+
db,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface Internals {
|
|
63
|
+
deps: { repos: { messages: {
|
|
64
|
+
listRecentByChannel(channelId: string, limit: number): Array<{ id: string, fromAgentId: string | null, toAgentId: string | null, partsJson: string, metadataJson: string }>
|
|
65
|
+
} } }
|
|
66
|
+
agentIndex: Map<string, unknown>
|
|
67
|
+
getTaskEngine(): {
|
|
68
|
+
get(id: string): { id: string, channelId: string, title: string, state: string, assigneeId: string, parentId?: string, artifacts: Array<{ parts: Array<{ text?: string }> }> } | undefined
|
|
69
|
+
list(channelId: string): Array<{ id: string, channelId: string, title: string, state: string, assigneeId: string, parentId?: string, artifacts: Array<{ parts: Array<{ text?: string }> }> }>
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function internalsOf(manager: AgentChannelManager): Internals {
|
|
74
|
+
return manager as unknown as Internals
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface Msg { id: string, from: string | null, to: string | null, text: string, metadata: Record<string, unknown> }
|
|
78
|
+
|
|
79
|
+
function listMessages(manager: AgentChannelManager, channelId: string, limit = 200): Msg[] {
|
|
80
|
+
return internalsOf(manager).deps.repos.messages.listRecentByChannel(channelId, limit).reverse().map(r => ({
|
|
81
|
+
id: r.id,
|
|
82
|
+
from: r.fromAgentId,
|
|
83
|
+
to: r.toAgentId,
|
|
84
|
+
text: (JSON.parse(r.partsJson) as Array<{ text?: string }>).map(p => p.text ?? '').join(' '),
|
|
85
|
+
metadata: JSON.parse(r.metadataJson) as Record<string, unknown>,
|
|
86
|
+
}))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function waitFor(cond: () => boolean, timeoutMs: number): Promise<boolean> {
|
|
90
|
+
const deadline = Date.now() + timeoutMs
|
|
91
|
+
while (Date.now() < deadline) {
|
|
92
|
+
if (cond()) return true
|
|
93
|
+
await sleep(100)
|
|
94
|
+
}
|
|
95
|
+
return cond()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function ompPidOf(manager: AgentChannelManager, channelId: string, agentId: string): number | null {
|
|
99
|
+
const runtime = internalsOf(manager).agentIndex.get(`${channelId}\u0000${agentId}`) as unknown as {
|
|
100
|
+
impl?: { client?: { child?: { pid?: number } } }
|
|
101
|
+
} | undefined
|
|
102
|
+
return runtime?.impl?.client?.child?.pid ?? null
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 全部任务 artifacts 文本 */
|
|
106
|
+
function artifactTexts(manager: AgentChannelManager, channelId: string): string[] {
|
|
107
|
+
const engine = internalsOf(manager).getTaskEngine()
|
|
108
|
+
return engine.list(channelId).flatMap(t => t.artifacts.flatMap(a => a.parts.map(p => p.text ?? '')))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const TIMING = { task: 240_000, dialog: 180_000, idle: 60_000 }
|
|
112
|
+
|
|
113
|
+
interface Team {
|
|
114
|
+
channelId: string
|
|
115
|
+
workspace: string
|
|
116
|
+
marker: string
|
|
117
|
+
leadId: string
|
|
118
|
+
workers: Array<{ id: string, name: string }>
|
|
119
|
+
memberIds: string[]
|
|
120
|
+
mon: WorkshopMonitor
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function buildTeam(
|
|
124
|
+
manager: AgentChannelManager,
|
|
125
|
+
name: string,
|
|
126
|
+
workspace: string,
|
|
127
|
+
marker: string,
|
|
128
|
+
): Promise<Team> {
|
|
129
|
+
writeFileSync(resolve(workspace, 'shared.txt'), `marker:${marker}`, 'utf8')
|
|
130
|
+
const ch = await manager.createChannel({
|
|
131
|
+
name,
|
|
132
|
+
workspace,
|
|
133
|
+
leadAgent: { name: `${name}-lead`, harness: 'omp', config: { provider: 'zhipu-coding-plan', model: 'glm-5.2' } },
|
|
134
|
+
})
|
|
135
|
+
const wTpl1 = await manager.createAgent({ name: `${name}-w1`, harness: 'omp', config: { provider: 'zhipu-coding-plan', model: 'glm-5.2' } })
|
|
136
|
+
const wTpl2 = await manager.createAgent({ name: `${name}-w2`, harness: 'omp', config: { provider: 'zhipu-coding-plan', model: 'glm-5.2' } })
|
|
137
|
+
const i1 = await manager.addAgentToChannel({ channelId: ch.channelId, agentId: wTpl1.id, role: 'worker' })
|
|
138
|
+
const i2 = await manager.addAgentToChannel({ channelId: ch.channelId, agentId: wTpl2.id, role: 'worker' })
|
|
139
|
+
// 先激活(装配 bus),monitor 订阅才有效
|
|
140
|
+
manager.ensureChannelActive(ch.channelId)
|
|
141
|
+
const mon = monitorChannel(manager, ch.channelId)
|
|
142
|
+
const members = await manager.listChannelAgents(ch.channelId)
|
|
143
|
+
return {
|
|
144
|
+
channelId: ch.channelId,
|
|
145
|
+
workspace,
|
|
146
|
+
marker,
|
|
147
|
+
leadId: members.find(m => m.role === 'lead')?.id ?? '',
|
|
148
|
+
workers: [i1, i2].map(w => ({ id: w.id, name: w.name })),
|
|
149
|
+
memberIds: members.map(m => m.id),
|
|
150
|
+
mon,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** monitor 事件流泄漏检查:本 monitor 是否出现对方 channel 的成员/任务 id */
|
|
155
|
+
function monitorSawIdsOf(mon: WorkshopMonitor, otherMemberIds: string[], otherTaskIds: string[]): { leakedAgentIds: string[], leakedTaskIds: string[] } {
|
|
156
|
+
const leakedAgentIds: string[] = []
|
|
157
|
+
const leakedTaskIds: string[] = []
|
|
158
|
+
for (const e of mon.events) {
|
|
159
|
+
const ev = e as MonitorEvent & { agentId?: string, taskId?: string }
|
|
160
|
+
if (ev.agentId && otherMemberIds.includes(ev.agentId)) leakedAgentIds.push(ev.agentId)
|
|
161
|
+
if (ev.taskId && otherTaskIds.includes(ev.taskId)) leakedTaskIds.push(ev.taskId)
|
|
162
|
+
}
|
|
163
|
+
return { leakedAgentIds, leakedTaskIds }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function main(): Promise<void> {
|
|
167
|
+
console.log('━━━ 双 Channel 隔离端到端(真实 omp):任务下发 + 点对点实时通信 + 隔离 + 监控 ━━━')
|
|
168
|
+
const root = resolve(process.cwd(), '.tmp-e2e-iso')
|
|
169
|
+
const db = openWorkshopDb(':memory:')
|
|
170
|
+
const manager = makeManager(db)
|
|
171
|
+
try {
|
|
172
|
+
// ---- 组队:两个 channel,各自独立 workspace ----
|
|
173
|
+
mkdirSync(resolve(root, 'A'), { recursive: true })
|
|
174
|
+
mkdirSync(resolve(root, 'B'), { recursive: true })
|
|
175
|
+
const teamA = await buildTeam(manager, '频道A', resolve(root, 'A'), 'CHANNEL-ALPHA-7734')
|
|
176
|
+
const teamB = await buildTeam(manager, '频道B', resolve(root, 'B'), 'CHANNEL-BETA-9918')
|
|
177
|
+
const engine = internalsOf(manager).getTaskEngine()
|
|
178
|
+
console.log(` A: ${teamA.channelId.slice(0, 8)} ws=${teamA.workspace}`)
|
|
179
|
+
console.log(` B: ${teamB.channelId.slice(0, 8)} ws=${teamB.workspace}`)
|
|
180
|
+
|
|
181
|
+
// ---- S1 任务下发 + workspace 隔离(两 channel 并行)----
|
|
182
|
+
console.log('\n━━━ S1. 任务下发 + workspace 隔离 ━━━')
|
|
183
|
+
const taskDesc = (marker: string) =>
|
|
184
|
+
`Read the file shared.txt in your working directory and report its EXACT full content (it starts with "marker:"). Then call complete_task with the file content as the deliverable. Expected prefix: "marker:${marker}" — but report what you actually read.`
|
|
185
|
+
const taskA = await manager.submitChannelTask({ channelId: teamA.channelId, title: '读取共享文件', description: taskDesc(teamA.marker) })
|
|
186
|
+
const taskB = await manager.submitChannelTask({ channelId: teamB.channelId, title: '读取共享文件', description: taskDesc(teamB.marker) })
|
|
187
|
+
|
|
188
|
+
const bothDone = await waitFor(() =>
|
|
189
|
+
engine.get(taskA.id)?.state === 'COMPLETED' && engine.get(taskB.id)?.state === 'COMPLETED',
|
|
190
|
+
TIMING.task)
|
|
191
|
+
check('S1: 两 channel 任务并行下发并全部完成', bothDone,
|
|
192
|
+
`A=${engine.get(taskA.id)?.state} B=${engine.get(taskB.id)?.state}`)
|
|
193
|
+
|
|
194
|
+
// 子任务下发到本 channel worker(任务下发链路)
|
|
195
|
+
const childA = engine.list(teamA.channelId).find(t => t.parentId === taskA.id)
|
|
196
|
+
check('S1: A 的 lead 把任务下发给本 channel worker', !!childA && teamA.workers.some(w => w.id === childA.assigneeId),
|
|
197
|
+
childA ? `assignee=${childA.assigneeId.slice(0, 8)} state=${childA.state}` : 'no child')
|
|
198
|
+
const childB = engine.list(teamB.channelId).find(t => t.parentId === taskB.id)
|
|
199
|
+
check('S1: B 的 lead 把任务下发给本 channel worker', !!childB && teamB.workers.some(w => w.id === childB.assigneeId),
|
|
200
|
+
childB ? `assignee=${childB.assigneeId.slice(0, 8)} state=${childB.state}` : 'no child')
|
|
201
|
+
|
|
202
|
+
// workspace 隔离:成果内容验证
|
|
203
|
+
const textsA = artifactTexts(manager, teamA.channelId).join('\n')
|
|
204
|
+
const textsB = artifactTexts(manager, teamB.channelId).join('\n')
|
|
205
|
+
check('S1: A 成果含本 workspace 标记', textsA.includes(teamA.marker), textsA.slice(0, 60))
|
|
206
|
+
check('S1: A 成果不含 B 的标记', !textsA.includes(teamB.marker))
|
|
207
|
+
check('S1: B 成果含本 workspace 标记', textsB.includes(teamB.marker), textsB.slice(0, 60))
|
|
208
|
+
check('S1: B 成果不含 A 的标记', !textsB.includes(teamA.marker))
|
|
209
|
+
|
|
210
|
+
// ---- S2 点对点实时通信(两 channel 并行;真实 LLM 回执)----
|
|
211
|
+
console.log('\n━━━ S2. 点对点实时通信(触发器回执)━━━')
|
|
212
|
+
const m1 = await manager.sendImmediateMessage({
|
|
213
|
+
channelId: teamA.channelId, fromAgentId: teamA.leadId, toAgentId: teamA.workers[0].id,
|
|
214
|
+
parts: [{ text: 'What is 2+3? Answer with just the number.' }], requireReply: true,
|
|
215
|
+
})
|
|
216
|
+
const m2 = await manager.sendA2A(teamA.channelId, teamA.workers[0].id, {
|
|
217
|
+
toAgentId: teamA.workers[1].id,
|
|
218
|
+
parts: [{ text: 'What is 10-4? Answer with just the number.' }],
|
|
219
|
+
metadata: { 'x-aw-msg-priority': 'task', 'x-aw-require-reply': 'true' },
|
|
220
|
+
})
|
|
221
|
+
const m3 = await manager.sendImmediateMessage({
|
|
222
|
+
channelId: teamB.channelId, fromAgentId: teamB.workers[0].id, toAgentId: teamB.leadId,
|
|
223
|
+
parts: [{ text: 'What is 6*7? Answer with just the number.' }], requireReply: true,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const [r1, r2, r3] = await Promise.all([
|
|
227
|
+
(async () => {
|
|
228
|
+
const deadline = Date.now() + TIMING.dialog
|
|
229
|
+
while (Date.now() < deadline) {
|
|
230
|
+
const hit = listMessages(manager, teamA.channelId).find(x => x.metadata['x-aw-in-reply-to'] === m1.messageId)
|
|
231
|
+
if (hit) return hit
|
|
232
|
+
await sleep(150)
|
|
233
|
+
}
|
|
234
|
+
return null
|
|
235
|
+
})(),
|
|
236
|
+
(async () => {
|
|
237
|
+
const deadline = Date.now() + TIMING.dialog
|
|
238
|
+
while (Date.now() < deadline) {
|
|
239
|
+
const hit = listMessages(manager, teamA.channelId).find(x => x.metadata['x-aw-in-reply-to'] === m2.messageId)
|
|
240
|
+
if (hit) return hit
|
|
241
|
+
await sleep(150)
|
|
242
|
+
}
|
|
243
|
+
return null
|
|
244
|
+
})(),
|
|
245
|
+
(async () => {
|
|
246
|
+
const deadline = Date.now() + TIMING.dialog
|
|
247
|
+
while (Date.now() < deadline) {
|
|
248
|
+
const hit = listMessages(manager, teamB.channelId).find(x => x.metadata['x-aw-in-reply-to'] === m3.messageId)
|
|
249
|
+
if (hit) return hit
|
|
250
|
+
await sleep(150)
|
|
251
|
+
}
|
|
252
|
+
return null
|
|
253
|
+
})(),
|
|
254
|
+
])
|
|
255
|
+
|
|
256
|
+
check('S2: A lead→worker 回执(2+3)', r1 !== null && r1.text.includes('5'), r1?.text.slice(0, 60) ?? 'timeout')
|
|
257
|
+
check('S2: A lead→worker in_reply_to 关联', r1?.metadata['x-aw-in-reply-to'] === m1.messageId)
|
|
258
|
+
check('S2: A worker↔worker 回执(10-4)', r2 !== null && r2.text.includes('6'), r2?.text.slice(0, 60) ?? 'timeout')
|
|
259
|
+
check('S2: A worker↔worker in_reply_to 关联', r2?.metadata['x-aw-in-reply-to'] === m2.messageId)
|
|
260
|
+
check('S2: B worker→lead 回执(6*7)', r3 !== null && r3.text.includes('42'), r3?.text.slice(0, 60) ?? 'timeout')
|
|
261
|
+
check('S2: B worker→lead in_reply_to 关联', r3?.metadata['x-aw-in-reply-to'] === m3.messageId)
|
|
262
|
+
|
|
263
|
+
// ---- S3 channel 隔离 ----
|
|
264
|
+
console.log('\n━━━ S3. Channel 隔离 ━━━')
|
|
265
|
+
const tasksA = engine.list(teamA.channelId)
|
|
266
|
+
const tasksB = engine.list(teamB.channelId)
|
|
267
|
+
check('S3: 任务不串 channel', !tasksA.some(t => t.channelId !== teamA.channelId) && !tasksB.some(t => t.channelId !== teamB.channelId),
|
|
268
|
+
`A=${tasksA.length} B=${tasksB.length}`)
|
|
269
|
+
const msgA = listMessages(manager, teamA.channelId)
|
|
270
|
+
const msgB = listMessages(manager, teamB.channelId)
|
|
271
|
+
check('S3: 消息不串 channel', !msgA.some(m => teamB.memberIds.includes(m.to ?? '')) && !msgB.some(m => teamA.memberIds.includes(m.to ?? '')))
|
|
272
|
+
// monitor 事件流隔离:A 的 monitor 不含 B 的成员/任务事件
|
|
273
|
+
const taskIdsB = engine.list(teamB.channelId).map(t => t.id)
|
|
274
|
+
const taskIdsA = engine.list(teamA.channelId).map(t => t.id)
|
|
275
|
+
const leakA = monitorSawIdsOf(teamA.mon, teamB.memberIds, taskIdsB)
|
|
276
|
+
const leakB = monitorSawIdsOf(teamB.mon, teamA.memberIds, taskIdsA)
|
|
277
|
+
check('S3: A monitor 无 B 成员事件泄漏', leakA.leakedAgentIds.length === 0, leakA.leakedAgentIds.join(',').slice(0, 40))
|
|
278
|
+
check('S3: A monitor 无 B 任务事件泄漏', leakA.leakedTaskIds.length === 0, `n=${leakA.leakedTaskIds.length}`)
|
|
279
|
+
check('S3: B monitor 无 A 成员事件泄漏', leakB.leakedAgentIds.length === 0, leakB.leakedAgentIds.join(',').slice(0, 40))
|
|
280
|
+
check('S3: B monitor 无 A 任务事件泄漏', leakB.leakedTaskIds.length === 0, `n=${leakB.leakedTaskIds.length}`)
|
|
281
|
+
|
|
282
|
+
// ---- S4 监控 + 实时状态 ----
|
|
283
|
+
console.log('\n━━━ S4. 监控与实时状态 ━━━')
|
|
284
|
+
const evA = teamA.mon.events
|
|
285
|
+
const evB = teamB.mon.events
|
|
286
|
+
check('S4: A monitor 捕获本 channel 任务完成', evA.some(e => e.kind === 'task.status' && e.taskId === taskA.id && e.state === 'COMPLETED'),
|
|
287
|
+
`events=${evA.length}`)
|
|
288
|
+
check('S4: B monitor 捕获本 channel 任务完成', evB.some(e => e.kind === 'task.status' && e.taskId === taskB.id && e.state === 'COMPLETED'),
|
|
289
|
+
`events=${evB.length}`)
|
|
290
|
+
check('S4: busy 事件携带 currentTaskId', evA.some(e => e.kind === 'agent.status' && e.state === 'busy' && e.currentTaskId != null))
|
|
291
|
+
|
|
292
|
+
// 全员最终 idle(未装配成员 runtimeState=null 视同空闲;装配过的须 idle)
|
|
293
|
+
const allIdle = await waitFor(() => {
|
|
294
|
+
return [teamA, teamB].every(t =>
|
|
295
|
+
t.memberIds.every((id) => {
|
|
296
|
+
const s = manager.getChannelAgent(id)?.runtimeState
|
|
297
|
+
return s === null || s === 'idle'
|
|
298
|
+
}))
|
|
299
|
+
}, TIMING.idle)
|
|
300
|
+
check('S4: 两 channel 全员最终 idle', allIdle)
|
|
301
|
+
|
|
302
|
+
const overviewA = await manager.queueOverview(teamA.channelId, teamA.leadId)
|
|
303
|
+
const overviewB = await manager.queueOverview(teamB.channelId, teamB.leadId)
|
|
304
|
+
check('S4: A queueOverview 仅含本 channel 3 成员', overviewA.length === 3 && overviewA.every(s => teamA.memberIds.includes(s.agentId)))
|
|
305
|
+
check('S4: B queueOverview 仅含本 channel 3 成员', overviewB.length === 3 && overviewB.every(s => teamB.memberIds.includes(s.agentId)))
|
|
306
|
+
check('S4: 全员队列已清空', overviewA.every(s => s.queuedCount === 0) && overviewB.every(s => s.queuedCount === 0))
|
|
307
|
+
|
|
308
|
+
// 独立进程:两 channel 全部已装配成员 PID 互不相同
|
|
309
|
+
const pids = [...teamA.memberIds.map(id => ompPidOf(manager, teamA.channelId, id)),
|
|
310
|
+
...teamB.memberIds.map(id => ompPidOf(manager, teamB.channelId, id))].filter(p => p !== null)
|
|
311
|
+
check('S4: 双 channel 子进程 PID 全部互异', new Set(pids).size === pids.length, `spawned=${pids.length} pids=${pids.join(',')}`)
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
await manager.shutdown()
|
|
315
|
+
rmSync(root, { recursive: true, force: true })
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
console.log(`\n${failures === 0 ? 'ALL PASS' : `${failures} FAILED`}`)
|
|
319
|
+
process.exit(failures === 0 ? 0 : 1)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
main().catch((e) => {
|
|
323
|
+
console.error('测试异常:', e)
|
|
324
|
+
process.exit(1)
|
|
325
|
+
})
|