@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,513 @@
|
|
|
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 { after, before, describe, it } from 'node:test'
|
|
6
|
+
|
|
7
|
+
const originalEnv = {
|
|
8
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
9
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
10
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let tempDir = ''
|
|
14
|
+
let delegationJobs: typeof import('./delegation-jobs')
|
|
15
|
+
|
|
16
|
+
before(async () => {
|
|
17
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-delegation-adv-'))
|
|
18
|
+
process.env.DATA_DIR = path.join(tempDir, 'data')
|
|
19
|
+
process.env.WORKSPACE_DIR = path.join(tempDir, 'workspace')
|
|
20
|
+
process.env.SWARMCLAW_BUILD_MODE = '1'
|
|
21
|
+
delegationJobs = await import('./delegation-jobs')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
after(() => {
|
|
25
|
+
if (originalEnv.DATA_DIR === undefined) delete process.env.DATA_DIR
|
|
26
|
+
else process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
27
|
+
if (originalEnv.WORKSPACE_DIR === undefined) delete process.env.WORKSPACE_DIR
|
|
28
|
+
else process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
29
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
|
|
30
|
+
else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
31
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('delegation-jobs-advanced', () => {
|
|
35
|
+
it('multi-agent delegation chain — parent→child→grandchild with ordered completions', () => {
|
|
36
|
+
const parent = delegationJobs.createDelegationJob({
|
|
37
|
+
kind: 'delegate',
|
|
38
|
+
task: 'Orchestrate full pipeline',
|
|
39
|
+
backend: 'claude',
|
|
40
|
+
parentSessionId: 'chain-root',
|
|
41
|
+
agentId: 'agent-orchestrator',
|
|
42
|
+
agentName: 'Orchestrator',
|
|
43
|
+
cwd: '/workspace',
|
|
44
|
+
})
|
|
45
|
+
const child = delegationJobs.createDelegationJob({
|
|
46
|
+
kind: 'subagent',
|
|
47
|
+
task: 'Implement feature X',
|
|
48
|
+
backend: 'codex',
|
|
49
|
+
parentSessionId: parent.id,
|
|
50
|
+
agentId: 'agent-developer',
|
|
51
|
+
agentName: 'Developer',
|
|
52
|
+
cwd: '/workspace/src',
|
|
53
|
+
})
|
|
54
|
+
const grandchild = delegationJobs.createDelegationJob({
|
|
55
|
+
kind: 'subagent',
|
|
56
|
+
task: 'Write tests for feature X',
|
|
57
|
+
parentSessionId: child.id,
|
|
58
|
+
agentId: 'agent-tester',
|
|
59
|
+
agentName: 'Tester',
|
|
60
|
+
cwd: '/workspace/tests',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// Verify initial state
|
|
64
|
+
assert.equal(parent.status, 'queued')
|
|
65
|
+
assert.equal(child.status, 'queued')
|
|
66
|
+
assert.equal(grandchild.status, 'queued')
|
|
67
|
+
assert.ok(parent.createdAt > 0)
|
|
68
|
+
assert.ok(child.createdAt >= parent.createdAt)
|
|
69
|
+
assert.ok(grandchild.createdAt >= child.createdAt)
|
|
70
|
+
|
|
71
|
+
// Start all three
|
|
72
|
+
const parentStarted = delegationJobs.startDelegationJob(parent.id)
|
|
73
|
+
const childStarted = delegationJobs.startDelegationJob(child.id)
|
|
74
|
+
const grandchildStarted = delegationJobs.startDelegationJob(grandchild.id)
|
|
75
|
+
|
|
76
|
+
assert.equal(parentStarted?.status, 'running')
|
|
77
|
+
assert.equal(childStarted?.status, 'running')
|
|
78
|
+
assert.equal(grandchildStarted?.status, 'running')
|
|
79
|
+
assert.ok(parentStarted?.startedAt)
|
|
80
|
+
assert.ok(childStarted?.startedAt)
|
|
81
|
+
assert.ok(grandchildStarted?.startedAt)
|
|
82
|
+
|
|
83
|
+
// Complete grandchild first, then child, then parent
|
|
84
|
+
const grandchildDone = delegationJobs.completeDelegationJob(grandchild.id, 'Tests passing: 42/42')
|
|
85
|
+
assert.equal(grandchildDone?.status, 'completed')
|
|
86
|
+
assert.equal(grandchildDone?.result, 'Tests passing: 42/42')
|
|
87
|
+
assert.ok(grandchildDone?.completedAt)
|
|
88
|
+
|
|
89
|
+
const childDone = delegationJobs.completeDelegationJob(child.id, 'Feature X implemented')
|
|
90
|
+
assert.equal(childDone?.status, 'completed')
|
|
91
|
+
assert.ok(childDone?.completedAt)
|
|
92
|
+
assert.ok(childDone.completedAt! >= grandchildDone!.completedAt!)
|
|
93
|
+
|
|
94
|
+
const parentDone = delegationJobs.completeDelegationJob(parent.id, 'Pipeline complete')
|
|
95
|
+
assert.equal(parentDone?.status, 'completed')
|
|
96
|
+
assert.ok(parentDone?.completedAt)
|
|
97
|
+
assert.ok(parentDone.completedAt! >= childDone!.completedAt!)
|
|
98
|
+
|
|
99
|
+
// Verify all three are retrievable
|
|
100
|
+
assert.equal(delegationJobs.getDelegationJob(parent.id)?.status, 'completed')
|
|
101
|
+
assert.equal(delegationJobs.getDelegationJob(child.id)?.status, 'completed')
|
|
102
|
+
assert.equal(delegationJobs.getDelegationJob(grandchild.id)?.status, 'completed')
|
|
103
|
+
|
|
104
|
+
// Verify timestamps are monotonically increasing
|
|
105
|
+
assert.ok(parentDone!.updatedAt >= childDone!.updatedAt)
|
|
106
|
+
assert.ok(childDone!.updatedAt >= grandchildDone!.updatedAt)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('concurrent delegation fan-out — 5 subagent jobs with mixed outcomes', () => {
|
|
110
|
+
const parentId = 'fanout-parent'
|
|
111
|
+
const jobs = Array.from({ length: 5 }, (_, i) =>
|
|
112
|
+
delegationJobs.createDelegationJob({
|
|
113
|
+
kind: 'subagent',
|
|
114
|
+
task: `Subtask ${i}`,
|
|
115
|
+
parentSessionId: parentId,
|
|
116
|
+
agentId: `agent-worker-${i}`,
|
|
117
|
+
agentName: `Worker ${i}`,
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
// Start all
|
|
122
|
+
for (const job of jobs) {
|
|
123
|
+
delegationJobs.startDelegationJob(job.id)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Complete jobs 0 and 1
|
|
127
|
+
delegationJobs.completeDelegationJob(jobs[0].id, 'Result 0')
|
|
128
|
+
delegationJobs.completeDelegationJob(jobs[1].id, 'Result 1')
|
|
129
|
+
|
|
130
|
+
// Fail job 2
|
|
131
|
+
delegationJobs.failDelegationJob(jobs[2].id, 'Out of memory')
|
|
132
|
+
|
|
133
|
+
// Cancel job 3
|
|
134
|
+
delegationJobs.cancelDelegationJob(jobs[3].id)
|
|
135
|
+
|
|
136
|
+
// Leave job 4 running
|
|
137
|
+
|
|
138
|
+
// Verify individual statuses
|
|
139
|
+
assert.equal(delegationJobs.getDelegationJob(jobs[0].id)?.status, 'completed')
|
|
140
|
+
assert.equal(delegationJobs.getDelegationJob(jobs[1].id)?.status, 'completed')
|
|
141
|
+
assert.equal(delegationJobs.getDelegationJob(jobs[2].id)?.status, 'failed')
|
|
142
|
+
assert.equal(delegationJobs.getDelegationJob(jobs[3].id)?.status, 'cancelled')
|
|
143
|
+
assert.equal(delegationJobs.getDelegationJob(jobs[4].id)?.status, 'running')
|
|
144
|
+
|
|
145
|
+
// Verify filter by status
|
|
146
|
+
const completedJobs = delegationJobs.listDelegationJobs({ parentSessionId: parentId, status: 'completed' })
|
|
147
|
+
assert.equal(completedJobs.length, 2)
|
|
148
|
+
|
|
149
|
+
const failedJobs = delegationJobs.listDelegationJobs({ parentSessionId: parentId, status: 'failed' })
|
|
150
|
+
assert.equal(failedJobs.length, 1)
|
|
151
|
+
assert.equal(failedJobs[0].error, 'Out of memory')
|
|
152
|
+
|
|
153
|
+
const cancelledJobs = delegationJobs.listDelegationJobs({ parentSessionId: parentId, status: 'cancelled' })
|
|
154
|
+
assert.equal(cancelledJobs.length, 1)
|
|
155
|
+
|
|
156
|
+
const runningJobs = delegationJobs.listDelegationJobs({ parentSessionId: parentId, status: 'running' })
|
|
157
|
+
assert.equal(runningJobs.length, 1)
|
|
158
|
+
assert.equal(runningJobs[0].id, jobs[4].id)
|
|
159
|
+
|
|
160
|
+
// Verify filter by parentSessionId only returns all 5
|
|
161
|
+
const allForParent = delegationJobs.listDelegationJobs({ parentSessionId: parentId })
|
|
162
|
+
assert.equal(allForParent.length, 5)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('checkpoint accumulation caps at 24 most recent entries', () => {
|
|
166
|
+
const job = delegationJobs.createDelegationJob({
|
|
167
|
+
kind: 'delegate',
|
|
168
|
+
task: 'Long running checkpoint test',
|
|
169
|
+
parentSessionId: 'checkpoint-parent',
|
|
170
|
+
backend: 'claude',
|
|
171
|
+
})
|
|
172
|
+
delegationJobs.startDelegationJob(job.id)
|
|
173
|
+
|
|
174
|
+
// Append 30 checkpoints (job already has 1 from creation = 31 total before capping)
|
|
175
|
+
for (let i = 0; i < 30; i++) {
|
|
176
|
+
delegationJobs.appendDelegationCheckpoint(job.id, `Checkpoint ${i}`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const final = delegationJobs.getDelegationJob(job.id)
|
|
180
|
+
assert.ok(final)
|
|
181
|
+
assert.ok(final.checkpoints)
|
|
182
|
+
assert.equal(final.checkpoints.length, 24)
|
|
183
|
+
|
|
184
|
+
// The most recent checkpoint should be the last one we appended
|
|
185
|
+
const lastCheckpoint = final.checkpoints[final.checkpoints.length - 1]
|
|
186
|
+
assert.equal(lastCheckpoint.note, 'Checkpoint 29')
|
|
187
|
+
|
|
188
|
+
// The first checkpoint should NOT be the original "Job queued" since it was pushed off
|
|
189
|
+
// With 31 total entries capped to 24, the first 7 are dropped
|
|
190
|
+
// Entry 0: "Job queued", entries 1-30: "Checkpoint 0" through "Checkpoint 29"
|
|
191
|
+
// Kept: entries 7-30, i.e. "Checkpoint 6" through "Checkpoint 29"
|
|
192
|
+
const firstCheckpoint = final.checkpoints[0]
|
|
193
|
+
assert.equal(firstCheckpoint.note, 'Checkpoint 6')
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('terminal status immutability — completed job resists state changes', () => {
|
|
197
|
+
const job = delegationJobs.createDelegationJob({
|
|
198
|
+
kind: 'delegate',
|
|
199
|
+
task: 'Immutability test',
|
|
200
|
+
parentSessionId: 'immutable-parent',
|
|
201
|
+
})
|
|
202
|
+
delegationJobs.startDelegationJob(job.id)
|
|
203
|
+
const completed = delegationJobs.completeDelegationJob(job.id, 'Final result')
|
|
204
|
+
assert.equal(completed?.status, 'completed')
|
|
205
|
+
|
|
206
|
+
// Try to start a completed job
|
|
207
|
+
const afterStart = delegationJobs.startDelegationJob(job.id)
|
|
208
|
+
assert.equal(afterStart?.status, 'completed')
|
|
209
|
+
|
|
210
|
+
// Try to fail a completed job
|
|
211
|
+
const afterFail = delegationJobs.failDelegationJob(job.id, 'Should not work')
|
|
212
|
+
assert.equal(afterFail?.status, 'completed')
|
|
213
|
+
assert.equal(afterFail?.error, null) // error should remain null from completion
|
|
214
|
+
|
|
215
|
+
// Try to cancel a completed job
|
|
216
|
+
const afterCancel = delegationJobs.cancelDelegationJob(job.id)
|
|
217
|
+
assert.equal(afterCancel?.status, 'completed')
|
|
218
|
+
|
|
219
|
+
// Try to append checkpoint with a different status
|
|
220
|
+
const afterCheckpoint = delegationJobs.appendDelegationCheckpoint(job.id, 'Sneaky', 'failed')
|
|
221
|
+
assert.equal(afterCheckpoint?.status, 'completed')
|
|
222
|
+
|
|
223
|
+
// Verify the result is still intact
|
|
224
|
+
const latest = delegationJobs.getDelegationJob(job.id)
|
|
225
|
+
assert.equal(latest?.status, 'completed')
|
|
226
|
+
assert.equal(latest?.result, 'Final result')
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('artifact accumulation with 24-cap across multiple batches', () => {
|
|
230
|
+
const job = delegationJobs.createDelegationJob({
|
|
231
|
+
kind: 'subagent',
|
|
232
|
+
task: 'Artifact accumulation test',
|
|
233
|
+
parentSessionId: 'artifact-parent',
|
|
234
|
+
})
|
|
235
|
+
delegationJobs.startDelegationJob(job.id)
|
|
236
|
+
|
|
237
|
+
// Batch 1: 10 artifacts
|
|
238
|
+
const batch1 = Array.from({ length: 10 }, (_, i) => ({
|
|
239
|
+
type: 'file' as const,
|
|
240
|
+
value: `/output/file-${i}.ts`,
|
|
241
|
+
label: `File ${i}`,
|
|
242
|
+
}))
|
|
243
|
+
delegationJobs.appendDelegationArtifacts(job.id, batch1)
|
|
244
|
+
|
|
245
|
+
const afterBatch1 = delegationJobs.getDelegationJob(job.id)
|
|
246
|
+
assert.ok(afterBatch1?.artifacts)
|
|
247
|
+
assert.equal(afterBatch1.artifacts.length, 10)
|
|
248
|
+
|
|
249
|
+
// Batch 2: 10 more artifacts (total 20, still under cap)
|
|
250
|
+
const batch2 = Array.from({ length: 10 }, (_, i) => ({
|
|
251
|
+
type: 'text' as const,
|
|
252
|
+
value: `Log output ${i}`,
|
|
253
|
+
label: `Log ${i}`,
|
|
254
|
+
}))
|
|
255
|
+
delegationJobs.appendDelegationArtifacts(job.id, batch2)
|
|
256
|
+
|
|
257
|
+
const afterBatch2 = delegationJobs.getDelegationJob(job.id)
|
|
258
|
+
assert.ok(afterBatch2?.artifacts)
|
|
259
|
+
assert.equal(afterBatch2.artifacts.length, 20)
|
|
260
|
+
|
|
261
|
+
// Batch 3: 10 more artifacts (total 30, should cap at 24)
|
|
262
|
+
const batch3 = Array.from({ length: 10 }, (_, i) => ({
|
|
263
|
+
type: 'image' as const,
|
|
264
|
+
value: `/screenshots/screenshot-${i}.png`,
|
|
265
|
+
label: `Screenshot ${i}`,
|
|
266
|
+
}))
|
|
267
|
+
delegationJobs.appendDelegationArtifacts(job.id, batch3)
|
|
268
|
+
|
|
269
|
+
const afterBatch3 = delegationJobs.getDelegationJob(job.id)
|
|
270
|
+
assert.ok(afterBatch3?.artifacts)
|
|
271
|
+
assert.equal(afterBatch3.artifacts.length, 24)
|
|
272
|
+
|
|
273
|
+
// Verify the 24 kept are the most recent (last 24 of 30)
|
|
274
|
+
// Dropped: first 6 from batch1 (file-0 through file-5)
|
|
275
|
+
// Kept: file-6..file-9 (4) + all batch2 (10) + all batch3 (10) = 24
|
|
276
|
+
const first = afterBatch3.artifacts[0]
|
|
277
|
+
assert.equal(first.type, 'file')
|
|
278
|
+
assert.equal(first.value, '/output/file-6.ts')
|
|
279
|
+
|
|
280
|
+
const last = afterBatch3.artifacts[23]
|
|
281
|
+
assert.equal(last.type, 'image')
|
|
282
|
+
assert.equal(last.value, '/screenshots/screenshot-9.png')
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('stale job recovery skips jobs with registered runtime handles', () => {
|
|
286
|
+
const staleSessions = ['stale-a', 'stale-b', 'stale-c']
|
|
287
|
+
const staleJobs = staleSessions.map((sid) => {
|
|
288
|
+
const job = delegationJobs.createDelegationJob({
|
|
289
|
+
kind: 'delegate',
|
|
290
|
+
task: `Stale task for ${sid}`,
|
|
291
|
+
parentSessionId: sid,
|
|
292
|
+
backend: 'claude',
|
|
293
|
+
})
|
|
294
|
+
delegationJobs.startDelegationJob(job.id)
|
|
295
|
+
return job
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
// Register a runtime handle only for the first job
|
|
299
|
+
let handleCancelCalled = false
|
|
300
|
+
delegationJobs.registerDelegationRuntime(staleJobs[0].id, {
|
|
301
|
+
cancel: () => { handleCancelCalled = true },
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
// Use maxAgeMs=-1 to make ALL jobs appear stale (threshold = now+1)
|
|
305
|
+
// Only jobs without runtime handles should be recovered.
|
|
306
|
+
// Note: other running jobs from previous tests may also be recovered,
|
|
307
|
+
// so we check >= 2 rather than exactly 2.
|
|
308
|
+
const recovered = delegationJobs.recoverStaleDelegationJobs(-1)
|
|
309
|
+
|
|
310
|
+
// At least the 2 stale jobs without handles should be failed
|
|
311
|
+
assert.ok(recovered >= 2, `Expected at least 2 recovered, got ${recovered}`)
|
|
312
|
+
|
|
313
|
+
assert.equal(delegationJobs.getDelegationJob(staleJobs[0].id)?.status, 'running')
|
|
314
|
+
assert.equal(delegationJobs.getDelegationJob(staleJobs[1].id)?.status, 'failed')
|
|
315
|
+
assert.equal(delegationJobs.getDelegationJob(staleJobs[2].id)?.status, 'failed')
|
|
316
|
+
|
|
317
|
+
// The handle's cancel should NOT have been called
|
|
318
|
+
assert.equal(handleCancelCalled, false)
|
|
319
|
+
|
|
320
|
+
// Verify error message on recovered jobs
|
|
321
|
+
assert.match(
|
|
322
|
+
delegationJobs.getDelegationJob(staleJobs[1].id)?.error ?? '',
|
|
323
|
+
/interrupted/i,
|
|
324
|
+
)
|
|
325
|
+
assert.match(
|
|
326
|
+
delegationJobs.getDelegationJob(staleJobs[2].id)?.error ?? '',
|
|
327
|
+
/interrupted/i,
|
|
328
|
+
)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('parent session cancellation cascade — preserves completed jobs', () => {
|
|
332
|
+
const parentId = 'cascade-parent'
|
|
333
|
+
|
|
334
|
+
// Create 4 jobs under the same parent
|
|
335
|
+
const runningA = delegationJobs.createDelegationJob({
|
|
336
|
+
kind: 'delegate',
|
|
337
|
+
task: 'Running task A',
|
|
338
|
+
parentSessionId: parentId,
|
|
339
|
+
backend: 'codex',
|
|
340
|
+
})
|
|
341
|
+
const runningB = delegationJobs.createDelegationJob({
|
|
342
|
+
kind: 'subagent',
|
|
343
|
+
task: 'Running task B',
|
|
344
|
+
parentSessionId: parentId,
|
|
345
|
+
agentId: 'agent-b',
|
|
346
|
+
})
|
|
347
|
+
const queued = delegationJobs.createDelegationJob({
|
|
348
|
+
kind: 'subagent',
|
|
349
|
+
task: 'Queued task',
|
|
350
|
+
parentSessionId: parentId,
|
|
351
|
+
agentId: 'agent-q',
|
|
352
|
+
})
|
|
353
|
+
const alreadyCompleted = delegationJobs.createDelegationJob({
|
|
354
|
+
kind: 'delegate',
|
|
355
|
+
task: 'Already completed task',
|
|
356
|
+
parentSessionId: parentId,
|
|
357
|
+
backend: 'claude',
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
// Set up states: 2 running, 1 queued, 1 completed
|
|
361
|
+
delegationJobs.startDelegationJob(runningA.id)
|
|
362
|
+
delegationJobs.startDelegationJob(runningB.id)
|
|
363
|
+
// queued stays queued
|
|
364
|
+
delegationJobs.startDelegationJob(alreadyCompleted.id)
|
|
365
|
+
delegationJobs.completeDelegationJob(alreadyCompleted.id, 'Previously completed')
|
|
366
|
+
|
|
367
|
+
// Verify pre-conditions
|
|
368
|
+
assert.equal(delegationJobs.getDelegationJob(runningA.id)?.status, 'running')
|
|
369
|
+
assert.equal(delegationJobs.getDelegationJob(runningB.id)?.status, 'running')
|
|
370
|
+
assert.equal(delegationJobs.getDelegationJob(queued.id)?.status, 'queued')
|
|
371
|
+
assert.equal(delegationJobs.getDelegationJob(alreadyCompleted.id)?.status, 'completed')
|
|
372
|
+
|
|
373
|
+
// Cancel all for parent session
|
|
374
|
+
const cancelledCount = delegationJobs.cancelDelegationJobsForParentSession(parentId, 'User aborted')
|
|
375
|
+
|
|
376
|
+
// Should cancel the 2 running + 1 queued = 3
|
|
377
|
+
assert.equal(cancelledCount, 3)
|
|
378
|
+
|
|
379
|
+
assert.equal(delegationJobs.getDelegationJob(runningA.id)?.status, 'cancelled')
|
|
380
|
+
assert.equal(delegationJobs.getDelegationJob(runningB.id)?.status, 'cancelled')
|
|
381
|
+
assert.equal(delegationJobs.getDelegationJob(queued.id)?.status, 'cancelled')
|
|
382
|
+
|
|
383
|
+
// Completed job must remain completed
|
|
384
|
+
assert.equal(delegationJobs.getDelegationJob(alreadyCompleted.id)?.status, 'completed')
|
|
385
|
+
assert.equal(delegationJobs.getDelegationJob(alreadyCompleted.id)?.result, 'Previously completed')
|
|
386
|
+
|
|
387
|
+
// Verify the cancellation note appears in checkpoints
|
|
388
|
+
const runningAFinal = delegationJobs.getDelegationJob(runningA.id)
|
|
389
|
+
assert.ok(
|
|
390
|
+
runningAFinal?.checkpoints?.some((cp) => cp.note === 'User aborted'),
|
|
391
|
+
'Expected cancellation note in checkpoints',
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('result preview truncation at 1000 characters', () => {
|
|
396
|
+
const job = delegationJobs.createDelegationJob({
|
|
397
|
+
kind: 'delegate',
|
|
398
|
+
task: 'Truncation test',
|
|
399
|
+
parentSessionId: 'truncation-parent',
|
|
400
|
+
})
|
|
401
|
+
delegationJobs.startDelegationJob(job.id)
|
|
402
|
+
|
|
403
|
+
// Create a 2000-character result
|
|
404
|
+
const longResult = 'A'.repeat(2000)
|
|
405
|
+
const completed = delegationJobs.completeDelegationJob(job.id, longResult)
|
|
406
|
+
|
|
407
|
+
assert.ok(completed)
|
|
408
|
+
assert.equal(completed.result?.length, 2000)
|
|
409
|
+
assert.equal(completed.resultPreview?.length, 1000)
|
|
410
|
+
assert.equal(completed.resultPreview, 'A'.repeat(1000))
|
|
411
|
+
|
|
412
|
+
// Verify via getDelegationJob too
|
|
413
|
+
const fetched = delegationJobs.getDelegationJob(job.id)
|
|
414
|
+
assert.equal(fetched?.resultPreview?.length, 1000)
|
|
415
|
+
assert.equal(fetched?.result?.length, 2000)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it('rapid status transitions — create→start→fail cannot be restarted', () => {
|
|
419
|
+
const job = delegationJobs.createDelegationJob({
|
|
420
|
+
kind: 'subagent',
|
|
421
|
+
task: 'Rapid transitions',
|
|
422
|
+
parentSessionId: 'rapid-parent',
|
|
423
|
+
agentId: 'agent-rapid',
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
assert.equal(job.status, 'queued')
|
|
427
|
+
assert.equal(job.startedAt, null)
|
|
428
|
+
|
|
429
|
+
const started = delegationJobs.startDelegationJob(job.id)
|
|
430
|
+
assert.equal(started?.status, 'running')
|
|
431
|
+
assert.ok(started?.startedAt)
|
|
432
|
+
|
|
433
|
+
const failed = delegationJobs.failDelegationJob(job.id, 'Connection lost')
|
|
434
|
+
assert.equal(failed?.status, 'failed')
|
|
435
|
+
assert.equal(failed?.error, 'Connection lost')
|
|
436
|
+
assert.ok(failed?.completedAt)
|
|
437
|
+
|
|
438
|
+
// Try to start again — should be immutable since 'failed' is terminal
|
|
439
|
+
const restartAttempt = delegationJobs.startDelegationJob(job.id)
|
|
440
|
+
assert.equal(restartAttempt?.status, 'failed')
|
|
441
|
+
assert.equal(restartAttempt?.error, 'Connection lost')
|
|
442
|
+
|
|
443
|
+
// Try to complete — should also be immutable
|
|
444
|
+
const completeAttempt = delegationJobs.completeDelegationJob(job.id, 'Late success')
|
|
445
|
+
assert.equal(completeAttempt?.status, 'failed')
|
|
446
|
+
|
|
447
|
+
// Try to cancel — should also be immutable
|
|
448
|
+
const cancelAttempt = delegationJobs.cancelDelegationJob(job.id)
|
|
449
|
+
assert.equal(cancelAttempt?.status, 'failed')
|
|
450
|
+
|
|
451
|
+
// Verify final state
|
|
452
|
+
const finalState = delegationJobs.getDelegationJob(job.id)
|
|
453
|
+
assert.equal(finalState?.status, 'failed')
|
|
454
|
+
assert.equal(finalState?.error, 'Connection lost')
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('mixed kind filtering — delegate and subagent jobs', () => {
|
|
458
|
+
const mixedParent = 'mixed-kind-parent'
|
|
459
|
+
|
|
460
|
+
const delegateJobs = Array.from({ length: 3 }, (_, i) =>
|
|
461
|
+
delegationJobs.createDelegationJob({
|
|
462
|
+
kind: 'delegate',
|
|
463
|
+
task: `Delegate task ${i}`,
|
|
464
|
+
parentSessionId: mixedParent,
|
|
465
|
+
backend: 'codex',
|
|
466
|
+
}),
|
|
467
|
+
)
|
|
468
|
+
const subagentJobs = Array.from({ length: 4 }, (_, i) =>
|
|
469
|
+
delegationJobs.createDelegationJob({
|
|
470
|
+
kind: 'subagent',
|
|
471
|
+
task: `Subagent task ${i}`,
|
|
472
|
+
parentSessionId: mixedParent,
|
|
473
|
+
agentId: `agent-mixed-${i}`,
|
|
474
|
+
agentName: `Mixed Agent ${i}`,
|
|
475
|
+
}),
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
// Verify all 7 are listed under the parent
|
|
479
|
+
const allJobs = delegationJobs.listDelegationJobs({ parentSessionId: mixedParent })
|
|
480
|
+
assert.equal(allJobs.length, 7)
|
|
481
|
+
|
|
482
|
+
// Verify kinds are correct
|
|
483
|
+
const delegates = allJobs.filter((j) => j.kind === 'delegate')
|
|
484
|
+
const subagents = allJobs.filter((j) => j.kind === 'subagent')
|
|
485
|
+
assert.equal(delegates.length, 3)
|
|
486
|
+
assert.equal(subagents.length, 4)
|
|
487
|
+
|
|
488
|
+
// Start and complete one delegate, start and fail one subagent
|
|
489
|
+
delegationJobs.startDelegationJob(delegateJobs[0].id)
|
|
490
|
+
delegationJobs.completeDelegationJob(delegateJobs[0].id, 'Delegate 0 done')
|
|
491
|
+
|
|
492
|
+
delegationJobs.startDelegationJob(subagentJobs[0].id)
|
|
493
|
+
delegationJobs.failDelegationJob(subagentJobs[0].id, 'Subagent 0 crashed')
|
|
494
|
+
|
|
495
|
+
// Verify status filtering works across kinds
|
|
496
|
+
const completedMixed = delegationJobs.listDelegationJobs({ parentSessionId: mixedParent, status: 'completed' })
|
|
497
|
+
assert.equal(completedMixed.length, 1)
|
|
498
|
+
assert.equal(completedMixed[0].kind, 'delegate')
|
|
499
|
+
|
|
500
|
+
const failedMixed = delegationJobs.listDelegationJobs({ parentSessionId: mixedParent, status: 'failed' })
|
|
501
|
+
assert.equal(failedMixed.length, 1)
|
|
502
|
+
assert.equal(failedMixed[0].kind, 'subagent')
|
|
503
|
+
|
|
504
|
+
const queuedMixed = delegationJobs.listDelegationJobs({ parentSessionId: mixedParent, status: 'queued' })
|
|
505
|
+
assert.equal(queuedMixed.length, 5)
|
|
506
|
+
|
|
507
|
+
// Verify delegate vs subagent fields
|
|
508
|
+
assert.ok(delegateJobs[0].backend)
|
|
509
|
+
assert.equal(delegateJobs[0].agentId, null)
|
|
510
|
+
assert.ok(subagentJobs[0].agentId)
|
|
511
|
+
assert.ok(subagentJobs[0].agentName)
|
|
512
|
+
})
|
|
513
|
+
})
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { afterEach, describe, it } from 'node:test'
|
|
6
|
+
import { resolveDevServerLaunchDir } from './devserver-launch'
|
|
7
|
+
|
|
8
|
+
const tempDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
while (tempDirs.length) {
|
|
12
|
+
const dir = tempDirs.pop()
|
|
13
|
+
if (!dir) continue
|
|
14
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
function makeTempDir(): string {
|
|
19
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-devserver-launch-'))
|
|
20
|
+
tempDirs.push(dir)
|
|
21
|
+
return dir
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('resolveDevServerLaunchDir', () => {
|
|
25
|
+
it('resolves the repo root when launched from src/app', () => {
|
|
26
|
+
const repoRoot = path.resolve(process.cwd())
|
|
27
|
+
const nested = path.join(repoRoot, 'src', 'app')
|
|
28
|
+
const result = resolveDevServerLaunchDir(nested)
|
|
29
|
+
assert.equal(result.launchDir, repoRoot)
|
|
30
|
+
assert.equal(result.packageRoot, repoRoot)
|
|
31
|
+
assert.equal(result.framework, 'next')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('returns the nearest npm package root for nested package folders', () => {
|
|
35
|
+
const root = makeTempDir()
|
|
36
|
+
const nested = path.join(root, 'src', 'feature')
|
|
37
|
+
fs.mkdirSync(nested, { recursive: true })
|
|
38
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({
|
|
39
|
+
name: 'fixture',
|
|
40
|
+
scripts: { dev: 'vite' },
|
|
41
|
+
devDependencies: { vite: '^6.0.0' },
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
const result = resolveDevServerLaunchDir(nested)
|
|
45
|
+
assert.equal(result.launchDir, root)
|
|
46
|
+
assert.equal(result.packageRoot, root)
|
|
47
|
+
assert.equal(result.framework, 'npm')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('falls back to the input directory when no package root exists', () => {
|
|
51
|
+
const root = makeTempDir()
|
|
52
|
+
const nested = path.join(root, 'plain', 'folder')
|
|
53
|
+
fs.mkdirSync(nested, { recursive: true })
|
|
54
|
+
|
|
55
|
+
const result = resolveDevServerLaunchDir(nested)
|
|
56
|
+
assert.equal(result.launchDir, nested)
|
|
57
|
+
assert.equal(result.packageRoot, null)
|
|
58
|
+
assert.equal(result.framework, 'unknown')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
|
|
4
|
+
type FrameworkKind = 'next' | 'npm' | 'unknown'
|
|
5
|
+
|
|
6
|
+
interface PackageJsonLike {
|
|
7
|
+
scripts?: Record<string, unknown>
|
|
8
|
+
dependencies?: Record<string, unknown>
|
|
9
|
+
devDependencies?: Record<string, unknown>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DevServerLaunchResolution {
|
|
13
|
+
inputDir: string
|
|
14
|
+
launchDir: string
|
|
15
|
+
packageRoot: string | null
|
|
16
|
+
framework: FrameworkKind
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const NEXT_CONFIG_FILES = [
|
|
20
|
+
'next.config.js',
|
|
21
|
+
'next.config.mjs',
|
|
22
|
+
'next.config.ts',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
function readPackageJson(dir: string): PackageJsonLike | null {
|
|
26
|
+
const pkgPath = path.join(dir, 'package.json')
|
|
27
|
+
if (!fs.existsSync(pkgPath)) return null
|
|
28
|
+
try {
|
|
29
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
30
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
31
|
+
? parsed as PackageJsonLike
|
|
32
|
+
: null
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function hasNextDependency(pkg: PackageJsonLike): boolean {
|
|
39
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
|
|
40
|
+
return typeof deps.next === 'string' && deps.next.trim().length > 0
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasNextScript(pkg: PackageJsonLike): boolean {
|
|
44
|
+
const scripts = Object.values(pkg.scripts || {})
|
|
45
|
+
return scripts.some((value) => typeof value === 'string' && /\bnext\b/.test(value))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hasNextConfig(dir: string): boolean {
|
|
49
|
+
return NEXT_CONFIG_FILES.some((file) => fs.existsSync(path.join(dir, file)))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function classifyPackageRoot(dir: string, pkg: PackageJsonLike): FrameworkKind {
|
|
53
|
+
return hasNextDependency(pkg) || hasNextScript(pkg) || hasNextConfig(dir)
|
|
54
|
+
? 'next'
|
|
55
|
+
: 'npm'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveDevServerLaunchDir(startDir: string): DevServerLaunchResolution {
|
|
59
|
+
const inputDir = path.resolve(startDir)
|
|
60
|
+
let current = inputDir
|
|
61
|
+
|
|
62
|
+
while (true) {
|
|
63
|
+
const pkg = readPackageJson(current)
|
|
64
|
+
if (pkg) {
|
|
65
|
+
const framework = classifyPackageRoot(current, pkg)
|
|
66
|
+
return {
|
|
67
|
+
inputDir,
|
|
68
|
+
launchDir: current,
|
|
69
|
+
packageRoot: current,
|
|
70
|
+
framework,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const parent = path.dirname(current)
|
|
75
|
+
if (parent === current) {
|
|
76
|
+
return {
|
|
77
|
+
inputDir,
|
|
78
|
+
launchDir: inputDir,
|
|
79
|
+
packageRoot: null,
|
|
80
|
+
framework: 'unknown',
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
current = parent
|
|
84
|
+
}
|
|
85
|
+
}
|