vellum 0.2.13 → 0.2.14
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 +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +113 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +137 -18
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +62 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +27 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +4 -4
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +93 -7
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +8 -0
- package/src/calls/elevenlabs-config.ts +7 -5
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +32 -37
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +29 -7
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +142 -34
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +10 -4
- package/src/config/schema.ts +80 -21
- package/src/config/types.ts +1 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- package/src/tools/playbooks/playbook-update.ts +0 -111
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, mock, afterEach } from 'bun:test';
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, rmSync, chmodSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const testDataDir = '/tmp/qdrant-manager-test-' + process.pid;
|
|
6
|
+
|
|
7
|
+
mock.module('../util/platform.js', () => ({
|
|
8
|
+
getDataDir: () => testDataDir,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
mock.module('../util/logger.js', () => ({
|
|
12
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
13
|
+
get: () => () => {},
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { QdrantManager } from '../memory/qdrant-manager.js';
|
|
18
|
+
|
|
19
|
+
function placeFakeBinary(script: string): string {
|
|
20
|
+
const binaryPath = join(testDataDir, 'qdrant', 'bin', 'qdrant');
|
|
21
|
+
writeFileSync(binaryPath, script);
|
|
22
|
+
chmodSync(binaryPath, 0o755);
|
|
23
|
+
return binaryPath;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let nextPort = 16500;
|
|
27
|
+
function getTestPort(): number {
|
|
28
|
+
return nextPort++;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
rmSync(testDataDir, { recursive: true, force: true });
|
|
33
|
+
mkdirSync(join(testDataDir, 'qdrant', 'bin'), { recursive: true });
|
|
34
|
+
delete process.env.QDRANT_URL;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
delete process.env.QDRANT_URL;
|
|
39
|
+
rmSync(testDataDir, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe('QdrantManager', () => {
|
|
43
|
+
|
|
44
|
+
// ── Constructor ──────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
describe('constructor', () => {
|
|
47
|
+
test('parses URL correctly', () => {
|
|
48
|
+
const mgr = new QdrantManager({ url: 'http://127.0.0.1:6333' });
|
|
49
|
+
expect(mgr.getUrl()).toBe('http://127.0.0.1:6333');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('defaults port to 6333 when not in URL', () => {
|
|
53
|
+
const mgr = new QdrantManager({ url: 'http://localhost' });
|
|
54
|
+
expect(mgr.getUrl()).toBe('http://localhost');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('accepts custom storagePath', () => {
|
|
58
|
+
const mgr = new QdrantManager({
|
|
59
|
+
url: 'http://127.0.0.1:6333',
|
|
60
|
+
storagePath: '/custom/storage',
|
|
61
|
+
});
|
|
62
|
+
expect(mgr.getUrl()).toBe('http://127.0.0.1:6333');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── getUrl ───────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
describe('getUrl', () => {
|
|
69
|
+
test('returns the configured URL', () => {
|
|
70
|
+
const mgr = new QdrantManager({ url: 'http://myhost:7777' });
|
|
71
|
+
expect(mgr.getUrl()).toBe('http://myhost:7777');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ── External Mode ────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
describe('external mode', () => {
|
|
78
|
+
test('enters external mode when QDRANT_URL is set', async () => {
|
|
79
|
+
process.env.QDRANT_URL = 'http://external:6333';
|
|
80
|
+
const port = getTestPort();
|
|
81
|
+
const mgr = new QdrantManager({ url: `http://127.0.0.1:${port}` });
|
|
82
|
+
|
|
83
|
+
// External mode goes straight to waitForReady, which will timeout
|
|
84
|
+
await expect(mgr.start()).rejects.toThrow('did not become ready');
|
|
85
|
+
}, 35_000);
|
|
86
|
+
|
|
87
|
+
test('does not enter external mode when QDRANT_URL is empty', () => {
|
|
88
|
+
process.env.QDRANT_URL = ' ';
|
|
89
|
+
const mgr = new QdrantManager({ url: 'http://127.0.0.1:6333' });
|
|
90
|
+
expect(mgr.getUrl()).toBe('http://127.0.0.1:6333');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('does not enter external mode when QDRANT_URL is unset', () => {
|
|
94
|
+
delete process.env.QDRANT_URL;
|
|
95
|
+
const mgr = new QdrantManager({ url: 'http://127.0.0.1:6333' });
|
|
96
|
+
expect(mgr.getUrl()).toBe('http://127.0.0.1:6333');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ── stop() without a running process ─────────────────────────
|
|
101
|
+
|
|
102
|
+
describe('stop() without running process', () => {
|
|
103
|
+
test('removes stale PID file', async () => {
|
|
104
|
+
const pidPath = join(testDataDir, 'qdrant', 'qdrant.pid');
|
|
105
|
+
writeFileSync(pidPath, '99999');
|
|
106
|
+
|
|
107
|
+
const mgr = new QdrantManager({ url: 'http://127.0.0.1:6333' });
|
|
108
|
+
await mgr.stop();
|
|
109
|
+
|
|
110
|
+
expect(existsSync(pidPath)).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('is a no-op when no PID file exists', async () => {
|
|
114
|
+
const mgr = new QdrantManager({ url: 'http://127.0.0.1:6333' });
|
|
115
|
+
await mgr.stop();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── Stale PID Cleanup ────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
describe('stale PID cleanup during start()', () => {
|
|
122
|
+
test('removes PID file for non-existent process', async () => {
|
|
123
|
+
const pidPath = join(testDataDir, 'qdrant', 'qdrant.pid');
|
|
124
|
+
writeFileSync(pidPath, '2147483647');
|
|
125
|
+
|
|
126
|
+
placeFakeBinary('#!/bin/sh\nexit 1');
|
|
127
|
+
|
|
128
|
+
const port = getTestPort();
|
|
129
|
+
const mgr = new QdrantManager({ url: `http://127.0.0.1:${port}` });
|
|
130
|
+
|
|
131
|
+
try { await mgr.start(); } catch { /* readyz timeout */ }
|
|
132
|
+
|
|
133
|
+
expect(existsSync(pidPath)).toBe(false);
|
|
134
|
+
}, 40_000);
|
|
135
|
+
|
|
136
|
+
test('handles invalid PID file contents', async () => {
|
|
137
|
+
const pidPath = join(testDataDir, 'qdrant', 'qdrant.pid');
|
|
138
|
+
writeFileSync(pidPath, 'garbage');
|
|
139
|
+
|
|
140
|
+
placeFakeBinary('#!/bin/sh\nexit 1');
|
|
141
|
+
|
|
142
|
+
const port = getTestPort();
|
|
143
|
+
const mgr = new QdrantManager({ url: `http://127.0.0.1:${port}` });
|
|
144
|
+
|
|
145
|
+
try { await mgr.start(); } catch { /* expected */ }
|
|
146
|
+
|
|
147
|
+
expect(existsSync(pidPath)).toBe(false);
|
|
148
|
+
}, 40_000);
|
|
149
|
+
|
|
150
|
+
test('handles empty PID file', async () => {
|
|
151
|
+
const pidPath = join(testDataDir, 'qdrant', 'qdrant.pid');
|
|
152
|
+
writeFileSync(pidPath, '');
|
|
153
|
+
|
|
154
|
+
placeFakeBinary('#!/bin/sh\nexit 1');
|
|
155
|
+
|
|
156
|
+
const port = getTestPort();
|
|
157
|
+
const mgr = new QdrantManager({ url: `http://127.0.0.1:${port}` });
|
|
158
|
+
|
|
159
|
+
try { await mgr.start(); } catch { /* expected */ }
|
|
160
|
+
|
|
161
|
+
expect(existsSync(pidPath)).toBe(false);
|
|
162
|
+
}, 40_000);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── Process Lifecycle ────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
describe('process lifecycle', () => {
|
|
168
|
+
test('writes PID file after spawning', async () => {
|
|
169
|
+
const pidPath = join(testDataDir, 'qdrant', 'qdrant.pid');
|
|
170
|
+
|
|
171
|
+
// Binary that stays alive. We'll stop it before readyz times out.
|
|
172
|
+
placeFakeBinary('#!/bin/sh\nexec sleep 300');
|
|
173
|
+
|
|
174
|
+
const port = getTestPort();
|
|
175
|
+
const mgr = new QdrantManager({ url: `http://127.0.0.1:${port}` });
|
|
176
|
+
|
|
177
|
+
// Start polls readyz forever; we race it with our assertions + stop
|
|
178
|
+
const startPromise = mgr.start();
|
|
179
|
+
|
|
180
|
+
// Wait for spawn to happen
|
|
181
|
+
await Bun.sleep(500);
|
|
182
|
+
|
|
183
|
+
// PID file should be written
|
|
184
|
+
expect(existsSync(pidPath)).toBe(true);
|
|
185
|
+
const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);
|
|
186
|
+
expect(isNaN(pid)).toBe(false);
|
|
187
|
+
expect(pid).toBeGreaterThan(0);
|
|
188
|
+
|
|
189
|
+
// Stop kills the process and cleans up PID
|
|
190
|
+
await mgr.stop();
|
|
191
|
+
expect(existsSync(pidPath)).toBe(false);
|
|
192
|
+
|
|
193
|
+
// start() should now reject because process was killed
|
|
194
|
+
await expect(startPromise).rejects.toThrow('did not become ready');
|
|
195
|
+
}, 40_000);
|
|
196
|
+
|
|
197
|
+
test('stop() escalates to SIGKILL after grace period', async () => {
|
|
198
|
+
const pidPath = join(testDataDir, 'qdrant', 'qdrant.pid');
|
|
199
|
+
|
|
200
|
+
// Binary that ignores SIGTERM
|
|
201
|
+
placeFakeBinary('#!/bin/sh\ntrap "" TERM\nexec sleep 300');
|
|
202
|
+
|
|
203
|
+
const port = getTestPort();
|
|
204
|
+
const mgr = new QdrantManager({ url: `http://127.0.0.1:${port}` });
|
|
205
|
+
|
|
206
|
+
const startPromise = mgr.start();
|
|
207
|
+
await Bun.sleep(500);
|
|
208
|
+
|
|
209
|
+
expect(existsSync(pidPath)).toBe(true);
|
|
210
|
+
|
|
211
|
+
const stopStart = Date.now();
|
|
212
|
+
await mgr.stop();
|
|
213
|
+
const stopElapsed = Date.now() - stopStart;
|
|
214
|
+
|
|
215
|
+
// Grace period is 5s — should wait at least that long
|
|
216
|
+
expect(stopElapsed).toBeGreaterThanOrEqual(4500);
|
|
217
|
+
expect(existsSync(pidPath)).toBe(false);
|
|
218
|
+
|
|
219
|
+
await expect(startPromise).rejects.toThrow('did not become ready');
|
|
220
|
+
}, 45_000);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ── Start Failure Cleanup ────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
describe('start failure cleanup', () => {
|
|
226
|
+
test('cleans up process on readyz timeout', async () => {
|
|
227
|
+
const pidPath = join(testDataDir, 'qdrant', 'qdrant.pid');
|
|
228
|
+
|
|
229
|
+
// Binary that stays alive but never serves readyz
|
|
230
|
+
placeFakeBinary('#!/bin/sh\nexec sleep 300');
|
|
231
|
+
|
|
232
|
+
const port = getTestPort();
|
|
233
|
+
const mgr = new QdrantManager({ url: `http://127.0.0.1:${port}` });
|
|
234
|
+
|
|
235
|
+
await expect(mgr.start()).rejects.toThrow('did not become ready');
|
|
236
|
+
expect(existsSync(pidPath)).toBe(false);
|
|
237
|
+
}, 40_000);
|
|
238
|
+
|
|
239
|
+
test('cleans up when process exits immediately', async () => {
|
|
240
|
+
const pidPath = join(testDataDir, 'qdrant', 'qdrant.pid');
|
|
241
|
+
|
|
242
|
+
placeFakeBinary('#!/bin/sh\nexit 1');
|
|
243
|
+
|
|
244
|
+
const port = getTestPort();
|
|
245
|
+
const mgr = new QdrantManager({ url: `http://127.0.0.1:${port}` });
|
|
246
|
+
|
|
247
|
+
await expect(mgr.start()).rejects.toThrow('did not become ready');
|
|
248
|
+
expect(existsSync(pidPath)).toBe(false);
|
|
249
|
+
}, 40_000);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// ── Binary Detection ─────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
describe('binary detection', () => {
|
|
255
|
+
test('skips download when binary exists', async () => {
|
|
256
|
+
placeFakeBinary('#!/bin/sh\nexit 1');
|
|
257
|
+
|
|
258
|
+
const port = getTestPort();
|
|
259
|
+
const mgr = new QdrantManager({ url: `http://127.0.0.1:${port}` });
|
|
260
|
+
|
|
261
|
+
try { await mgr.start(); } catch { /* readyz timeout */ }
|
|
262
|
+
|
|
263
|
+
const binaryPath = join(testDataDir, 'qdrant', 'bin', 'qdrant');
|
|
264
|
+
expect(existsSync(binaryPath)).toBe(true);
|
|
265
|
+
}, 40_000);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
@@ -76,3 +76,100 @@ describe('RRULE set engine support', () => {
|
|
|
76
76
|
expect(next).toBeGreaterThan(Date.now() - 1);
|
|
77
77
|
});
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
// ── EXRULE behavioral tests ──────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
describe('EXRULE engine behavior', () => {
|
|
83
|
+
test('EXRULE excludes matching occurrences from daily series', () => {
|
|
84
|
+
// RRULE: every day at 09:00 starting Jan 1 2099
|
|
85
|
+
// EXRULE: every Saturday and Sunday (weekends)
|
|
86
|
+
// Expected: weekday occurrences only
|
|
87
|
+
const expr = [
|
|
88
|
+
'DTSTART:20990101T090000Z',
|
|
89
|
+
'RRULE:FREQ=DAILY;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
90
|
+
'EXRULE:FREQ=WEEKLY;BYDAY=SA,SU;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
91
|
+
].join('\n');
|
|
92
|
+
|
|
93
|
+
expect(isValidScheduleExpression({ syntax: 'rrule', expression: expr })).toBe(true);
|
|
94
|
+
|
|
95
|
+
// Jan 1 2099 is a Thursday. Enumerate the first several occurrences
|
|
96
|
+
// and verify that no Saturday (Jan 3) or Sunday (Jan 4) appears.
|
|
97
|
+
const thu = new Date('2099-01-01T09:00:00Z').getTime(); // Thu
|
|
98
|
+
const fri = new Date('2099-01-02T09:00:00Z').getTime(); // Fri
|
|
99
|
+
const sat = new Date('2099-01-03T09:00:00Z').getTime(); // Sat
|
|
100
|
+
const sun = new Date('2099-01-04T09:00:00Z').getTime(); // Sun
|
|
101
|
+
const mon = new Date('2099-01-05T09:00:00Z').getTime(); // Mon
|
|
102
|
+
|
|
103
|
+
// After Thu -> should be Fri (not Sat/Sun)
|
|
104
|
+
const afterThu = computeNextRunAt({ syntax: 'rrule', expression: expr }, thu + 1);
|
|
105
|
+
expect(afterThu).toBe(fri);
|
|
106
|
+
|
|
107
|
+
// After Fri -> should skip Sat+Sun, land on Mon
|
|
108
|
+
const afterFri = computeNextRunAt({ syntax: 'rrule', expression: expr }, fri + 1);
|
|
109
|
+
expect(afterFri).toBe(mon);
|
|
110
|
+
|
|
111
|
+
// Explicitly confirm Sat and Sun are never returned
|
|
112
|
+
expect(afterFri).not.toBe(sat);
|
|
113
|
+
expect(afterFri).not.toBe(sun);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('EXRULE with FREQ acts as repeating exclusion series', () => {
|
|
117
|
+
// RRULE: every day starting Jan 1 2099
|
|
118
|
+
// EXRULE: every 3rd day starting Jan 1 2099 (excludes Jan 1, Jan 4, Jan 7, ...)
|
|
119
|
+
const expr = [
|
|
120
|
+
'DTSTART:20990101T090000Z',
|
|
121
|
+
'RRULE:FREQ=DAILY;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
122
|
+
'EXRULE:FREQ=DAILY;INTERVAL=3;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
123
|
+
].join('\n');
|
|
124
|
+
|
|
125
|
+
expect(isValidScheduleExpression({ syntax: 'rrule', expression: expr })).toBe(true);
|
|
126
|
+
|
|
127
|
+
const jan1 = new Date('2099-01-01T09:00:00Z').getTime();
|
|
128
|
+
const jan2 = new Date('2099-01-02T09:00:00Z').getTime();
|
|
129
|
+
const jan3 = new Date('2099-01-03T09:00:00Z').getTime();
|
|
130
|
+
const jan4 = new Date('2099-01-04T09:00:00Z').getTime();
|
|
131
|
+
const jan5 = new Date('2099-01-05T09:00:00Z').getTime();
|
|
132
|
+
|
|
133
|
+
// Jan 1 is excluded (EXRULE fires on DTSTART). First occurrence after
|
|
134
|
+
// DTSTART should be Jan 2.
|
|
135
|
+
const first = computeNextRunAt({ syntax: 'rrule', expression: expr }, jan1 - 1);
|
|
136
|
+
expect(first).toBe(jan2);
|
|
137
|
+
|
|
138
|
+
// After Jan 2 -> Jan 3 (Jan 3 not excluded)
|
|
139
|
+
const second = computeNextRunAt({ syntax: 'rrule', expression: expr }, jan2 + 1);
|
|
140
|
+
expect(second).toBe(jan3);
|
|
141
|
+
|
|
142
|
+
// After Jan 3 -> should skip Jan 4 (excluded, 3 days after Jan 1) -> Jan 5
|
|
143
|
+
const third = computeNextRunAt({ syntax: 'rrule', expression: expr }, jan3 + 1);
|
|
144
|
+
expect(third).toBe(jan5);
|
|
145
|
+
expect(third).not.toBe(jan4);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('EXRULE does not affect non-matching occurrences', () => {
|
|
149
|
+
// RRULE: every weekday (Mon-Fri) at 09:00
|
|
150
|
+
// EXRULE: every Saturday at 09:00 (no overlap with weekday rule)
|
|
151
|
+
// Expected: all weekday occurrences remain intact
|
|
152
|
+
const expr = [
|
|
153
|
+
'DTSTART:20990105T090000Z',
|
|
154
|
+
'RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
155
|
+
'EXRULE:FREQ=WEEKLY;BYDAY=SA;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
156
|
+
].join('\n');
|
|
157
|
+
|
|
158
|
+
expect(isValidScheduleExpression({ syntax: 'rrule', expression: expr })).toBe(true);
|
|
159
|
+
|
|
160
|
+
// Jan 5 2099 is a Monday. Verify all five weekdays in the first week appear.
|
|
161
|
+
const mon = new Date('2099-01-05T09:00:00Z').getTime();
|
|
162
|
+
const tue = new Date('2099-01-06T09:00:00Z').getTime();
|
|
163
|
+
const wed = new Date('2099-01-07T09:00:00Z').getTime();
|
|
164
|
+
const thu = new Date('2099-01-08T09:00:00Z').getTime();
|
|
165
|
+
const fri = new Date('2099-01-09T09:00:00Z').getTime();
|
|
166
|
+
const nextMon = new Date('2099-01-12T09:00:00Z').getTime();
|
|
167
|
+
|
|
168
|
+
expect(computeNextRunAt({ syntax: 'rrule', expression: expr }, mon + 1)).toBe(tue);
|
|
169
|
+
expect(computeNextRunAt({ syntax: 'rrule', expression: expr }, tue + 1)).toBe(wed);
|
|
170
|
+
expect(computeNextRunAt({ syntax: 'rrule', expression: expr }, wed + 1)).toBe(thu);
|
|
171
|
+
expect(computeNextRunAt({ syntax: 'rrule', expression: expr }, thu + 1)).toBe(fri);
|
|
172
|
+
// After Fri -> skips weekend entirely (Sat excluded + Sun not in RRULE) -> Mon
|
|
173
|
+
expect(computeNextRunAt({ syntax: 'rrule', expression: expr }, fri + 1)).toBe(nextMon);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -59,6 +59,15 @@ describe('recurrence engine — rrule', () => {
|
|
|
59
59
|
expect(() => computeNextRunAt({ syntax: 'rrule', expression: 'RRULE:FREQ=DAILY' })).toThrow(/DTSTART/);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
+
test('preserves TZID parameter values when normalizing lowercase prefixes', () => {
|
|
63
|
+
// TZID contains case-sensitive timezone names (e.g. America/New_York)
|
|
64
|
+
// that must not be uppercased during prefix normalization.
|
|
65
|
+
const expr = 'dtstart;TZID=America/New_York:20990601T090000\nrrule:FREQ=DAILY';
|
|
66
|
+
expect(isValidScheduleExpression({ syntax: 'rrule', expression: expr })).toBe(true);
|
|
67
|
+
const next = computeNextRunAt({ syntax: 'rrule', expression: expr });
|
|
68
|
+
expect(next).toBeGreaterThan(Date.now());
|
|
69
|
+
});
|
|
70
|
+
|
|
62
71
|
test('computes next run for RRULE with EXDATE set construct', () => {
|
|
63
72
|
const expr = 'DTSTART:20990101T090000Z\nRRULE:FREQ=DAILY\nEXDATE:20990101T090000Z';
|
|
64
73
|
const next = computeNextRunAt({ syntax: 'rrule', expression: expr });
|
|
@@ -56,6 +56,14 @@ describe('normalizeScheduleSyntax', () => {
|
|
|
56
56
|
expect(result).toEqual({ syntax: 'cron', expression: '0 9 * * *' });
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
test('honors explicit syntax hint in legacyCronExpression fallback', () => {
|
|
60
|
+
const result = normalizeScheduleSyntax({
|
|
61
|
+
syntax: 'rrule',
|
|
62
|
+
legacyCronExpression: '0 9 * * *',
|
|
63
|
+
});
|
|
64
|
+
expect(result).toEqual({ syntax: 'rrule', expression: '0 9 * * *' });
|
|
65
|
+
});
|
|
66
|
+
|
|
59
67
|
test('returns null when nothing is provided', () => {
|
|
60
68
|
expect(normalizeScheduleSyntax({})).toBeNull();
|
|
61
69
|
});
|
|
@@ -176,7 +176,7 @@ describe('tool manifest', () => {
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
test('eager module tool names list contains expected count', () => {
|
|
179
|
-
expect(eagerModuleToolNames.length).toBe(
|
|
179
|
+
expect(eagerModuleToolNames.length).toBe(16);
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
test('explicit tools list includes memory, credential, watch, catalog, and discover tools', () => {
|
|
@@ -92,7 +92,7 @@ function makeFailingSession(errorMsg: string): Session {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/** Session whose agent loop emits a confirmation_request. */
|
|
95
|
-
function makeConfirmationSession(toolName: string
|
|
95
|
+
function makeConfirmationSession(toolName: string): Session {
|
|
96
96
|
let clientHandler: (msg: ServerMessage) => void = () => {};
|
|
97
97
|
return {
|
|
98
98
|
isProcessing: () => false,
|
|
@@ -110,9 +110,6 @@ function makeConfirmationSession(toolName: string, principal?: { kind?: string;
|
|
|
110
110
|
riskLevel: 'medium',
|
|
111
111
|
allowlistOptions: [],
|
|
112
112
|
scopeOptions: [],
|
|
113
|
-
principalKind: principal?.kind,
|
|
114
|
-
principalId: principal?.id,
|
|
115
|
-
principalVersion: principal?.version,
|
|
116
113
|
});
|
|
117
114
|
// Hang to simulate waiting for decision
|
|
118
115
|
await new Promise<void>(() => {});
|
|
@@ -241,27 +238,6 @@ describe('runtime runs — swarm lifecycle', () => {
|
|
|
241
238
|
expect(orchestrator.getRun('nonexistent-id')).toBeNull();
|
|
242
239
|
});
|
|
243
240
|
|
|
244
|
-
test('principal context flows through to pending confirmation', async () => {
|
|
245
|
-
const conversation = createConversation('principal context test');
|
|
246
|
-
const orchestrator = new RunOrchestrator({
|
|
247
|
-
getOrCreateSession: async () => makeConfirmationSession('bash', {
|
|
248
|
-
kind: 'skill',
|
|
249
|
-
id: 'weather-skill',
|
|
250
|
-
version: 'sha256:abcdef1234567890',
|
|
251
|
-
}),
|
|
252
|
-
resolveAttachments: () => [],
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
const run = await orchestrator.startRun(conversation.id, 'Run with principal');
|
|
256
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
257
|
-
|
|
258
|
-
const stored = orchestrator.getRun(run.id);
|
|
259
|
-
expect(stored?.status).toBe('needs_confirmation');
|
|
260
|
-
expect(stored?.pendingConfirmation?.principalKind).toBe('skill');
|
|
261
|
-
expect(stored?.pendingConfirmation?.principalId).toBe('weather-skill');
|
|
262
|
-
expect(stored?.pendingConfirmation?.principalVersion).toBe('sha256:abcdef1234567890');
|
|
263
|
-
});
|
|
264
|
-
|
|
265
241
|
test('submitDecision returns run_not_found for unknown run', () => {
|
|
266
242
|
const orchestrator = new RunOrchestrator({
|
|
267
243
|
getOrCreateSession: async () => makeCompletingSession(),
|
|
@@ -435,27 +435,29 @@ describe('claimDueSchedules', () => {
|
|
|
435
435
|
});
|
|
436
436
|
|
|
437
437
|
test('claims exhausted RRULE schedule and disables it', () => {
|
|
438
|
-
// COUNT=1
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
438
|
+
// COUNT=1 with a past DTSTART means the single occurrence has already
|
|
439
|
+
// passed, so computeNextRunAt returns null — triggering the exhaustion path.
|
|
440
|
+
// We insert directly via SQL because createSchedule validates that at least
|
|
441
|
+
// one future run exists, which would reject an already-exhausted schedule.
|
|
442
|
+
const yesterday = new Date(Date.now() - 86_400_000);
|
|
443
|
+
const dtstart = yesterday.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
|
|
444
|
+
const rrule = `DTSTART:${dtstart}\nRRULE:FREQ=DAILY;COUNT=1`;
|
|
445
|
+
const id = 'exhausted-rrule-test';
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
getRawDb().run(
|
|
448
|
+
`INSERT INTO cron_jobs (id, name, enabled, cron_expression, schedule_syntax, message, next_run_at, retry_count, created_by, created_at, updated_at)
|
|
449
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
450
|
+
[id, 'Finite RRULE', 1, rrule, 'rrule', 'one-shot', now - 1000, 0, 'agent', now, now],
|
|
451
|
+
);
|
|
450
452
|
|
|
451
453
|
const claimed = claimDueSchedules(Date.now());
|
|
452
454
|
expect(claimed.length).toBe(1);
|
|
453
|
-
expect(claimed[0].id).toBe(
|
|
455
|
+
expect(claimed[0].id).toBe(id);
|
|
454
456
|
expect(claimed[0].enabled).toBe(false);
|
|
455
457
|
expect(claimed[0].nextRunAt).toBe(0);
|
|
456
458
|
|
|
457
459
|
// Verify the schedule is disabled in the DB
|
|
458
|
-
const persisted = getSchedule(
|
|
460
|
+
const persisted = getSchedule(id);
|
|
459
461
|
expect(persisted!.enabled).toBe(false);
|
|
460
462
|
|
|
461
463
|
// A subsequent claim should not pick it up
|
|
@@ -420,6 +420,41 @@ describe('schedule_update with RRULE', () => {
|
|
|
420
420
|
expect(result.content).toContain('Syntax: rrule');
|
|
421
421
|
expect(result.content).toContain('RRULE:FREQ=DAILY');
|
|
422
422
|
});
|
|
423
|
+
|
|
424
|
+
test('auto-detects rrule syntax when updating expression without explicit syntax', async () => {
|
|
425
|
+
await executeScheduleCreate({
|
|
426
|
+
name: 'Auto-detect on update',
|
|
427
|
+
cron_expression: '0 9 * * *',
|
|
428
|
+
message: 'test',
|
|
429
|
+
}, ctx);
|
|
430
|
+
|
|
431
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
432
|
+
const result = await executeScheduleUpdate({
|
|
433
|
+
job_id: row.id,
|
|
434
|
+
expression: 'DTSTART:20250601T120000Z\nRRULE:FREQ=WEEKLY;BYDAY=MO',
|
|
435
|
+
}, ctx);
|
|
436
|
+
|
|
437
|
+
expect(result.isError).toBe(false);
|
|
438
|
+
expect(result.content).toContain('Syntax: rrule');
|
|
439
|
+
expect(result.content).toContain('RRULE:FREQ=WEEKLY');
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test('auto-detects cron syntax when updating expression without explicit syntax', async () => {
|
|
443
|
+
await executeScheduleCreate({
|
|
444
|
+
name: 'Cron auto-detect',
|
|
445
|
+
cron_expression: '0 9 * * *',
|
|
446
|
+
message: 'test',
|
|
447
|
+
}, ctx);
|
|
448
|
+
|
|
449
|
+
const row = getRawDb().query('SELECT id FROM cron_jobs LIMIT 1').get() as { id: string };
|
|
450
|
+
const result = await executeScheduleUpdate({
|
|
451
|
+
job_id: row.id,
|
|
452
|
+
expression: '30 17 * * 1-5',
|
|
453
|
+
}, ctx);
|
|
454
|
+
|
|
455
|
+
expect(result.isError).toBe(false);
|
|
456
|
+
expect(result.content).toContain('Syntax: cron');
|
|
457
|
+
});
|
|
423
458
|
});
|
|
424
459
|
|
|
425
460
|
describe('schedule_list with RRULE', () => {
|
|
@@ -657,6 +692,54 @@ describe('schedule_list with RRULE set', () => {
|
|
|
657
692
|
});
|
|
658
693
|
});
|
|
659
694
|
|
|
695
|
+
// ── EXRULE support in schedule tools ──────────────────────────────────
|
|
696
|
+
|
|
697
|
+
describe('schedule_create with RRULE + EXRULE', () => {
|
|
698
|
+
beforeEach(() => {
|
|
699
|
+
getRawDb().run('DELETE FROM cron_runs');
|
|
700
|
+
getRawDb().run('DELETE FROM cron_jobs');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test('creates a schedule with RRULE + EXRULE', async () => {
|
|
704
|
+
const expression = [
|
|
705
|
+
'DTSTART:20990101T090000Z',
|
|
706
|
+
'RRULE:FREQ=DAILY;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
707
|
+
'EXRULE:FREQ=WEEKLY;BYDAY=SA,SU;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
708
|
+
].join('\n');
|
|
709
|
+
|
|
710
|
+
const result = await executeScheduleCreate({
|
|
711
|
+
name: 'Weekday-only via EXRULE',
|
|
712
|
+
syntax: 'rrule',
|
|
713
|
+
expression,
|
|
714
|
+
message: 'EXRULE test',
|
|
715
|
+
}, ctx);
|
|
716
|
+
|
|
717
|
+
expect(result.isError).toBe(false);
|
|
718
|
+
expect(result.content).toContain('Schedule created successfully');
|
|
719
|
+
expect(result.content).toContain('Syntax: rrule');
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test('list output shows [RRULE set] label for EXRULE expression', async () => {
|
|
723
|
+
const expression = [
|
|
724
|
+
'DTSTART:20990101T090000Z',
|
|
725
|
+
'RRULE:FREQ=DAILY;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
726
|
+
'EXRULE:FREQ=WEEKLY;BYDAY=SA,SU;BYHOUR=9;BYMINUTE=0;BYSECOND=0',
|
|
727
|
+
].join('\n');
|
|
728
|
+
|
|
729
|
+
await executeScheduleCreate({
|
|
730
|
+
name: 'EXRULE Set Schedule',
|
|
731
|
+
syntax: 'rrule',
|
|
732
|
+
expression,
|
|
733
|
+
message: 'EXRULE set test',
|
|
734
|
+
}, ctx);
|
|
735
|
+
|
|
736
|
+
const result = await executeScheduleList({}, ctx);
|
|
737
|
+
|
|
738
|
+
expect(result.isError).toBe(false);
|
|
739
|
+
expect(result.content).toContain('[RRULE set]');
|
|
740
|
+
});
|
|
741
|
+
});
|
|
742
|
+
|
|
660
743
|
// ── schedule_delete ─────────────────────────────────────────────────
|
|
661
744
|
|
|
662
745
|
describe('schedule_delete tool', () => {
|