@swarmclawai/swarmclaw 0.7.7 → 0.8.0
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/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -0,0 +1,852 @@
|
|
|
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-tasks-adv-'))
|
|
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
|
+
},
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
timeout: 30_000,
|
|
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
|
+
/** Helper: seed agents + return manage_tasks / manage_projects tool invocation boilerplate. */
|
|
37
|
+
const AGENT_SETUP = `
|
|
38
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
39
|
+
const crudMod = await import('./src/lib/server/session-tools/crud.ts')
|
|
40
|
+
const storage = storageMod.default || storageMod
|
|
41
|
+
const crud = crudMod.default || crudMod
|
|
42
|
+
|
|
43
|
+
const now = Date.now()
|
|
44
|
+
storage.saveAgents({
|
|
45
|
+
agent1: {
|
|
46
|
+
id: 'agent1',
|
|
47
|
+
name: 'Alpha',
|
|
48
|
+
description: 'Test agent',
|
|
49
|
+
systemPrompt: '',
|
|
50
|
+
provider: 'openai',
|
|
51
|
+
model: 'gpt-test',
|
|
52
|
+
createdAt: now,
|
|
53
|
+
updatedAt: now,
|
|
54
|
+
},
|
|
55
|
+
agent2: {
|
|
56
|
+
id: 'agent2',
|
|
57
|
+
name: 'Beta',
|
|
58
|
+
description: 'Second test agent',
|
|
59
|
+
systemPrompt: '',
|
|
60
|
+
provider: 'openai',
|
|
61
|
+
model: 'gpt-test',
|
|
62
|
+
createdAt: now,
|
|
63
|
+
updatedAt: now,
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const cwd = process.env.WORKSPACE_DIR
|
|
68
|
+
const tools = crud.buildCrudTools({
|
|
69
|
+
cwd,
|
|
70
|
+
ctx: { sessionId: 'session-1', agentId: 'agent1', platformAssignScope: 'self' },
|
|
71
|
+
hasPlugin: (name) => name === 'manage_tasks' || name === 'manage_projects',
|
|
72
|
+
})
|
|
73
|
+
const taskTool = tools.find((entry) => entry.name === 'manage_tasks')
|
|
74
|
+
const projectTool = tools.find((entry) => entry.name === 'manage_projects')
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
/** Helper to import dequeueNextRunnableTask (CJS-compatible). */
|
|
78
|
+
const QUEUE_IMPORT = `
|
|
79
|
+
const _queueMod = await import('./src/lib/server/queue.ts')
|
|
80
|
+
const _queue = _queueMod.default || _queueMod
|
|
81
|
+
const dequeueNextRunnableTask = _queue.dequeueNextRunnableTask
|
|
82
|
+
`
|
|
83
|
+
|
|
84
|
+
/** A result string long enough to pass task validation (>= 20 chars). */
|
|
85
|
+
const VALID_RESULT = 'Task completed successfully with all objectives met and verified'
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Task lifecycle
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
describe('manage_tasks: task lifecycle', () => {
|
|
91
|
+
it('1. creates a task in backlog with correct fields', () => {
|
|
92
|
+
const output = runWithTempDataDir(`
|
|
93
|
+
${AGENT_SETUP}
|
|
94
|
+
const raw = await taskTool.invoke({
|
|
95
|
+
action: 'create',
|
|
96
|
+
data: JSON.stringify({ title: 'Build dashboard', description: 'Build the main dashboard view' }),
|
|
97
|
+
})
|
|
98
|
+
const tasks = storage.loadTasks()
|
|
99
|
+
const task = Object.values(tasks)[0]
|
|
100
|
+
console.log(JSON.stringify({ task }))
|
|
101
|
+
`)
|
|
102
|
+
assert.equal(output.task.title, 'Build dashboard')
|
|
103
|
+
assert.equal(output.task.status, 'backlog')
|
|
104
|
+
assert.ok(output.task.id)
|
|
105
|
+
assert.ok(output.task.createdAt)
|
|
106
|
+
assert.ok(output.task.updatedAt)
|
|
107
|
+
assert.equal(output.task.description, 'Build the main dashboard view')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('2. queues a backlog task — status becomes queued, queuedAt set', () => {
|
|
111
|
+
const output = runWithTempDataDir(`
|
|
112
|
+
${AGENT_SETUP}
|
|
113
|
+
await taskTool.invoke({
|
|
114
|
+
action: 'create',
|
|
115
|
+
data: JSON.stringify({ title: 'Queue me', description: 'A task to queue', status: 'queued' }),
|
|
116
|
+
})
|
|
117
|
+
const tasks = storage.loadTasks()
|
|
118
|
+
const task = Object.values(tasks)[0]
|
|
119
|
+
console.log(JSON.stringify({ task }))
|
|
120
|
+
`)
|
|
121
|
+
assert.equal(output.task.status, 'queued')
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('3. marks a queued task as running — stays queued (normalized)', () => {
|
|
125
|
+
const output = runWithTempDataDir(`
|
|
126
|
+
${AGENT_SETUP}
|
|
127
|
+
const raw = await taskTool.invoke({
|
|
128
|
+
action: 'create',
|
|
129
|
+
data: JSON.stringify({ title: 'Run me', description: 'Test running status', status: 'queued' }),
|
|
130
|
+
})
|
|
131
|
+
const created = JSON.parse(raw)
|
|
132
|
+
await taskTool.invoke({
|
|
133
|
+
action: 'update',
|
|
134
|
+
id: created.id,
|
|
135
|
+
data: JSON.stringify({ status: 'running' }),
|
|
136
|
+
})
|
|
137
|
+
const tasks = storage.loadTasks()
|
|
138
|
+
const task = tasks[created.id]
|
|
139
|
+
console.log(JSON.stringify({ task }))
|
|
140
|
+
`)
|
|
141
|
+
// 'running' from non-running normalizes to 'queued'
|
|
142
|
+
assert.equal(output.task.status, 'queued')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('4. completes a task — status completed, result stored', () => {
|
|
146
|
+
const output = runWithTempDataDir(`
|
|
147
|
+
${AGENT_SETUP}
|
|
148
|
+
const raw = await taskTool.invoke({
|
|
149
|
+
action: 'create',
|
|
150
|
+
data: JSON.stringify({ title: 'Report generation', description: 'Produce the quarterly report' }),
|
|
151
|
+
})
|
|
152
|
+
const created = JSON.parse(raw)
|
|
153
|
+
await taskTool.invoke({
|
|
154
|
+
action: 'update',
|
|
155
|
+
id: created.id,
|
|
156
|
+
data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }),
|
|
157
|
+
})
|
|
158
|
+
const tasks = storage.loadTasks()
|
|
159
|
+
const task = tasks[created.id]
|
|
160
|
+
console.log(JSON.stringify({ task }))
|
|
161
|
+
`)
|
|
162
|
+
assert.equal(output.task.status, 'completed')
|
|
163
|
+
assert.equal(output.task.result, VALID_RESULT)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('5. fails a running task — status failed, error stored', () => {
|
|
167
|
+
const output = runWithTempDataDir(`
|
|
168
|
+
${AGENT_SETUP}
|
|
169
|
+
const raw = await taskTool.invoke({
|
|
170
|
+
action: 'create',
|
|
171
|
+
data: JSON.stringify({ title: 'Fail me', description: 'Will fail' }),
|
|
172
|
+
})
|
|
173
|
+
const created = JSON.parse(raw)
|
|
174
|
+
await taskTool.invoke({
|
|
175
|
+
action: 'update',
|
|
176
|
+
id: created.id,
|
|
177
|
+
data: JSON.stringify({ status: 'failed', error: 'Something went wrong during execution' }),
|
|
178
|
+
})
|
|
179
|
+
const tasks = storage.loadTasks()
|
|
180
|
+
const task = tasks[created.id]
|
|
181
|
+
console.log(JSON.stringify({ task }))
|
|
182
|
+
`)
|
|
183
|
+
assert.equal(output.task.status, 'failed')
|
|
184
|
+
assert.equal(output.task.error, 'Something went wrong during execution')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('6. archives a completed task — status archived', () => {
|
|
188
|
+
const output = runWithTempDataDir(`
|
|
189
|
+
${AGENT_SETUP}
|
|
190
|
+
const raw = await taskTool.invoke({
|
|
191
|
+
action: 'create',
|
|
192
|
+
data: JSON.stringify({ title: 'Archive candidate', description: 'Will be archived' }),
|
|
193
|
+
})
|
|
194
|
+
const created = JSON.parse(raw)
|
|
195
|
+
await taskTool.invoke({
|
|
196
|
+
action: 'update',
|
|
197
|
+
id: created.id,
|
|
198
|
+
data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }),
|
|
199
|
+
})
|
|
200
|
+
await taskTool.invoke({
|
|
201
|
+
action: 'update',
|
|
202
|
+
id: created.id,
|
|
203
|
+
data: JSON.stringify({ status: 'archived' }),
|
|
204
|
+
})
|
|
205
|
+
const tasks = storage.loadTasks()
|
|
206
|
+
const task = tasks[created.id]
|
|
207
|
+
console.log(JSON.stringify({ task }))
|
|
208
|
+
`)
|
|
209
|
+
assert.equal(output.task.status, 'archived')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Task dependencies
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
describe('manage_tasks: task dependencies', () => {
|
|
217
|
+
it('7. creates tasks where C is blockedBy [A, B]', () => {
|
|
218
|
+
const output = runWithTempDataDir(`
|
|
219
|
+
${AGENT_SETUP}
|
|
220
|
+
const rawA = await taskTool.invoke({
|
|
221
|
+
action: 'create',
|
|
222
|
+
data: JSON.stringify({ title: 'Task A', description: 'First task' }),
|
|
223
|
+
})
|
|
224
|
+
const rawB = await taskTool.invoke({
|
|
225
|
+
action: 'create',
|
|
226
|
+
data: JSON.stringify({ title: 'Task B', description: 'Second task' }),
|
|
227
|
+
})
|
|
228
|
+
const a = JSON.parse(rawA)
|
|
229
|
+
const b = JSON.parse(rawB)
|
|
230
|
+
const rawC = await taskTool.invoke({
|
|
231
|
+
action: 'create',
|
|
232
|
+
data: JSON.stringify({ title: 'Task C', description: 'Blocked task', blockedBy: [a.id, b.id] }),
|
|
233
|
+
})
|
|
234
|
+
const c = JSON.parse(rawC)
|
|
235
|
+
const tasks = storage.loadTasks()
|
|
236
|
+
console.log(JSON.stringify({ a, b, c: tasks[c.id] }))
|
|
237
|
+
`)
|
|
238
|
+
assert.ok(Array.isArray(output.c.blockedBy))
|
|
239
|
+
assert.ok(output.c.blockedBy.includes(output.a.id))
|
|
240
|
+
assert.ok(output.c.blockedBy.includes(output.b.id))
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('8. complete A — C still blocked (B pending)', () => {
|
|
244
|
+
const output = runWithTempDataDir(`
|
|
245
|
+
${AGENT_SETUP}
|
|
246
|
+
${QUEUE_IMPORT}
|
|
247
|
+
const rawA = await taskTool.invoke({
|
|
248
|
+
action: 'create',
|
|
249
|
+
data: JSON.stringify({ title: 'Dep A', description: 'First dep', status: 'queued' }),
|
|
250
|
+
})
|
|
251
|
+
const rawB = await taskTool.invoke({
|
|
252
|
+
action: 'create',
|
|
253
|
+
data: JSON.stringify({ title: 'Dep B', description: 'Second dep', status: 'queued' }),
|
|
254
|
+
})
|
|
255
|
+
const a = JSON.parse(rawA)
|
|
256
|
+
const b = JSON.parse(rawB)
|
|
257
|
+
const rawC = await taskTool.invoke({
|
|
258
|
+
action: 'create',
|
|
259
|
+
data: JSON.stringify({ title: 'Dep C', description: 'Blocked by A and B', status: 'queued', blockedBy: [a.id, b.id] }),
|
|
260
|
+
})
|
|
261
|
+
const c = JSON.parse(rawC)
|
|
262
|
+
// Complete A
|
|
263
|
+
await taskTool.invoke({
|
|
264
|
+
action: 'update',
|
|
265
|
+
id: a.id,
|
|
266
|
+
data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }),
|
|
267
|
+
})
|
|
268
|
+
const tasks = storage.loadTasks()
|
|
269
|
+
const queue = [c.id]
|
|
270
|
+
const result = dequeueNextRunnableTask(queue, tasks)
|
|
271
|
+
console.log(JSON.stringify({ result }))
|
|
272
|
+
`)
|
|
273
|
+
// C should still be blocked since B is not completed
|
|
274
|
+
assert.equal(output.result, null)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('9. complete B — C becomes unblocked (ready to dequeue)', () => {
|
|
278
|
+
const output = runWithTempDataDir(`
|
|
279
|
+
${AGENT_SETUP}
|
|
280
|
+
${QUEUE_IMPORT}
|
|
281
|
+
const rawA = await taskTool.invoke({
|
|
282
|
+
action: 'create',
|
|
283
|
+
data: JSON.stringify({ title: 'Unblock A', description: 'First unblock dep', status: 'queued' }),
|
|
284
|
+
})
|
|
285
|
+
const rawB = await taskTool.invoke({
|
|
286
|
+
action: 'create',
|
|
287
|
+
data: JSON.stringify({ title: 'Unblock B', description: 'Second unblock dep', status: 'queued' }),
|
|
288
|
+
})
|
|
289
|
+
const a = JSON.parse(rawA)
|
|
290
|
+
const b = JSON.parse(rawB)
|
|
291
|
+
const rawC = await taskTool.invoke({
|
|
292
|
+
action: 'create',
|
|
293
|
+
data: JSON.stringify({ title: 'Unblock C', description: 'Blocked by A and B', status: 'queued', blockedBy: [a.id, b.id] }),
|
|
294
|
+
})
|
|
295
|
+
const c = JSON.parse(rawC)
|
|
296
|
+
// Complete both A and B
|
|
297
|
+
await taskTool.invoke({
|
|
298
|
+
action: 'update',
|
|
299
|
+
id: a.id,
|
|
300
|
+
data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }),
|
|
301
|
+
})
|
|
302
|
+
await taskTool.invoke({
|
|
303
|
+
action: 'update',
|
|
304
|
+
id: b.id,
|
|
305
|
+
data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }),
|
|
306
|
+
})
|
|
307
|
+
const tasks = storage.loadTasks()
|
|
308
|
+
const queue = [c.id]
|
|
309
|
+
const result = dequeueNextRunnableTask(queue, tasks)
|
|
310
|
+
console.log(JSON.stringify({ result, cId: c.id }))
|
|
311
|
+
`)
|
|
312
|
+
assert.equal(output.result, output.cId)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it('10. diamond dependency: D→E,F→G', () => {
|
|
316
|
+
const output = runWithTempDataDir(`
|
|
317
|
+
${AGENT_SETUP}
|
|
318
|
+
${QUEUE_IMPORT}
|
|
319
|
+
const rawD = await taskTool.invoke({
|
|
320
|
+
action: 'create',
|
|
321
|
+
data: JSON.stringify({ title: 'Diamond D', description: 'Root node', status: 'queued' }),
|
|
322
|
+
})
|
|
323
|
+
const d = JSON.parse(rawD)
|
|
324
|
+
const rawE = await taskTool.invoke({
|
|
325
|
+
action: 'create',
|
|
326
|
+
data: JSON.stringify({ title: 'Diamond E', description: 'Left branch', status: 'queued', blockedBy: [d.id] }),
|
|
327
|
+
})
|
|
328
|
+
const rawF = await taskTool.invoke({
|
|
329
|
+
action: 'create',
|
|
330
|
+
data: JSON.stringify({ title: 'Diamond F', description: 'Right branch', status: 'queued', blockedBy: [d.id] }),
|
|
331
|
+
})
|
|
332
|
+
const e = JSON.parse(rawE)
|
|
333
|
+
const f = JSON.parse(rawF)
|
|
334
|
+
const rawG = await taskTool.invoke({
|
|
335
|
+
action: 'create',
|
|
336
|
+
data: JSON.stringify({ title: 'Diamond G', description: 'Convergence point', status: 'queued', blockedBy: [e.id, f.id] }),
|
|
337
|
+
})
|
|
338
|
+
const g = JSON.parse(rawG)
|
|
339
|
+
|
|
340
|
+
// Before completing D: E, F, G all blocked
|
|
341
|
+
let tasks = storage.loadTasks()
|
|
342
|
+
const r1 = dequeueNextRunnableTask([e.id, f.id, g.id], tasks)
|
|
343
|
+
|
|
344
|
+
// Complete D: E and F unblocked
|
|
345
|
+
await taskTool.invoke({ action: 'update', id: d.id, data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }) })
|
|
346
|
+
tasks = storage.loadTasks()
|
|
347
|
+
const r2 = dequeueNextRunnableTask([e.id, f.id, g.id], tasks)
|
|
348
|
+
|
|
349
|
+
// Complete E: G still blocked
|
|
350
|
+
await taskTool.invoke({ action: 'update', id: e.id, data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }) })
|
|
351
|
+
tasks = storage.loadTasks()
|
|
352
|
+
const r3 = dequeueNextRunnableTask([g.id], tasks)
|
|
353
|
+
|
|
354
|
+
// Complete F: G unblocked
|
|
355
|
+
await taskTool.invoke({ action: 'update', id: f.id, data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }) })
|
|
356
|
+
tasks = storage.loadTasks()
|
|
357
|
+
const r4 = dequeueNextRunnableTask([g.id], tasks)
|
|
358
|
+
|
|
359
|
+
console.log(JSON.stringify({ r1, r2, r3, r4, eId: e.id, fId: f.id, gId: g.id }))
|
|
360
|
+
`)
|
|
361
|
+
assert.equal(output.r1, null) // all blocked before D completes
|
|
362
|
+
assert.ok(output.r2 === output.eId || output.r2 === output.fId) // E or F dequeued
|
|
363
|
+
assert.equal(output.r3, null) // G still blocked (F incomplete)
|
|
364
|
+
assert.equal(output.r4, output.gId) // G unblocked
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
it('11. complete D → E,F unblocked; complete E → G still blocked; complete F → G unblocked', () => {
|
|
368
|
+
const output = runWithTempDataDir(`
|
|
369
|
+
${AGENT_SETUP}
|
|
370
|
+
${QUEUE_IMPORT}
|
|
371
|
+
const rawD = await taskTool.invoke({
|
|
372
|
+
action: 'create',
|
|
373
|
+
data: JSON.stringify({ title: 'Step D', description: 'Root node for step test', status: 'queued' }),
|
|
374
|
+
})
|
|
375
|
+
const d = JSON.parse(rawD)
|
|
376
|
+
const rawE = await taskTool.invoke({
|
|
377
|
+
action: 'create',
|
|
378
|
+
data: JSON.stringify({ title: 'Step E', description: 'Depends on D', status: 'queued', blockedBy: [d.id] }),
|
|
379
|
+
})
|
|
380
|
+
const rawF = await taskTool.invoke({
|
|
381
|
+
action: 'create',
|
|
382
|
+
data: JSON.stringify({ title: 'Step F', description: 'Depends on D too', status: 'queued', blockedBy: [d.id] }),
|
|
383
|
+
})
|
|
384
|
+
const e = JSON.parse(rawE)
|
|
385
|
+
const f = JSON.parse(rawF)
|
|
386
|
+
const rawG = await taskTool.invoke({
|
|
387
|
+
action: 'create',
|
|
388
|
+
data: JSON.stringify({ title: 'Step G', description: 'Depends on E and F', status: 'queued', blockedBy: [e.id, f.id] }),
|
|
389
|
+
})
|
|
390
|
+
const g = JSON.parse(rawG)
|
|
391
|
+
|
|
392
|
+
// Complete D
|
|
393
|
+
await taskTool.invoke({ action: 'update', id: d.id, data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }) })
|
|
394
|
+
let tasks = storage.loadTasks()
|
|
395
|
+
const rE = dequeueNextRunnableTask([e.id], tasks)
|
|
396
|
+
const rF = dequeueNextRunnableTask([f.id], tasks)
|
|
397
|
+
|
|
398
|
+
// Complete E only
|
|
399
|
+
await taskTool.invoke({ action: 'update', id: e.id, data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }) })
|
|
400
|
+
tasks = storage.loadTasks()
|
|
401
|
+
const rGstillBlocked = dequeueNextRunnableTask([g.id], tasks)
|
|
402
|
+
|
|
403
|
+
// Complete F
|
|
404
|
+
await taskTool.invoke({ action: 'update', id: f.id, data: JSON.stringify({ status: 'completed', result: '${VALID_RESULT}' }) })
|
|
405
|
+
tasks = storage.loadTasks()
|
|
406
|
+
const rGunblocked = dequeueNextRunnableTask([g.id], tasks)
|
|
407
|
+
|
|
408
|
+
console.log(JSON.stringify({ rE, rF, rGstillBlocked, rGunblocked, eId: e.id, fId: f.id, gId: g.id }))
|
|
409
|
+
`)
|
|
410
|
+
assert.equal(output.rE, output.eId)
|
|
411
|
+
assert.equal(output.rF, output.fId)
|
|
412
|
+
assert.equal(output.rGstillBlocked, null)
|
|
413
|
+
assert.equal(output.rGunblocked, output.gId)
|
|
414
|
+
})
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
// Task queue dequeue logic
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
describe('manage_tasks: dequeue logic', () => {
|
|
421
|
+
it('12. first task has unmet dep, second ready → second dequeued', () => {
|
|
422
|
+
const output = runWithTempDataDir(`
|
|
423
|
+
${AGENT_SETUP}
|
|
424
|
+
${QUEUE_IMPORT}
|
|
425
|
+
const rawBlocker = await taskTool.invoke({
|
|
426
|
+
action: 'create',
|
|
427
|
+
data: JSON.stringify({ title: 'Blocker task', description: 'Blocks first task', status: 'queued' }),
|
|
428
|
+
})
|
|
429
|
+
const blocker = JSON.parse(rawBlocker)
|
|
430
|
+
const rawBlocked = await taskTool.invoke({
|
|
431
|
+
action: 'create',
|
|
432
|
+
data: JSON.stringify({ title: 'Blocked task', description: 'Has dependency', status: 'queued', blockedBy: [blocker.id] }),
|
|
433
|
+
})
|
|
434
|
+
const blocked = JSON.parse(rawBlocked)
|
|
435
|
+
const rawReady = await taskTool.invoke({
|
|
436
|
+
action: 'create',
|
|
437
|
+
data: JSON.stringify({ title: 'Ready task', description: 'No dependencies here', status: 'queued' }),
|
|
438
|
+
})
|
|
439
|
+
const ready = JSON.parse(rawReady)
|
|
440
|
+
|
|
441
|
+
const tasks = storage.loadTasks()
|
|
442
|
+
const queue = [blocked.id, ready.id]
|
|
443
|
+
const result = dequeueNextRunnableTask(queue, tasks)
|
|
444
|
+
console.log(JSON.stringify({ result, readyId: ready.id }))
|
|
445
|
+
`)
|
|
446
|
+
assert.equal(output.result, output.readyId)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it('13. empty queue → returns null', () => {
|
|
450
|
+
const output = runWithTempDataDir(`
|
|
451
|
+
${AGENT_SETUP}
|
|
452
|
+
${QUEUE_IMPORT}
|
|
453
|
+
const tasks = storage.loadTasks()
|
|
454
|
+
const result = dequeueNextRunnableTask([], tasks)
|
|
455
|
+
console.log(JSON.stringify({ result }))
|
|
456
|
+
`)
|
|
457
|
+
assert.equal(output.result, null)
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
it('14. all tasks blocked → returns null', () => {
|
|
461
|
+
const output = runWithTempDataDir(`
|
|
462
|
+
${AGENT_SETUP}
|
|
463
|
+
${QUEUE_IMPORT}
|
|
464
|
+
const rawA = await taskTool.invoke({
|
|
465
|
+
action: 'create',
|
|
466
|
+
data: JSON.stringify({ title: 'Block source', description: 'Blocker origin', status: 'queued' }),
|
|
467
|
+
})
|
|
468
|
+
const a = JSON.parse(rawA)
|
|
469
|
+
const rawB = await taskTool.invoke({
|
|
470
|
+
action: 'create',
|
|
471
|
+
data: JSON.stringify({ title: 'Blocked one', description: 'Blocked by A', status: 'queued', blockedBy: [a.id] }),
|
|
472
|
+
})
|
|
473
|
+
const rawC = await taskTool.invoke({
|
|
474
|
+
action: 'create',
|
|
475
|
+
data: JSON.stringify({ title: 'Blocked two', description: 'Also blocked by A', status: 'queued', blockedBy: [a.id] }),
|
|
476
|
+
})
|
|
477
|
+
const b = JSON.parse(rawB)
|
|
478
|
+
const c = JSON.parse(rawC)
|
|
479
|
+
|
|
480
|
+
const tasks = storage.loadTasks()
|
|
481
|
+
const queue = [b.id, c.id]
|
|
482
|
+
const result = dequeueNextRunnableTask(queue, tasks)
|
|
483
|
+
console.log(JSON.stringify({ result }))
|
|
484
|
+
`)
|
|
485
|
+
assert.equal(output.result, null)
|
|
486
|
+
})
|
|
487
|
+
|
|
488
|
+
it('15. FIFO: multiple ready tasks → first queued wins', () => {
|
|
489
|
+
const output = runWithTempDataDir(`
|
|
490
|
+
${AGENT_SETUP}
|
|
491
|
+
${QUEUE_IMPORT}
|
|
492
|
+
const rawFirst = await taskTool.invoke({
|
|
493
|
+
action: 'create',
|
|
494
|
+
data: JSON.stringify({ title: 'First queued task', description: 'First in line', status: 'queued' }),
|
|
495
|
+
})
|
|
496
|
+
const rawSecond = await taskTool.invoke({
|
|
497
|
+
action: 'create',
|
|
498
|
+
data: JSON.stringify({ title: 'Second queued task', description: 'Second in line', status: 'queued' }),
|
|
499
|
+
})
|
|
500
|
+
const first = JSON.parse(rawFirst)
|
|
501
|
+
const second = JSON.parse(rawSecond)
|
|
502
|
+
|
|
503
|
+
const tasks = storage.loadTasks()
|
|
504
|
+
const queue = [first.id, second.id]
|
|
505
|
+
const result = dequeueNextRunnableTask(queue, tasks)
|
|
506
|
+
console.log(JSON.stringify({ result, firstId: first.id }))
|
|
507
|
+
`)
|
|
508
|
+
assert.equal(output.result, output.firstId)
|
|
509
|
+
})
|
|
510
|
+
})
|
|
511
|
+
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
// Status normalization
|
|
514
|
+
// ---------------------------------------------------------------------------
|
|
515
|
+
describe('manage_tasks: status normalization', () => {
|
|
516
|
+
it('16. setting status to running from non-running → converts to queued', () => {
|
|
517
|
+
const output = runWithTempDataDir(`
|
|
518
|
+
${AGENT_SETUP}
|
|
519
|
+
const raw = await taskTool.invoke({
|
|
520
|
+
action: 'create',
|
|
521
|
+
data: JSON.stringify({ title: 'Status norm test', description: 'Testing normalization', status: 'running' }),
|
|
522
|
+
})
|
|
523
|
+
const created = JSON.parse(raw)
|
|
524
|
+
const tasks = storage.loadTasks()
|
|
525
|
+
console.log(JSON.stringify({ status: tasks[created.id].status }))
|
|
526
|
+
`)
|
|
527
|
+
assert.equal(output.status, 'queued')
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('17. setting status to running from already running → stays running', () => {
|
|
531
|
+
const output = runWithTempDataDir(`
|
|
532
|
+
${AGENT_SETUP}
|
|
533
|
+
// Seed a task already in running state directly in storage
|
|
534
|
+
const tasks = storage.loadTasks()
|
|
535
|
+
tasks['manual-running'] = {
|
|
536
|
+
id: 'manual-running',
|
|
537
|
+
title: 'Already running',
|
|
538
|
+
description: 'Pre-set to running',
|
|
539
|
+
status: 'running',
|
|
540
|
+
agentId: 'agent1',
|
|
541
|
+
createdAt: now,
|
|
542
|
+
updatedAt: now,
|
|
543
|
+
}
|
|
544
|
+
storage.saveTasks(tasks)
|
|
545
|
+
|
|
546
|
+
// Now update it to running again
|
|
547
|
+
await taskTool.invoke({
|
|
548
|
+
action: 'update',
|
|
549
|
+
id: 'manual-running',
|
|
550
|
+
data: JSON.stringify({ status: 'running' }),
|
|
551
|
+
})
|
|
552
|
+
const final = storage.loadTasks()
|
|
553
|
+
console.log(JSON.stringify({ status: final['manual-running'].status }))
|
|
554
|
+
`)
|
|
555
|
+
assert.equal(output.status, 'running')
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
it('18. invalid status string → status not changed', () => {
|
|
559
|
+
const output = runWithTempDataDir(`
|
|
560
|
+
${AGENT_SETUP}
|
|
561
|
+
const raw = await taskTool.invoke({
|
|
562
|
+
action: 'create',
|
|
563
|
+
data: JSON.stringify({ title: 'Invalid status test', description: 'Testing invalid status' }),
|
|
564
|
+
})
|
|
565
|
+
const created = JSON.parse(raw)
|
|
566
|
+
await taskTool.invoke({
|
|
567
|
+
action: 'update',
|
|
568
|
+
id: created.id,
|
|
569
|
+
data: JSON.stringify({ status: 'bananas' }),
|
|
570
|
+
})
|
|
571
|
+
const tasks = storage.loadTasks()
|
|
572
|
+
console.log(JSON.stringify({ status: tasks[created.id].status }))
|
|
573
|
+
`)
|
|
574
|
+
// Invalid status should be stripped; task keeps its original status
|
|
575
|
+
assert.equal(output.status, 'backlog')
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
// Title derivation
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
describe('manage_tasks: title derivation', () => {
|
|
583
|
+
it('19. explicit title used as-is', () => {
|
|
584
|
+
const output = runWithTempDataDir(`
|
|
585
|
+
${AGENT_SETUP}
|
|
586
|
+
const raw = await taskTool.invoke({
|
|
587
|
+
action: 'create',
|
|
588
|
+
data: JSON.stringify({ title: 'My Custom Title', description: 'Some description' }),
|
|
589
|
+
})
|
|
590
|
+
const created = JSON.parse(raw)
|
|
591
|
+
console.log(JSON.stringify({ title: created.title }))
|
|
592
|
+
`)
|
|
593
|
+
assert.equal(output.title, 'My Custom Title')
|
|
594
|
+
})
|
|
595
|
+
|
|
596
|
+
it('20. no title, has description → first sentence extracted', () => {
|
|
597
|
+
const output = runWithTempDataDir(`
|
|
598
|
+
${AGENT_SETUP}
|
|
599
|
+
const raw = await taskTool.invoke({
|
|
600
|
+
action: 'create',
|
|
601
|
+
data: JSON.stringify({ description: 'Analyze the quarterly results. Then produce a summary report.' }),
|
|
602
|
+
})
|
|
603
|
+
const created = JSON.parse(raw)
|
|
604
|
+
console.log(JSON.stringify({ title: created.title }))
|
|
605
|
+
`)
|
|
606
|
+
assert.equal(output.title, 'Analyze the quarterly results')
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
it('21. "Untitled task" title treated as empty → derives from description', () => {
|
|
610
|
+
const output = runWithTempDataDir(`
|
|
611
|
+
${AGENT_SETUP}
|
|
612
|
+
const raw = await taskTool.invoke({
|
|
613
|
+
action: 'create',
|
|
614
|
+
data: JSON.stringify({ title: 'Untitled task', description: 'Deploy the staging environment. Verify it works.' }),
|
|
615
|
+
})
|
|
616
|
+
const created = JSON.parse(raw)
|
|
617
|
+
console.log(JSON.stringify({ title: created.title }))
|
|
618
|
+
`)
|
|
619
|
+
assert.equal(output.title, 'Deploy the staging environment')
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('22. strips "please" and action verbs from derived titles', () => {
|
|
623
|
+
const output = runWithTempDataDir(`
|
|
624
|
+
${AGENT_SETUP}
|
|
625
|
+
const raw = await taskTool.invoke({
|
|
626
|
+
action: 'create',
|
|
627
|
+
data: JSON.stringify({ description: 'Please create a new login page for the app.' }),
|
|
628
|
+
})
|
|
629
|
+
const created = JSON.parse(raw)
|
|
630
|
+
console.log(JSON.stringify({ title: created.title }))
|
|
631
|
+
`)
|
|
632
|
+
// "Please create a new login page for the app." → stripped "please" and "create"
|
|
633
|
+
// Trailing period retained because split(/[.!?]\s+/) only splits on punctuation followed by space
|
|
634
|
+
assert.equal(output.title, 'a new login page for the app.')
|
|
635
|
+
})
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// Project integration
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
describe('manage_tasks: project integration', () => {
|
|
642
|
+
it('23. creates a project with objective', () => {
|
|
643
|
+
const output = runWithTempDataDir(`
|
|
644
|
+
${AGENT_SETUP}
|
|
645
|
+
const raw = await projectTool.invoke({
|
|
646
|
+
action: 'create',
|
|
647
|
+
data: JSON.stringify({
|
|
648
|
+
name: 'Test Project',
|
|
649
|
+
description: 'A test project',
|
|
650
|
+
objective: 'Ship the MVP',
|
|
651
|
+
}),
|
|
652
|
+
})
|
|
653
|
+
const projects = storage.loadProjects()
|
|
654
|
+
const project = Object.values(projects)[0]
|
|
655
|
+
console.log(JSON.stringify({ project }))
|
|
656
|
+
`)
|
|
657
|
+
assert.equal(output.project.name, 'Test Project')
|
|
658
|
+
assert.equal(output.project.objective, 'Ship the MVP')
|
|
659
|
+
assert.ok(output.project.id)
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('24. creates tasks assigned to a project via projectId', () => {
|
|
663
|
+
const output = runWithTempDataDir(`
|
|
664
|
+
${AGENT_SETUP}
|
|
665
|
+
const projRaw = await projectTool.invoke({
|
|
666
|
+
action: 'create',
|
|
667
|
+
data: JSON.stringify({ name: 'Proj Alpha', description: 'Test project' }),
|
|
668
|
+
})
|
|
669
|
+
const proj = JSON.parse(projRaw)
|
|
670
|
+
const raw = await taskTool.invoke({
|
|
671
|
+
action: 'create',
|
|
672
|
+
data: JSON.stringify({ title: 'Project task', description: 'Belongs to project', projectId: proj.id }),
|
|
673
|
+
})
|
|
674
|
+
const created = JSON.parse(raw)
|
|
675
|
+
console.log(JSON.stringify({ projectId: created.projectId, projId: proj.id }))
|
|
676
|
+
`)
|
|
677
|
+
assert.equal(output.projectId, output.projId)
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
it('25. tasks can be filtered by projectId', () => {
|
|
681
|
+
const output = runWithTempDataDir(`
|
|
682
|
+
${AGENT_SETUP}
|
|
683
|
+
const projRaw = await projectTool.invoke({
|
|
684
|
+
action: 'create',
|
|
685
|
+
data: JSON.stringify({ name: 'Filter Project', description: 'For filtering' }),
|
|
686
|
+
})
|
|
687
|
+
const proj = JSON.parse(projRaw)
|
|
688
|
+
await taskTool.invoke({
|
|
689
|
+
action: 'create',
|
|
690
|
+
data: JSON.stringify({ title: 'In project', description: 'Has projectId set', projectId: proj.id }),
|
|
691
|
+
})
|
|
692
|
+
await taskTool.invoke({
|
|
693
|
+
action: 'create',
|
|
694
|
+
data: JSON.stringify({ title: 'No project', description: 'No projectId set here' }),
|
|
695
|
+
})
|
|
696
|
+
const tasks = storage.loadTasks()
|
|
697
|
+
const projectTasks = Object.values(tasks).filter((t) => t.projectId === proj.id)
|
|
698
|
+
const otherTasks = Object.values(tasks).filter((t) => t.projectId !== proj.id)
|
|
699
|
+
console.log(JSON.stringify({ projectCount: projectTasks.length, otherCount: otherTasks.length }))
|
|
700
|
+
`)
|
|
701
|
+
assert.equal(output.projectCount, 1)
|
|
702
|
+
assert.equal(output.otherCount, 1)
|
|
703
|
+
})
|
|
704
|
+
})
|
|
705
|
+
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
707
|
+
// Task with retry scheduling
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
describe('manage_tasks: retry scheduling', () => {
|
|
710
|
+
it('26. task with retryScheduledAt in future → not dequeued', () => {
|
|
711
|
+
const output = runWithTempDataDir(`
|
|
712
|
+
${AGENT_SETUP}
|
|
713
|
+
${QUEUE_IMPORT}
|
|
714
|
+
const tasks = storage.loadTasks()
|
|
715
|
+
tasks['retry-future'] = {
|
|
716
|
+
id: 'retry-future',
|
|
717
|
+
title: 'Future retry',
|
|
718
|
+
description: 'Retry in the future',
|
|
719
|
+
status: 'queued',
|
|
720
|
+
agentId: 'agent1',
|
|
721
|
+
retryScheduledAt: Date.now() + 60_000,
|
|
722
|
+
createdAt: now,
|
|
723
|
+
updatedAt: now,
|
|
724
|
+
}
|
|
725
|
+
storage.saveTasks(tasks)
|
|
726
|
+
|
|
727
|
+
const queue = ['retry-future']
|
|
728
|
+
const result = dequeueNextRunnableTask(queue, tasks)
|
|
729
|
+
console.log(JSON.stringify({ result }))
|
|
730
|
+
`)
|
|
731
|
+
assert.equal(output.result, null)
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
it('27. task with retryScheduledAt in past → eligible for dequeue', () => {
|
|
735
|
+
const output = runWithTempDataDir(`
|
|
736
|
+
${AGENT_SETUP}
|
|
737
|
+
${QUEUE_IMPORT}
|
|
738
|
+
const tasks = storage.loadTasks()
|
|
739
|
+
tasks['retry-past'] = {
|
|
740
|
+
id: 'retry-past',
|
|
741
|
+
title: 'Past retry',
|
|
742
|
+
description: 'Retry in the past',
|
|
743
|
+
status: 'queued',
|
|
744
|
+
agentId: 'agent1',
|
|
745
|
+
retryScheduledAt: Date.now() - 60_000,
|
|
746
|
+
createdAt: now,
|
|
747
|
+
updatedAt: now,
|
|
748
|
+
}
|
|
749
|
+
storage.saveTasks(tasks)
|
|
750
|
+
|
|
751
|
+
const queue = ['retry-past']
|
|
752
|
+
const result = dequeueNextRunnableTask(queue, tasks)
|
|
753
|
+
console.log(JSON.stringify({ result }))
|
|
754
|
+
`)
|
|
755
|
+
assert.equal(output.result, 'retry-past')
|
|
756
|
+
})
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
// ---------------------------------------------------------------------------
|
|
760
|
+
// Task fingerprint dedup
|
|
761
|
+
// ---------------------------------------------------------------------------
|
|
762
|
+
describe('manage_tasks: fingerprint dedup', () => {
|
|
763
|
+
it('28. two tasks with same agentId + normalized title → duplicate detected', () => {
|
|
764
|
+
const output = runWithTempDataDir(`
|
|
765
|
+
${AGENT_SETUP}
|
|
766
|
+
const raw1 = await taskTool.invoke({
|
|
767
|
+
action: 'create',
|
|
768
|
+
data: JSON.stringify({ title: 'Unique dedup title', description: 'First task' }),
|
|
769
|
+
})
|
|
770
|
+
const raw2 = await taskTool.invoke({
|
|
771
|
+
action: 'create',
|
|
772
|
+
data: JSON.stringify({ title: 'Unique dedup title', description: 'Duplicate' }),
|
|
773
|
+
})
|
|
774
|
+
const first = JSON.parse(raw1)
|
|
775
|
+
const second = JSON.parse(raw2)
|
|
776
|
+
console.log(JSON.stringify({ firstId: first.id, secondDeduplicated: second.deduplicated, secondId: second.id }))
|
|
777
|
+
`)
|
|
778
|
+
assert.equal(output.secondDeduplicated, true)
|
|
779
|
+
// Dedup returns the original task, so IDs should match
|
|
780
|
+
assert.equal(output.secondId, output.firstId)
|
|
781
|
+
})
|
|
782
|
+
|
|
783
|
+
it('29. different agents → not duplicates', () => {
|
|
784
|
+
const output = runWithTempDataDir(`
|
|
785
|
+
const storageMod = await import('./src/lib/server/storage.ts')
|
|
786
|
+
const crudMod = await import('./src/lib/server/session-tools/crud.ts')
|
|
787
|
+
const storage = storageMod.default || storageMod
|
|
788
|
+
const crud = crudMod.default || crudMod
|
|
789
|
+
|
|
790
|
+
const now = Date.now()
|
|
791
|
+
storage.saveAgents({
|
|
792
|
+
agent1: { id: 'agent1', name: 'Alpha', description: '', systemPrompt: '', provider: 'openai', model: 'gpt-test', createdAt: now, updatedAt: now },
|
|
793
|
+
agent2: { id: 'agent2', name: 'Beta', description: '', systemPrompt: '', provider: 'openai', model: 'gpt-test', createdAt: now, updatedAt: now },
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
const cwd = process.env.WORKSPACE_DIR
|
|
797
|
+
const tools1 = crud.buildCrudTools({
|
|
798
|
+
cwd,
|
|
799
|
+
ctx: { sessionId: 's1', agentId: 'agent1', platformAssignScope: 'self' },
|
|
800
|
+
hasPlugin: (name) => name === 'manage_tasks',
|
|
801
|
+
})
|
|
802
|
+
const tool1 = tools1.find((e) => e.name === 'manage_tasks')
|
|
803
|
+
|
|
804
|
+
const tools2 = crud.buildCrudTools({
|
|
805
|
+
cwd,
|
|
806
|
+
ctx: { sessionId: 's2', agentId: 'agent2', platformAssignScope: 'self' },
|
|
807
|
+
hasPlugin: (name) => name === 'manage_tasks',
|
|
808
|
+
})
|
|
809
|
+
const tool2 = tools2.find((e) => e.name === 'manage_tasks')
|
|
810
|
+
|
|
811
|
+
const raw1 = await tool1.invoke({
|
|
812
|
+
action: 'create',
|
|
813
|
+
data: JSON.stringify({ title: 'Same title diff agent', description: 'Agent1 version' }),
|
|
814
|
+
})
|
|
815
|
+
const raw2 = await tool2.invoke({
|
|
816
|
+
action: 'create',
|
|
817
|
+
data: JSON.stringify({ title: 'Same title diff agent', description: 'Agent2 version' }),
|
|
818
|
+
})
|
|
819
|
+
const first = JSON.parse(raw1)
|
|
820
|
+
const second = JSON.parse(raw2)
|
|
821
|
+
console.log(JSON.stringify({ firstId: first.id, secondId: second.id, secondDeduplicated: second.deduplicated || false }))
|
|
822
|
+
`)
|
|
823
|
+
assert.notEqual(output.firstId, output.secondId)
|
|
824
|
+
assert.equal(output.secondDeduplicated, false)
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
it('30. different titles → not duplicates', () => {
|
|
828
|
+
const output = runWithTempDataDir(`
|
|
829
|
+
${AGENT_SETUP}
|
|
830
|
+
const raw1 = await taskTool.invoke({
|
|
831
|
+
action: 'create',
|
|
832
|
+
data: JSON.stringify({ title: 'Title alpha', description: 'First task' }),
|
|
833
|
+
})
|
|
834
|
+
const raw2 = await taskTool.invoke({
|
|
835
|
+
action: 'create',
|
|
836
|
+
data: JSON.stringify({ title: 'Title beta', description: 'Second task' }),
|
|
837
|
+
})
|
|
838
|
+
const first = JSON.parse(raw1)
|
|
839
|
+
const second = JSON.parse(raw2)
|
|
840
|
+
const tasks = storage.loadTasks()
|
|
841
|
+
console.log(JSON.stringify({
|
|
842
|
+
count: Object.keys(tasks).length,
|
|
843
|
+
firstId: first.id,
|
|
844
|
+
secondId: second.id,
|
|
845
|
+
secondDeduplicated: second.deduplicated || false,
|
|
846
|
+
}))
|
|
847
|
+
`)
|
|
848
|
+
assert.equal(output.count, 2)
|
|
849
|
+
assert.notEqual(output.firstId, output.secondId)
|
|
850
|
+
assert.equal(output.secondDeduplicated, false)
|
|
851
|
+
})
|
|
852
|
+
})
|