vellum 0.2.1 → 0.2.7
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 +71 -100
- package/package.json +5 -3
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +4 -3
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +130 -4
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-store.test.ts +216 -1
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +305 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twilio-config.test.ts +221 -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 +71 -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-regressions.test.ts +100 -2
- 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-commit-message-generator.test.ts +303 -0
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +5 -3
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
- package/src/__tests__/run-orchestrator.test.ts +4 -4
- package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
- package/src/__tests__/runtime-runs-http.test.ts +4 -4
- package/src/__tests__/runtime-runs.test.ts +4 -4
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-conflict-gate.test.ts +28 -25
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +71 -48
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +218 -3
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +186 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +12 -8
- package/src/calls/__tests__/twilio-webhook-urls.test.ts +162 -0
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +43 -5
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +43 -17
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-store.ts +192 -5
- package/src/calls/relay-server.ts +41 -4
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +8 -8
- package/src/calls/twilio-provider.ts +13 -9
- package/src/calls/twilio-routes.ts +90 -76
- package/src/calls/twilio-webhook-urls.ts +50 -0
- package/src/calls/types.ts +1 -1
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +251 -1
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +270 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +82 -23
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +34 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +165 -1
- package/src/config/system-prompt.ts +61 -16
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +4 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -5
- 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 +205 -5
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -277
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +384 -68
- package/src/daemon/ipc-contract-inventory.json +32 -4
- package/src/daemon/ipc-contract.ts +156 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +21 -0
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +75 -10
- package/src/daemon/server.ts +143 -26
- package/src/daemon/session-agent-loop.ts +922 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-conflict-gate.ts +18 -109
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +216 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1404
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1153
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/conflict-intent.ts +114 -0
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +96 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/job-handlers/conflict.ts +23 -1
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +23 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/gateway-client.ts +36 -0
- package/src/runtime/http-server.ts +166 -22
- package/src/runtime/routes/attachment-routes.ts +2 -3
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +125 -88
- package/src/runtime/routes/conversation-routes.ts +5 -5
- package/src/runtime/routes/run-routes.ts +2 -2
- package/src/runtime/run-orchestrator.ts +9 -3
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +293 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +17 -67
- package/src/tools/calls/call-start.ts +24 -85
- package/src/tools/calls/call-status.ts +35 -51
- package/src/tools/claude-code/claude-code.ts +207 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +20 -4
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -8
- package/src/tools/tasks/task-delete.ts +60 -88
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +72 -108
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +183 -215
- package/src/tools/tasks/work-item-list.ts +33 -63
- package/src/tools/tasks/work-item-remove.ts +45 -97
- package/src/tools/tasks/work-item-update.ts +91 -163
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +0 -62
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +27 -2
- package/src/workspace/commit-message-enrichment-service.ts +31 -7
- package/src/workspace/git-service.ts +87 -22
- package/src/workspace/provider-commit-message-generator.ts +269 -0
- package/src/workspace/turn-commit.ts +62 -3
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -40,6 +40,16 @@ function buildSanitizedEnv(): Record<string, string> {
|
|
|
40
40
|
env[key] = process.env[key]!;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
// Ensure ~/.local/bin and ~/.bun/bin are in PATH so `vellum` and `bun` are
|
|
44
|
+
// always reachable, even when the daemon is launched from a macOS app
|
|
45
|
+
// bundle that inherits a minimal PATH.
|
|
46
|
+
const home = homedir();
|
|
47
|
+
const extraDirs = [`${home}/.local/bin`, `${home}/.bun/bin`];
|
|
48
|
+
const currentPath = env.PATH ?? '';
|
|
49
|
+
const missing = extraDirs.filter(d => !currentPath.split(':').includes(d));
|
|
50
|
+
if (missing.length > 0) {
|
|
51
|
+
env.PATH = [...missing, currentPath].filter(Boolean).join(':');
|
|
52
|
+
}
|
|
43
53
|
return env;
|
|
44
54
|
}
|
|
45
55
|
|
|
@@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm';
|
|
|
2
2
|
import { v4 as uuid } from 'uuid';
|
|
3
3
|
import type { AssistantConfig } from '../../config/types.js';
|
|
4
4
|
import { getLogger } from '../../util/logger.js';
|
|
5
|
+
import { truncate } from '../../util/truncate.js';
|
|
5
6
|
import { getDb } from '../../memory/db.js';
|
|
6
7
|
import { computeMemoryFingerprint } from '../../memory/fingerprint.js';
|
|
7
8
|
import { memoryItems } from '../../memory/schema.js';
|
|
@@ -84,14 +85,14 @@ export async function handleMemorySave(
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
const subject = typeof args.subject === 'string' && args.subject.trim().length > 0
|
|
87
|
-
? args.subject.trim()
|
|
88
|
+
? truncate(args.subject.trim(), 80, '')
|
|
88
89
|
: inferSubjectFromStatement(statement.trim());
|
|
89
90
|
|
|
90
91
|
try {
|
|
91
92
|
const db = getDb();
|
|
92
93
|
const id = uuid();
|
|
93
94
|
const now = Date.now();
|
|
94
|
-
const trimmedStatement = statement.trim()
|
|
95
|
+
const trimmedStatement = truncate(statement.trim(), 500, '');
|
|
95
96
|
|
|
96
97
|
const fingerprint = computeMemoryFingerprint(scopeId, kind, subject, trimmedStatement);
|
|
97
98
|
|
|
@@ -185,7 +186,7 @@ export async function handleMemoryUpdate(
|
|
|
185
186
|
}
|
|
186
187
|
|
|
187
188
|
const now = Date.now();
|
|
188
|
-
const trimmedStatement = statement.trim()
|
|
189
|
+
const trimmedStatement = truncate(statement.trim(), 500, '');
|
|
189
190
|
|
|
190
191
|
const fingerprint = computeMemoryFingerprint(scopeId, existing.kind, existing.subject, trimmedStatement);
|
|
191
192
|
|
|
@@ -232,7 +233,7 @@ export async function handleMemoryUpdate(
|
|
|
232
233
|
function inferSubjectFromStatement(statement: string): string {
|
|
233
234
|
// Take first few words as a subject label
|
|
234
235
|
const words = statement.split(/\s+/).slice(0, 6).join(' ');
|
|
235
|
-
return words
|
|
236
|
+
return truncate(words, 80, '');
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
/**
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test';
|
|
3
|
+
|
|
4
|
+
// Mutable mock state — set per test
|
|
5
|
+
let mockWebSearchProvider: string | undefined = 'perplexity';
|
|
6
|
+
let mockBraveConfigKey: string | undefined;
|
|
7
|
+
let mockPerplexityConfigKey: string | undefined;
|
|
8
|
+
let mockBraveSecureKey: string | undefined;
|
|
9
|
+
let mockPerplexitySecureKey: string | undefined;
|
|
10
|
+
|
|
11
|
+
// Capture the registered tool
|
|
12
|
+
let capturedTool: any = null;
|
|
13
|
+
|
|
14
|
+
mock.module('../../registry.js', () => ({
|
|
15
|
+
registerTool: (tool: any) => { capturedTool = tool; },
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
mock.module('../../../config/loader.js', () => ({
|
|
19
|
+
getConfig: () => ({
|
|
20
|
+
webSearchProvider: mockWebSearchProvider,
|
|
21
|
+
apiKeys: { brave: mockBraveConfigKey, perplexity: mockPerplexityConfigKey },
|
|
22
|
+
}),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
mock.module('../../../security/secure-keys.js', () => ({
|
|
26
|
+
getSecureKey: (provider: string) => {
|
|
27
|
+
if (provider === 'brave') return mockBraveSecureKey;
|
|
28
|
+
if (provider === 'perplexity') return mockPerplexitySecureKey;
|
|
29
|
+
return undefined;
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module('../../../util/logger.js', () => ({
|
|
34
|
+
getLogger: () => new Proxy({} as Record<string, unknown>, {
|
|
35
|
+
get: () => () => {},
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
mock.module('../../../permissions/types.js', () => ({
|
|
40
|
+
RiskLevel: { Low: 'low', Medium: 'medium', High: 'high' },
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
// Force the module to load (triggers registerTool)
|
|
44
|
+
await import('../web-search.js');
|
|
45
|
+
|
|
46
|
+
describe('web_search tool', () => {
|
|
47
|
+
let originalFetch: typeof globalThis.fetch;
|
|
48
|
+
let savedBraveKey: string | undefined;
|
|
49
|
+
let savedPerplexityKey: string | undefined;
|
|
50
|
+
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
originalFetch = globalThis.fetch;
|
|
53
|
+
mockWebSearchProvider = 'perplexity';
|
|
54
|
+
mockBraveConfigKey = undefined;
|
|
55
|
+
mockPerplexityConfigKey = undefined;
|
|
56
|
+
mockBraveSecureKey = undefined;
|
|
57
|
+
mockPerplexitySecureKey = undefined;
|
|
58
|
+
|
|
59
|
+
// Isolate from host env so getApiKey() doesn't short-circuit on real keys
|
|
60
|
+
savedBraveKey = process.env.BRAVE_API_KEY;
|
|
61
|
+
savedPerplexityKey = process.env.PERPLEXITY_API_KEY;
|
|
62
|
+
delete process.env.BRAVE_API_KEY;
|
|
63
|
+
delete process.env.PERPLEXITY_API_KEY;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
globalThis.fetch = originalFetch;
|
|
68
|
+
|
|
69
|
+
if (savedBraveKey !== undefined) process.env.BRAVE_API_KEY = savedBraveKey;
|
|
70
|
+
else delete process.env.BRAVE_API_KEY;
|
|
71
|
+
|
|
72
|
+
if (savedPerplexityKey !== undefined) process.env.PERPLEXITY_API_KEY = savedPerplexityKey;
|
|
73
|
+
else delete process.env.PERPLEXITY_API_KEY;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function execute(input: Record<string, unknown>) {
|
|
77
|
+
return capturedTool.execute(input, {} as any);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- Input validation ---------------------------------------------------
|
|
81
|
+
|
|
82
|
+
test('rejects missing query', async () => {
|
|
83
|
+
const result = await execute({});
|
|
84
|
+
expect(result.isError).toBe(true);
|
|
85
|
+
expect(result.content).toContain('query is required');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('rejects non-string query', async () => {
|
|
89
|
+
const result = await execute({ query: 42 });
|
|
90
|
+
expect(result.isError).toBe(true);
|
|
91
|
+
expect(result.content).toContain('query is required');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---- No API key configured ----------------------------------------------
|
|
95
|
+
|
|
96
|
+
test('returns error when no API key is available', async () => {
|
|
97
|
+
const result = await execute({ query: 'test' });
|
|
98
|
+
expect(result.isError).toBe(true);
|
|
99
|
+
expect(result.content).toContain('No web search API key configured');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ---- Perplexity provider ------------------------------------------------
|
|
103
|
+
|
|
104
|
+
test('executes Perplexity search successfully', async () => {
|
|
105
|
+
mockPerplexityConfigKey = 'pplx-test-key';
|
|
106
|
+
globalThis.fetch = (async (_url: string, _init?: RequestInit) => {
|
|
107
|
+
return new Response(JSON.stringify({
|
|
108
|
+
choices: [{ message: { content: 'Perplexity answer about TypeScript' } }],
|
|
109
|
+
citations: ['https://typescriptlang.org', 'https://example.com/ts'],
|
|
110
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
111
|
+
}) as any;
|
|
112
|
+
|
|
113
|
+
const result = await execute({ query: 'what is TypeScript' });
|
|
114
|
+
expect(result.isError).toBe(false);
|
|
115
|
+
expect(result.content).toContain('Perplexity answer about TypeScript');
|
|
116
|
+
expect(result.content).toContain('Sources:');
|
|
117
|
+
expect(result.content).toContain('typescriptlang.org');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('Perplexity sends correct request format', async () => {
|
|
121
|
+
mockPerplexityConfigKey = 'pplx-test-key';
|
|
122
|
+
let capturedUrl = '';
|
|
123
|
+
let capturedBody: any = null;
|
|
124
|
+
let capturedHeaders: any = null;
|
|
125
|
+
globalThis.fetch = (async (url: string, init?: RequestInit) => {
|
|
126
|
+
capturedUrl = url;
|
|
127
|
+
capturedBody = JSON.parse(init?.body as string);
|
|
128
|
+
capturedHeaders = new Headers(init?.headers);
|
|
129
|
+
return new Response(JSON.stringify({
|
|
130
|
+
choices: [{ message: { content: 'answer' } }],
|
|
131
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
132
|
+
}) as any;
|
|
133
|
+
|
|
134
|
+
await execute({ query: 'test query' });
|
|
135
|
+
expect(capturedUrl).toContain('perplexity.ai');
|
|
136
|
+
expect(capturedBody.model).toBe('sonar');
|
|
137
|
+
expect(capturedBody.messages[0].content).toBe('test query');
|
|
138
|
+
expect(capturedHeaders.get('authorization')).toBe('Bearer pplx-test-key');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('Perplexity returns no results message when response is empty', async () => {
|
|
142
|
+
mockPerplexityConfigKey = 'pplx-test-key';
|
|
143
|
+
globalThis.fetch = (async () => {
|
|
144
|
+
return new Response(JSON.stringify({ choices: [] }), {
|
|
145
|
+
status: 200, headers: { 'content-type': 'application/json' },
|
|
146
|
+
});
|
|
147
|
+
}) as any;
|
|
148
|
+
|
|
149
|
+
const result = await execute({ query: 'obscure query' });
|
|
150
|
+
expect(result.isError).toBe(false);
|
|
151
|
+
expect(result.content).toContain('No results found');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('Perplexity handles 401/403 auth errors', async () => {
|
|
155
|
+
mockPerplexityConfigKey = 'bad-key';
|
|
156
|
+
globalThis.fetch = (async () => {
|
|
157
|
+
return new Response('Unauthorized', { status: 401 });
|
|
158
|
+
}) as any;
|
|
159
|
+
|
|
160
|
+
const result = await execute({ query: 'test' });
|
|
161
|
+
expect(result.isError).toBe(true);
|
|
162
|
+
expect(result.content).toContain('Invalid or expired Perplexity API key');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('Perplexity handles 429 rate limit after max retries', async () => {
|
|
166
|
+
mockPerplexityConfigKey = 'pplx-key';
|
|
167
|
+
let callCount = 0;
|
|
168
|
+
globalThis.fetch = (async () => {
|
|
169
|
+
callCount++;
|
|
170
|
+
return new Response('Too Many Requests', {
|
|
171
|
+
status: 429,
|
|
172
|
+
headers: { 'retry-after': '0' },
|
|
173
|
+
});
|
|
174
|
+
}) as any;
|
|
175
|
+
|
|
176
|
+
const result = await execute({ query: 'test' });
|
|
177
|
+
expect(result.isError).toBe(true);
|
|
178
|
+
expect(result.content).toContain('rate limit exceeded');
|
|
179
|
+
// 1 initial + 3 retries = 4 calls
|
|
180
|
+
expect(callCount).toBe(4);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('Perplexity handles generic server error', async () => {
|
|
184
|
+
mockPerplexityConfigKey = 'pplx-key';
|
|
185
|
+
globalThis.fetch = (async () => {
|
|
186
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
187
|
+
}) as any;
|
|
188
|
+
|
|
189
|
+
const result = await execute({ query: 'test' });
|
|
190
|
+
expect(result.isError).toBe(true);
|
|
191
|
+
expect(result.content).toContain('status 500');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ---- Brave provider -----------------------------------------------------
|
|
195
|
+
|
|
196
|
+
test('executes Brave search successfully', async () => {
|
|
197
|
+
mockWebSearchProvider = 'brave';
|
|
198
|
+
mockBraveConfigKey = 'brave-test-key';
|
|
199
|
+
globalThis.fetch = (async (_url: string) => {
|
|
200
|
+
return new Response(JSON.stringify({
|
|
201
|
+
web: {
|
|
202
|
+
results: [
|
|
203
|
+
{ title: 'Result 1', url: 'https://example.com/1', description: 'First result', age: '2 days ago' },
|
|
204
|
+
{ title: 'Result 2', url: 'https://example.com/2', description: 'Second result', extra_snippets: ['Extra info'] },
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
208
|
+
}) as any;
|
|
209
|
+
|
|
210
|
+
const result = await execute({ query: 'test search' });
|
|
211
|
+
expect(result.isError).toBe(false);
|
|
212
|
+
expect(result.content).toContain('Result 1');
|
|
213
|
+
expect(result.content).toContain('https://example.com/1');
|
|
214
|
+
expect(result.content).toContain('2 days ago');
|
|
215
|
+
expect(result.content).toContain('Result 2');
|
|
216
|
+
expect(result.content).toContain('Extra info');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('Brave sends correct query parameters', async () => {
|
|
220
|
+
mockWebSearchProvider = 'brave';
|
|
221
|
+
mockBraveConfigKey = 'brave-key';
|
|
222
|
+
let capturedUrl = '';
|
|
223
|
+
globalThis.fetch = (async (url: string) => {
|
|
224
|
+
capturedUrl = url;
|
|
225
|
+
return new Response(JSON.stringify({ web: { results: [] } }), {
|
|
226
|
+
status: 200, headers: { 'content-type': 'application/json' },
|
|
227
|
+
});
|
|
228
|
+
}) as any;
|
|
229
|
+
|
|
230
|
+
await execute({ query: 'test query', count: 5, offset: 2, freshness: 'pw' });
|
|
231
|
+
const parsed = new URL(capturedUrl);
|
|
232
|
+
expect(parsed.searchParams.get('q')).toBe('test query');
|
|
233
|
+
expect(parsed.searchParams.get('count')).toBe('5');
|
|
234
|
+
expect(parsed.searchParams.get('offset')).toBe('2');
|
|
235
|
+
expect(parsed.searchParams.get('freshness')).toBe('pw');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('Brave clamps count and offset', async () => {
|
|
239
|
+
mockWebSearchProvider = 'brave';
|
|
240
|
+
mockBraveConfigKey = 'brave-key';
|
|
241
|
+
let capturedUrl = '';
|
|
242
|
+
globalThis.fetch = (async (url: string) => {
|
|
243
|
+
capturedUrl = url;
|
|
244
|
+
return new Response(JSON.stringify({ web: { results: [] } }), {
|
|
245
|
+
status: 200, headers: { 'content-type': 'application/json' },
|
|
246
|
+
});
|
|
247
|
+
}) as any;
|
|
248
|
+
|
|
249
|
+
await execute({ query: 'test', count: 100, offset: 50 });
|
|
250
|
+
const parsed = new URL(capturedUrl);
|
|
251
|
+
expect(parsed.searchParams.get('count')).toBe('20');
|
|
252
|
+
expect(parsed.searchParams.get('offset')).toBe('9');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('Brave skips invalid freshness values', async () => {
|
|
256
|
+
mockWebSearchProvider = 'brave';
|
|
257
|
+
mockBraveConfigKey = 'brave-key';
|
|
258
|
+
let capturedUrl = '';
|
|
259
|
+
globalThis.fetch = (async (url: string) => {
|
|
260
|
+
capturedUrl = url;
|
|
261
|
+
return new Response(JSON.stringify({ web: { results: [] } }), {
|
|
262
|
+
status: 200, headers: { 'content-type': 'application/json' },
|
|
263
|
+
});
|
|
264
|
+
}) as any;
|
|
265
|
+
|
|
266
|
+
await execute({ query: 'test', freshness: 'invalid' });
|
|
267
|
+
const parsed = new URL(capturedUrl);
|
|
268
|
+
expect(parsed.searchParams.has('freshness')).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('Brave handles empty results', async () => {
|
|
272
|
+
mockWebSearchProvider = 'brave';
|
|
273
|
+
mockBraveConfigKey = 'brave-key';
|
|
274
|
+
globalThis.fetch = (async () => {
|
|
275
|
+
return new Response(JSON.stringify({ web: { results: [] } }), {
|
|
276
|
+
status: 200, headers: { 'content-type': 'application/json' },
|
|
277
|
+
});
|
|
278
|
+
}) as any;
|
|
279
|
+
|
|
280
|
+
const result = await execute({ query: 'no results for this' });
|
|
281
|
+
expect(result.isError).toBe(false);
|
|
282
|
+
expect(result.content).toContain('No results found');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('Brave handles 401 auth error', async () => {
|
|
286
|
+
mockWebSearchProvider = 'brave';
|
|
287
|
+
mockBraveConfigKey = 'bad-key';
|
|
288
|
+
globalThis.fetch = (async () => {
|
|
289
|
+
return new Response('Forbidden', { status: 403 });
|
|
290
|
+
}) as any;
|
|
291
|
+
|
|
292
|
+
const result = await execute({ query: 'test' });
|
|
293
|
+
expect(result.isError).toBe(true);
|
|
294
|
+
expect(result.content).toContain('Invalid or expired Brave Search API key');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('Brave handles 429 rate limit with Retry-After header', async () => {
|
|
298
|
+
mockWebSearchProvider = 'brave';
|
|
299
|
+
mockBraveConfigKey = 'brave-key';
|
|
300
|
+
let callCount = 0;
|
|
301
|
+
globalThis.fetch = (async () => {
|
|
302
|
+
callCount++;
|
|
303
|
+
if (callCount <= 3) {
|
|
304
|
+
return new Response('Rate Limited', {
|
|
305
|
+
status: 429,
|
|
306
|
+
headers: { 'retry-after': '0' },
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return new Response(JSON.stringify({ web: { results: [{ title: 'Success', url: 'https://example.com', description: 'Got it' }] } }), {
|
|
310
|
+
status: 200, headers: { 'content-type': 'application/json' },
|
|
311
|
+
});
|
|
312
|
+
}) as any;
|
|
313
|
+
|
|
314
|
+
const result = await execute({ query: 'test' });
|
|
315
|
+
expect(result.isError).toBe(false);
|
|
316
|
+
expect(result.content).toContain('Success');
|
|
317
|
+
expect(callCount).toBe(4);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ---- Provider fallback --------------------------------------------------
|
|
321
|
+
|
|
322
|
+
test('falls back from perplexity to brave when perplexity has no key', async () => {
|
|
323
|
+
mockWebSearchProvider = 'perplexity';
|
|
324
|
+
mockBraveConfigKey = 'brave-fallback-key';
|
|
325
|
+
let capturedUrl = '';
|
|
326
|
+
globalThis.fetch = (async (url: string) => {
|
|
327
|
+
capturedUrl = url;
|
|
328
|
+
return new Response(JSON.stringify({ web: { results: [] } }), {
|
|
329
|
+
status: 200, headers: { 'content-type': 'application/json' },
|
|
330
|
+
});
|
|
331
|
+
}) as any;
|
|
332
|
+
|
|
333
|
+
const result = await execute({ query: 'fallback test' });
|
|
334
|
+
expect(result.isError).toBe(false);
|
|
335
|
+
expect(capturedUrl).toContain('brave');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('falls back from brave to perplexity when brave has no key', async () => {
|
|
339
|
+
mockWebSearchProvider = 'brave';
|
|
340
|
+
mockPerplexityConfigKey = 'pplx-fallback-key';
|
|
341
|
+
let capturedUrl = '';
|
|
342
|
+
globalThis.fetch = (async (url: string, _init?: RequestInit) => {
|
|
343
|
+
capturedUrl = url;
|
|
344
|
+
return new Response(JSON.stringify({
|
|
345
|
+
choices: [{ message: { content: 'fallback result' } }],
|
|
346
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
347
|
+
}) as any;
|
|
348
|
+
|
|
349
|
+
const result = await execute({ query: 'fallback test' });
|
|
350
|
+
expect(result.isError).toBe(false);
|
|
351
|
+
expect(capturedUrl).toContain('perplexity');
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('maps anthropic-native to perplexity', async () => {
|
|
355
|
+
mockWebSearchProvider = 'anthropic-native';
|
|
356
|
+
mockPerplexityConfigKey = 'pplx-key';
|
|
357
|
+
let capturedUrl = '';
|
|
358
|
+
globalThis.fetch = (async (url: string) => {
|
|
359
|
+
capturedUrl = url;
|
|
360
|
+
return new Response(JSON.stringify({
|
|
361
|
+
choices: [{ message: { content: 'result' } }],
|
|
362
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
363
|
+
}) as any;
|
|
364
|
+
|
|
365
|
+
const result = await execute({ query: 'test' });
|
|
366
|
+
expect(result.isError).toBe(false);
|
|
367
|
+
expect(capturedUrl).toContain('perplexity');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// ---- Env var keys -------------------------------------------------------
|
|
371
|
+
|
|
372
|
+
test('uses PERPLEXITY_API_KEY env var when available', async () => {
|
|
373
|
+
const origEnv = process.env.PERPLEXITY_API_KEY;
|
|
374
|
+
process.env.PERPLEXITY_API_KEY = 'env-pplx-key';
|
|
375
|
+
try {
|
|
376
|
+
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
|
377
|
+
const headers = new Headers(init?.headers);
|
|
378
|
+
expect(headers.get('authorization')).toBe('Bearer env-pplx-key');
|
|
379
|
+
return new Response(JSON.stringify({
|
|
380
|
+
choices: [{ message: { content: 'env key works' } }],
|
|
381
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
382
|
+
}) as any;
|
|
383
|
+
|
|
384
|
+
const result = await execute({ query: 'test' });
|
|
385
|
+
expect(result.isError).toBe(false);
|
|
386
|
+
} finally {
|
|
387
|
+
if (origEnv === undefined) {
|
|
388
|
+
delete process.env.PERPLEXITY_API_KEY;
|
|
389
|
+
} else {
|
|
390
|
+
process.env.PERPLEXITY_API_KEY = origEnv;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ---- Network errors -----------------------------------------------------
|
|
396
|
+
|
|
397
|
+
test('handles fetch exceptions', async () => {
|
|
398
|
+
mockPerplexityConfigKey = 'pplx-key';
|
|
399
|
+
globalThis.fetch = (async () => {
|
|
400
|
+
throw new Error('Network error: connection refused');
|
|
401
|
+
}) as any;
|
|
402
|
+
|
|
403
|
+
const result = await execute({ query: 'test' });
|
|
404
|
+
expect(result.isError).toBe(true);
|
|
405
|
+
expect(result.content).toContain('Web search failed');
|
|
406
|
+
expect(result.content).toContain('connection refused');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ---- Secure key precedence ----------------------------------------------
|
|
410
|
+
|
|
411
|
+
test('prefers secure key over config key for brave', async () => {
|
|
412
|
+
mockWebSearchProvider = 'brave';
|
|
413
|
+
mockBraveConfigKey = 'config-key';
|
|
414
|
+
mockBraveSecureKey = 'secure-key';
|
|
415
|
+
let capturedHeaders: Headers | null = null;
|
|
416
|
+
globalThis.fetch = (async (_url: string, init?: RequestInit) => {
|
|
417
|
+
capturedHeaders = new Headers(init?.headers);
|
|
418
|
+
return new Response(JSON.stringify({ web: { results: [] } }), {
|
|
419
|
+
status: 200, headers: { 'content-type': 'application/json' },
|
|
420
|
+
});
|
|
421
|
+
}) as any;
|
|
422
|
+
|
|
423
|
+
await execute({ query: 'test' });
|
|
424
|
+
// Brave uses X-Subscription-Token header with the secure key
|
|
425
|
+
expect(capturedHeaders!.get('x-subscription-token')).toBe('secure-key');
|
|
426
|
+
});
|
|
427
|
+
});
|