/ src / lib / server / agents / agent-thread-session.test.ts
agent-thread-session.test.ts
  1  import assert from 'node:assert/strict'
  2  import fs from 'node:fs'
  3  import os from 'node:os'
  4  import path from 'node:path'
  5  import { spawnSync } from 'node:child_process'
  6  import { describe, it } from 'node:test'
  7  
  8  const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), '../../..')
  9  
 10  function runWithTempDataDir(script: string) {
 11    const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-agent-thread-'))
 12    try {
 13      const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '--eval', script], {
 14        cwd: repoRoot,
 15        env: {
 16          ...process.env,
 17          DATA_DIR: path.join(tempDir, 'data'),
 18          WORKSPACE_DIR: path.join(tempDir, 'workspace'),
 19          BROWSER_PROFILES_DIR: path.join(tempDir, 'browser-profiles'),
 20        },
 21        encoding: 'utf-8',
 22      })
 23      assert.equal(result.status, 0, result.stderr || result.stdout || 'subprocess failed')
 24      const lines = (result.stdout || '')
 25        .trim()
 26        .split('\n')
 27        .map((line) => line.trim())
 28        .filter(Boolean)
 29      const jsonLine = [...lines].reverse().find((line) => line.startsWith('{'))
 30      return JSON.parse(jsonLine || '{}')
 31    } finally {
 32      fs.rmSync(tempDir, { recursive: true, force: true })
 33    }
 34  }
 35  
 36  describe('ensureAgentThreadSession', () => {
 37    it('creates and reuses an agent shortcut chat for heartbeat-enabled agents', () => {
 38      const output = runWithTempDataDir(`
 39        const storageMod = await import('@/lib/server/storage')
 40        const storage = storageMod.default || storageMod['module.exports'] || storageMod
 41        const helperMod = await import('@/lib/server/agents/agent-thread-session')
 42        const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
 43          || helperMod.default?.ensureAgentThreadSession
 44          || helperMod['module.exports']?.ensureAgentThreadSession
 45  
 46        const now = Date.now()
 47        storage.saveAgents({
 48          molly: {
 49            id: 'molly',
 50            name: 'Molly',
 51            description: 'Autonomous helper',
 52            provider: 'openai',
 53            model: 'gpt-test',
 54            credentialId: null,
 55            apiEndpoint: null,
 56            fallbackCredentialIds: [],
 57            heartbeatEnabled: true,
 58            heartbeatIntervalSec: 600,
 59            memoryScopeMode: 'agent',
 60            memoryTierPreference: 'blended',
 61            projectId: 'proj-1',
 62            createdAt: now,
 63            updatedAt: now,
 64            extensions: ['memory', 'web_search'],
 65          },
 66        })
 67  
 68        const first = ensureAgentThreadSession('molly')
 69        const second = ensureAgentThreadSession('molly')
 70        const agents = storage.loadAgents()
 71        const sessions = storage.loadSessions()
 72  
 73        console.log(JSON.stringify({
 74          firstId: first?.id,
 75          secondId: second?.id,
 76          threadSessionId: agents.molly?.threadSessionId || null,
 77          session: first ? sessions[first.id] : null,
 78        }))
 79      `)
 80  
 81      assert.equal(output.firstId, output.secondId)
 82      assert.equal(output.threadSessionId, output.firstId)
 83      assert.equal(output.session.shortcutForAgentId, 'molly')
 84      assert.equal(output.session.agentId, 'molly')
 85      assert.equal(output.session.heartbeatEnabled, true)
 86      assert.equal(output.session.memoryScopeMode, 'agent')
 87      assert.equal(output.session.memoryTierPreference, 'blended')
 88      assert.equal(output.session.projectId, 'proj-1')
 89    })
 90  
 91    it('does not create a new shortcut chat when the agent is disabled', () => {
 92      const output = runWithTempDataDir(`
 93        const storageMod = await import('@/lib/server/storage')
 94        const storage = storageMod.default || storageMod['module.exports'] || storageMod
 95        const helperMod = await import('@/lib/server/agents/agent-thread-session')
 96        const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
 97          || helperMod.default?.ensureAgentThreadSession
 98          || helperMod['module.exports']?.ensureAgentThreadSession
 99  
100        const now = Date.now()
101        storage.saveAgents({
102          molly: {
103            id: 'molly',
104            name: 'Molly',
105            description: 'Temporarily disabled helper',
106            provider: 'openai',
107            model: 'gpt-test',
108            credentialId: null,
109            apiEndpoint: null,
110            fallbackCredentialIds: [],
111            disabled: true,
112            heartbeatEnabled: true,
113            heartbeatIntervalSec: 600,
114            createdAt: now,
115            updatedAt: now,
116            extensions: ['memory'],
117          },
118        })
119  
120        const session = ensureAgentThreadSession('molly')
121        const agents = storage.loadAgents()
122        const sessions = storage.loadSessions()
123  
124        console.log(JSON.stringify({
125          sessionId: session?.id || null,
126          threadSessionId: agents.molly?.threadSessionId || null,
127          sessionCount: Object.keys(sessions).length,
128        }))
129      `)
130  
131      assert.equal(output.sessionId, null)
132      assert.equal(output.threadSessionId, null)
133      assert.equal(output.sessionCount, 0)
134    })
135  
136    it('propagates explicit OpenClaw gateway agent ids into the shortcut session', () => {
137      const output = runWithTempDataDir(`
138        const storageMod = await import('@/lib/server/storage')
139        const storage = storageMod.default || storageMod['module.exports'] || storageMod
140        const helperMod = await import('@/lib/server/agents/agent-thread-session')
141        const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
142          || helperMod.default?.ensureAgentThreadSession
143          || helperMod['module.exports']?.ensureAgentThreadSession
144  
145        const now = Date.now()
146        storage.saveAgents({
147          oc: {
148            id: 'oc',
149            name: 'OpenClaw Ops',
150            description: 'OpenClaw-backed helper',
151            provider: 'openclaw',
152            model: 'default',
153            credentialId: null,
154            apiEndpoint: null,
155            gatewayProfileId: 'gateway-test',
156            fallbackCredentialIds: [],
157            openclawAgentId: 'main',
158            heartbeatEnabled: true,
159            heartbeatIntervalSec: 600,
160            createdAt: now,
161            updatedAt: now,
162            extensions: [],
163          },
164        })
165  
166        const session = ensureAgentThreadSession('oc')
167        const sessions = storage.loadSessions()
168  
169        console.log(JSON.stringify({
170          session: session ? sessions[session.id] : null,
171        }))
172      `)
173  
174      assert.equal(output.session.openclawAgentId, 'main')
175    })
176  
177    it('clears stale connector routing state from an existing agent shortcut session', () => {
178      const output = runWithTempDataDir(`
179        const storageMod = await import('@/lib/server/storage')
180        const storage = storageMod.default || storageMod['module.exports'] || storageMod
181        const helperMod = await import('@/lib/server/agents/agent-thread-session')
182        const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
183          || helperMod.default?.ensureAgentThreadSession
184          || helperMod['module.exports']?.ensureAgentThreadSession
185  
186        const now = Date.now()
187        storage.saveAgents({
188          molly: {
189            id: 'molly',
190            name: 'Molly',
191            provider: 'openai',
192            model: 'gpt-test',
193            credentialId: null,
194            apiEndpoint: null,
195            fallbackCredentialIds: [],
196            heartbeatEnabled: true,
197            heartbeatIntervalSec: 600,
198            threadSessionId: 'agent-chat-molly-existing',
199            createdAt: now,
200            updatedAt: now,
201            extensions: ['memory'],
202          },
203        })
204        storage.saveSessions({
205          'agent-chat-molly-existing': {
206            id: 'agent-chat-molly-existing',
207            name: 'Molly',
208            cwd: process.env.WORKSPACE_DIR,
209            user: 'default',
210            provider: 'openai',
211            model: 'gpt-old',
212            claudeSessionId: null,
213            codexThreadId: null,
214            opencodeSessionId: null,
215            delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
216            messages: [],
217            createdAt: now,
218            lastActiveAt: now,
219            sessionType: 'human',
220            agentId: 'molly',
221            extensions: ['memory'],
222            connectorContext: {
223              connectorId: 'conn-1',
224              channelId: 'wrong-chat',
225              senderId: 'wrong-user',
226            },
227          },
228        })
229  
230        const session = ensureAgentThreadSession('molly')
231        const persisted = storage.loadSessions()[session.id]
232  
233        console.log(JSON.stringify({
234          connectorContext: persisted.connectorContext || null,
235        }))
236      `)
237  
238      assert.equal(output.connectorContext, null)
239    })
240  
241    it('repairs an existing Ollama Cloud thread session away from a stale local endpoint', () => {
242      const output = runWithTempDataDir(`
243        const storageMod = await import('@/lib/server/storage')
244        const storage = storageMod.default || storageMod['module.exports'] || storageMod
245        const helperMod = await import('@/lib/server/agents/agent-thread-session')
246        const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
247          || helperMod.default?.ensureAgentThreadSession
248          || helperMod['module.exports']?.ensureAgentThreadSession
249  
250        const now = Date.now()
251        storage.saveCredentials({
252          'cred-ollama-cloud': {
253            id: 'cred-ollama-cloud',
254            provider: 'ollama',
255            name: 'Ollama Cloud',
256            encryptedKey: storage.encryptKey('ollama-cloud-key'),
257            createdAt: now,
258          },
259        })
260        storage.saveAgents({
261          hal: {
262            id: 'hal',
263            name: 'Hal2k',
264            provider: 'ollama',
265            model: 'glm-5:cloud',
266            ollamaMode: 'cloud',
267            credentialId: 'cred-ollama-cloud',
268            apiEndpoint: null,
269            fallbackCredentialIds: [],
270            heartbeatEnabled: true,
271            heartbeatIntervalSec: 600,
272            threadSessionId: 'agent-chat-hal-existing',
273            createdAt: now,
274            updatedAt: now,
275          },
276        })
277        storage.saveSessions({
278          'agent-chat-hal-existing': {
279            id: 'agent-chat-hal-existing',
280            name: 'Hal2k',
281            cwd: process.env.WORKSPACE_DIR,
282            user: 'default',
283            provider: 'ollama',
284            model: 'glm-5:cloud',
285            ollamaMode: 'cloud',
286            credentialId: 'cred-ollama-cloud',
287            apiEndpoint: 'http://localhost:11434',
288            claudeSessionId: null,
289            codexThreadId: null,
290            opencodeSessionId: null,
291            delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
292            messages: [],
293            createdAt: now,
294            lastActiveAt: now,
295            sessionType: 'human',
296            agentId: 'hal',
297            shortcutForAgentId: 'hal',
298          },
299        })
300  
301        const session = ensureAgentThreadSession('hal')
302        const persisted = storage.loadSessions()[session.id]
303        const healedAgent = storage.loadAgents().hal
304  
305        console.log(JSON.stringify({
306          sessionId: session.id,
307          ollamaMode: persisted.ollamaMode || null,
308          apiEndpoint: persisted.apiEndpoint || null,
309          credentialId: persisted.credentialId || null,
310          agentCredentialId: healedAgent?.credentialId || null,
311        }))
312      `)
313  
314      assert.equal(output.sessionId, 'agent-chat-hal-existing')
315      assert.equal(output.ollamaMode, 'cloud')
316      assert.equal(output.credentialId, 'cred-ollama-cloud')
317      assert.equal(output.agentCredentialId, 'cred-ollama-cloud')
318      assert.equal(output.apiEndpoint, 'https://ollama.com')
319    })
320  })