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 })