vellum 0.2.0 → 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 +161 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/app-bundler.test.ts +12 -33
- 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 +5 -8
- 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 +454 -0
- 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-state.test.ts +133 -0
- package/src/__tests__/call-store.test.ts +691 -0
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
- 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 +348 -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__/doordash-session.test.ts +9 -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 +96 -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 +17 -10
- 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 +222 -0
- package/src/__tests__/run-orchestrator.test.ts +7 -7
- package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
- package/src/__tests__/runtime-runs-http.test.ts +5 -23
- package/src/__tests__/runtime-runs.test.ts +11 -11
- 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 +89 -16
- 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 +273 -2
- 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 +403 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +35 -14
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +48 -0
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +390 -0
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-state.ts +64 -0
- package/src/calls/call-store.ts +416 -0
- package/src/calls/relay-server.ts +335 -0
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +34 -0
- package/src/calls/twilio-provider.ts +173 -0
- package/src/calls/twilio-routes.ts +250 -0
- package/src/calls/types.ts +37 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +256 -25
- 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 +163 -0
- 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.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
- 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 +44 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +218 -1
- package/src/config/system-prompt.ts +100 -6
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +5 -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 +192 -4
- 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 -271
- 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 +495 -39
- package/src/daemon/ipc-contract-inventory.json +40 -4
- package/src/daemon/ipc-contract.ts +185 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +48 -5
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +144 -29
- 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 +222 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1365
- 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 -1151
- package/src/media/gemini-image-service.ts +1 -1
- 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 +362 -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 +65 -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 +277 -25
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/routes/attachment-routes.ts +5 -6
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +12 -19
- package/src/runtime/routes/conversation-routes.ts +5 -9
- package/src/runtime/routes/run-routes.ts +4 -8
- package/src/runtime/run-orchestrator.ts +39 -6
- 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 +67 -0
- package/src/tools/calls/call-start.ts +73 -0
- package/src/tools/calls/call-status.ts +81 -0
- 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 +21 -5
- 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/registry.ts +2 -4
- 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 -6
- package/src/tools/tasks/task-delete.ts +69 -56
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +74 -102
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +192 -134
- package/src/tools/tasks/work-item-list.ts +33 -78
- package/src/tools/tasks/work-item-remove.ts +60 -0
- package/src/tools/tasks/work-item-update.ts +114 -0
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +20 -74
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +6 -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 +236 -2
- package/src/workspace/commit-message-enrichment-service.ts +284 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +272 -52
- package/src/workspace/heartbeat-service.ts +70 -13
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +100 -51
- 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,691 @@
|
|
|
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(), 'call-store-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
|
+
}));
|
|
19
|
+
|
|
20
|
+
mock.module('../util/logger.js', () => ({
|
|
21
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
22
|
+
get: () => () => {},
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
27
|
+
import { conversations } from '../memory/schema.js';
|
|
28
|
+
import {
|
|
29
|
+
createCallSession,
|
|
30
|
+
getCallSession,
|
|
31
|
+
getCallSessionByCallSid,
|
|
32
|
+
getActiveCallSessionForConversation,
|
|
33
|
+
updateCallSession,
|
|
34
|
+
recordCallEvent,
|
|
35
|
+
getCallEvents,
|
|
36
|
+
createPendingQuestion,
|
|
37
|
+
getPendingQuestion,
|
|
38
|
+
answerPendingQuestion,
|
|
39
|
+
expirePendingQuestions,
|
|
40
|
+
claimCallback,
|
|
41
|
+
releaseCallbackClaim,
|
|
42
|
+
finalizeCallbackClaim,
|
|
43
|
+
} from '../calls/call-store.js';
|
|
44
|
+
|
|
45
|
+
initializeDb();
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
resetDb();
|
|
49
|
+
try { rmSync(testDir, { recursive: true }); } catch { /* best effort */ }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** Ensure a conversation row exists for the given ID so FK constraints pass. */
|
|
53
|
+
let ensuredConvIds = new Set<string>();
|
|
54
|
+
function ensureConversation(id: string): void {
|
|
55
|
+
if (ensuredConvIds.has(id)) return;
|
|
56
|
+
const db = getDb();
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
db.insert(conversations).values({
|
|
59
|
+
id,
|
|
60
|
+
title: `Test conversation ${id}`,
|
|
61
|
+
createdAt: now,
|
|
62
|
+
updatedAt: now,
|
|
63
|
+
}).run();
|
|
64
|
+
ensuredConvIds.add(id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resetTables() {
|
|
68
|
+
const db = getDb();
|
|
69
|
+
db.run('DELETE FROM call_pending_questions');
|
|
70
|
+
db.run('DELETE FROM call_events');
|
|
71
|
+
db.run('DELETE FROM call_sessions');
|
|
72
|
+
db.run('DELETE FROM processed_callbacks');
|
|
73
|
+
db.run('DELETE FROM conversations');
|
|
74
|
+
ensuredConvIds = new Set();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Wrapper that ensures the FK conversation row exists before creating a session. */
|
|
78
|
+
function createTestCallSession(opts: Parameters<typeof createCallSession>[0]) {
|
|
79
|
+
ensureConversation(opts.conversationId);
|
|
80
|
+
return createCallSession(opts);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('call-store', () => {
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
resetTables();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ── Call Sessions ─────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
test('createCallSession creates a session with correct defaults', () => {
|
|
91
|
+
const session = createTestCallSession({
|
|
92
|
+
conversationId: 'conv-1',
|
|
93
|
+
provider: 'twilio',
|
|
94
|
+
fromNumber: '+15551234567',
|
|
95
|
+
toNumber: '+15559876543',
|
|
96
|
+
task: 'Book appointment',
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(session.id).toBeDefined();
|
|
100
|
+
expect(session.conversationId).toBe('conv-1');
|
|
101
|
+
expect(session.provider).toBe('twilio');
|
|
102
|
+
expect(session.fromNumber).toBe('+15551234567');
|
|
103
|
+
expect(session.toNumber).toBe('+15559876543');
|
|
104
|
+
expect(session.task).toBe('Book appointment');
|
|
105
|
+
expect(session.status).toBe('initiated');
|
|
106
|
+
expect(session.providerCallSid).toBeNull();
|
|
107
|
+
expect(session.startedAt).toBeNull();
|
|
108
|
+
expect(session.endedAt).toBeNull();
|
|
109
|
+
expect(session.lastError).toBeNull();
|
|
110
|
+
expect(typeof session.createdAt).toBe('number');
|
|
111
|
+
expect(typeof session.updatedAt).toBe('number');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('createCallSession defaults task to null when not provided', () => {
|
|
115
|
+
const session = createTestCallSession({
|
|
116
|
+
conversationId: 'conv-2',
|
|
117
|
+
provider: 'twilio',
|
|
118
|
+
fromNumber: '+15551111111',
|
|
119
|
+
toNumber: '+15552222222',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(session.task).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('getCallSession retrieves by ID', () => {
|
|
126
|
+
const created = createTestCallSession({
|
|
127
|
+
conversationId: 'conv-3',
|
|
128
|
+
provider: 'twilio',
|
|
129
|
+
fromNumber: '+15551111111',
|
|
130
|
+
toNumber: '+15552222222',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const retrieved = getCallSession(created.id);
|
|
134
|
+
expect(retrieved).not.toBeNull();
|
|
135
|
+
expect(retrieved!.id).toBe(created.id);
|
|
136
|
+
expect(retrieved!.conversationId).toBe('conv-3');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('getCallSession returns null for missing ID', () => {
|
|
140
|
+
const result = getCallSession('nonexistent-id');
|
|
141
|
+
expect(result).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('getCallSessionByCallSid looks up by provider call SID', () => {
|
|
145
|
+
const session = createTestCallSession({
|
|
146
|
+
conversationId: 'conv-4',
|
|
147
|
+
provider: 'twilio',
|
|
148
|
+
fromNumber: '+15551111111',
|
|
149
|
+
toNumber: '+15552222222',
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
updateCallSession(session.id, { providerCallSid: 'CA_test_sid_123' });
|
|
153
|
+
|
|
154
|
+
const found = getCallSessionByCallSid('CA_test_sid_123');
|
|
155
|
+
expect(found).not.toBeNull();
|
|
156
|
+
expect(found!.id).toBe(session.id);
|
|
157
|
+
expect(found!.providerCallSid).toBe('CA_test_sid_123');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('getCallSessionByCallSid returns null for unknown SID', () => {
|
|
161
|
+
const result = getCallSessionByCallSid('CA_unknown');
|
|
162
|
+
expect(result).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('getActiveCallSessionForConversation finds non-terminal sessions', () => {
|
|
166
|
+
const session = createTestCallSession({
|
|
167
|
+
conversationId: 'conv-5',
|
|
168
|
+
provider: 'twilio',
|
|
169
|
+
fromNumber: '+15551111111',
|
|
170
|
+
toNumber: '+15552222222',
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const active = getActiveCallSessionForConversation('conv-5');
|
|
174
|
+
expect(active).not.toBeNull();
|
|
175
|
+
expect(active!.id).toBe(session.id);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('getActiveCallSessionForConversation returns null when all sessions are completed', () => {
|
|
179
|
+
const session = createTestCallSession({
|
|
180
|
+
conversationId: 'conv-6',
|
|
181
|
+
provider: 'twilio',
|
|
182
|
+
fromNumber: '+15551111111',
|
|
183
|
+
toNumber: '+15552222222',
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
updateCallSession(session.id, { status: 'completed' });
|
|
187
|
+
|
|
188
|
+
const active = getActiveCallSessionForConversation('conv-6');
|
|
189
|
+
expect(active).toBeNull();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('getActiveCallSessionForConversation returns null when all sessions are failed', () => {
|
|
193
|
+
const session = createTestCallSession({
|
|
194
|
+
conversationId: 'conv-7',
|
|
195
|
+
provider: 'twilio',
|
|
196
|
+
fromNumber: '+15551111111',
|
|
197
|
+
toNumber: '+15552222222',
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
updateCallSession(session.id, { status: 'failed' });
|
|
201
|
+
|
|
202
|
+
const active = getActiveCallSessionForConversation('conv-7');
|
|
203
|
+
expect(active).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('getActiveCallSessionForConversation returns most recent active session', () => {
|
|
207
|
+
// Create two sessions for the same conversation
|
|
208
|
+
const older = createTestCallSession({
|
|
209
|
+
conversationId: 'conv-8',
|
|
210
|
+
provider: 'twilio',
|
|
211
|
+
fromNumber: '+15551111111',
|
|
212
|
+
toNumber: '+15552222222',
|
|
213
|
+
});
|
|
214
|
+
// Mark older as completed
|
|
215
|
+
updateCallSession(older.id, { status: 'completed' });
|
|
216
|
+
|
|
217
|
+
const newer = createTestCallSession({
|
|
218
|
+
conversationId: 'conv-8',
|
|
219
|
+
provider: 'twilio',
|
|
220
|
+
fromNumber: '+15551111111',
|
|
221
|
+
toNumber: '+15553333333',
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const active = getActiveCallSessionForConversation('conv-8');
|
|
225
|
+
expect(active).not.toBeNull();
|
|
226
|
+
expect(active!.id).toBe(newer.id);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('updateCallSession updates status, providerCallSid, and timestamps', () => {
|
|
230
|
+
const session = createTestCallSession({
|
|
231
|
+
conversationId: 'conv-9',
|
|
232
|
+
provider: 'twilio',
|
|
233
|
+
fromNumber: '+15551111111',
|
|
234
|
+
toNumber: '+15552222222',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
updateCallSession(session.id, {
|
|
239
|
+
status: 'in_progress',
|
|
240
|
+
providerCallSid: 'CA_updated_sid',
|
|
241
|
+
startedAt: now,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const updated = getCallSession(session.id);
|
|
245
|
+
expect(updated).not.toBeNull();
|
|
246
|
+
expect(updated!.status).toBe('in_progress');
|
|
247
|
+
expect(updated!.providerCallSid).toBe('CA_updated_sid');
|
|
248
|
+
expect(updated!.startedAt).toBe(now);
|
|
249
|
+
// updatedAt should be updated
|
|
250
|
+
expect(updated!.updatedAt).toBeGreaterThanOrEqual(session.updatedAt);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('updateCallSession sets endedAt and lastError', () => {
|
|
254
|
+
const session = createTestCallSession({
|
|
255
|
+
conversationId: 'conv-10',
|
|
256
|
+
provider: 'twilio',
|
|
257
|
+
fromNumber: '+15551111111',
|
|
258
|
+
toNumber: '+15552222222',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const endTime = Date.now();
|
|
262
|
+
updateCallSession(session.id, {
|
|
263
|
+
status: 'failed',
|
|
264
|
+
endedAt: endTime,
|
|
265
|
+
lastError: 'Network timeout',
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const updated = getCallSession(session.id);
|
|
269
|
+
expect(updated!.status).toBe('failed');
|
|
270
|
+
expect(updated!.endedAt).toBe(endTime);
|
|
271
|
+
expect(updated!.lastError).toBe('Network timeout');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// ── Call Events ───────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
test('recordCallEvent creates events with correct fields', () => {
|
|
277
|
+
const session = createTestCallSession({
|
|
278
|
+
conversationId: 'conv-11',
|
|
279
|
+
provider: 'twilio',
|
|
280
|
+
fromNumber: '+15551111111',
|
|
281
|
+
toNumber: '+15552222222',
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const event = recordCallEvent(session.id, 'call_started', { twilioStatus: 'initiated' });
|
|
285
|
+
|
|
286
|
+
expect(event.id).toBeDefined();
|
|
287
|
+
expect(event.callSessionId).toBe(session.id);
|
|
288
|
+
expect(event.eventType).toBe('call_started');
|
|
289
|
+
expect(typeof event.createdAt).toBe('number');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('recordCallEvent stores JSON payload', () => {
|
|
293
|
+
const session = createTestCallSession({
|
|
294
|
+
conversationId: 'conv-12',
|
|
295
|
+
provider: 'twilio',
|
|
296
|
+
fromNumber: '+15551111111',
|
|
297
|
+
toNumber: '+15552222222',
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const payload = { text: 'Hello, how are you?', lang: 'en-US' };
|
|
301
|
+
const event = recordCallEvent(session.id, 'caller_spoke', payload);
|
|
302
|
+
|
|
303
|
+
const parsed = JSON.parse(event.payloadJson);
|
|
304
|
+
expect(parsed.text).toBe('Hello, how are you?');
|
|
305
|
+
expect(parsed.lang).toBe('en-US');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('recordCallEvent defaults payload to empty JSON object', () => {
|
|
309
|
+
const session = createTestCallSession({
|
|
310
|
+
conversationId: 'conv-13',
|
|
311
|
+
provider: 'twilio',
|
|
312
|
+
fromNumber: '+15551111111',
|
|
313
|
+
toNumber: '+15552222222',
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const event = recordCallEvent(session.id, 'call_connected');
|
|
317
|
+
|
|
318
|
+
expect(event.payloadJson).toBe('{}');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('getCallEvents retrieves events in creation order', () => {
|
|
322
|
+
const session = createTestCallSession({
|
|
323
|
+
conversationId: 'conv-14',
|
|
324
|
+
provider: 'twilio',
|
|
325
|
+
fromNumber: '+15551111111',
|
|
326
|
+
toNumber: '+15552222222',
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
recordCallEvent(session.id, 'call_started');
|
|
330
|
+
recordCallEvent(session.id, 'call_connected');
|
|
331
|
+
recordCallEvent(session.id, 'caller_spoke', { transcript: 'Hi' });
|
|
332
|
+
|
|
333
|
+
const events = getCallEvents(session.id);
|
|
334
|
+
expect(events).toHaveLength(3);
|
|
335
|
+
expect(events[0].eventType).toBe('call_started');
|
|
336
|
+
expect(events[1].eventType).toBe('call_connected');
|
|
337
|
+
expect(events[2].eventType).toBe('caller_spoke');
|
|
338
|
+
// Should be in ascending creation order
|
|
339
|
+
expect(events[0].createdAt).toBeLessThanOrEqual(events[1].createdAt);
|
|
340
|
+
expect(events[1].createdAt).toBeLessThanOrEqual(events[2].createdAt);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('getCallEvents returns empty array for session with no events', () => {
|
|
344
|
+
const session = createTestCallSession({
|
|
345
|
+
conversationId: 'conv-15',
|
|
346
|
+
provider: 'twilio',
|
|
347
|
+
fromNumber: '+15551111111',
|
|
348
|
+
toNumber: '+15552222222',
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const events = getCallEvents(session.id);
|
|
352
|
+
expect(events).toHaveLength(0);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ── Pending Questions ─────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
test('createPendingQuestion creates with status pending', () => {
|
|
358
|
+
const session = createTestCallSession({
|
|
359
|
+
conversationId: 'conv-16',
|
|
360
|
+
provider: 'twilio',
|
|
361
|
+
fromNumber: '+15551111111',
|
|
362
|
+
toNumber: '+15552222222',
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const question = createPendingQuestion(session.id, 'What is your preferred date?');
|
|
366
|
+
|
|
367
|
+
expect(question.id).toBeDefined();
|
|
368
|
+
expect(question.callSessionId).toBe(session.id);
|
|
369
|
+
expect(question.questionText).toBe('What is your preferred date?');
|
|
370
|
+
expect(question.status).toBe('pending');
|
|
371
|
+
expect(typeof question.askedAt).toBe('number');
|
|
372
|
+
expect(question.answeredAt).toBeNull();
|
|
373
|
+
expect(question.answerText).toBeNull();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test('getPendingQuestion finds pending question for session', () => {
|
|
377
|
+
const session = createTestCallSession({
|
|
378
|
+
conversationId: 'conv-17',
|
|
379
|
+
provider: 'twilio',
|
|
380
|
+
fromNumber: '+15551111111',
|
|
381
|
+
toNumber: '+15552222222',
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const created = createPendingQuestion(session.id, 'What is your name?');
|
|
385
|
+
|
|
386
|
+
const found = getPendingQuestion(session.id);
|
|
387
|
+
expect(found).not.toBeNull();
|
|
388
|
+
expect(found!.id).toBe(created.id);
|
|
389
|
+
expect(found!.questionText).toBe('What is your name?');
|
|
390
|
+
expect(found!.status).toBe('pending');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test('getPendingQuestion returns null when no pending questions', () => {
|
|
394
|
+
const session = createTestCallSession({
|
|
395
|
+
conversationId: 'conv-18',
|
|
396
|
+
provider: 'twilio',
|
|
397
|
+
fromNumber: '+15551111111',
|
|
398
|
+
toNumber: '+15552222222',
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const found = getPendingQuestion(session.id);
|
|
402
|
+
expect(found).toBeNull();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test('answerPendingQuestion updates status to answered', () => {
|
|
406
|
+
const session = createTestCallSession({
|
|
407
|
+
conversationId: 'conv-19',
|
|
408
|
+
provider: 'twilio',
|
|
409
|
+
fromNumber: '+15551111111',
|
|
410
|
+
toNumber: '+15552222222',
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const question = createPendingQuestion(session.id, 'What color?');
|
|
414
|
+
answerPendingQuestion(question.id, 'Blue');
|
|
415
|
+
|
|
416
|
+
// Should no longer appear as pending
|
|
417
|
+
const pending = getPendingQuestion(session.id);
|
|
418
|
+
expect(pending).toBeNull();
|
|
419
|
+
|
|
420
|
+
// Verify the record was updated by querying directly
|
|
421
|
+
const db = getDb();
|
|
422
|
+
const raw = (db as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
423
|
+
const updated = raw.query('SELECT * FROM call_pending_questions WHERE id = ?').get(question.id) as {
|
|
424
|
+
status: string;
|
|
425
|
+
answer_text: string;
|
|
426
|
+
answered_at: number;
|
|
427
|
+
};
|
|
428
|
+
expect(updated.status).toBe('answered');
|
|
429
|
+
expect(updated.answer_text).toBe('Blue');
|
|
430
|
+
expect(typeof updated.answered_at).toBe('number');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('expirePendingQuestions marks all pending questions as expired', () => {
|
|
434
|
+
const session = createTestCallSession({
|
|
435
|
+
conversationId: 'conv-20',
|
|
436
|
+
provider: 'twilio',
|
|
437
|
+
fromNumber: '+15551111111',
|
|
438
|
+
toNumber: '+15552222222',
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
createPendingQuestion(session.id, 'Question 1');
|
|
442
|
+
createPendingQuestion(session.id, 'Question 2');
|
|
443
|
+
|
|
444
|
+
expirePendingQuestions(session.id);
|
|
445
|
+
|
|
446
|
+
// No more pending questions
|
|
447
|
+
const pending = getPendingQuestion(session.id);
|
|
448
|
+
expect(pending).toBeNull();
|
|
449
|
+
|
|
450
|
+
// Verify both were expired
|
|
451
|
+
const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
452
|
+
const rows = raw.query('SELECT status FROM call_pending_questions WHERE call_session_id = ?').all(session.id) as Array<{ status: string }>;
|
|
453
|
+
expect(rows).toHaveLength(2);
|
|
454
|
+
for (const row of rows) {
|
|
455
|
+
expect(row.status).toBe('expired');
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test('expirePendingQuestions does not affect already-answered questions', () => {
|
|
460
|
+
const session = createTestCallSession({
|
|
461
|
+
conversationId: 'conv-21',
|
|
462
|
+
provider: 'twilio',
|
|
463
|
+
fromNumber: '+15551111111',
|
|
464
|
+
toNumber: '+15552222222',
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const q1 = createPendingQuestion(session.id, 'Question 1');
|
|
468
|
+
createPendingQuestion(session.id, 'Question 2');
|
|
469
|
+
|
|
470
|
+
// Answer q1 first
|
|
471
|
+
answerPendingQuestion(q1.id, 'Answer 1');
|
|
472
|
+
|
|
473
|
+
// Then expire all pending
|
|
474
|
+
expirePendingQuestions(session.id);
|
|
475
|
+
|
|
476
|
+
// q1 should still be answered, not expired
|
|
477
|
+
const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
478
|
+
const q1Row = raw.query('SELECT status FROM call_pending_questions WHERE id = ?').get(q1.id) as { status: string };
|
|
479
|
+
expect(q1Row.status).toBe('answered');
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// ── Callback Claim ──────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
test('claimCallback returns a claim ID on first call', () => {
|
|
485
|
+
const session = createTestCallSession({
|
|
486
|
+
conversationId: 'conv-22',
|
|
487
|
+
provider: 'twilio',
|
|
488
|
+
fromNumber: '+15551111111',
|
|
489
|
+
toNumber: '+15552222222',
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const result = claimCallback('test-dedupe-key-1', session.id);
|
|
493
|
+
expect(result).toBeTypeOf('string');
|
|
494
|
+
expect(result!.length).toBeGreaterThan(0);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test('claimCallback returns null on duplicate key', () => {
|
|
498
|
+
const session = createTestCallSession({
|
|
499
|
+
conversationId: 'conv-23',
|
|
500
|
+
provider: 'twilio',
|
|
501
|
+
fromNumber: '+15551111111',
|
|
502
|
+
toNumber: '+15552222222',
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const first = claimCallback('test-dedupe-key-2', session.id);
|
|
506
|
+
const second = claimCallback('test-dedupe-key-2', session.id);
|
|
507
|
+
|
|
508
|
+
expect(first).toBeTypeOf('string');
|
|
509
|
+
expect(second).toBeNull();
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test('releaseCallbackClaim allows re-claim', () => {
|
|
513
|
+
const session = createTestCallSession({
|
|
514
|
+
conversationId: 'conv-24',
|
|
515
|
+
provider: 'twilio',
|
|
516
|
+
fromNumber: '+15551111111',
|
|
517
|
+
toNumber: '+15552222222',
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const first = claimCallback('test-dedupe-key-3', session.id);
|
|
521
|
+
expect(first).toBeTypeOf('string');
|
|
522
|
+
|
|
523
|
+
releaseCallbackClaim('test-dedupe-key-3', first!);
|
|
524
|
+
|
|
525
|
+
const second = claimCallback('test-dedupe-key-3', session.id);
|
|
526
|
+
expect(second).toBeTypeOf('string');
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
test('releaseCallbackClaim with wrong claimId does not release', () => {
|
|
530
|
+
const session = createTestCallSession({
|
|
531
|
+
conversationId: 'conv-24b',
|
|
532
|
+
provider: 'twilio',
|
|
533
|
+
fromNumber: '+15551111111',
|
|
534
|
+
toNumber: '+15552222222',
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const claimId = claimCallback('test-dedupe-key-3b', session.id);
|
|
538
|
+
expect(claimId).toBeTypeOf('string');
|
|
539
|
+
|
|
540
|
+
// Attempt to release with a wrong claim ID — should be a no-op
|
|
541
|
+
releaseCallbackClaim('test-dedupe-key-3b', 'wrong-claim-id');
|
|
542
|
+
|
|
543
|
+
// The claim should still be held, so re-claiming should fail
|
|
544
|
+
const second = claimCallback('test-dedupe-key-3b', session.id);
|
|
545
|
+
expect(second).toBeNull();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test('claimCallback INSERT OR IGNORE pattern is safe for same key', () => {
|
|
549
|
+
const session = createTestCallSession({
|
|
550
|
+
conversationId: 'conv-25',
|
|
551
|
+
provider: 'twilio',
|
|
552
|
+
fromNumber: '+15551111111',
|
|
553
|
+
toNumber: '+15552222222',
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Claim the key
|
|
557
|
+
const first = claimCallback('test-dedupe-key-4', session.id);
|
|
558
|
+
expect(first).toBeTypeOf('string');
|
|
559
|
+
|
|
560
|
+
// Subsequent claims with the same key should all return null without throwing
|
|
561
|
+
expect(claimCallback('test-dedupe-key-4', session.id)).toBeNull();
|
|
562
|
+
expect(claimCallback('test-dedupe-key-4', session.id)).toBeNull();
|
|
563
|
+
|
|
564
|
+
// Only one row should exist in the table for this key
|
|
565
|
+
const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
566
|
+
const rows = raw.query('SELECT COUNT(*) as cnt FROM processed_callbacks WHERE dedupe_key = ?').get('test-dedupe-key-4') as { cnt: number };
|
|
567
|
+
expect(rows.cnt).toBe(1);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test('claimCallback reclaims expired orphaned claims', () => {
|
|
571
|
+
const session = createTestCallSession({
|
|
572
|
+
conversationId: 'conv-26',
|
|
573
|
+
provider: 'twilio',
|
|
574
|
+
fromNumber: '+15551111111',
|
|
575
|
+
toNumber: '+15552222222',
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// Claim the key
|
|
579
|
+
const first = claimCallback('test-dedupe-key-expired', session.id);
|
|
580
|
+
expect(first).toBeTypeOf('string');
|
|
581
|
+
|
|
582
|
+
// Simulate an orphaned claim by backdating the created_at to well past expiry
|
|
583
|
+
const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
584
|
+
const oldTimestamp = Date.now() - 120_000; // 2 minutes ago, well past 60s expiry
|
|
585
|
+
raw.query('UPDATE processed_callbacks SET created_at = ? WHERE dedupe_key = ?').run(oldTimestamp, 'test-dedupe-key-expired');
|
|
586
|
+
|
|
587
|
+
// Reclaim should succeed because the old claim has expired
|
|
588
|
+
const second = claimCallback('test-dedupe-key-expired', session.id);
|
|
589
|
+
expect(second).toBeTypeOf('string');
|
|
590
|
+
|
|
591
|
+
// The new claim should have a different claim ID
|
|
592
|
+
expect(second).not.toBe(first);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test('claimCallback does not reclaim finalized claims', () => {
|
|
596
|
+
const session = createTestCallSession({
|
|
597
|
+
conversationId: 'conv-27',
|
|
598
|
+
provider: 'twilio',
|
|
599
|
+
fromNumber: '+15551111111',
|
|
600
|
+
toNumber: '+15552222222',
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Claim and finalize
|
|
604
|
+
const first = claimCallback('test-dedupe-key-finalized', session.id);
|
|
605
|
+
expect(first).toBeTypeOf('string');
|
|
606
|
+
finalizeCallbackClaim('test-dedupe-key-finalized', first!);
|
|
607
|
+
|
|
608
|
+
// Attempting to reclaim a finalized key should fail because the far-future
|
|
609
|
+
// timestamp means it will never be considered expired
|
|
610
|
+
const second = claimCallback('test-dedupe-key-finalized', session.id);
|
|
611
|
+
expect(second).toBeNull();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test('finalizeCallbackClaim makes claim permanent', () => {
|
|
615
|
+
const session = createTestCallSession({
|
|
616
|
+
conversationId: 'conv-28',
|
|
617
|
+
provider: 'twilio',
|
|
618
|
+
fromNumber: '+15551111111',
|
|
619
|
+
toNumber: '+15552222222',
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// Claim and finalize
|
|
623
|
+
const claimId = claimCallback('test-dedupe-key-permanent', session.id)!;
|
|
624
|
+
finalizeCallbackClaim('test-dedupe-key-permanent', claimId);
|
|
625
|
+
|
|
626
|
+
// Verify the created_at is set far in the future
|
|
627
|
+
const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
628
|
+
const row = raw.query('SELECT created_at FROM processed_callbacks WHERE dedupe_key = ?').get('test-dedupe-key-permanent') as { created_at: number };
|
|
629
|
+
// Should be at least 50 years in the future from now
|
|
630
|
+
const fiftyYearsMs = 50 * 365 * 24 * 60 * 60 * 1000;
|
|
631
|
+
expect(row.created_at).toBeGreaterThan(Date.now() + fiftyYearsMs);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test('finalizeCallbackClaim with wrong claimId does not finalize', () => {
|
|
635
|
+
const session = createTestCallSession({
|
|
636
|
+
conversationId: 'conv-28b',
|
|
637
|
+
provider: 'twilio',
|
|
638
|
+
fromNumber: '+15551111111',
|
|
639
|
+
toNumber: '+15552222222',
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Claim the key
|
|
643
|
+
const claimId = claimCallback('test-dedupe-key-permanent-b', session.id)!;
|
|
644
|
+
expect(claimId).toBeTypeOf('string');
|
|
645
|
+
|
|
646
|
+
// Try to finalize with wrong claimId — should be a no-op
|
|
647
|
+
finalizeCallbackClaim('test-dedupe-key-permanent-b', 'wrong-claim-id');
|
|
648
|
+
|
|
649
|
+
// Verify the created_at was NOT set to far-future (it should still be close to now)
|
|
650
|
+
const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
651
|
+
const row = raw.query('SELECT created_at FROM processed_callbacks WHERE dedupe_key = ?').get('test-dedupe-key-permanent-b') as { created_at: number };
|
|
652
|
+
const oneMinuteMs = 60 * 1000;
|
|
653
|
+
expect(row.created_at).toBeLessThan(Date.now() + oneMinuteMs);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test('handler A cannot release handler B claim after reclaim', () => {
|
|
657
|
+
const session = createTestCallSession({
|
|
658
|
+
conversationId: 'conv-29',
|
|
659
|
+
provider: 'twilio',
|
|
660
|
+
fromNumber: '+15551111111',
|
|
661
|
+
toNumber: '+15552222222',
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Handler A claims
|
|
665
|
+
const claimA = claimCallback('test-dedupe-key-ownership', session.id)!;
|
|
666
|
+
expect(claimA).toBeTypeOf('string');
|
|
667
|
+
|
|
668
|
+
// Simulate handler A taking too long: backdate the claim so it expires
|
|
669
|
+
const raw = (getDb() as unknown as { $client: import('bun:sqlite').Database }).$client;
|
|
670
|
+
const oldTimestamp = Date.now() - 120_000;
|
|
671
|
+
raw.query('UPDATE processed_callbacks SET created_at = ? WHERE dedupe_key = ?').run(oldTimestamp, 'test-dedupe-key-ownership');
|
|
672
|
+
|
|
673
|
+
// Handler B reclaims (succeeds because the old claim expired)
|
|
674
|
+
const claimB = claimCallback('test-dedupe-key-ownership', session.id)!;
|
|
675
|
+
expect(claimB).toBeTypeOf('string');
|
|
676
|
+
expect(claimB).not.toBe(claimA);
|
|
677
|
+
|
|
678
|
+
// Handler B finalizes
|
|
679
|
+
finalizeCallbackClaim('test-dedupe-key-ownership', claimB);
|
|
680
|
+
|
|
681
|
+
// Handler A tries to release using its old claimId — should be a no-op
|
|
682
|
+
releaseCallbackClaim('test-dedupe-key-ownership', claimA);
|
|
683
|
+
|
|
684
|
+
// Verify B's finalized claim is still intact
|
|
685
|
+
const row = raw.query('SELECT created_at, claim_id FROM processed_callbacks WHERE dedupe_key = ?').get('test-dedupe-key-ownership') as { created_at: number; claim_id: string };
|
|
686
|
+
expect(row).not.toBeNull();
|
|
687
|
+
expect(row.claim_id).toBe(claimB);
|
|
688
|
+
const fiftyYearsMs = 50 * 365 * 24 * 60 * 60 * 1000;
|
|
689
|
+
expect(row.created_at).toBeGreaterThan(Date.now() + fiftyYearsMs);
|
|
690
|
+
});
|
|
691
|
+
});
|
|
@@ -71,7 +71,7 @@ describe('cliDiscoverTool', () => {
|
|
|
71
71
|
expect(result.isError).toBe(false);
|
|
72
72
|
// Should at least find git which is nearly universally available
|
|
73
73
|
expect(result.content).toContain('**git**');
|
|
74
|
-
},
|
|
74
|
+
}, 60_000);
|
|
75
75
|
|
|
76
76
|
test('includes version info for found CLIs', async () => {
|
|
77
77
|
const result = await cliDiscoverTool.execute(
|