vellum 0.2.1 → 0.2.2
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 +15 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +299 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +62 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-provider.ts +10 -6
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +246 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +33 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +161 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +163 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +28 -4
- package/src/daemon/ipc-contract.ts +133 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/http-server.ts +108 -20
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +5 -10
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +220 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +77 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
const testDir = mkdtempSync(join(tmpdir(), 'schedule-tools-test-'));
|
|
7
|
+
|
|
8
|
+
mock.module('../util/platform.js', () => ({
|
|
9
|
+
getDataDir: () => testDir,
|
|
10
|
+
isMacOS: () => process.platform === 'darwin',
|
|
11
|
+
isLinux: () => process.platform === 'linux',
|
|
12
|
+
isWindows: () => process.platform === 'win32',
|
|
13
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
14
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
15
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
16
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
17
|
+
ensureDataDir: () => {},
|
|
18
|
+
migrateToDataLayout: () => {},
|
|
19
|
+
migrateToWorkspaceLayout: () => {},
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
mock.module('../util/logger.js', () => ({
|
|
23
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
24
|
+
get: () => () => {},
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
mock.module('../config/loader.js', () => ({
|
|
29
|
+
getConfig: () => ({ memory: {} }),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
import type { Database } from 'bun:sqlite';
|
|
33
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
34
|
+
import type { ToolContext } from '../tools/types.js';
|
|
35
|
+
import { executeScheduleCreate } from '../tools/schedule/create.js';
|
|
36
|
+
import { executeScheduleList } from '../tools/schedule/list.js';
|
|
37
|
+
import { executeScheduleUpdate } from '../tools/schedule/update.js';
|
|
38
|
+
import { executeScheduleDelete } from '../tools/schedule/delete.js';
|
|
39
|
+
|
|
40
|
+
initializeDb();
|
|
41
|
+
|
|
42
|
+
afterAll(() => {
|
|
43
|
+
resetDb();
|
|
44
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function getRawDb(): Database {
|
|
48
|
+
return (getDb() as unknown as { $client: Database }).$client;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ctx: ToolContext = {
|
|
52
|
+
workingDir: '/tmp',
|
|
53
|
+
sessionId: 'test-session',
|
|
54
|
+
conversationId: 'test-conversation',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ── schedule_create ─────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
describe('schedule_create tool', () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
62
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('creates a schedule with valid cron expression', async () => {
|
|
66
|
+
const result = await executeScheduleCreate({
|
|
67
|
+
name: 'Daily standup',
|
|
68
|
+
cron_expression: '0 9 * * 1-5',
|
|
69
|
+
message: 'Time for standup!',
|
|
70
|
+
}, ctx);
|
|
71
|
+
|
|
72
|
+
expect(result.isError).toBe(false);
|
|
73
|
+
expect(result.content).toContain('Schedule created successfully');
|
|
74
|
+
expect(result.content).toContain('Daily standup');
|
|
75
|
+
expect(result.content).toContain('Every weekday at 9:00 AM');
|
|
76
|
+
expect(result.content).toContain('Enabled: true');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('creates a disabled schedule', async () => {
|
|
80
|
+
const result = await executeScheduleCreate({
|
|
81
|
+
name: 'Paused job',
|
|
82
|
+
cron_expression: '0 12 * * *',
|
|
83
|
+
message: 'Noon check',
|
|
84
|
+
enabled: false,
|
|
85
|
+
}, ctx);
|
|
86
|
+
|
|
87
|
+
expect(result.isError).toBe(false);
|
|
88
|
+
expect(result.content).toContain('Enabled: false');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('creates a schedule with timezone', async () => {
|
|
92
|
+
const result = await executeScheduleCreate({
|
|
93
|
+
name: 'LA morning',
|
|
94
|
+
cron_expression: '0 8 * * *',
|
|
95
|
+
message: 'Good morning LA',
|
|
96
|
+
timezone: 'America/Los_Angeles',
|
|
97
|
+
}, ctx);
|
|
98
|
+
|
|
99
|
+
expect(result.isError).toBe(false);
|
|
100
|
+
expect(result.content).toContain('America/Los_Angeles');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('rejects missing name', async () => {
|
|
104
|
+
const result = await executeScheduleCreate({
|
|
105
|
+
cron_expression: '0 9 * * *',
|
|
106
|
+
message: 'test',
|
|
107
|
+
}, ctx);
|
|
108
|
+
|
|
109
|
+
expect(result.isError).toBe(true);
|
|
110
|
+
expect(result.content).toContain('name is required');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('rejects missing expression', async () => {
|
|
114
|
+
const result = await executeScheduleCreate({
|
|
115
|
+
name: 'Test',
|
|
116
|
+
message: 'test',
|
|
117
|
+
}, ctx);
|
|
118
|
+
|
|
119
|
+
expect(result.isError).toBe(true);
|
|
120
|
+
expect(result.content).toContain('expression (or cron_expression) is required');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('rejects missing message', async () => {
|
|
124
|
+
const result = await executeScheduleCreate({
|
|
125
|
+
name: 'Test',
|
|
126
|
+
cron_expression: '0 9 * * *',
|
|
127
|
+
}, ctx);
|
|
128
|
+
|
|
129
|
+
expect(result.isError).toBe(true);
|
|
130
|
+
expect(result.content).toContain('message is required');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('rejects invalid cron expression', async () => {
|
|
134
|
+
const result = await executeScheduleCreate({
|
|
135
|
+
name: 'Bad cron',
|
|
136
|
+
cron_expression: 'not-a-cron',
|
|
137
|
+
message: 'test',
|
|
138
|
+
}, ctx);
|
|
139
|
+
|
|
140
|
+
expect(result.isError).toBe(true);
|
|
141
|
+
expect(result.content).toContain('Invalid cron expression');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── schedule_list ───────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
describe('schedule_list tool', () => {
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
150
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('returns empty message when no schedules exist', async () => {
|
|
154
|
+
const result = await executeScheduleList({}, ctx);
|
|
155
|
+
|
|
156
|
+
expect(result.isError).toBe(false);
|
|
157
|
+
expect(result.content).toContain('No schedules found');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('lists all schedules', async () => {
|
|
161
|
+
await executeScheduleCreate({
|
|
162
|
+
name: 'Job Alpha',
|
|
163
|
+
cron_expression: '0 9 * * *',
|
|
164
|
+
message: 'Alpha',
|
|
165
|
+
}, ctx);
|
|
166
|
+
await executeScheduleCreate({
|
|
167
|
+
name: 'Job Beta',
|
|
168
|
+
cron_expression: '0 17 * * *',
|
|
169
|
+
message: 'Beta',
|
|
170
|
+
}, ctx);
|
|
171
|
+
|
|
172
|
+
const result = await executeScheduleList({}, ctx);
|
|
173
|
+
|
|
174
|
+
expect(result.isError).toBe(false);
|
|
175
|
+
expect(result.content).toContain('Schedules (2)');
|
|
176
|
+
expect(result.content).toContain('Job Alpha');
|
|
177
|
+
expect(result.content).toContain('Job Beta');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('filters to enabled only', async () => {
|
|
181
|
+
await executeScheduleCreate({
|
|
182
|
+
name: 'Enabled Job',
|
|
183
|
+
cron_expression: '0 9 * * *',
|
|
184
|
+
message: 'enabled',
|
|
185
|
+
}, ctx);
|
|
186
|
+
await executeScheduleCreate({
|
|
187
|
+
name: 'Disabled Job',
|
|
188
|
+
cron_expression: '0 17 * * *',
|
|
189
|
+
message: 'disabled',
|
|
190
|
+
enabled: false,
|
|
191
|
+
}, ctx);
|
|
192
|
+
|
|
193
|
+
const result = await executeScheduleList({ enabled_only: true }, ctx);
|
|
194
|
+
|
|
195
|
+
expect(result.isError).toBe(false);
|
|
196
|
+
expect(result.content).toContain('Enabled Job');
|
|
197
|
+
expect(result.content).not.toContain('Disabled Job');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('shows detail for a specific job', async () => {
|
|
201
|
+
await executeScheduleCreate({
|
|
202
|
+
name: 'Detail Job',
|
|
203
|
+
cron_expression: '30 14 * * *',
|
|
204
|
+
message: 'Afternoon check',
|
|
205
|
+
}, ctx);
|
|
206
|
+
|
|
207
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
208
|
+
|
|
209
|
+
const result = await executeScheduleList({ job_id: row.id }, ctx);
|
|
210
|
+
|
|
211
|
+
expect(result.isError).toBe(false);
|
|
212
|
+
expect(result.content).toContain('Schedule: Detail Job');
|
|
213
|
+
expect(result.content).toContain('Every day at 2:30 PM');
|
|
214
|
+
expect(result.content).toContain('Message: Afternoon check');
|
|
215
|
+
expect(result.content).toContain('Enabled: true');
|
|
216
|
+
expect(result.content).toContain('No runs yet');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('returns error for nonexistent job_id', async () => {
|
|
220
|
+
const result = await executeScheduleList({ job_id: 'nonexistent' }, ctx);
|
|
221
|
+
|
|
222
|
+
expect(result.isError).toBe(true);
|
|
223
|
+
expect(result.content).toContain('Schedule not found');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ── schedule_update ─────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
describe('schedule_update tool', () => {
|
|
230
|
+
beforeEach(() => {
|
|
231
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
232
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('updates the name of a schedule', async () => {
|
|
236
|
+
await executeScheduleCreate({
|
|
237
|
+
name: 'Old Name',
|
|
238
|
+
cron_expression: '0 9 * * *',
|
|
239
|
+
message: 'test',
|
|
240
|
+
}, ctx);
|
|
241
|
+
|
|
242
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
243
|
+
const result = await executeScheduleUpdate({
|
|
244
|
+
job_id: row.id,
|
|
245
|
+
name: 'New Name',
|
|
246
|
+
}, ctx);
|
|
247
|
+
|
|
248
|
+
expect(result.isError).toBe(false);
|
|
249
|
+
expect(result.content).toContain('Schedule updated successfully');
|
|
250
|
+
expect(result.content).toContain('New Name');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('updates the cron expression', async () => {
|
|
254
|
+
await executeScheduleCreate({
|
|
255
|
+
name: 'Timing Test',
|
|
256
|
+
cron_expression: '0 9 * * *',
|
|
257
|
+
message: 'test',
|
|
258
|
+
}, ctx);
|
|
259
|
+
|
|
260
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
261
|
+
const result = await executeScheduleUpdate({
|
|
262
|
+
job_id: row.id,
|
|
263
|
+
cron_expression: '0 17 * * *',
|
|
264
|
+
}, ctx);
|
|
265
|
+
|
|
266
|
+
expect(result.isError).toBe(false);
|
|
267
|
+
expect(result.content).toContain('Every day at 5:00 PM');
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test('disables a schedule', async () => {
|
|
271
|
+
await executeScheduleCreate({
|
|
272
|
+
name: 'Disable Me',
|
|
273
|
+
cron_expression: '0 9 * * *',
|
|
274
|
+
message: 'test',
|
|
275
|
+
}, ctx);
|
|
276
|
+
|
|
277
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
278
|
+
const result = await executeScheduleUpdate({
|
|
279
|
+
job_id: row.id,
|
|
280
|
+
enabled: false,
|
|
281
|
+
}, ctx);
|
|
282
|
+
|
|
283
|
+
expect(result.isError).toBe(false);
|
|
284
|
+
expect(result.content).toContain('Enabled: false');
|
|
285
|
+
expect(result.content).toContain('n/a (disabled)');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('rejects missing job_id', async () => {
|
|
289
|
+
const result = await executeScheduleUpdate({ name: 'test' }, ctx);
|
|
290
|
+
|
|
291
|
+
expect(result.isError).toBe(true);
|
|
292
|
+
expect(result.content).toContain('job_id is required');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('rejects update with no fields', async () => {
|
|
296
|
+
await executeScheduleCreate({
|
|
297
|
+
name: 'No Update',
|
|
298
|
+
cron_expression: '0 9 * * *',
|
|
299
|
+
message: 'test',
|
|
300
|
+
}, ctx);
|
|
301
|
+
|
|
302
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
303
|
+
const result = await executeScheduleUpdate({ job_id: row.id }, ctx);
|
|
304
|
+
|
|
305
|
+
expect(result.isError).toBe(true);
|
|
306
|
+
expect(result.content).toContain('No updates provided');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('returns error for nonexistent job_id', async () => {
|
|
310
|
+
const result = await executeScheduleUpdate({
|
|
311
|
+
job_id: 'nonexistent',
|
|
312
|
+
name: 'test',
|
|
313
|
+
}, ctx);
|
|
314
|
+
|
|
315
|
+
expect(result.isError).toBe(true);
|
|
316
|
+
expect(result.content).toContain('Schedule not found');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('rejects invalid cron expression in update', async () => {
|
|
320
|
+
await executeScheduleCreate({
|
|
321
|
+
name: 'Bad Update',
|
|
322
|
+
cron_expression: '0 9 * * *',
|
|
323
|
+
message: 'test',
|
|
324
|
+
}, ctx);
|
|
325
|
+
|
|
326
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
327
|
+
const result = await executeScheduleUpdate({
|
|
328
|
+
job_id: row.id,
|
|
329
|
+
cron_expression: 'invalid',
|
|
330
|
+
}, ctx);
|
|
331
|
+
|
|
332
|
+
expect(result.isError).toBe(true);
|
|
333
|
+
expect(result.content).toContain('Invalid cron expression');
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── RRULE support in schedule tools ─────────────────────────────────
|
|
338
|
+
|
|
339
|
+
describe('schedule_create with RRULE', () => {
|
|
340
|
+
beforeEach(() => {
|
|
341
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
342
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('creates a schedule with legacy cron_expression', async () => {
|
|
346
|
+
const result = await executeScheduleCreate({
|
|
347
|
+
name: 'Legacy cron',
|
|
348
|
+
cron_expression: '0 9 * * 1-5',
|
|
349
|
+
message: 'Legacy test',
|
|
350
|
+
}, ctx);
|
|
351
|
+
|
|
352
|
+
expect(result.isError).toBe(false);
|
|
353
|
+
expect(result.content).toContain('Schedule created successfully');
|
|
354
|
+
expect(result.content).toContain('Syntax: cron');
|
|
355
|
+
expect(result.content).toContain('Every weekday at 9:00 AM');
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test('creates a schedule with RRULE syntax + expression', async () => {
|
|
359
|
+
const result = await executeScheduleCreate({
|
|
360
|
+
name: 'RRULE daily',
|
|
361
|
+
syntax: 'rrule',
|
|
362
|
+
expression: 'DTSTART:20250101T090000Z\nRRULE:FREQ=DAILY',
|
|
363
|
+
message: 'RRULE test',
|
|
364
|
+
}, ctx);
|
|
365
|
+
|
|
366
|
+
expect(result.isError).toBe(false);
|
|
367
|
+
expect(result.content).toContain('Schedule created successfully');
|
|
368
|
+
expect(result.content).toContain('Syntax: rrule');
|
|
369
|
+
expect(result.content).toContain('RRULE:FREQ=DAILY');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('auto-detects RRULE syntax when syntax is omitted', async () => {
|
|
373
|
+
const result = await executeScheduleCreate({
|
|
374
|
+
name: 'Auto-detect RRULE',
|
|
375
|
+
expression: 'DTSTART:20250601T120000Z\nRRULE:FREQ=WEEKLY;BYDAY=MO',
|
|
376
|
+
message: 'Auto-detect test',
|
|
377
|
+
}, ctx);
|
|
378
|
+
|
|
379
|
+
expect(result.isError).toBe(false);
|
|
380
|
+
expect(result.content).toContain('Syntax: rrule');
|
|
381
|
+
expect(result.content).toContain('RRULE:FREQ=WEEKLY');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('rejects RRULE missing DTSTART with deterministic message', async () => {
|
|
385
|
+
const result = await executeScheduleCreate({
|
|
386
|
+
name: 'No DTSTART',
|
|
387
|
+
syntax: 'rrule',
|
|
388
|
+
expression: 'RRULE:FREQ=DAILY',
|
|
389
|
+
message: 'Should fail',
|
|
390
|
+
}, ctx);
|
|
391
|
+
|
|
392
|
+
expect(result.isError).toBe(true);
|
|
393
|
+
expect(result.content).toContain('DTSTART');
|
|
394
|
+
expect(result.content).toContain('deterministic');
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe('schedule_update with RRULE', () => {
|
|
399
|
+
beforeEach(() => {
|
|
400
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
401
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('switches a cron schedule to rrule', async () => {
|
|
405
|
+
await executeScheduleCreate({
|
|
406
|
+
name: 'Cron to RRULE',
|
|
407
|
+
cron_expression: '0 9 * * *',
|
|
408
|
+
message: 'test',
|
|
409
|
+
}, ctx);
|
|
410
|
+
|
|
411
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
412
|
+
const result = await executeScheduleUpdate({
|
|
413
|
+
job_id: row.id,
|
|
414
|
+
syntax: 'rrule',
|
|
415
|
+
expression: 'DTSTART:20250101T090000Z\nRRULE:FREQ=DAILY',
|
|
416
|
+
}, ctx);
|
|
417
|
+
|
|
418
|
+
expect(result.isError).toBe(false);
|
|
419
|
+
expect(result.content).toContain('Schedule updated successfully');
|
|
420
|
+
expect(result.content).toContain('Syntax: rrule');
|
|
421
|
+
expect(result.content).toContain('RRULE:FREQ=DAILY');
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('schedule_list with RRULE', () => {
|
|
426
|
+
beforeEach(() => {
|
|
427
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
428
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test('shows syntax-aware output for cron schedules', async () => {
|
|
432
|
+
await executeScheduleCreate({
|
|
433
|
+
name: 'Cron Job',
|
|
434
|
+
cron_expression: '0 9 * * 1-5',
|
|
435
|
+
message: 'Cron test',
|
|
436
|
+
}, ctx);
|
|
437
|
+
|
|
438
|
+
const result = await executeScheduleList({}, ctx);
|
|
439
|
+
|
|
440
|
+
expect(result.isError).toBe(false);
|
|
441
|
+
expect(result.content).toContain('[cron]');
|
|
442
|
+
expect(result.content).toContain('Every weekday at 9:00 AM');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
test('shows syntax-aware output for rrule schedules', async () => {
|
|
446
|
+
await executeScheduleCreate({
|
|
447
|
+
name: 'RRULE Job',
|
|
448
|
+
syntax: 'rrule',
|
|
449
|
+
expression: 'DTSTART:20250101T090000Z\nRRULE:FREQ=DAILY',
|
|
450
|
+
message: 'RRULE test',
|
|
451
|
+
}, ctx);
|
|
452
|
+
|
|
453
|
+
const result = await executeScheduleList({}, ctx);
|
|
454
|
+
|
|
455
|
+
expect(result.isError).toBe(false);
|
|
456
|
+
expect(result.content).toContain('[rrule]');
|
|
457
|
+
expect(result.content).toContain('RRULE:FREQ=DAILY');
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test('shows syntax and expression in detail mode', async () => {
|
|
461
|
+
await executeScheduleCreate({
|
|
462
|
+
name: 'Detail RRULE',
|
|
463
|
+
syntax: 'rrule',
|
|
464
|
+
expression: 'DTSTART:20250601T120000Z\nRRULE:FREQ=WEEKLY;BYDAY=MO',
|
|
465
|
+
message: 'Detail test',
|
|
466
|
+
}, ctx);
|
|
467
|
+
|
|
468
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
469
|
+
const result = await executeScheduleList({ job_id: row.id }, ctx);
|
|
470
|
+
|
|
471
|
+
expect(result.isError).toBe(false);
|
|
472
|
+
expect(result.content).toContain('Syntax: rrule');
|
|
473
|
+
expect(result.content).toContain('Expression:');
|
|
474
|
+
expect(result.content).toContain('RRULE:FREQ=WEEKLY');
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ── RRULE set support in schedule tools ──────────────────────────────
|
|
479
|
+
|
|
480
|
+
describe('schedule_create with RRULE set (EXDATE)', () => {
|
|
481
|
+
beforeEach(() => {
|
|
482
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
483
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test('creates a schedule with RRULE + EXDATE', async () => {
|
|
487
|
+
const expression = [
|
|
488
|
+
'DTSTART:20250101T090000Z',
|
|
489
|
+
'RRULE:FREQ=DAILY;INTERVAL=1',
|
|
490
|
+
'EXDATE:20250102T090000Z',
|
|
491
|
+
].join('\n');
|
|
492
|
+
|
|
493
|
+
const result = await executeScheduleCreate({
|
|
494
|
+
name: 'Daily with exclusion',
|
|
495
|
+
syntax: 'rrule',
|
|
496
|
+
expression,
|
|
497
|
+
message: 'RRULE set test',
|
|
498
|
+
}, ctx);
|
|
499
|
+
|
|
500
|
+
expect(result.isError).toBe(false);
|
|
501
|
+
expect(result.content).toContain('Schedule created successfully');
|
|
502
|
+
expect(result.content).toContain('Syntax: rrule');
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test('creates a schedule with RRULE + RDATE', async () => {
|
|
506
|
+
const expression = [
|
|
507
|
+
'DTSTART:20250101T090000Z',
|
|
508
|
+
'RRULE:FREQ=WEEKLY;BYDAY=MO',
|
|
509
|
+
'RDATE:20250115T090000Z',
|
|
510
|
+
].join('\n');
|
|
511
|
+
|
|
512
|
+
const result = await executeScheduleCreate({
|
|
513
|
+
name: 'Weekly with extra date',
|
|
514
|
+
syntax: 'rrule',
|
|
515
|
+
expression,
|
|
516
|
+
message: 'RDATE test',
|
|
517
|
+
}, ctx);
|
|
518
|
+
|
|
519
|
+
expect(result.isError).toBe(false);
|
|
520
|
+
expect(result.content).toContain('Schedule created successfully');
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
test('rejects unsupported line types in RRULE set', async () => {
|
|
524
|
+
const expression = [
|
|
525
|
+
'DTSTART:20250101T090000Z',
|
|
526
|
+
'RRULE:FREQ=DAILY',
|
|
527
|
+
'VTIMEZONE:America/New_York',
|
|
528
|
+
].join('\n');
|
|
529
|
+
|
|
530
|
+
const result = await executeScheduleCreate({
|
|
531
|
+
name: 'Bad set line',
|
|
532
|
+
syntax: 'rrule',
|
|
533
|
+
expression,
|
|
534
|
+
message: 'Should fail',
|
|
535
|
+
}, ctx);
|
|
536
|
+
|
|
537
|
+
expect(result.isError).toBe(true);
|
|
538
|
+
expect(result.content).toContain('Unsupported recurrence line');
|
|
539
|
+
expect(result.content).toContain('Supported line types');
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test('rejects RRULE set missing DTSTART', async () => {
|
|
543
|
+
const expression = [
|
|
544
|
+
'RRULE:FREQ=DAILY',
|
|
545
|
+
'EXDATE:20250102T090000Z',
|
|
546
|
+
].join('\n');
|
|
547
|
+
|
|
548
|
+
const result = await executeScheduleCreate({
|
|
549
|
+
name: 'Set without DTSTART',
|
|
550
|
+
syntax: 'rrule',
|
|
551
|
+
expression,
|
|
552
|
+
message: 'Should fail',
|
|
553
|
+
}, ctx);
|
|
554
|
+
|
|
555
|
+
expect(result.isError).toBe(true);
|
|
556
|
+
expect(result.content).toContain('DTSTART');
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe('schedule_update with RRULE set', () => {
|
|
561
|
+
beforeEach(() => {
|
|
562
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
563
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test('updates a cron schedule to RRULE set with EXDATE', async () => {
|
|
567
|
+
await executeScheduleCreate({
|
|
568
|
+
name: 'Cron to set',
|
|
569
|
+
cron_expression: '0 9 * * *',
|
|
570
|
+
message: 'test',
|
|
571
|
+
}, ctx);
|
|
572
|
+
|
|
573
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
574
|
+
|
|
575
|
+
const expression = [
|
|
576
|
+
'DTSTART:20250101T090000Z',
|
|
577
|
+
'RRULE:FREQ=DAILY;INTERVAL=1',
|
|
578
|
+
'EXDATE:20250102T090000Z',
|
|
579
|
+
].join('\n');
|
|
580
|
+
|
|
581
|
+
const result = await executeScheduleUpdate({
|
|
582
|
+
job_id: row.id,
|
|
583
|
+
syntax: 'rrule',
|
|
584
|
+
expression,
|
|
585
|
+
}, ctx);
|
|
586
|
+
|
|
587
|
+
expect(result.isError).toBe(false);
|
|
588
|
+
expect(result.content).toContain('Schedule updated successfully');
|
|
589
|
+
expect(result.content).toContain('Syntax: rrule');
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test('rejects update with unsupported set lines', async () => {
|
|
593
|
+
await executeScheduleCreate({
|
|
594
|
+
name: 'Bad set update',
|
|
595
|
+
cron_expression: '0 9 * * *',
|
|
596
|
+
message: 'test',
|
|
597
|
+
}, ctx);
|
|
598
|
+
|
|
599
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
600
|
+
|
|
601
|
+
const expression = [
|
|
602
|
+
'DTSTART:20250101T090000Z',
|
|
603
|
+
'RRULE:FREQ=DAILY',
|
|
604
|
+
'VCALENDAR:BEGIN',
|
|
605
|
+
].join('\n');
|
|
606
|
+
|
|
607
|
+
const result = await executeScheduleUpdate({
|
|
608
|
+
job_id: row.id,
|
|
609
|
+
syntax: 'rrule',
|
|
610
|
+
expression,
|
|
611
|
+
}, ctx);
|
|
612
|
+
|
|
613
|
+
expect(result.isError).toBe(true);
|
|
614
|
+
expect(result.content).toContain('Unsupported recurrence line');
|
|
615
|
+
expect(result.content).toContain('Supported line types');
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
describe('schedule_list with RRULE set', () => {
|
|
620
|
+
beforeEach(() => {
|
|
621
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
622
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test('shows [RRULE set] label for schedules with EXDATE', async () => {
|
|
626
|
+
const expression = [
|
|
627
|
+
'DTSTART:20250101T090000Z',
|
|
628
|
+
'RRULE:FREQ=DAILY;INTERVAL=1',
|
|
629
|
+
'EXDATE:20250102T090000Z',
|
|
630
|
+
].join('\n');
|
|
631
|
+
|
|
632
|
+
await executeScheduleCreate({
|
|
633
|
+
name: 'Set Schedule',
|
|
634
|
+
syntax: 'rrule',
|
|
635
|
+
expression,
|
|
636
|
+
message: 'set test',
|
|
637
|
+
}, ctx);
|
|
638
|
+
|
|
639
|
+
const result = await executeScheduleList({}, ctx);
|
|
640
|
+
|
|
641
|
+
expect(result.isError).toBe(false);
|
|
642
|
+
expect(result.content).toContain('[RRULE set]');
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test('does not show [RRULE set] label for simple RRULE', async () => {
|
|
646
|
+
await executeScheduleCreate({
|
|
647
|
+
name: 'Simple RRULE',
|
|
648
|
+
syntax: 'rrule',
|
|
649
|
+
expression: 'DTSTART:20250101T090000Z\nRRULE:FREQ=DAILY',
|
|
650
|
+
message: 'simple test',
|
|
651
|
+
}, ctx);
|
|
652
|
+
|
|
653
|
+
const result = await executeScheduleList({}, ctx);
|
|
654
|
+
|
|
655
|
+
expect(result.isError).toBe(false);
|
|
656
|
+
expect(result.content).not.toContain('[RRULE set]');
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// ── schedule_delete ─────────────────────────────────────────────────
|
|
661
|
+
|
|
662
|
+
describe('schedule_delete tool', () => {
|
|
663
|
+
beforeEach(() => {
|
|
664
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
665
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test('deletes a schedule', async () => {
|
|
669
|
+
await executeScheduleCreate({
|
|
670
|
+
name: 'Delete Me',
|
|
671
|
+
cron_expression: '0 9 * * *',
|
|
672
|
+
message: 'test',
|
|
673
|
+
}, ctx);
|
|
674
|
+
|
|
675
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
676
|
+
const result = await executeScheduleDelete({ job_id: row.id }, ctx);
|
|
677
|
+
|
|
678
|
+
expect(result.isError).toBe(false);
|
|
679
|
+
expect(result.content).toContain('Schedule deleted');
|
|
680
|
+
expect(result.content).toContain('Delete Me');
|
|
681
|
+
|
|
682
|
+
// Verify it's actually gone
|
|
683
|
+
const count = getRawDb().query('SELECT COUNT(*) as c FROM cron_jobs').get() as { c: number };
|
|
684
|
+
expect(count.c).toBe(0);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test('rejects missing job_id', async () => {
|
|
688
|
+
const result = await executeScheduleDelete({}, ctx);
|
|
689
|
+
|
|
690
|
+
expect(result.isError).toBe(true);
|
|
691
|
+
expect(result.content).toContain('job_id is required');
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
test('returns error for nonexistent job_id', async () => {
|
|
695
|
+
const result = await executeScheduleDelete({ job_id: 'nonexistent' }, ctx);
|
|
696
|
+
|
|
697
|
+
expect(result.isError).toBe(true);
|
|
698
|
+
expect(result.content).toContain('Schedule not found');
|
|
699
|
+
});
|
|
700
|
+
});
|