@vellumai/assistant 0.7.2 → 0.7.3
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/ARCHITECTURE.md +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -3,16 +3,36 @@ import { afterEach, describe, expect, jest, mock, test } from "bun:test";
|
|
|
3
3
|
const sentMessages: unknown[] = [];
|
|
4
4
|
let mockHasClient = false;
|
|
5
5
|
|
|
6
|
+
interface MockClient {
|
|
7
|
+
clientId: string;
|
|
8
|
+
capabilities: string[];
|
|
9
|
+
actorPrincipalId?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let mockClients: MockClient[] = [];
|
|
13
|
+
|
|
6
14
|
mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
7
15
|
broadcastMessage: (msg: unknown) => sentMessages.push(msg),
|
|
8
16
|
assistantEventHub: {
|
|
9
|
-
getMostRecentClientByCapability: (cap: string) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
getMostRecentClientByCapability: (cap: string) => {
|
|
18
|
+
if (mockClients.length > 0) {
|
|
19
|
+
return mockClients.find((c) => c.capabilities.includes(cap));
|
|
20
|
+
}
|
|
21
|
+
return cap === "host_file" && mockHasClient
|
|
22
|
+
? { id: "mock-client" }
|
|
23
|
+
: null;
|
|
24
|
+
},
|
|
25
|
+
listClientsByCapability: (cap: string) => {
|
|
26
|
+
if (mockClients.length > 0) {
|
|
27
|
+
return mockClients.filter((c) => c.capabilities.includes(cap));
|
|
28
|
+
}
|
|
29
|
+
return cap === "host_file" && mockHasClient
|
|
13
30
|
? [{ clientId: "mock-client", capabilities: ["host_file"] }]
|
|
14
|
-
: []
|
|
15
|
-
|
|
31
|
+
: [];
|
|
32
|
+
},
|
|
33
|
+
getClientById: (id: string) => mockClients.find((c) => c.clientId === id),
|
|
34
|
+
getActorPrincipalIdForClient: (id: string) =>
|
|
35
|
+
mockClients.find((c) => c.clientId === id)?.actorPrincipalId,
|
|
16
36
|
},
|
|
17
37
|
}));
|
|
18
38
|
|
|
@@ -32,6 +52,7 @@ describe("HostFileProxy", () => {
|
|
|
32
52
|
function setup() {
|
|
33
53
|
sentMessages.length = 0;
|
|
34
54
|
mockHasClient = false;
|
|
55
|
+
mockClients = [];
|
|
35
56
|
pendingInteractions.clear();
|
|
36
57
|
proxy = new (HostFileProxy as any)();
|
|
37
58
|
}
|
|
@@ -584,4 +605,245 @@ describe("HostFileProxy", () => {
|
|
|
584
605
|
expect(pendingInteractions.get(requestId)).toBeUndefined();
|
|
585
606
|
});
|
|
586
607
|
});
|
|
608
|
+
|
|
609
|
+
describe("same-user binding (sourceActorPrincipalId)", () => {
|
|
610
|
+
test("targeted request from same user reaches pendingInteractions", async () => {
|
|
611
|
+
setup();
|
|
612
|
+
mockClients = [
|
|
613
|
+
{
|
|
614
|
+
clientId: "client-A",
|
|
615
|
+
capabilities: ["host_file"],
|
|
616
|
+
actorPrincipalId: "user-A",
|
|
617
|
+
},
|
|
618
|
+
];
|
|
619
|
+
|
|
620
|
+
const resultPromise = proxy.request(
|
|
621
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
622
|
+
"session-1",
|
|
623
|
+
undefined,
|
|
624
|
+
"client-A",
|
|
625
|
+
"user-A",
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
// Request was registered (made it past the same-user gate).
|
|
629
|
+
expect(sentMessages).toHaveLength(1);
|
|
630
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
631
|
+
const requestId = sent.requestId as string;
|
|
632
|
+
expect(pendingInteractions.get(requestId)).toBeDefined();
|
|
633
|
+
|
|
634
|
+
// Drain to avoid leaks.
|
|
635
|
+
proxy.resolve(requestId, { content: "ok", isError: false });
|
|
636
|
+
await resultPromise;
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("targeted request from a different user is rejected", async () => {
|
|
640
|
+
setup();
|
|
641
|
+
mockClients = [
|
|
642
|
+
{
|
|
643
|
+
clientId: "client-A",
|
|
644
|
+
capabilities: ["host_file"],
|
|
645
|
+
actorPrincipalId: "user-A",
|
|
646
|
+
},
|
|
647
|
+
];
|
|
648
|
+
|
|
649
|
+
const result = await proxy.request(
|
|
650
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
651
|
+
"session-1",
|
|
652
|
+
undefined,
|
|
653
|
+
"client-A",
|
|
654
|
+
"user-B",
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
expect(result.isError).toBe(true);
|
|
658
|
+
expect(result.content).toContain(
|
|
659
|
+
"Submitting actor does not match the target client's actor",
|
|
660
|
+
);
|
|
661
|
+
// No host_file_request was broadcast.
|
|
662
|
+
expect(sentMessages).toHaveLength(0);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("targeted request to a client with no actor principal is rejected", async () => {
|
|
666
|
+
setup();
|
|
667
|
+
mockClients = [
|
|
668
|
+
{
|
|
669
|
+
clientId: "client-A",
|
|
670
|
+
capabilities: ["host_file"],
|
|
671
|
+
// actorPrincipalId omitted (legacy/service-token client).
|
|
672
|
+
},
|
|
673
|
+
];
|
|
674
|
+
|
|
675
|
+
const result = await proxy.request(
|
|
676
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
677
|
+
"session-1",
|
|
678
|
+
undefined,
|
|
679
|
+
"client-A",
|
|
680
|
+
"user-A",
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
expect(result.isError).toBe(true);
|
|
684
|
+
expect(result.content).toContain(
|
|
685
|
+
"Submitting actor does not match the target client's actor",
|
|
686
|
+
);
|
|
687
|
+
expect(sentMessages).toHaveLength(0);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test("targeted request without source principal is rejected", async () => {
|
|
691
|
+
setup();
|
|
692
|
+
mockClients = [
|
|
693
|
+
{
|
|
694
|
+
clientId: "client-A",
|
|
695
|
+
capabilities: ["host_file"],
|
|
696
|
+
actorPrincipalId: "user-A",
|
|
697
|
+
},
|
|
698
|
+
];
|
|
699
|
+
|
|
700
|
+
const result = await proxy.request(
|
|
701
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
702
|
+
"session-1",
|
|
703
|
+
undefined,
|
|
704
|
+
"client-A",
|
|
705
|
+
undefined,
|
|
706
|
+
);
|
|
707
|
+
|
|
708
|
+
expect(result.isError).toBe(true);
|
|
709
|
+
expect(result.content).toContain(
|
|
710
|
+
"Submitting actor does not match the target client's actor",
|
|
711
|
+
);
|
|
712
|
+
expect(sentMessages).toHaveLength(0);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
test("untargeted request with no auto-resolve match still broadcasts (legacy path unchanged)", async () => {
|
|
716
|
+
setup();
|
|
717
|
+
// No matching same-user clients available.
|
|
718
|
+
mockClients = [
|
|
719
|
+
{
|
|
720
|
+
clientId: "client-A",
|
|
721
|
+
capabilities: ["host_file"],
|
|
722
|
+
actorPrincipalId: "user-A",
|
|
723
|
+
},
|
|
724
|
+
];
|
|
725
|
+
|
|
726
|
+
const resultPromise = proxy.request(
|
|
727
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
728
|
+
"session-1",
|
|
729
|
+
undefined,
|
|
730
|
+
undefined,
|
|
731
|
+
"user-B", // No same-user match → no auto-resolve, broadcast untargeted.
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
expect(sentMessages).toHaveLength(1);
|
|
735
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
736
|
+
expect(sent.targetClientId).toBeUndefined();
|
|
737
|
+
const requestId = sent.requestId as string;
|
|
738
|
+
|
|
739
|
+
proxy.resolve(requestId, { content: "ok", isError: false });
|
|
740
|
+
await resultPromise;
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
test("auto-resolve picks the same-user client when there's exactly one", async () => {
|
|
744
|
+
setup();
|
|
745
|
+
mockClients = [
|
|
746
|
+
{
|
|
747
|
+
clientId: "client-A",
|
|
748
|
+
capabilities: ["host_file"],
|
|
749
|
+
actorPrincipalId: "user-A",
|
|
750
|
+
},
|
|
751
|
+
{
|
|
752
|
+
clientId: "client-B",
|
|
753
|
+
capabilities: ["host_file"],
|
|
754
|
+
actorPrincipalId: "user-B",
|
|
755
|
+
},
|
|
756
|
+
];
|
|
757
|
+
|
|
758
|
+
const resultPromise = proxy.request(
|
|
759
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
760
|
+
"session-1",
|
|
761
|
+
undefined,
|
|
762
|
+
undefined,
|
|
763
|
+
"user-A",
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
// Auto-resolved to client-A and broadcast targeted at it.
|
|
767
|
+
expect(sentMessages).toHaveLength(1);
|
|
768
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
769
|
+
expect(sent.targetClientId).toBe("client-A");
|
|
770
|
+
const requestId = sent.requestId as string;
|
|
771
|
+
|
|
772
|
+
proxy.resolve(requestId, { content: "ok", isError: false });
|
|
773
|
+
await resultPromise;
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test("auto-resolve falls through when no client matches the source user", async () => {
|
|
777
|
+
setup();
|
|
778
|
+
mockClients = [
|
|
779
|
+
{
|
|
780
|
+
clientId: "client-A",
|
|
781
|
+
capabilities: ["host_file"],
|
|
782
|
+
actorPrincipalId: "user-A",
|
|
783
|
+
},
|
|
784
|
+
];
|
|
785
|
+
|
|
786
|
+
const resultPromise = proxy.request(
|
|
787
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
788
|
+
"session-1",
|
|
789
|
+
undefined,
|
|
790
|
+
undefined,
|
|
791
|
+
"user-C",
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
// No same-user client → no auto-resolve, broadcast untargeted.
|
|
795
|
+
expect(sentMessages).toHaveLength(1);
|
|
796
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
797
|
+
expect(sent.targetClientId).toBeUndefined();
|
|
798
|
+
const requestId = sent.requestId as string;
|
|
799
|
+
|
|
800
|
+
proxy.resolve(requestId, { content: "ok", isError: false });
|
|
801
|
+
await resultPromise;
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
test("legacy embedded targetClientId in input still goes through the same-user gate", async () => {
|
|
805
|
+
setup();
|
|
806
|
+
mockClients = [
|
|
807
|
+
{
|
|
808
|
+
clientId: "client-A",
|
|
809
|
+
capabilities: ["host_file"],
|
|
810
|
+
actorPrincipalId: "user-A",
|
|
811
|
+
},
|
|
812
|
+
];
|
|
813
|
+
|
|
814
|
+
// Same-user via embedded input.targetClientId — should succeed.
|
|
815
|
+
const okPromise = proxy.request(
|
|
816
|
+
{
|
|
817
|
+
operation: "read",
|
|
818
|
+
path: "/tmp/ok.txt",
|
|
819
|
+
targetClientId: "client-A",
|
|
820
|
+
},
|
|
821
|
+
"session-1",
|
|
822
|
+
undefined,
|
|
823
|
+
undefined,
|
|
824
|
+
"user-A",
|
|
825
|
+
);
|
|
826
|
+
expect(sentMessages).toHaveLength(1);
|
|
827
|
+
const okRequestId = (sentMessages[0] as Record<string, unknown>)
|
|
828
|
+
.requestId as string;
|
|
829
|
+
proxy.resolve(okRequestId, { content: "ok", isError: false });
|
|
830
|
+
await okPromise;
|
|
831
|
+
|
|
832
|
+
// Cross-user via embedded input.targetClientId — should be rejected.
|
|
833
|
+
sentMessages.length = 0;
|
|
834
|
+
const rejectResult = await proxy.request(
|
|
835
|
+
{
|
|
836
|
+
operation: "read",
|
|
837
|
+
path: "/tmp/bad.txt",
|
|
838
|
+
targetClientId: "client-A",
|
|
839
|
+
},
|
|
840
|
+
"session-1",
|
|
841
|
+
undefined,
|
|
842
|
+
undefined,
|
|
843
|
+
"user-B",
|
|
844
|
+
);
|
|
845
|
+
expect(rejectResult.isError).toBe(true);
|
|
846
|
+
expect(sentMessages).toHaveLength(0);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
587
849
|
});
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the host-file-result route 403 guard
|
|
2
|
+
* Tests for the host-file-result route 403 guard.
|
|
3
3
|
*
|
|
4
4
|
* Covers:
|
|
5
|
-
* 1. Targeted + correct x-vellum-client-id header → 200
|
|
6
|
-
* 2. Targeted + missing header → 400 BadRequestError
|
|
7
|
-
* 3. Targeted + wrong header → 403 ForbiddenError, interaction NOT consumed
|
|
5
|
+
* 1. Targeted + correct x-vellum-client-id header (and matching actor) → 200
|
|
6
|
+
* 2. Targeted + missing client-id header → 400 BadRequestError
|
|
7
|
+
* 3. Targeted + wrong client-id header → 403 ForbiddenError, interaction NOT consumed
|
|
8
8
|
* 4. Untargeted (no targetClientId, no header) → 200 accepted (regression)
|
|
9
|
+
* 5. Targeted + matching client-id but mismatched actor principal → 403, NOT consumed
|
|
10
|
+
* 6. Targeted + matching client-id but missing actor principal header → 403, NOT consumed
|
|
11
|
+
* 7. Targeted + matching client-id but target client has no stored actor → 403, NOT consumed
|
|
9
12
|
*/
|
|
10
13
|
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
14
|
|
|
@@ -61,12 +64,20 @@ mock.module("../daemon/host-file-proxy.js", () => ({
|
|
|
61
64
|
},
|
|
62
65
|
}));
|
|
63
66
|
|
|
67
|
+
// Stub event hub so tests control what actorPrincipalId is associated with
|
|
68
|
+
// each connected client.
|
|
69
|
+
const clientActors = new Map<string, string>();
|
|
70
|
+
|
|
71
|
+
mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
72
|
+
assistantEventHub: {
|
|
73
|
+
getActorPrincipalIdForClient: (clientId: string) =>
|
|
74
|
+
clientActors.get(clientId),
|
|
75
|
+
},
|
|
76
|
+
}));
|
|
77
|
+
|
|
64
78
|
// ── Real imports (after mocks) ──────────────────────────────────────────────
|
|
65
79
|
|
|
66
|
-
import {
|
|
67
|
-
BadRequestError,
|
|
68
|
-
ForbiddenError,
|
|
69
|
-
} from "../runtime/routes/errors.js";
|
|
80
|
+
import { BadRequestError, ForbiddenError } from "../runtime/routes/errors.js";
|
|
70
81
|
import { ROUTES } from "../runtime/routes/host-file-routes.js";
|
|
71
82
|
|
|
72
83
|
afterAll(() => {
|
|
@@ -83,10 +94,16 @@ function registerPending(
|
|
|
83
94
|
requestId: string,
|
|
84
95
|
overrides: Partial<PendingInteraction> = {},
|
|
85
96
|
): void {
|
|
97
|
+
const targetActorPrincipalId =
|
|
98
|
+
overrides.targetActorPrincipalId ??
|
|
99
|
+
(overrides.targetClientId
|
|
100
|
+
? clientActors.get(overrides.targetClientId)
|
|
101
|
+
: undefined);
|
|
86
102
|
pendingStore.set(requestId, {
|
|
87
103
|
conversationId: "conv-1",
|
|
88
104
|
kind: "host_file",
|
|
89
105
|
...overrides,
|
|
106
|
+
targetActorPrincipalId,
|
|
90
107
|
});
|
|
91
108
|
}
|
|
92
109
|
|
|
@@ -100,23 +117,28 @@ function fileBody(requestId: string): Record<string, unknown> {
|
|
|
100
117
|
|
|
101
118
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
102
119
|
|
|
103
|
-
describe("handleHostFileResult —
|
|
120
|
+
describe("handleHostFileResult — targetClientId guard", () => {
|
|
104
121
|
beforeEach(() => {
|
|
105
122
|
pendingStore.clear();
|
|
106
123
|
resolvedIds.length = 0;
|
|
107
124
|
resolveSpy.length = 0;
|
|
125
|
+
clientActors.clear();
|
|
108
126
|
});
|
|
109
127
|
|
|
110
|
-
// ── 1. Targeted + correct
|
|
128
|
+
// ── 1. Targeted + correct headers (client + actor) → 200 ──────────────────
|
|
111
129
|
|
|
112
130
|
describe("targeted + correct x-vellum-client-id header", () => {
|
|
113
131
|
test("returns { accepted: true } and resolves the interaction", async () => {
|
|
114
132
|
const requestId = "req-file-targeted-match";
|
|
133
|
+
clientActors.set("client-A", "actor-1");
|
|
115
134
|
registerPending(requestId, { targetClientId: "client-A" });
|
|
116
135
|
|
|
117
136
|
const result = await handleHostFileResult({
|
|
118
137
|
body: fileBody(requestId),
|
|
119
|
-
headers: {
|
|
138
|
+
headers: {
|
|
139
|
+
"x-vellum-client-id": "client-A",
|
|
140
|
+
"x-vellum-actor-principal-id": "actor-1",
|
|
141
|
+
},
|
|
120
142
|
});
|
|
121
143
|
|
|
122
144
|
expect(result).toEqual({ accepted: true });
|
|
@@ -127,27 +149,31 @@ describe("handleHostFileResult — Phase 2 targetClientId guard", () => {
|
|
|
127
149
|
|
|
128
150
|
test("trims whitespace from header before comparing", async () => {
|
|
129
151
|
const requestId = "req-file-targeted-trim";
|
|
152
|
+
clientActors.set("client-A", "actor-1");
|
|
130
153
|
registerPending(requestId, { targetClientId: "client-A" });
|
|
131
154
|
|
|
132
155
|
const result = await handleHostFileResult({
|
|
133
156
|
body: fileBody(requestId),
|
|
134
|
-
headers: {
|
|
157
|
+
headers: {
|
|
158
|
+
"x-vellum-client-id": " client-A ",
|
|
159
|
+
"x-vellum-actor-principal-id": " actor-1 ",
|
|
160
|
+
},
|
|
135
161
|
});
|
|
136
162
|
|
|
137
163
|
expect(result).toEqual({ accepted: true });
|
|
138
164
|
});
|
|
139
165
|
});
|
|
140
166
|
|
|
141
|
-
// ── 2. Targeted + missing header → 400
|
|
167
|
+
// ── 2. Targeted + missing client-id header → 400 ──────────────────────────
|
|
142
168
|
|
|
143
169
|
describe("targeted + missing x-vellum-client-id header", () => {
|
|
144
170
|
test("throws BadRequestError (400) when header is absent", () => {
|
|
145
171
|
const requestId = "req-file-targeted-no-header";
|
|
146
172
|
registerPending(requestId, { targetClientId: "client-A" });
|
|
147
173
|
|
|
148
|
-
expect(() =>
|
|
149
|
-
|
|
150
|
-
)
|
|
174
|
+
expect(() => handleHostFileResult({ body: fileBody(requestId) })).toThrow(
|
|
175
|
+
BadRequestError,
|
|
176
|
+
);
|
|
151
177
|
});
|
|
152
178
|
|
|
153
179
|
test("throws BadRequestError (400) when header is empty string", () => {
|
|
@@ -177,7 +203,7 @@ describe("handleHostFileResult — Phase 2 targetClientId guard", () => {
|
|
|
177
203
|
});
|
|
178
204
|
});
|
|
179
205
|
|
|
180
|
-
// ── 3. Targeted + wrong header → 403
|
|
206
|
+
// ── 3. Targeted + wrong client-id header → 403 ────────────────────────────
|
|
181
207
|
|
|
182
208
|
describe("targeted + wrong x-vellum-client-id header", () => {
|
|
183
209
|
test("throws ForbiddenError (403) when client ID does not match", () => {
|
|
@@ -259,4 +285,136 @@ describe("handleHostFileResult — Phase 2 targetClientId guard", () => {
|
|
|
259
285
|
expect(resolveSpy).toHaveLength(1);
|
|
260
286
|
});
|
|
261
287
|
});
|
|
288
|
+
|
|
289
|
+
// ── 5. Targeted + matching client but mismatched actor → 403 ──────────────
|
|
290
|
+
|
|
291
|
+
describe("targeted + actor principal mismatch", () => {
|
|
292
|
+
test("throws ForbiddenError (403) when submitting actor does not match target client's actor", () => {
|
|
293
|
+
const requestId = "req-file-actor-mismatch";
|
|
294
|
+
clientActors.set("client-A", "actor-1");
|
|
295
|
+
registerPending(requestId, { targetClientId: "client-A" });
|
|
296
|
+
|
|
297
|
+
expect(() =>
|
|
298
|
+
handleHostFileResult({
|
|
299
|
+
body: fileBody(requestId),
|
|
300
|
+
headers: {
|
|
301
|
+
"x-vellum-client-id": "client-A",
|
|
302
|
+
"x-vellum-actor-principal-id": "actor-2",
|
|
303
|
+
},
|
|
304
|
+
}),
|
|
305
|
+
).toThrow(ForbiddenError);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("interaction is NOT consumed on actor-mismatch 403", () => {
|
|
309
|
+
const requestId = "req-file-actor-mismatch-stays";
|
|
310
|
+
clientActors.set("client-A", "actor-1");
|
|
311
|
+
registerPending(requestId, { targetClientId: "client-A" });
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
handleHostFileResult({
|
|
315
|
+
body: fileBody(requestId),
|
|
316
|
+
headers: {
|
|
317
|
+
"x-vellum-client-id": "client-A",
|
|
318
|
+
"x-vellum-actor-principal-id": "actor-2",
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
} catch {
|
|
322
|
+
// expected
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
expect(resolvedIds).not.toContain(requestId);
|
|
326
|
+
expect(pendingStore.has(requestId)).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ── 6. Targeted + matching client but missing actor header → 403 ──────────
|
|
331
|
+
|
|
332
|
+
describe("targeted + missing x-vellum-actor-principal-id header", () => {
|
|
333
|
+
test("throws ForbiddenError (403) when submitting actor header is absent", () => {
|
|
334
|
+
const requestId = "req-file-actor-missing";
|
|
335
|
+
clientActors.set("client-A", "actor-1");
|
|
336
|
+
registerPending(requestId, { targetClientId: "client-A" });
|
|
337
|
+
|
|
338
|
+
expect(() =>
|
|
339
|
+
handleHostFileResult({
|
|
340
|
+
body: fileBody(requestId),
|
|
341
|
+
headers: { "x-vellum-client-id": "client-A" },
|
|
342
|
+
}),
|
|
343
|
+
).toThrow(ForbiddenError);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("throws ForbiddenError (403) when submitting actor header is empty string", () => {
|
|
347
|
+
const requestId = "req-file-actor-empty";
|
|
348
|
+
clientActors.set("client-A", "actor-1");
|
|
349
|
+
registerPending(requestId, { targetClientId: "client-A" });
|
|
350
|
+
|
|
351
|
+
expect(() =>
|
|
352
|
+
handleHostFileResult({
|
|
353
|
+
body: fileBody(requestId),
|
|
354
|
+
headers: {
|
|
355
|
+
"x-vellum-client-id": "client-A",
|
|
356
|
+
"x-vellum-actor-principal-id": " ",
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
).toThrow(ForbiddenError);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("interaction is NOT consumed when submitting actor is missing", () => {
|
|
363
|
+
const requestId = "req-file-actor-missing-stays";
|
|
364
|
+
clientActors.set("client-A", "actor-1");
|
|
365
|
+
registerPending(requestId, { targetClientId: "client-A" });
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
handleHostFileResult({
|
|
369
|
+
body: fileBody(requestId),
|
|
370
|
+
headers: { "x-vellum-client-id": "client-A" },
|
|
371
|
+
});
|
|
372
|
+
} catch {
|
|
373
|
+
// expected
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
expect(resolvedIds).not.toContain(requestId);
|
|
377
|
+
expect(pendingStore.has(requestId)).toBe(true);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// ── 7. Targeted + target client has no stored actor → 403 ─────────────────
|
|
382
|
+
|
|
383
|
+
describe("targeted + target client has no stored actor", () => {
|
|
384
|
+
test("throws ForbiddenError (403) when target client has no actorPrincipalId on record", () => {
|
|
385
|
+
const requestId = "req-file-target-no-actor";
|
|
386
|
+
registerPending(requestId, { targetClientId: "client-A" });
|
|
387
|
+
// intentionally do not set clientActors entry for client-A
|
|
388
|
+
|
|
389
|
+
expect(() =>
|
|
390
|
+
handleHostFileResult({
|
|
391
|
+
body: fileBody(requestId),
|
|
392
|
+
headers: {
|
|
393
|
+
"x-vellum-client-id": "client-A",
|
|
394
|
+
"x-vellum-actor-principal-id": "actor-1",
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
).toThrow(ForbiddenError);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("interaction is NOT consumed when target client has no stored actor", () => {
|
|
401
|
+
const requestId = "req-file-target-no-actor-stays";
|
|
402
|
+
registerPending(requestId, { targetClientId: "client-A" });
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
handleHostFileResult({
|
|
406
|
+
body: fileBody(requestId),
|
|
407
|
+
headers: {
|
|
408
|
+
"x-vellum-client-id": "client-A",
|
|
409
|
+
"x-vellum-actor-principal-id": "actor-1",
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
} catch {
|
|
413
|
+
// expected
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
expect(resolvedIds).not.toContain(requestId);
|
|
417
|
+
expect(pendingStore.has(requestId)).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
262
420
|
});
|