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,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 持久化 + 懒加载 Runtime 测试
|
|
3
|
+
*
|
|
4
|
+
* 验证:
|
|
5
|
+
* A. 数据驱动持久化:channel/agent 创建即落库,重启(重建 manager)后可读回
|
|
6
|
+
* B. 懒加载:创建后不装配运行时;任务提交才装配 lead + 调度循环;dispatch 才装配 worker
|
|
7
|
+
* C. 卸载:任务完成后 unloadIdleAgents 释放运行时(杀 omp 进程),DB 行保留
|
|
8
|
+
* D. CRUD:channel 增删改查 + agent 增删改查
|
|
9
|
+
* E. restore:重建 manager 后,有待办任务的 channel 自动激活
|
|
10
|
+
*
|
|
11
|
+
* 运行: npx tsx scripts/test-persistence-lazy.ts
|
|
12
|
+
*/
|
|
13
|
+
import { rmSync } from 'node:fs'
|
|
14
|
+
import { resolve } from 'node:path'
|
|
15
|
+
import { openWorkshopDb } from '../server/services/workshop/db/database'
|
|
16
|
+
import { createChannelRepo } from '../server/services/workshop/db/channel.repo'
|
|
17
|
+
import { createAgentRepo } from '../server/services/workshop/db/agent.repo'
|
|
18
|
+
import { createChannelAgentRepo } from '../server/services/workshop/db/channel-agent.repo'
|
|
19
|
+
import { createTaskRepo } from '../server/services/workshop/db/task.repo'
|
|
20
|
+
import { createMemoryRepo } from '../server/services/workshop/db/memory.repo'
|
|
21
|
+
import { createChannelEventRepo } from '../server/services/workshop/db/channel-event.repo'
|
|
22
|
+
import { createTeamRepo } from '../server/services/workshop/db/team.repo'
|
|
23
|
+
import { createTeamMemberRepo } from '../server/services/workshop/db/team-member.repo'
|
|
24
|
+
import { createMessageRepo } from '../server/services/workshop/db/message.repo'
|
|
25
|
+
import { createSubscriptionRepo } from '../server/services/workshop/db/subscription.repo'
|
|
26
|
+
import { createAgentChannelManager } from '../server/services/workshop/runtime/manager'
|
|
27
|
+
import type { AgentChannelManager } from '../server/services/workshop/runtime/manager'
|
|
28
|
+
import { createAgentImpl } from '../server/services/workshop/agents/factory'
|
|
29
|
+
import type { WorkspaceTask } from '../server/services/workshop/types/task'
|
|
30
|
+
|
|
31
|
+
let failures = 0
|
|
32
|
+
let testCount = 0
|
|
33
|
+
function check(name: string, ok: boolean, detail = ''): void {
|
|
34
|
+
testCount += 1
|
|
35
|
+
console.log(` ${ok ? '✅' : '❌'} ${name}${detail ? ` — ${detail}` : ''}`)
|
|
36
|
+
if (!ok) failures += 1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms))
|
|
40
|
+
|
|
41
|
+
async function waitUntil(cond: () => boolean | Promise<boolean>, timeoutMs: number): Promise<boolean> {
|
|
42
|
+
const deadline = Date.now() + timeoutMs
|
|
43
|
+
while (Date.now() < deadline) {
|
|
44
|
+
if (await cond()) return true
|
|
45
|
+
await sleep(50)
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildRepos(db: ReturnType<typeof openWorkshopDb>) {
|
|
51
|
+
return {
|
|
52
|
+
channels: createChannelRepo(db),
|
|
53
|
+
agents: createAgentRepo(db),
|
|
54
|
+
channelAgents: createChannelAgentRepo(db),
|
|
55
|
+
messages: createMessageRepo(db),
|
|
56
|
+
subscriptions: createSubscriptionRepo(db),
|
|
57
|
+
tasks: createTaskRepo(db),
|
|
58
|
+
memories: createMemoryRepo(db),
|
|
59
|
+
|
|
60
|
+
channelEvents: createChannelEventRepo(db),
|
|
61
|
+
|
|
62
|
+
teams: createTeamRepo(db),
|
|
63
|
+
|
|
64
|
+
teamMembers: createTeamMemberRepo(db),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function makeManager(db: ReturnType<typeof openWorkshopDb>): AgentChannelManager {
|
|
69
|
+
return createAgentChannelManager({ repos: buildRepos(db), implFactory: createAgentImpl, db })
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getEngine(manager: AgentChannelManager) {
|
|
73
|
+
return (manager as unknown as {
|
|
74
|
+
getTaskEngine(): {
|
|
75
|
+
get(id: string): WorkspaceTask | undefined
|
|
76
|
+
list(channelId: string): WorkspaceTask[]
|
|
77
|
+
}
|
|
78
|
+
}).getTaskEngine()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ===== 测试 A+B: 持久化 + 懒加载 =====
|
|
82
|
+
async function testPersistenceAndLazy(): Promise<void> {
|
|
83
|
+
console.log('\n━━━ 测试 A+B: 数据持久化 + 懒加载装配 ━━━')
|
|
84
|
+
|
|
85
|
+
const dbPath = resolve(process.cwd(), '.tmp-persist-test.sqlite')
|
|
86
|
+
rmSync(dbPath, { force: true })
|
|
87
|
+
rmSync(`${dbPath}-wal`, { force: true })
|
|
88
|
+
rmSync(`${dbPath}-shm`, { force: true })
|
|
89
|
+
|
|
90
|
+
const db1 = openWorkshopDb(dbPath)
|
|
91
|
+
const manager1 = makeManager(db1)
|
|
92
|
+
let channelId!: string
|
|
93
|
+
let leadId!: string
|
|
94
|
+
let workerId!: string
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// 1. 创建 channel + agents → 仅持久化,不装配
|
|
98
|
+
const ch = await manager1.createChannel({
|
|
99
|
+
name: '持久化测试',
|
|
100
|
+
leadAgent: { name: 'lead', harness: 'mock', config: { delayMs: 30 } },
|
|
101
|
+
})
|
|
102
|
+
channelId = ch.channelId
|
|
103
|
+
leadId = ch.leadAgentId!
|
|
104
|
+
const wTpl = await manager1.createAgent({ name: 'worker', harness: 'mock', config: { delayMs: 30 } })
|
|
105
|
+
const w = await manager1.addAgentToChannel({ channelId, agentId: wTpl.id, role: 'worker' })
|
|
106
|
+
workerId = w.id
|
|
107
|
+
|
|
108
|
+
const st1 = manager1.runtimeStatus()
|
|
109
|
+
check('创建后未装配任何运行时(懒加载)', st1.wiredAgents.length === 0 && st1.activeChannels.length === 0, `wired=${st1.wiredAgents.length}, active=${st1.activeChannels.length}`)
|
|
110
|
+
|
|
111
|
+
// 2. 提交任务 → 装配 lead + 调度循环
|
|
112
|
+
const task = await manager1.submitChannelTask({ channelId, title: '懒加载任务' })
|
|
113
|
+
const st2 = manager1.runtimeStatus()
|
|
114
|
+
check('任务提交后装配了 lead 运行时', st2.wiredAgents.includes(leadId), `wired=${st2.wiredAgents.length}`)
|
|
115
|
+
check('任务提交后 channel 活跃', st2.activeChannels.includes(channelId))
|
|
116
|
+
|
|
117
|
+
// 3. lead dispatch → worker 装配
|
|
118
|
+
await waitUntil(() => getEngine(manager1).get(task.id)?.state === 'COMPLETED', 10_000)
|
|
119
|
+
const st3 = manager1.runtimeStatus()
|
|
120
|
+
check('worker 执行期间被装配(懒加载)', st3.wiredAgents.includes(workerId) || st3.wiredAgents.includes(leadId), `wired=[${st3.wiredAgents.map(id => id.slice(0, 6)).join(',')}]`)
|
|
121
|
+
|
|
122
|
+
// 4. 任务完成后卸载
|
|
123
|
+
await sleep(100)
|
|
124
|
+
await manager1.unloadIdleAgents()
|
|
125
|
+
const st4 = manager1.runtimeStatus()
|
|
126
|
+
check('任务完成后可卸载(内存回收)', st4.wiredAgents.length === 0, `wired=${st4.wiredAgents.length}`)
|
|
127
|
+
|
|
128
|
+
// 5. 卸载后 DB 行保留(数据驱动)
|
|
129
|
+
const agentAfterUnload = manager1.getChannelAgent(workerId)
|
|
130
|
+
check('卸载后 agent DB 行保留', !!agentAfterUnload, `name=${agentAfterUnload?.name}`)
|
|
131
|
+
const chAfter = await manager1.getChannel(channelId)
|
|
132
|
+
check('卸载后 channel DB 行保留', chAfter.id === channelId)
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
// 完整关闭:busy 的 agent 仍在跑 consumeLoop,直接 close db 会用到已 finalize 的语句
|
|
136
|
+
await manager1.shutdown()
|
|
137
|
+
db1.close()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 6. 重建 manager(模拟重启)→ 数据可读回
|
|
141
|
+
const db2 = openWorkshopDb(dbPath)
|
|
142
|
+
const manager2 = makeManager(db2)
|
|
143
|
+
try {
|
|
144
|
+
const channels = await manager2.listChannels()
|
|
145
|
+
check('重启后 channel 持久化读回', channels.length === 1 && channels[0]!.id === channelId)
|
|
146
|
+
const agents = await manager2.listChannelAgents(channelId)
|
|
147
|
+
check('重启后 agent 持久化读回', agents.length === 2)
|
|
148
|
+
check('重启后不自动装配(无待办任务)', manager2.runtimeStatus().wiredAgents.length === 0)
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
await manager2.shutdown()
|
|
152
|
+
db2.close()
|
|
153
|
+
rmSync(dbPath, { force: true })
|
|
154
|
+
rmSync(`${dbPath}-wal`, { force: true })
|
|
155
|
+
rmSync(`${dbPath}-shm`, { force: true })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ===== 测试 C: restore 激活有待办任务的 channel =====
|
|
160
|
+
async function testRestoreWithPending(): Promise<void> {
|
|
161
|
+
console.log('\n━━━ 测试 C: restore 激活有待办任务的 channel ━━━')
|
|
162
|
+
|
|
163
|
+
const dbPath = resolve(process.cwd(), '.tmp-restore-test.sqlite')
|
|
164
|
+
rmSync(dbPath, { force: true })
|
|
165
|
+
rmSync(`${dbPath}-wal`, { force: true })
|
|
166
|
+
rmSync(`${dbPath}-shm`, { force: true })
|
|
167
|
+
|
|
168
|
+
// 创建含未完成任务的 channel
|
|
169
|
+
const db1 = openWorkshopDb(dbPath)
|
|
170
|
+
const manager1 = makeManager(db1)
|
|
171
|
+
let channelId!: string
|
|
172
|
+
try {
|
|
173
|
+
const ch = await manager1.createChannel({ name: 'restore', leadAgent: { name: 'lead', harness: 'mock', config: { delayMs: 200 } } })
|
|
174
|
+
channelId = ch.channelId
|
|
175
|
+
const wTpl = await manager1.createAgent({ name: 'w', harness: 'mock', config: { delayMs: 200 } })
|
|
176
|
+
await manager1.addAgentToChannel({ channelId, agentId: wTpl.id, role: 'worker' })
|
|
177
|
+
// 提交一个任务但不完成(不等它完成就关闭)
|
|
178
|
+
const task = await manager1.submitChannelTask({ channelId, title: '未完成任务' })
|
|
179
|
+
// 等它被 dispatch 进入工作状态
|
|
180
|
+
await sleep(100)
|
|
181
|
+
check('关闭前有未完成任务', getEngine(manager1).get(task.id)?.state !== 'COMPLETED')
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
// 完整关闭:busy 的 agent 仍在跑 consumeLoop,直接 close db 会用到已 finalize 的语句
|
|
185
|
+
await manager1.shutdown()
|
|
186
|
+
db1.close()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 重启:restore 应激活该 channel(有待办任务),且任务恢复执行至完成
|
|
190
|
+
const db2 = openWorkshopDb(dbPath)
|
|
191
|
+
const manager2 = makeManager(db2)
|
|
192
|
+
try {
|
|
193
|
+
manager2.restore()
|
|
194
|
+
const st = manager2.runtimeStatus()
|
|
195
|
+
check('restore 激活有待办任务的 channel', st.activeChannels.includes(channelId), `active=[${st.activeChannels.map(c => c.slice(0, 6)).join(',')}]`)
|
|
196
|
+
// 非终态任务经 restore 后必须恢复执行:调度循环重派 + worker 消费重投的 assign 消息
|
|
197
|
+
const done = await waitUntil(async () => {
|
|
198
|
+
const tasks = getEngine(manager2).list(channelId)
|
|
199
|
+
return tasks.some(t => t.state === 'COMPLETED')
|
|
200
|
+
}, 10_000)
|
|
201
|
+
check('restore 后任务恢复执行至 COMPLETED', done, '中途崩溃的任务不应滞留 ASSIGNED/WORKING')
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
await manager2.shutdown()
|
|
205
|
+
db2.close()
|
|
206
|
+
rmSync(dbPath, { force: true })
|
|
207
|
+
rmSync(`${dbPath}-wal`, { force: true })
|
|
208
|
+
rmSync(`${dbPath}-shm`, { force: true })
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ===== 测试 D: CRUD 完整 =====
|
|
213
|
+
async function testCrud(): Promise<void> {
|
|
214
|
+
console.log('\n━━━ 测试 D: Channel + Agent CRUD ━━━')
|
|
215
|
+
|
|
216
|
+
const db = openWorkshopDb(':memory:')
|
|
217
|
+
const manager = makeManager(db)
|
|
218
|
+
const tmpWs = resolve(process.cwd(), '.tmp-crud-ws')
|
|
219
|
+
try {
|
|
220
|
+
// Channel create/list/get/update/delete
|
|
221
|
+
const ch = await manager.createChannel({ name: 'crud', description: '初始描述' })
|
|
222
|
+
check('channel create', !!ch.channelId)
|
|
223
|
+
|
|
224
|
+
const listed = await manager.listChannels()
|
|
225
|
+
check('channel list', listed.some(c => c.id === ch.channelId))
|
|
226
|
+
|
|
227
|
+
const got = await manager.getChannel(ch.channelId)
|
|
228
|
+
check('channel get', got.name === 'crud' && got.workspace.length > 0)
|
|
229
|
+
|
|
230
|
+
const updated = await manager.updateChannel(ch.channelId, { name: 'renamed', workspace: tmpWs })
|
|
231
|
+
check('channel update(name+workspace)', updated.name === 'renamed' && updated.workspace === tmpWs)
|
|
232
|
+
|
|
233
|
+
// Agent(模板)+ 实例 create/list/get/update/delete
|
|
234
|
+
const tpl = await manager.createAgent({ name: 'agent1', harness: 'mock', config: { k: 'v' } })
|
|
235
|
+
const agent = await manager.addAgentToChannel({ channelId: ch.channelId, agentId: tpl.id, role: 'worker' })
|
|
236
|
+
check('agent create', !!agent.id && agent.id !== tpl.id)
|
|
237
|
+
|
|
238
|
+
const agentList = await manager.listChannelAgents(ch.channelId)
|
|
239
|
+
check('agent list', agentList.some(a => a.id === agent.id))
|
|
240
|
+
|
|
241
|
+
const agentGot = manager.getChannelAgent(agent.id)
|
|
242
|
+
check('agent get', agentGot?.name === 'agent1' && (agentGot.config as { k: string }).k === 'v')
|
|
243
|
+
|
|
244
|
+
const agentUpdated = await manager.updateChannelAgent(agent.id, { name: 'agent1-renamed', config: { k: 'v2' } })
|
|
245
|
+
check('agent update', agentUpdated.name === 'agent1-renamed' && (agentUpdated.config as { k: string }).k === 'v2')
|
|
246
|
+
|
|
247
|
+
await manager.removeAgentFromChannel(ch.channelId, agent.id)
|
|
248
|
+
check('agent delete', manager.getChannelAgent(agent.id) === undefined)
|
|
249
|
+
|
|
250
|
+
await manager.removeChannel(ch.channelId)
|
|
251
|
+
check('channel delete', (await manager.listChannels()).every(c => c.id !== ch.channelId))
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
await manager.unloadIdleAgents()
|
|
255
|
+
db.close()
|
|
256
|
+
rmSync(tmpWs, { recursive: true, force: true })
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ===== 测试 E: 禁用/启用 channel 与 agent =====
|
|
261
|
+
async function testEnableDisable(): Promise<void> {
|
|
262
|
+
console.log('\n━━━ 测试 E: 启用/禁用控制 ━━━')
|
|
263
|
+
|
|
264
|
+
const db = openWorkshopDb(':memory:')
|
|
265
|
+
const manager = makeManager(db)
|
|
266
|
+
try {
|
|
267
|
+
const ch = await manager.createChannel({ name: 'en', leadAgent: { name: 'lead', harness: 'mock', config: {} } })
|
|
268
|
+
const wTpl = await manager.createAgent({ name: 'w', harness: 'mock', config: {} })
|
|
269
|
+
const w = await manager.addAgentToChannel({ channelId: ch.channelId, agentId: wTpl.id, role: 'worker' })
|
|
270
|
+
|
|
271
|
+
// 禁用实例
|
|
272
|
+
await manager.updateChannelAgent(w.id, { enabled: 0 })
|
|
273
|
+
const wDisabled = manager.getChannelAgent(w.id)
|
|
274
|
+
check('agent 禁用持久化', wDisabled?.wired === false)
|
|
275
|
+
|
|
276
|
+
// 禁用 channel
|
|
277
|
+
await manager.updateChannel(ch.channelId, { enabled: 0 })
|
|
278
|
+
const chDisabled = (await manager.listChannels()).find(c => c.id === ch.channelId)
|
|
279
|
+
check('channel 禁用持久化', chDisabled?.enabled === 0)
|
|
280
|
+
|
|
281
|
+
// 禁用的 channel 提交任务应失败(NO_LEAD_AGENT 之前会先检查 channel 是否有效)
|
|
282
|
+
let threw = false
|
|
283
|
+
try {
|
|
284
|
+
await manager.submitChannelTask({ channelId: ch.channelId, title: 'x' })
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
threw = true
|
|
288
|
+
}
|
|
289
|
+
check('禁用 channel 无法提交任务', threw)
|
|
290
|
+
|
|
291
|
+
// 重新启用
|
|
292
|
+
await manager.updateChannel(ch.channelId, { enabled: 1 })
|
|
293
|
+
await manager.updateChannelAgent(w.id, { enabled: 1 })
|
|
294
|
+
const chRe = (await manager.listChannels()).find(c => c.id === ch.channelId)
|
|
295
|
+
check('channel 重新启用', chRe?.enabled === 1)
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
await manager.unloadIdleAgents()
|
|
299
|
+
db.close()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// ===== main =====
|
|
304
|
+
async function main(): Promise<void> {
|
|
305
|
+
console.log('╔════════════════════════════════════════════════════════════╗')
|
|
306
|
+
console.log('║ 持久化 + 懒加载 Runtime 测试 ║')
|
|
307
|
+
console.log('╚════════════════════════════════════════════════════════════╝')
|
|
308
|
+
|
|
309
|
+
await testPersistenceAndLazy()
|
|
310
|
+
await testRestoreWithPending()
|
|
311
|
+
await testCrud()
|
|
312
|
+
await testEnableDisable()
|
|
313
|
+
|
|
314
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
315
|
+
console.log(` ${failures === 0 ? `🎉 全部通过(${testCount} 项检查)` : `❌ ${failures}/${testCount} 项失败`}`)
|
|
316
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
317
|
+
process.exit(failures === 0 ? 0 : 1)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
main().catch((e) => {
|
|
321
|
+
console.error('测试异常:', e)
|
|
322
|
+
process.exit(1)
|
|
323
|
+
})
|