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,454 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterAll, mock, type Mock } from 'bun:test';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
6
|
+
|
|
7
|
+
const testDir = mkdtempSync(join(tmpdir(), 'call-orchestrator-test-'));
|
|
8
|
+
|
|
9
|
+
// ── Platform + logger mocks (must come before any source imports) ────
|
|
10
|
+
|
|
11
|
+
mock.module('../util/platform.js', () => ({
|
|
12
|
+
getDataDir: () => testDir,
|
|
13
|
+
isMacOS: () => process.platform === 'darwin',
|
|
14
|
+
isLinux: () => process.platform === 'linux',
|
|
15
|
+
isWindows: () => process.platform === 'win32',
|
|
16
|
+
getSocketPath: () => join(testDir, 'test.sock'),
|
|
17
|
+
getPidPath: () => join(testDir, 'test.pid'),
|
|
18
|
+
getDbPath: () => join(testDir, 'test.db'),
|
|
19
|
+
getLogPath: () => join(testDir, 'test.log'),
|
|
20
|
+
ensureDataDir: () => {},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
mock.module('../util/logger.js', () => ({
|
|
24
|
+
getLogger: () =>
|
|
25
|
+
new Proxy({} as Record<string, unknown>, {
|
|
26
|
+
get: () => () => {},
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// ── Config mock ─────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
mock.module('../config/loader.js', () => ({
|
|
33
|
+
getConfig: () => ({
|
|
34
|
+
apiKeys: { anthropic: 'test-key' },
|
|
35
|
+
calls: {
|
|
36
|
+
enabled: true,
|
|
37
|
+
provider: 'twilio',
|
|
38
|
+
maxDurationSeconds: 12 * 60,
|
|
39
|
+
userConsultTimeoutSeconds: 90,
|
|
40
|
+
userConsultationTimeoutSeconds: 90,
|
|
41
|
+
silenceTimeoutSeconds: 30,
|
|
42
|
+
disclosure: { enabled: false, text: '' },
|
|
43
|
+
safety: { denyCategories: [] },
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
// ── Helpers for building mock streaming responses ───────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a mock Anthropic stream object that emits 'text' events
|
|
52
|
+
* for each token and resolves `finalMessage()` with the full response.
|
|
53
|
+
*/
|
|
54
|
+
function createMockStream(tokens: string[]) {
|
|
55
|
+
const emitter = new EventEmitter();
|
|
56
|
+
const fullText = tokens.join('');
|
|
57
|
+
|
|
58
|
+
const stream = {
|
|
59
|
+
on: (event: string, handler: (...args: unknown[]) => void) => {
|
|
60
|
+
emitter.on(event, handler);
|
|
61
|
+
return stream;
|
|
62
|
+
},
|
|
63
|
+
finalMessage: () => {
|
|
64
|
+
// Emit tokens synchronously so the on('text') handler has fired
|
|
65
|
+
// before finalMessage resolves.
|
|
66
|
+
for (const token of tokens) {
|
|
67
|
+
emitter.emit('text', token);
|
|
68
|
+
}
|
|
69
|
+
return Promise.resolve({
|
|
70
|
+
content: [{ type: 'text', text: fullText }],
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return stream;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Anthropic SDK mock ──────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
let mockStreamFn: Mock<(...args: unknown[]) => unknown>;
|
|
81
|
+
|
|
82
|
+
mock.module('@anthropic-ai/sdk', () => {
|
|
83
|
+
mockStreamFn = mock((..._args: unknown[]) => createMockStream(['Hello', ' there']));
|
|
84
|
+
return {
|
|
85
|
+
default: class MockAnthropic {
|
|
86
|
+
messages = {
|
|
87
|
+
stream: (...args: unknown[]) => mockStreamFn(...args),
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ── Import source modules after all mocks are registered ────────────
|
|
94
|
+
|
|
95
|
+
import { initializeDb, getDb, resetDb } from '../memory/db.js';
|
|
96
|
+
import { conversations } from '../memory/schema.js';
|
|
97
|
+
import {
|
|
98
|
+
createCallSession,
|
|
99
|
+
getCallSession,
|
|
100
|
+
getPendingQuestion,
|
|
101
|
+
updateCallSession,
|
|
102
|
+
} from '../calls/call-store.js';
|
|
103
|
+
import {
|
|
104
|
+
getCallOrchestrator,
|
|
105
|
+
} from '../calls/call-state.js';
|
|
106
|
+
import { CallOrchestrator } from '../calls/call-orchestrator.js';
|
|
107
|
+
import type { RelayConnection } from '../calls/relay-server.js';
|
|
108
|
+
|
|
109
|
+
initializeDb();
|
|
110
|
+
|
|
111
|
+
afterAll(() => {
|
|
112
|
+
resetDb();
|
|
113
|
+
try {
|
|
114
|
+
rmSync(testDir, { recursive: true });
|
|
115
|
+
} catch {
|
|
116
|
+
/* best effort */
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ── RelayConnection mock factory ────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
interface MockRelay extends RelayConnection {
|
|
123
|
+
sentTokens: Array<{ token: string; last: boolean }>;
|
|
124
|
+
endCalled: boolean;
|
|
125
|
+
endReason: string | undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createMockRelay(): MockRelay {
|
|
129
|
+
const state = {
|
|
130
|
+
sentTokens: [] as Array<{ token: string; last: boolean }>,
|
|
131
|
+
_endCalled: false,
|
|
132
|
+
_endReason: undefined as string | undefined,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
get sentTokens() { return state.sentTokens; },
|
|
137
|
+
get endCalled() { return state._endCalled; },
|
|
138
|
+
get endReason() { return state._endReason; },
|
|
139
|
+
sendTextToken(token: string, last: boolean) {
|
|
140
|
+
state.sentTokens.push({ token, last });
|
|
141
|
+
},
|
|
142
|
+
endSession(reason?: string) {
|
|
143
|
+
state._endCalled = true;
|
|
144
|
+
state._endReason = reason;
|
|
145
|
+
},
|
|
146
|
+
} as unknown as MockRelay;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
let ensuredConvIds = new Set<string>();
|
|
152
|
+
function ensureConversation(id: string): void {
|
|
153
|
+
if (ensuredConvIds.has(id)) return;
|
|
154
|
+
const db = getDb();
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
db.insert(conversations).values({
|
|
157
|
+
id,
|
|
158
|
+
title: `Test conversation ${id}`,
|
|
159
|
+
createdAt: now,
|
|
160
|
+
updatedAt: now,
|
|
161
|
+
}).run();
|
|
162
|
+
ensuredConvIds.add(id);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function resetTables() {
|
|
166
|
+
const db = getDb();
|
|
167
|
+
db.run('DELETE FROM call_pending_questions');
|
|
168
|
+
db.run('DELETE FROM call_events');
|
|
169
|
+
db.run('DELETE FROM call_sessions');
|
|
170
|
+
db.run('DELETE FROM conversations');
|
|
171
|
+
ensuredConvIds = new Set();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Create a call session and an orchestrator wired to a mock relay.
|
|
176
|
+
*/
|
|
177
|
+
function setupOrchestrator(task?: string) {
|
|
178
|
+
ensureConversation('conv-orch-test');
|
|
179
|
+
const session = createCallSession({
|
|
180
|
+
conversationId: 'conv-orch-test',
|
|
181
|
+
provider: 'twilio',
|
|
182
|
+
fromNumber: '+15551111111',
|
|
183
|
+
toNumber: '+15552222222',
|
|
184
|
+
task,
|
|
185
|
+
});
|
|
186
|
+
updateCallSession(session.id, { status: 'in_progress' });
|
|
187
|
+
const relay = createMockRelay();
|
|
188
|
+
const orchestrator = new CallOrchestrator(session.id, relay as unknown as RelayConnection, task ?? null);
|
|
189
|
+
return { session, relay, orchestrator };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
describe('call-orchestrator', () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
resetTables();
|
|
195
|
+
// Reset the stream mock to default behaviour
|
|
196
|
+
mockStreamFn.mockImplementation(() => createMockStream(['Hello', ' there']));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ── handleCallerUtterance ─────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
test('handleCallerUtterance: streams tokens via sendTextToken', async () => {
|
|
202
|
+
mockStreamFn.mockImplementation(() => createMockStream(['Hi', ', how', ' are you?']));
|
|
203
|
+
const { relay, orchestrator } = setupOrchestrator();
|
|
204
|
+
|
|
205
|
+
await orchestrator.handleCallerUtterance('Hello');
|
|
206
|
+
|
|
207
|
+
// Verify tokens were sent to the relay
|
|
208
|
+
const nonEmptyTokens = relay.sentTokens.filter((t) => t.token.length > 0);
|
|
209
|
+
expect(nonEmptyTokens.length).toBeGreaterThan(0);
|
|
210
|
+
// The last token should have last=true (empty string token signaling end)
|
|
211
|
+
const lastToken = relay.sentTokens[relay.sentTokens.length - 1];
|
|
212
|
+
expect(lastToken.last).toBe(true);
|
|
213
|
+
|
|
214
|
+
orchestrator.destroy();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('handleCallerUtterance: sends last=true at end of turn', async () => {
|
|
218
|
+
mockStreamFn.mockImplementation(() => createMockStream(['Simple response.']));
|
|
219
|
+
const { relay, orchestrator } = setupOrchestrator();
|
|
220
|
+
|
|
221
|
+
await orchestrator.handleCallerUtterance('Test');
|
|
222
|
+
|
|
223
|
+
// Find the final empty-string token that marks end of turn
|
|
224
|
+
const endMarkers = relay.sentTokens.filter((t) => t.last === true);
|
|
225
|
+
expect(endMarkers.length).toBeGreaterThanOrEqual(1);
|
|
226
|
+
|
|
227
|
+
orchestrator.destroy();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('handleCallerUtterance: includes speaker context in model message', async () => {
|
|
231
|
+
mockStreamFn.mockImplementation((...args: unknown[]) => {
|
|
232
|
+
const firstArg = args[0] as { messages: Array<{ role: string; content: string }> };
|
|
233
|
+
const userMessage = firstArg.messages.find((m) => m.role === 'user');
|
|
234
|
+
expect(userMessage?.content).toContain('[SPEAKER id="speaker-1" label="Aaron" source="provider" confidence="0.91"]');
|
|
235
|
+
expect(userMessage?.content).toContain('Can you summarize this meeting?');
|
|
236
|
+
return createMockStream(['Sure, here is a summary.']);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const { orchestrator } = setupOrchestrator();
|
|
240
|
+
|
|
241
|
+
await orchestrator.handleCallerUtterance('Can you summarize this meeting?', {
|
|
242
|
+
speakerId: 'speaker-1',
|
|
243
|
+
speakerLabel: 'Aaron',
|
|
244
|
+
speakerConfidence: 0.91,
|
|
245
|
+
source: 'provider',
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
orchestrator.destroy();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ── ASK_USER pattern ──────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
test('ASK_USER pattern: detects pattern, creates pending question, enters waiting_on_user', async () => {
|
|
254
|
+
mockStreamFn.mockImplementation(() =>
|
|
255
|
+
createMockStream(['Let me check on that. ', '[ASK_USER: What date works best?]']),
|
|
256
|
+
);
|
|
257
|
+
const { session, relay, orchestrator } = setupOrchestrator('Book appointment');
|
|
258
|
+
|
|
259
|
+
await orchestrator.handleCallerUtterance('I need to schedule something');
|
|
260
|
+
|
|
261
|
+
// Verify a pending question was created
|
|
262
|
+
const question = getPendingQuestion(session.id);
|
|
263
|
+
expect(question).not.toBeNull();
|
|
264
|
+
expect(question!.questionText).toBe('What date works best?');
|
|
265
|
+
expect(question!.status).toBe('pending');
|
|
266
|
+
|
|
267
|
+
// Verify session status was updated to waiting_on_user
|
|
268
|
+
const updatedSession = getCallSession(session.id);
|
|
269
|
+
expect(updatedSession!.status).toBe('waiting_on_user');
|
|
270
|
+
|
|
271
|
+
// The ASK_USER marker text should NOT appear in the relay tokens
|
|
272
|
+
const allText = relay.sentTokens.map((t) => t.token).join('');
|
|
273
|
+
expect(allText).not.toContain('[ASK_USER:');
|
|
274
|
+
|
|
275
|
+
orchestrator.destroy();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ── END_CALL pattern ──────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
test('END_CALL pattern: detects marker, calls endSession, updates status to completed', async () => {
|
|
281
|
+
mockStreamFn.mockImplementation(() =>
|
|
282
|
+
createMockStream(['Thank you for calling, goodbye! ', '[END_CALL]']),
|
|
283
|
+
);
|
|
284
|
+
const { session, relay, orchestrator } = setupOrchestrator();
|
|
285
|
+
|
|
286
|
+
await orchestrator.handleCallerUtterance('That is all, thanks');
|
|
287
|
+
|
|
288
|
+
// endSession should have been called
|
|
289
|
+
expect(relay.endCalled).toBe(true);
|
|
290
|
+
|
|
291
|
+
// Session status should be completed
|
|
292
|
+
const updatedSession = getCallSession(session.id);
|
|
293
|
+
expect(updatedSession!.status).toBe('completed');
|
|
294
|
+
expect(updatedSession!.endedAt).not.toBeNull();
|
|
295
|
+
|
|
296
|
+
// The END_CALL marker text should NOT appear in the relay tokens
|
|
297
|
+
const allText = relay.sentTokens.map((t) => t.token).join('');
|
|
298
|
+
expect(allText).not.toContain('[END_CALL]');
|
|
299
|
+
|
|
300
|
+
orchestrator.destroy();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ── handleUserAnswer ──────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
test('handleUserAnswer: returns true immediately and fires LLM asynchronously', async () => {
|
|
306
|
+
// First utterance triggers ASK_USER
|
|
307
|
+
mockStreamFn.mockImplementation(() =>
|
|
308
|
+
createMockStream(['Hold on. [ASK_USER: Preferred time?]']),
|
|
309
|
+
);
|
|
310
|
+
const { relay, orchestrator } = setupOrchestrator();
|
|
311
|
+
|
|
312
|
+
await orchestrator.handleCallerUtterance('I need an appointment');
|
|
313
|
+
|
|
314
|
+
// Now provide the answer — reset mock for second LLM call
|
|
315
|
+
mockStreamFn.mockImplementation((...args: unknown[]) => {
|
|
316
|
+
// Verify the messages include the USER_ANSWERED marker
|
|
317
|
+
const firstArg = args[0] as { messages: Array<{ role: string; content: string }> };
|
|
318
|
+
const lastUserMsg = firstArg.messages.filter((m: { role: string }) => m.role === 'user').pop();
|
|
319
|
+
expect(lastUserMsg?.content).toContain('[USER_ANSWERED: 3pm tomorrow]');
|
|
320
|
+
return createMockStream(['Great, I have scheduled for 3pm tomorrow.']);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const accepted = await orchestrator.handleUserAnswer('3pm tomorrow');
|
|
324
|
+
expect(accepted).toBe(true);
|
|
325
|
+
|
|
326
|
+
// handleUserAnswer fires runLlm without awaiting, so give the
|
|
327
|
+
// microtask queue a tick to let the async LLM work complete.
|
|
328
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
329
|
+
|
|
330
|
+
// Should have streamed a response for the answer
|
|
331
|
+
const tokensAfterAnswer = relay.sentTokens.filter((t) => t.token.includes('3pm'));
|
|
332
|
+
expect(tokensAfterAnswer.length).toBeGreaterThan(0);
|
|
333
|
+
|
|
334
|
+
orchestrator.destroy();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// ── Full mid-call question flow ──────────────────────────────────
|
|
338
|
+
|
|
339
|
+
test('mid-call question flow: unavailable time → ask user → user confirms → resumed call', async () => {
|
|
340
|
+
// Step 1: Caller says "7:30" but it's unavailable. The LLM asks the user.
|
|
341
|
+
mockStreamFn.mockImplementation(() =>
|
|
342
|
+
createMockStream(['I\'m sorry, 7:30 is not available. ', '[ASK_USER: Is 8:00 okay instead?]']),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const { session, relay, orchestrator } = setupOrchestrator('Schedule a haircut');
|
|
346
|
+
|
|
347
|
+
await orchestrator.handleCallerUtterance('Can I book for 7:30?');
|
|
348
|
+
|
|
349
|
+
// Verify we're in waiting_on_user state
|
|
350
|
+
expect(orchestrator.getState()).toBe('waiting_on_user');
|
|
351
|
+
const question = getPendingQuestion(session.id);
|
|
352
|
+
expect(question).not.toBeNull();
|
|
353
|
+
expect(question!.questionText).toBe('Is 8:00 okay instead?');
|
|
354
|
+
|
|
355
|
+
// Verify session status
|
|
356
|
+
const midSession = getCallSession(session.id);
|
|
357
|
+
expect(midSession!.status).toBe('waiting_on_user');
|
|
358
|
+
|
|
359
|
+
// Step 2: User answers "Yes, 8:00 works"
|
|
360
|
+
mockStreamFn.mockImplementation(() =>
|
|
361
|
+
createMockStream(['Great, I\'ve booked you for 8:00. See you then! ', '[END_CALL]']),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const accepted = await orchestrator.handleUserAnswer('Yes, 8:00 works for me');
|
|
365
|
+
expect(accepted).toBe(true);
|
|
366
|
+
|
|
367
|
+
// Give the fire-and-forget LLM call time to complete
|
|
368
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
369
|
+
|
|
370
|
+
// Step 3: Verify call completed
|
|
371
|
+
const endSession = getCallSession(session.id);
|
|
372
|
+
expect(endSession!.status).toBe('completed');
|
|
373
|
+
expect(endSession!.endedAt).not.toBeNull();
|
|
374
|
+
|
|
375
|
+
// Verify the END_CALL marker triggered endSession on relay
|
|
376
|
+
expect(relay.endCalled).toBe(true);
|
|
377
|
+
|
|
378
|
+
orchestrator.destroy();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ── Provider / LLM failure paths ───────────────────────────────
|
|
382
|
+
|
|
383
|
+
test('LLM error: sends error message to caller and returns to idle', async () => {
|
|
384
|
+
// Make the stream throw an error on finalMessage
|
|
385
|
+
mockStreamFn.mockImplementation(() => {
|
|
386
|
+
const emitter = new EventEmitter();
|
|
387
|
+
return {
|
|
388
|
+
on: (event: string, handler: (...args: unknown[]) => void) => {
|
|
389
|
+
emitter.on(event, handler);
|
|
390
|
+
return { on: () => ({ on: () => ({}) }) };
|
|
391
|
+
},
|
|
392
|
+
finalMessage: () => Promise.reject(new Error('API rate limit exceeded')),
|
|
393
|
+
};
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
const { relay, orchestrator } = setupOrchestrator();
|
|
397
|
+
|
|
398
|
+
await orchestrator.handleCallerUtterance('Hello');
|
|
399
|
+
|
|
400
|
+
// Should have sent an error recovery message
|
|
401
|
+
const errorTokens = relay.sentTokens.filter((t) =>
|
|
402
|
+
t.token.includes('technical issue'),
|
|
403
|
+
);
|
|
404
|
+
expect(errorTokens.length).toBeGreaterThan(0);
|
|
405
|
+
|
|
406
|
+
// State should return to idle after error
|
|
407
|
+
expect(orchestrator.getState()).toBe('idle');
|
|
408
|
+
|
|
409
|
+
orchestrator.destroy();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('handleUserAnswer: returns false when not in waiting_on_user state', async () => {
|
|
413
|
+
const { orchestrator } = setupOrchestrator();
|
|
414
|
+
|
|
415
|
+
// Orchestrator starts in idle state
|
|
416
|
+
const result = await orchestrator.handleUserAnswer('some answer');
|
|
417
|
+
expect(result).toBe(false);
|
|
418
|
+
|
|
419
|
+
orchestrator.destroy();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// ── handleInterrupt ───────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
test('handleInterrupt: resets state to idle', () => {
|
|
425
|
+
const { orchestrator } = setupOrchestrator();
|
|
426
|
+
|
|
427
|
+
// Calling handleInterrupt should not throw
|
|
428
|
+
orchestrator.handleInterrupt();
|
|
429
|
+
|
|
430
|
+
orchestrator.destroy();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// ── destroy ───────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
test('destroy: unregisters orchestrator', () => {
|
|
436
|
+
const { session, orchestrator } = setupOrchestrator();
|
|
437
|
+
|
|
438
|
+
// Orchestrator should be registered
|
|
439
|
+
expect(getCallOrchestrator(session.id)).toBeDefined();
|
|
440
|
+
|
|
441
|
+
orchestrator.destroy();
|
|
442
|
+
|
|
443
|
+
// After destroy, orchestrator should be unregistered
|
|
444
|
+
expect(getCallOrchestrator(session.id)).toBeUndefined();
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test('destroy: can be called multiple times without error', () => {
|
|
448
|
+
const { orchestrator } = setupOrchestrator();
|
|
449
|
+
|
|
450
|
+
orchestrator.destroy();
|
|
451
|
+
// Second destroy should not throw
|
|
452
|
+
expect(() => orchestrator.destroy()).not.toThrow();
|
|
453
|
+
});
|
|
454
|
+
});
|