/ src / lib / server / runtime / queue-reconcile.test.ts
queue-reconcile.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-queue-reconcile-'))
 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          SWARMCLAW_BUILD_MODE: '1',
 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('reconcileFinishedRunningTasks', () => {
 37    it('finalizes a completed one-off scheduled task from its finished session and deletes the schedule', () => {
 38      const output = runWithTempDataDir(`
 39        const storageMod = await import('@/lib/server/storage')
 40        const queueMod = await import('@/lib/server/runtime/queue')
 41        const storage = storageMod.default || storageMod
 42        const queue = queueMod.default || queueMod
 43  
 44        const now = Date.now()
 45        const workspace = process.env.WORKSPACE_DIR
 46        storage.saveAgents({
 47          agent_birthday: {
 48            id: 'agent_birthday',
 49            name: 'Birthday Bot',
 50            description: '',
 51            systemPrompt: '',
 52            provider: 'openai',
 53            model: 'gpt-test',
 54            threadSessionId: 'thread-birthday',
 55            createdAt: now,
 56            updatedAt: now,
 57          },
 58        })
 59        storage.saveSessions({
 60          'origin-birthday': {
 61            id: 'origin-birthday',
 62            name: 'Origin Chat',
 63            cwd: workspace,
 64            user: 'tester',
 65            provider: 'openai',
 66            model: 'gpt-test',
 67            claudeSessionId: null,
 68            messages: [],
 69            createdAt: now - 10_000,
 70            lastActiveAt: now - 5_000,
 71            active: true,
 72            currentRunId: null,
 73            agentId: 'agent_birthday',
 74          },
 75          'thread-birthday': {
 76            id: 'thread-birthday',
 77            name: 'Birthday Bot',
 78            cwd: workspace,
 79            user: 'tester',
 80            provider: 'openai',
 81            model: 'gpt-test',
 82            claudeSessionId: null,
 83            messages: [],
 84            createdAt: now - 10_000,
 85            lastActiveAt: now - 5_000,
 86            active: true,
 87            currentRunId: null,
 88            agentId: 'agent_birthday',
 89            shortcutForAgentId: 'agent_birthday',
 90          },
 91          'session-birthday': {
 92            id: 'session-birthday',
 93            name: 'Birthday Run',
 94            cwd: workspace,
 95            user: 'tester',
 96            provider: 'openai',
 97            model: 'gpt-test',
 98            claudeSessionId: null,
 99            messages: [
100              {
101                role: 'assistant',
102                text: 'Happy birthday. I sent a WhatsApp follow-up to the user directly and confirmed delivery with message id 3EB0B7262FF68B7BD261D4.',
103                time: now,
104              },
105            ],
106            createdAt: now - 10_000,
107            lastActiveAt: now,
108            active: false,
109            currentRunId: null,
110            heartbeatEnabled: true,
111          },
112        })
113        storage.saveSchedules({
114          'schedule-birthday': {
115            id: 'schedule-birthday',
116            name: 'Birthday Reminder',
117            scheduleType: 'once',
118            status: 'completed',
119            agentId: 'agent_birthday',
120            createdByAgentId: 'agent_birthday',
121            createdInSessionId: 'origin-birthday',
122            createdAt: now - 20_000,
123            updatedAt: now - 5_000,
124          },
125        })
126        storage.saveTasks({
127          'task-birthday': {
128            id: 'task-birthday',
129            title: 'Birthday follow-up',
130            description: 'Wish me happy birthday tomorrow over WhatsApp.',
131            status: 'running',
132            agentId: 'agent_birthday',
133            createdAt: now - 20_000,
134            updatedAt: now - 5_000,
135            startedAt: now - 15_000,
136            sessionId: 'session-birthday',
137            sourceType: 'schedule',
138            sourceScheduleId: 'schedule-birthday',
139            sourceScheduleName: 'Birthday Reminder',
140            createdInSessionId: 'origin-birthday',
141            maxAttempts: 3,
142            retryBackoffSec: 30,
143          },
144        })
145  
146        const result = queue.reconcileFinishedRunningTasks()
147        console.log(JSON.stringify({
148          result,
149          task: storage.loadTasks()['task-birthday'],
150          schedule: storage.loadSchedules()['schedule-birthday'] || null,
151          session: storage.loadSessions()['session-birthday'],
152          originMessages: storage.loadSessions()['origin-birthday'].messages,
153          threadMessages: storage.loadSessions()['thread-birthday'].messages,
154        }))
155      `)
156  
157      assert.equal(output.result.reconciled, 1)
158      assert.equal(output.task.status, 'completed')
159      assert.equal(output.schedule?.status, 'completed')
160      assert.equal(output.session.heartbeatEnabled, false)
161      assert.match(output.task.result, /WhatsApp follow-up/i)
162      assert.deepEqual(output.originMessages, [])
163      assert.deepEqual(output.threadMessages, [])
164    })
165  
166    it('posts exactly one terminal update to the originating session for user-created tasks', () => {
167      const output = runWithTempDataDir(`
168        const storageMod = await import('@/lib/server/storage')
169        const queueMod = await import('@/lib/server/runtime/queue')
170        const storage = storageMod.default || storageMod
171        const queue = queueMod.default || queueMod
172  
173        const now = Date.now()
174        const workspace = process.env.WORKSPACE_DIR
175        storage.saveAgents({
176          agent_writer: {
177            id: 'agent_writer',
178            name: 'Writer Bot',
179            description: '',
180            systemPrompt: '',
181            provider: 'openai',
182            model: 'gpt-test',
183            threadSessionId: 'thread-writer',
184            createdAt: now,
185            updatedAt: now,
186          },
187        })
188        storage.saveSessions({
189          'origin-task': {
190            id: 'origin-task',
191            name: 'Project Chat',
192            cwd: workspace,
193            user: 'tester',
194            provider: 'openai',
195            model: 'gpt-test',
196            claudeSessionId: null,
197            messages: [],
198            createdAt: now - 10_000,
199            lastActiveAt: now - 5_000,
200            active: true,
201            currentRunId: null,
202            agentId: 'agent_writer',
203          },
204          'thread-writer': {
205            id: 'thread-writer',
206            name: 'Writer Bot',
207            cwd: workspace,
208            user: 'tester',
209            provider: 'openai',
210            model: 'gpt-test',
211            claudeSessionId: null,
212            messages: [],
213            createdAt: now - 10_000,
214            lastActiveAt: now - 5_000,
215            active: true,
216            currentRunId: null,
217            agentId: 'agent_writer',
218            shortcutForAgentId: 'agent_writer',
219          },
220          'run-task': {
221            id: 'run-task',
222            name: 'Execution Session',
223            cwd: workspace,
224            user: 'tester',
225            provider: 'openai',
226            model: 'gpt-test',
227            claudeSessionId: null,
228            messages: [
229              {
230                role: 'assistant',
231                text: 'Updated docs/summary.md, verified with npm test passed, and confirmed the summary reflects the final meeting decisions.',
232                time: now,
233              },
234            ],
235            createdAt: now - 10_000,
236            lastActiveAt: now,
237            active: false,
238            currentRunId: null,
239            heartbeatEnabled: true,
240          },
241        })
242        storage.saveTasks({
243          'task-manual': {
244            id: 'task-manual',
245            title: 'Write summary',
246            description: 'Summarize the meeting notes.',
247            status: 'running',
248            agentId: 'agent_writer',
249            createdAt: now - 20_000,
250            updatedAt: now - 5_000,
251            startedAt: now - 15_000,
252            sessionId: 'run-task',
253            createdInSessionId: 'origin-task',
254            sourceType: 'manual',
255            maxAttempts: 3,
256            retryBackoffSec: 30,
257          },
258        })
259  
260        const result = queue.reconcileFinishedRunningTasks()
261        const sessions = storage.loadSessions()
262        console.log(JSON.stringify({
263          result,
264          task: storage.loadTasks()['task-manual'],
265          originMessages: sessions['origin-task'].messages,
266          threadMessages: sessions['thread-writer'].messages,
267        }))
268      `)
269  
270      assert.equal(output.result.reconciled, 1)
271      assert.equal(output.task.status, 'completed')
272      assert.equal(output.originMessages.length, 1)
273      assert.match(output.originMessages[0].text, /^Task completed: \*\*\[Write summary\]\(#task:task-manual\)\*\*/)
274      assert.match(output.originMessages[0].text, /docs\/summary\.md/)
275      assert.deepEqual(output.threadMessages, [])
276    })
277  
278    it('keeps agent-created task completions out of user-facing chat sessions', () => {
279      const output = runWithTempDataDir(`
280        const storageMod = await import('@/lib/server/storage')
281        const queueMod = await import('@/lib/server/runtime/queue')
282        const storage = storageMod.default || storageMod
283        const queue = queueMod.default || queueMod
284  
285        const now = Date.now()
286        const workspace = process.env.WORKSPACE_DIR
287        storage.saveAgents({
288          agent_ops: {
289            id: 'agent_ops',
290            name: 'Ops Bot',
291            description: '',
292            systemPrompt: '',
293            provider: 'openai',
294            model: 'gpt-test',
295            threadSessionId: 'thread-ops',
296            createdAt: now,
297            updatedAt: now,
298          },
299        })
300        storage.saveSessions({
301          'origin-ops': {
302            id: 'origin-ops',
303            name: 'Origin Chat',
304            cwd: workspace,
305            user: 'tester',
306            provider: 'openai',
307            model: 'gpt-test',
308            claudeSessionId: null,
309            messages: [],
310            createdAt: now - 10_000,
311            lastActiveAt: now - 5_000,
312            active: true,
313            currentRunId: null,
314            agentId: 'agent_ops',
315          },
316          'thread-ops': {
317            id: 'thread-ops',
318            name: 'Ops Bot',
319            cwd: workspace,
320            user: 'tester',
321            provider: 'openai',
322            model: 'gpt-test',
323            claudeSessionId: null,
324            messages: [],
325            createdAt: now - 10_000,
326            lastActiveAt: now - 5_000,
327            active: true,
328            currentRunId: null,
329            agentId: 'agent_ops',
330            shortcutForAgentId: 'agent_ops',
331          },
332          'run-ops': {
333            id: 'run-ops',
334            name: 'Execution Session',
335            cwd: workspace,
336            user: 'tester',
337            provider: 'openai',
338            model: 'gpt-test',
339            claudeSessionId: null,
340            messages: [
341              {
342                role: 'assistant',
343                text: 'Health check completed successfully.',
344                time: now,
345              },
346            ],
347            createdAt: now - 10_000,
348            lastActiveAt: now,
349            active: false,
350            currentRunId: null,
351            heartbeatEnabled: true,
352          },
353        })
354        storage.saveTasks({
355          'task-agent': {
356            id: 'task-agent',
357            title: 'Self check',
358            description: 'Run an internal health check.',
359            status: 'running',
360            agentId: 'agent_ops',
361            createdAt: now - 20_000,
362            updatedAt: now - 5_000,
363            startedAt: now - 15_000,
364            sessionId: 'run-ops',
365            createdInSessionId: 'origin-ops',
366            createdByAgentId: 'agent_ops',
367            sourceType: 'manual',
368            maxAttempts: 3,
369            retryBackoffSec: 30,
370          },
371        })
372  
373        const result = queue.reconcileFinishedRunningTasks()
374        const sessions = storage.loadSessions()
375        console.log(JSON.stringify({
376          result,
377          task: storage.loadTasks()['task-agent'],
378          originMessages: sessions['origin-ops'].messages,
379          threadMessages: sessions['thread-ops'].messages,
380        }))
381      `)
382  
383      assert.equal(output.result.reconciled, 1)
384      assert.equal(output.task.status, 'completed')
385      assert.deepEqual(output.originMessages, [])
386      assert.deepEqual(output.threadMessages, [])
387    })
388  })