@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,564 @@
|
|
|
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 { describe, it, before, after } from 'node:test'
|
|
6
|
+
|
|
7
|
+
// ── Temp-dir env isolation ────────────────────────────────────────────
|
|
8
|
+
let tempDir: string
|
|
9
|
+
let storage: typeof import('../storage')
|
|
10
|
+
let dedupe: typeof import('../../schedule-dedupe')
|
|
11
|
+
let origin: typeof import('../../schedule-origin')
|
|
12
|
+
let scheduleName: typeof import('../../schedule-name')
|
|
13
|
+
let normalization: typeof import('../schedule-normalization')
|
|
14
|
+
|
|
15
|
+
const originalEnv = {
|
|
16
|
+
DATA_DIR: process.env.DATA_DIR,
|
|
17
|
+
WORKSPACE_DIR: process.env.WORKSPACE_DIR,
|
|
18
|
+
SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
before(async () => {
|
|
22
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-sched-adv-'))
|
|
23
|
+
const dataDir = path.join(tempDir, 'data')
|
|
24
|
+
const workspaceDir = path.join(tempDir, 'workspace')
|
|
25
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
26
|
+
fs.mkdirSync(workspaceDir, { recursive: true })
|
|
27
|
+
|
|
28
|
+
process.env.DATA_DIR = dataDir
|
|
29
|
+
process.env.WORKSPACE_DIR = workspaceDir
|
|
30
|
+
delete process.env.SWARMCLAW_BUILD_MODE
|
|
31
|
+
|
|
32
|
+
storage = await import('../storage')
|
|
33
|
+
dedupe = await import('../../schedule-dedupe')
|
|
34
|
+
origin = await import('../../schedule-origin')
|
|
35
|
+
scheduleName = await import('../../schedule-name')
|
|
36
|
+
normalization = await import('../schedule-normalization')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
after(() => {
|
|
40
|
+
process.env.DATA_DIR = originalEnv.DATA_DIR
|
|
41
|
+
process.env.WORKSPACE_DIR = originalEnv.WORKSPACE_DIR
|
|
42
|
+
if (originalEnv.SWARMCLAW_BUILD_MODE != null) {
|
|
43
|
+
process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
|
|
44
|
+
}
|
|
45
|
+
if (tempDir) fs.rmSync(tempDir, { recursive: true, force: true })
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
49
|
+
function makeSchedule(overrides: Record<string, unknown> = {}) {
|
|
50
|
+
const now = Date.now()
|
|
51
|
+
return {
|
|
52
|
+
id: `sched-${Math.random().toString(36).slice(2, 8)}`,
|
|
53
|
+
name: 'Test Schedule',
|
|
54
|
+
agentId: 'agent-1',
|
|
55
|
+
taskPrompt: 'check server status',
|
|
56
|
+
scheduleType: 'interval' as const,
|
|
57
|
+
intervalMs: 60_000,
|
|
58
|
+
status: 'active' as const,
|
|
59
|
+
createdAt: now,
|
|
60
|
+
updatedAt: now,
|
|
61
|
+
...overrides,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
66
|
+
// Schedule normalization
|
|
67
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
68
|
+
describe('schedule normalization', () => {
|
|
69
|
+
it('1. interval schedule → nextRunAt = now + intervalMs', () => {
|
|
70
|
+
const now = 1_700_000_000_000
|
|
71
|
+
const result = normalization.normalizeSchedulePayload(
|
|
72
|
+
{ scheduleType: 'interval', intervalMs: 60_000, agentId: 'a1', taskPrompt: 'do stuff' },
|
|
73
|
+
{ now },
|
|
74
|
+
)
|
|
75
|
+
assert.equal(result.ok, true)
|
|
76
|
+
if (result.ok) {
|
|
77
|
+
assert.equal(result.value.nextRunAt, now + 60_000)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('2. once schedule → nextRunAt = runAt', () => {
|
|
82
|
+
const runAt = 1_700_000_060_000
|
|
83
|
+
const result = normalization.normalizeSchedulePayload(
|
|
84
|
+
{ scheduleType: 'once', runAt, agentId: 'a1', taskPrompt: 'one-shot' },
|
|
85
|
+
{ now: 1_700_000_000_000 },
|
|
86
|
+
)
|
|
87
|
+
assert.equal(result.ok, true)
|
|
88
|
+
if (result.ok) {
|
|
89
|
+
assert.equal(result.value.nextRunAt, runAt)
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('3. missing taskPrompt with action/command → derives taskPrompt', () => {
|
|
94
|
+
const cwd = process.env.WORKSPACE_DIR!
|
|
95
|
+
const scriptPath = path.join(cwd, 'test_script.py')
|
|
96
|
+
fs.writeFileSync(scriptPath, 'print("ok")\n')
|
|
97
|
+
|
|
98
|
+
const result = normalization.normalizeSchedulePayload(
|
|
99
|
+
{ scheduleType: 'interval', intervalMs: 60_000, agentId: 'a1', action: 'run_script', path: 'test_script.py' },
|
|
100
|
+
{ cwd },
|
|
101
|
+
)
|
|
102
|
+
assert.equal(result.ok, true)
|
|
103
|
+
if (result.ok) {
|
|
104
|
+
assert.ok(typeof result.value.taskPrompt === 'string')
|
|
105
|
+
assert.ok((result.value.taskPrompt as string).length > 0)
|
|
106
|
+
assert.ok((result.value.taskPrompt as string).includes('test_script.py'))
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('4. invalid scheduleType → defaults to interval', () => {
|
|
111
|
+
const result = normalization.normalizeSchedulePayload(
|
|
112
|
+
{ scheduleType: 'bogus', intervalMs: 5000, agentId: 'a1', taskPrompt: 'hello' },
|
|
113
|
+
{ now: Date.now() },
|
|
114
|
+
)
|
|
115
|
+
assert.equal(result.ok, true)
|
|
116
|
+
if (result.ok) {
|
|
117
|
+
assert.equal(result.value.scheduleType, 'interval')
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
123
|
+
// Schedule creation & storage
|
|
124
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
125
|
+
describe('schedule creation & storage', () => {
|
|
126
|
+
it('5. create interval schedule → stored in loadSchedules()', () => {
|
|
127
|
+
const sched = makeSchedule({ id: 'int-1', scheduleType: 'interval', intervalMs: 30_000 })
|
|
128
|
+
storage.saveSchedules({ 'int-1': sched })
|
|
129
|
+
const loaded = storage.loadSchedules()
|
|
130
|
+
assert.ok(loaded['int-1'])
|
|
131
|
+
assert.equal(loaded['int-1'].scheduleType, 'interval')
|
|
132
|
+
assert.equal(loaded['int-1'].intervalMs, 30_000)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('6. create cron schedule → stored with cron expression', () => {
|
|
136
|
+
const sched = makeSchedule({ id: 'cron-1', scheduleType: 'cron', cron: '*/5 * * * *' })
|
|
137
|
+
storage.saveSchedules({ 'cron-1': sched })
|
|
138
|
+
const loaded = storage.loadSchedules()
|
|
139
|
+
assert.ok(loaded['cron-1'])
|
|
140
|
+
assert.equal(loaded['cron-1'].cron, '*/5 * * * *')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('7. create once schedule with runAt → stored correctly', () => {
|
|
144
|
+
const runAt = Date.now() + 3_600_000
|
|
145
|
+
const sched = makeSchedule({ id: 'once-1', scheduleType: 'once', runAt })
|
|
146
|
+
storage.saveSchedules({ 'once-1': sched })
|
|
147
|
+
const loaded = storage.loadSchedules()
|
|
148
|
+
assert.ok(loaded['once-1'])
|
|
149
|
+
assert.equal(loaded['once-1'].runAt, runAt)
|
|
150
|
+
assert.equal(loaded['once-1'].scheduleType, 'once')
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
155
|
+
// Dedup on create
|
|
156
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
157
|
+
describe('dedup on create', () => {
|
|
158
|
+
it('8. same agent, prompt, cadence → duplicate detected', () => {
|
|
159
|
+
const existing = {
|
|
160
|
+
s1: makeSchedule({ id: 's1', agentId: 'a1', taskPrompt: 'deploy app', scheduleType: 'interval', intervalMs: 60_000 }),
|
|
161
|
+
}
|
|
162
|
+
const candidate = { agentId: 'a1', taskPrompt: 'deploy app', scheduleType: 'interval' as const, intervalMs: 60_000 }
|
|
163
|
+
const dup = dedupe.findDuplicateSchedule(existing, candidate)
|
|
164
|
+
assert.ok(dup, 'expected duplicate to be found')
|
|
165
|
+
assert.equal(dup.id, 's1')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('9. whitespace-normalized prompts match as duplicates', () => {
|
|
169
|
+
const existing = {
|
|
170
|
+
s1: makeSchedule({ id: 's1', agentId: 'a1', taskPrompt: ' deploy app ', scheduleType: 'interval', intervalMs: 60_000 }),
|
|
171
|
+
}
|
|
172
|
+
const candidate = { agentId: 'a1', taskPrompt: 'deploy app', scheduleType: 'interval' as const, intervalMs: 60_000 }
|
|
173
|
+
const dup = dedupe.findDuplicateSchedule(existing, candidate)
|
|
174
|
+
assert.ok(dup, 'whitespace-normalized prompt should match')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('10. different agents → not duplicates', () => {
|
|
178
|
+
const existing = {
|
|
179
|
+
s1: makeSchedule({ id: 's1', agentId: 'agent-A', taskPrompt: 'deploy app', scheduleType: 'interval', intervalMs: 60_000 }),
|
|
180
|
+
}
|
|
181
|
+
const candidate = { agentId: 'agent-B', taskPrompt: 'deploy app', scheduleType: 'interval' as const, intervalMs: 60_000 }
|
|
182
|
+
const dup = dedupe.findDuplicateSchedule(existing, candidate)
|
|
183
|
+
assert.equal(dup, null, 'different agents should not be duplicates')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('11. same prompt but different cadence type → not exact duplicates', () => {
|
|
187
|
+
const existing = {
|
|
188
|
+
s1: makeSchedule({ id: 's1', agentId: 'a1', taskPrompt: 'deploy app', scheduleType: 'interval', intervalMs: 60_000 }),
|
|
189
|
+
}
|
|
190
|
+
const candidate = { agentId: 'a1', taskPrompt: 'deploy app', scheduleType: 'cron' as const, cron: '*/1 * * * *' }
|
|
191
|
+
const dup = dedupe.findDuplicateSchedule(existing, candidate)
|
|
192
|
+
assert.equal(dup, null, 'different cadence type without session scope should not match')
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('12. fuzzy: similar prompts with same session → fuzzy match', () => {
|
|
196
|
+
const existing = {
|
|
197
|
+
s1: makeSchedule({
|
|
198
|
+
id: 's1',
|
|
199
|
+
agentId: 'a1',
|
|
200
|
+
taskPrompt: 'check server status',
|
|
201
|
+
scheduleType: 'interval',
|
|
202
|
+
intervalMs: 3_600_000,
|
|
203
|
+
createdByAgentId: 'a1',
|
|
204
|
+
createdInSessionId: 'sess-1',
|
|
205
|
+
}),
|
|
206
|
+
}
|
|
207
|
+
const candidate = {
|
|
208
|
+
agentId: 'a1',
|
|
209
|
+
taskPrompt: 'check the server status',
|
|
210
|
+
scheduleType: 'interval' as const,
|
|
211
|
+
intervalMs: 3_600_000,
|
|
212
|
+
createdByAgentId: 'a1',
|
|
213
|
+
createdInSessionId: 'sess-1',
|
|
214
|
+
}
|
|
215
|
+
const dup = dedupe.findDuplicateSchedule(existing, candidate, {
|
|
216
|
+
creatorScope: { sessionId: 'sess-1' },
|
|
217
|
+
})
|
|
218
|
+
assert.ok(dup, 'fuzzy prompt match should be found within same session')
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
223
|
+
// Schedule name derivation
|
|
224
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
225
|
+
describe('schedule name derivation', () => {
|
|
226
|
+
it('13. name provided → used as-is', () => {
|
|
227
|
+
const name = scheduleName.resolveScheduleName({ name: 'My Custom Name', taskPrompt: 'do stuff' })
|
|
228
|
+
assert.equal(name, 'My Custom Name')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('14. no name, has taskPrompt → derived from prompt', () => {
|
|
232
|
+
const name = scheduleName.resolveScheduleName({ taskPrompt: 'backup the database daily' })
|
|
233
|
+
assert.ok(name.length > 0)
|
|
234
|
+
assert.notEqual(name, '')
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
it('15. long prompt → truncated name', () => {
|
|
238
|
+
const longPrompt = 'a'.repeat(200)
|
|
239
|
+
const name = scheduleName.resolveScheduleName({ taskPrompt: longPrompt })
|
|
240
|
+
assert.ok(name.length <= 83, `name should be truncated, got length ${name.length}`)
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
245
|
+
// Creator scope
|
|
246
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
247
|
+
describe('creator scope', () => {
|
|
248
|
+
it('16. schedule with createdByAgentId → isAgentCreatedSchedule returns true', () => {
|
|
249
|
+
const sched = makeSchedule({ createdByAgentId: 'agent-1' })
|
|
250
|
+
assert.equal(origin.isAgentCreatedSchedule(sched), true)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('17. schedule without createdByAgentId → returns false', () => {
|
|
254
|
+
const sched = makeSchedule({ createdByAgentId: undefined })
|
|
255
|
+
// Remove the key entirely
|
|
256
|
+
const plain = { ...sched }
|
|
257
|
+
delete (plain as Record<string, unknown>).createdByAgentId
|
|
258
|
+
assert.equal(origin.isAgentCreatedSchedule(plain), false)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('18. empty string createdByAgentId → returns false', () => {
|
|
262
|
+
const sched = makeSchedule({ createdByAgentId: '' })
|
|
263
|
+
assert.equal(origin.isAgentCreatedSchedule(sched), false)
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
268
|
+
// Auto-delete logic
|
|
269
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
270
|
+
describe('auto-delete logic', () => {
|
|
271
|
+
it('19. once + agent-created → shouldAutoDelete = true', () => {
|
|
272
|
+
const sched = makeSchedule({ scheduleType: 'once', createdByAgentId: 'agent-1' })
|
|
273
|
+
assert.equal(origin.shouldAutoDeleteScheduleAfterTerminalRun(sched), true)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('20. interval + agent-created → false', () => {
|
|
277
|
+
const sched = makeSchedule({ scheduleType: 'interval', createdByAgentId: 'agent-1' })
|
|
278
|
+
assert.equal(origin.shouldAutoDeleteScheduleAfterTerminalRun(sched), false)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('21. cron + agent-created → false', () => {
|
|
282
|
+
const sched = makeSchedule({ scheduleType: 'cron', createdByAgentId: 'agent-1' })
|
|
283
|
+
assert.equal(origin.shouldAutoDeleteScheduleAfterTerminalRun(sched), false)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('22. once + manual (no createdByAgentId) → false', () => {
|
|
287
|
+
const plain = { scheduleType: 'once' as const, createdByAgentId: undefined }
|
|
288
|
+
assert.equal(origin.shouldAutoDeleteScheduleAfterTerminalRun(plain), false)
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
293
|
+
// Related schedule discovery
|
|
294
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
295
|
+
describe('related schedule discovery', () => {
|
|
296
|
+
it('23-24. findEquivalentSchedules returns all equivalent schedules', () => {
|
|
297
|
+
const base = {
|
|
298
|
+
agentId: 'a1',
|
|
299
|
+
taskPrompt: 'send weekly digest',
|
|
300
|
+
scheduleType: 'interval' as const,
|
|
301
|
+
intervalMs: 3_600_000,
|
|
302
|
+
status: 'active' as const,
|
|
303
|
+
createdByAgentId: 'a1',
|
|
304
|
+
createdInSessionId: 'sess-1',
|
|
305
|
+
}
|
|
306
|
+
const schedules: Record<string, ReturnType<typeof makeSchedule>> = {
|
|
307
|
+
eq1: makeSchedule({ ...base, id: 'eq1' }),
|
|
308
|
+
eq2: makeSchedule({ ...base, id: 'eq2' }),
|
|
309
|
+
eq3: makeSchedule({ ...base, id: 'eq3' }),
|
|
310
|
+
}
|
|
311
|
+
const candidate = { ...base, id: 'eq1' }
|
|
312
|
+
const equivalents = dedupe.findEquivalentSchedules(schedules, candidate, { ignoreId: 'eq1' })
|
|
313
|
+
assert.equal(equivalents.length, 2, 'should find 2 equivalents (excluding self)')
|
|
314
|
+
const ids = equivalents.map((s) => s.id)
|
|
315
|
+
assert.ok(ids.includes('eq2'))
|
|
316
|
+
assert.ok(ids.includes('eq3'))
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('25. paused schedule still found by findEquivalentSchedules (default includes paused)', () => {
|
|
320
|
+
const base = {
|
|
321
|
+
agentId: 'a1',
|
|
322
|
+
taskPrompt: 'send weekly digest',
|
|
323
|
+
scheduleType: 'interval' as const,
|
|
324
|
+
intervalMs: 3_600_000,
|
|
325
|
+
createdByAgentId: 'a1',
|
|
326
|
+
createdInSessionId: 'sess-1',
|
|
327
|
+
}
|
|
328
|
+
const schedules: Record<string, ReturnType<typeof makeSchedule>> = {
|
|
329
|
+
eq1: makeSchedule({ ...base, id: 'eq1', status: 'active' }),
|
|
330
|
+
eq2: makeSchedule({ ...base, id: 'eq2', status: 'paused' }),
|
|
331
|
+
}
|
|
332
|
+
const candidate = { ...base, id: 'eq1' }
|
|
333
|
+
const equivalents = dedupe.findEquivalentSchedules(schedules, candidate, { ignoreId: 'eq1' })
|
|
334
|
+
assert.equal(equivalents.length, 1)
|
|
335
|
+
assert.equal(equivalents[0].id, 'eq2')
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
340
|
+
// Signature key stability
|
|
341
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
342
|
+
describe('signature key stability', () => {
|
|
343
|
+
it('26. same schedule → same signature key', () => {
|
|
344
|
+
const sched = makeSchedule({ id: 'k1', agentId: 'a1', taskPrompt: 'hello world', scheduleType: 'interval', intervalMs: 60_000 })
|
|
345
|
+
const key1 = dedupe.getScheduleSignatureKey(sched)
|
|
346
|
+
const key2 = dedupe.getScheduleSignatureKey(sched)
|
|
347
|
+
assert.equal(key1, key2)
|
|
348
|
+
assert.ok(key1.length > 0, 'key should not be empty')
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it('27. different prompt → different key', () => {
|
|
352
|
+
const sched1 = makeSchedule({ agentId: 'a1', taskPrompt: 'hello world', scheduleType: 'interval', intervalMs: 60_000 })
|
|
353
|
+
const sched2 = makeSchedule({ agentId: 'a1', taskPrompt: 'goodbye world', scheduleType: 'interval', intervalMs: 60_000 })
|
|
354
|
+
assert.notEqual(dedupe.getScheduleSignatureKey(sched1), dedupe.getScheduleSignatureKey(sched2))
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('28. different agent → different key', () => {
|
|
358
|
+
const sched1 = makeSchedule({ agentId: 'agent-A', taskPrompt: 'hello world', scheduleType: 'interval', intervalMs: 60_000 })
|
|
359
|
+
const sched2 = makeSchedule({ agentId: 'agent-B', taskPrompt: 'hello world', scheduleType: 'interval', intervalMs: 60_000 })
|
|
360
|
+
assert.notEqual(dedupe.getScheduleSignatureKey(sched1), dedupe.getScheduleSignatureKey(sched2))
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('29. different cadence → different key', () => {
|
|
364
|
+
const sched1 = makeSchedule({ agentId: 'a1', taskPrompt: 'hello world', scheduleType: 'interval', intervalMs: 60_000 })
|
|
365
|
+
const sched2 = makeSchedule({ agentId: 'a1', taskPrompt: 'hello world', scheduleType: 'interval', intervalMs: 120_000 })
|
|
366
|
+
assert.notEqual(dedupe.getScheduleSignatureKey(sched1), dedupe.getScheduleSignatureKey(sched2))
|
|
367
|
+
})
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
371
|
+
// Status transitions
|
|
372
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
373
|
+
describe('status transitions', () => {
|
|
374
|
+
it('30. active → paused → active round-trip', () => {
|
|
375
|
+
const sched = makeSchedule({ id: 'rt-1', status: 'active' })
|
|
376
|
+
storage.saveSchedules({ 'rt-1': sched })
|
|
377
|
+
|
|
378
|
+
// Pause
|
|
379
|
+
const all1 = storage.loadSchedules()
|
|
380
|
+
all1['rt-1'].status = 'paused'
|
|
381
|
+
storage.saveSchedules(all1)
|
|
382
|
+
assert.equal(storage.loadSchedules()['rt-1'].status, 'paused')
|
|
383
|
+
|
|
384
|
+
// Reactivate
|
|
385
|
+
const all2 = storage.loadSchedules()
|
|
386
|
+
all2['rt-1'].status = 'active'
|
|
387
|
+
storage.saveSchedules(all2)
|
|
388
|
+
assert.equal(storage.loadSchedules()['rt-1'].status, 'active')
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('31. active → completed (once schedule after execution)', () => {
|
|
392
|
+
const sched = makeSchedule({ id: 'oc-1', scheduleType: 'once', status: 'active', runAt: Date.now() })
|
|
393
|
+
storage.saveSchedules({ 'oc-1': sched })
|
|
394
|
+
|
|
395
|
+
const all = storage.loadSchedules()
|
|
396
|
+
all['oc-1'].status = 'completed'
|
|
397
|
+
all['oc-1'].lastRunAt = Date.now()
|
|
398
|
+
storage.saveSchedules(all)
|
|
399
|
+
assert.equal(storage.loadSchedules()['oc-1'].status, 'completed')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('32. schedule with status failed → excluded from normal dedup searches', () => {
|
|
403
|
+
const existing = {
|
|
404
|
+
f1: makeSchedule({
|
|
405
|
+
id: 'f1',
|
|
406
|
+
agentId: 'a1',
|
|
407
|
+
taskPrompt: 'deploy app',
|
|
408
|
+
scheduleType: 'interval',
|
|
409
|
+
intervalMs: 60_000,
|
|
410
|
+
status: 'failed',
|
|
411
|
+
}),
|
|
412
|
+
}
|
|
413
|
+
const candidate = { agentId: 'a1', taskPrompt: 'deploy app', scheduleType: 'interval' as const, intervalMs: 60_000 }
|
|
414
|
+
// Default includeStatuses is ['active', 'paused'] — 'failed' excluded
|
|
415
|
+
const dup = dedupe.findDuplicateSchedule(existing, candidate)
|
|
416
|
+
assert.equal(dup, null, 'failed schedules should be excluded from dedup')
|
|
417
|
+
})
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
421
|
+
// Edge cases
|
|
422
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
423
|
+
describe('edge cases', () => {
|
|
424
|
+
it('33. empty taskPrompt → validation error', () => {
|
|
425
|
+
const result = normalization.normalizeSchedulePayload(
|
|
426
|
+
{ scheduleType: 'interval', intervalMs: 5000, agentId: 'a1', taskPrompt: '' },
|
|
427
|
+
{ now: Date.now() },
|
|
428
|
+
)
|
|
429
|
+
assert.equal(result.ok, false)
|
|
430
|
+
if (!result.ok) {
|
|
431
|
+
assert.ok(result.error.length > 0)
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('34. null agentId → validation error', () => {
|
|
436
|
+
const result = normalization.normalizeSchedulePayload(
|
|
437
|
+
{ scheduleType: 'interval', intervalMs: 5000, agentId: null, taskPrompt: 'hello' },
|
|
438
|
+
{ now: Date.now() },
|
|
439
|
+
)
|
|
440
|
+
assert.equal(result.ok, false)
|
|
441
|
+
if (!result.ok) {
|
|
442
|
+
assert.ok(result.error.includes('agentId'))
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('35. very long cron expression → stored correctly', () => {
|
|
447
|
+
const longCron = '*/5 * * * *'
|
|
448
|
+
const sched = makeSchedule({ id: 'lc-1', scheduleType: 'cron', cron: longCron })
|
|
449
|
+
storage.saveSchedules({ 'lc-1': sched })
|
|
450
|
+
const loaded = storage.loadSchedules()
|
|
451
|
+
assert.equal(loaded['lc-1'].cron, longCron)
|
|
452
|
+
})
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
456
|
+
// Additional edge cases & integration
|
|
457
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
458
|
+
describe('additional scenarios', () => {
|
|
459
|
+
it('36. getScheduleSignatureKey returns empty for missing agentId', () => {
|
|
460
|
+
const sched = makeSchedule({ agentId: '', taskPrompt: 'hello' })
|
|
461
|
+
const key = dedupe.getScheduleSignatureKey(sched)
|
|
462
|
+
assert.equal(key, '')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('37. getScheduleSignatureKey returns empty for missing taskPrompt', () => {
|
|
466
|
+
const sched = makeSchedule({ agentId: 'a1', taskPrompt: '' })
|
|
467
|
+
const key = dedupe.getScheduleSignatureKey(sched)
|
|
468
|
+
assert.equal(key, '')
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('38. cron schedule normalization does not set nextRunAt (no interval fallback)', () => {
|
|
472
|
+
const result = normalization.normalizeSchedulePayload(
|
|
473
|
+
{ scheduleType: 'cron', cron: '0 9 * * *', agentId: 'a1', taskPrompt: 'daily task' },
|
|
474
|
+
{ now: Date.now() },
|
|
475
|
+
)
|
|
476
|
+
assert.equal(result.ok, true)
|
|
477
|
+
if (result.ok) {
|
|
478
|
+
// cron nextRunAt is not set by normalizeSchedulePayload (calculated by the scheduler)
|
|
479
|
+
assert.equal(result.value.nextRunAt, undefined)
|
|
480
|
+
}
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('39. duplicate with ignoreId → self is excluded', () => {
|
|
484
|
+
const existing = {
|
|
485
|
+
s1: makeSchedule({ id: 's1', agentId: 'a1', taskPrompt: 'deploy app', scheduleType: 'interval', intervalMs: 60_000 }),
|
|
486
|
+
}
|
|
487
|
+
const candidate = { id: 's1', agentId: 'a1', taskPrompt: 'deploy app', scheduleType: 'interval' as const, intervalMs: 60_000 }
|
|
488
|
+
const dup = dedupe.findDuplicateSchedule(existing, candidate, { ignoreId: 's1' })
|
|
489
|
+
assert.equal(dup, null, 'should not match self when ignoreId is set')
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('40. resolveScheduleName for generic name falls back to prompt derivation', () => {
|
|
493
|
+
const name = scheduleName.resolveScheduleName({ name: 'schedule', taskPrompt: 'backup the database' })
|
|
494
|
+
// 'schedule' is generic, so it should derive from taskPrompt
|
|
495
|
+
assert.notEqual(name, 'schedule')
|
|
496
|
+
assert.ok(name.length > 0)
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
it('41. saveSchedules overwrites entire collection', () => {
|
|
500
|
+
storage.saveSchedules({ x1: makeSchedule({ id: 'x1' }) })
|
|
501
|
+
storage.saveSchedules({ x2: makeSchedule({ id: 'x2' }) })
|
|
502
|
+
const loaded = storage.loadSchedules()
|
|
503
|
+
assert.equal(loaded['x1'], undefined, 'x1 should be gone after full overwrite')
|
|
504
|
+
assert.ok(loaded['x2'])
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
it('42. normalizeSchedulePayload with command → derives taskPrompt from command', () => {
|
|
508
|
+
const result = normalization.normalizeSchedulePayload(
|
|
509
|
+
{ scheduleType: 'interval', intervalMs: 5000, agentId: 'a1', command: 'echo hello' },
|
|
510
|
+
{ now: Date.now() },
|
|
511
|
+
)
|
|
512
|
+
assert.equal(result.ok, true)
|
|
513
|
+
if (result.ok) {
|
|
514
|
+
assert.ok(typeof result.value.taskPrompt === 'string')
|
|
515
|
+
assert.ok((result.value.taskPrompt as string).includes('echo hello'))
|
|
516
|
+
}
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
it('43. once schedule without runAt still normalizes', () => {
|
|
520
|
+
const result = normalization.normalizeSchedulePayload(
|
|
521
|
+
{ scheduleType: 'once', agentId: 'a1', taskPrompt: 'one shot' },
|
|
522
|
+
{ now: Date.now() },
|
|
523
|
+
)
|
|
524
|
+
assert.equal(result.ok, true)
|
|
525
|
+
if (result.ok) {
|
|
526
|
+
// No runAt means no nextRunAt
|
|
527
|
+
assert.equal(result.value.nextRunAt, undefined)
|
|
528
|
+
}
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it('44. findEquivalentSchedules with completed status excluded by default', () => {
|
|
532
|
+
const existing = {
|
|
533
|
+
c1: makeSchedule({
|
|
534
|
+
id: 'c1',
|
|
535
|
+
agentId: 'a1',
|
|
536
|
+
taskPrompt: 'run backup',
|
|
537
|
+
scheduleType: 'interval',
|
|
538
|
+
intervalMs: 60_000,
|
|
539
|
+
status: 'completed',
|
|
540
|
+
}),
|
|
541
|
+
}
|
|
542
|
+
const candidate = { agentId: 'a1', taskPrompt: 'run backup', scheduleType: 'interval' as const, intervalMs: 60_000 }
|
|
543
|
+
const results = dedupe.findEquivalentSchedules(existing, candidate)
|
|
544
|
+
assert.equal(results.length, 0, 'completed should be excluded by default')
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
it('45. findEquivalentSchedules with explicit includeStatuses includes completed', () => {
|
|
548
|
+
const existing = {
|
|
549
|
+
c1: makeSchedule({
|
|
550
|
+
id: 'c1',
|
|
551
|
+
agentId: 'a1',
|
|
552
|
+
taskPrompt: 'run backup',
|
|
553
|
+
scheduleType: 'interval',
|
|
554
|
+
intervalMs: 60_000,
|
|
555
|
+
status: 'completed',
|
|
556
|
+
}),
|
|
557
|
+
}
|
|
558
|
+
const candidate = { agentId: 'a1', taskPrompt: 'run backup', scheduleType: 'interval' as const, intervalMs: 60_000 }
|
|
559
|
+
const results = dedupe.findEquivalentSchedules(existing, candidate, {
|
|
560
|
+
includeStatuses: ['active', 'paused', 'completed'],
|
|
561
|
+
})
|
|
562
|
+
assert.equal(results.length, 1)
|
|
563
|
+
})
|
|
564
|
+
})
|