@vellumai/assistant 0.8.3 → 0.8.4
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/docker-entrypoint.sh +0 -1
- package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
- package/openapi.yaml +610 -16
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
- package/src/__tests__/agent-loop.test.ts +88 -3
- package/src/__tests__/anthropic-provider.test.ts +272 -0
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/channel-delivery-store.test.ts +193 -0
- package/src/__tests__/channel-reply-delivery.test.ts +284 -5
- package/src/__tests__/channel-retry-sweep.test.ts +274 -1
- package/src/__tests__/compaction-events.test.ts +1 -1
- package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
- package/src/__tests__/config-watcher.test.ts +1 -1
- package/src/__tests__/context-token-estimator.test.ts +91 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
- package/src/__tests__/conversation-agent-loop.test.ts +25 -7
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-clean-command.test.ts +137 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-fork-crud.test.ts +161 -0
- package/src/__tests__/conversation-lifecycle.test.ts +1 -1
- package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
- package/src/__tests__/conversation-pairing.test.ts +2 -2
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
- package/src/__tests__/conversation-seed-composer.test.ts +66 -4
- package/src/__tests__/conversation-slash-commands.test.ts +36 -8
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +6 -0
- package/src/__tests__/cu-unified-flow.test.ts +10 -1
- package/src/__tests__/dm-backfill.test.ts +64 -0
- package/src/__tests__/dm-persistence.test.ts +33 -0
- package/src/__tests__/document-find-replace.test.ts +501 -0
- package/src/__tests__/first-greeting.test.ts +23 -2
- package/src/__tests__/headless-browser-navigate.test.ts +172 -0
- package/src/__tests__/host-bash-proxy.test.ts +6 -0
- package/src/__tests__/host-browser-proxy.test.ts +10 -0
- package/src/__tests__/host-cu-proxy.test.ts +8 -1
- package/src/__tests__/host-file-proxy.test.ts +8 -1
- package/src/__tests__/host-transfer-proxy.test.ts +8 -1
- package/src/__tests__/identity-routes.test.ts +57 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
- package/src/__tests__/injector-chain.test.ts +2 -0
- package/src/__tests__/injector-document-comments.test.ts +378 -0
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
- package/src/__tests__/list-messages-attachments.test.ts +21 -17
- package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
- package/src/__tests__/list-messages-page-latest.test.ts +130 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
- package/src/__tests__/llm-context-normalization.test.ts +0 -2
- package/src/__tests__/llm-resolver.test.ts +85 -1
- package/src/__tests__/log-export-routes.test.ts +99 -2
- package/src/__tests__/message-queue-steer.test.ts +114 -0
- package/src/__tests__/openai-provider.test.ts +105 -0
- package/src/__tests__/openai-responses-provider.test.ts +4 -4
- package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
- package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
- package/src/__tests__/platform.test.ts +0 -3
- package/src/__tests__/plugin-source-watcher.test.ts +302 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -51
- package/src/__tests__/process-message-display-content.test.ts +21 -16
- package/src/__tests__/server-history-render.test.ts +83 -4
- package/src/__tests__/steer-tool-repair.test.ts +249 -0
- package/src/__tests__/system-prompt.test.ts +51 -28
- package/src/__tests__/terminal-tools.test.ts +11 -1
- package/src/__tests__/thinking-block-replay.test.ts +113 -0
- package/src/__tests__/thread-backfill.test.ts +370 -22
- package/src/__tests__/tool-executor.test.ts +90 -1
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
- package/src/__tests__/twilio-routes.test.ts +1 -1
- package/src/__tests__/web-fetch.test.ts +2 -2
- package/src/__tests__/workspace-git-service.test.ts +88 -5
- package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
- package/src/agent/attachments.ts +1 -0
- package/src/agent/loop.ts +57 -20
- package/src/background-wake/next-wake.test.ts +289 -0
- package/src/background-wake/next-wake.ts +172 -0
- package/src/browser/operations.ts +15 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
- package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
- package/src/cli/commands/conversations.ts +128 -1
- package/src/cli/commands/inference-providers.ts +147 -1
- package/src/cli/commands/memory-v2.ts +308 -0
- package/src/cli/commands/notifications.ts +24 -2
- package/src/cli/utils/conversation-id.ts +17 -5
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
- package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
- package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
- package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -0
- package/src/config/bundled-tool-registry.ts +22 -12
- package/src/config/call-site-defaults.ts +19 -0
- package/src/config/feature-flag-registry.json +99 -3
- package/src/config/llm-resolver.ts +16 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
- package/src/config/schemas/call-site-catalog.ts +21 -0
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-v2.ts +48 -1
- package/src/context/compactor.ts +8 -1
- package/src/context/token-estimator.ts +47 -4
- package/src/context/window-manager.ts +25 -0
- package/src/credential-health/credential-health-service.ts +34 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
- package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
- package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
- package/src/daemon/conversation-agent-loop.ts +223 -54
- package/src/daemon/conversation-lifecycle.ts +142 -116
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +273 -0
- package/src/daemon/conversation-queue-manager.ts +14 -0
- package/src/daemon/conversation-runtime-assembly.ts +135 -75
- package/src/daemon/conversation-slash.ts +37 -5
- package/src/daemon/conversation-surfaces.ts +45 -2
- package/src/daemon/conversation-tool-setup.ts +7 -0
- package/src/daemon/conversation.ts +42 -5
- package/src/daemon/first-greeting.ts +10 -0
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
- package/src/daemon/handlers/config-a2a.ts +160 -0
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +79 -0
- package/src/daemon/handlers/shared.ts +92 -29
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +18 -4
- package/src/daemon/message-protocol.ts +4 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/document-comments.ts +50 -0
- package/src/daemon/message-types/messages.ts +68 -1
- package/src/daemon/message-types/surfaces.ts +3 -1
- package/src/daemon/message-types/web-activity.ts +57 -0
- package/src/daemon/plugin-source-watcher.ts +135 -3
- package/src/daemon/process-message.ts +69 -12
- package/src/daemon/query-complexity-router.ts +75 -0
- package/src/daemon/trust-context.ts +6 -0
- package/src/documents/document-comments-store.test.ts +338 -0
- package/src/documents/document-comments-store.ts +237 -0
- package/src/documents/document-store.ts +202 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
- package/src/heartbeat/heartbeat-service.ts +1 -0
- package/src/home/__tests__/suggested-prompts.test.ts +33 -2
- package/src/home/feed-types.ts +6 -1
- package/src/home/home-content-refresh.ts +52 -0
- package/src/home/home-greeting-cache.ts +69 -0
- package/src/home/home-greeting.ts +94 -0
- package/src/home/suggested-prompts.ts +177 -9
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
- package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
- package/src/memory/conversation-crud.ts +133 -43
- package/src/memory/db-init.ts +16 -0
- package/src/memory/delivery-crud.ts +41 -0
- package/src/memory/delivery-status.ts +141 -15
- package/src/memory/external-conversation-store.ts +32 -1
- package/src/memory/jobs-worker.ts +21 -1
- package/src/memory/memory-retrospective-constants.ts +28 -0
- package/src/memory/memory-retrospective-enqueue.ts +3 -2
- package/src/memory/memory-retrospective-job.ts +408 -18
- package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
- package/src/memory/memory-v2-activation-log-store.ts +26 -8
- package/src/memory/migrations/100-core-tables.ts +1 -0
- package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
- package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
- package/src/memory/migrations/253-document-comments.ts +47 -0
- package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
- package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
- package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
- package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
- package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
- package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
- package/src/memory/migrations/index.ts +17 -0
- package/src/memory/migrations/registry.ts +25 -0
- package/src/memory/onboarding-events-store.ts +7 -0
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/conversations.ts +3 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
- package/src/memory/v2/__tests__/injection.test.ts +31 -14
- package/src/memory/v2/__tests__/page-index.test.ts +365 -1
- package/src/memory/v2/__tests__/router.test.ts +489 -1
- package/src/memory/v2/consolidation-job.ts +14 -0
- package/src/memory/v2/injection-events.ts +101 -0
- package/src/memory/v2/injection.ts +21 -10
- package/src/memory/v2/page-index.ts +209 -7
- package/src/memory/v2/page-store.ts +18 -0
- package/src/memory/v2/router.ts +209 -55
- package/src/messaging/providers/index.ts +7 -1
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
- package/src/messaging/providers/slack/adapter.ts +178 -25
- package/src/messaging/providers/slack/api.test.ts +54 -0
- package/src/messaging/providers/slack/api.ts +119 -3
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/deep-link.ts +20 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
- package/src/messaging/providers/slack/message-metadata.ts +156 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
- package/src/messaging/providers/slack/render-transcript.ts +176 -49
- package/src/messaging/providers/slack/send.test.ts +77 -0
- package/src/messaging/providers/slack/send.ts +8 -2
- package/src/messaging/providers/slack/types.ts +14 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
- package/src/notifications/conversation-seed-composer.ts +14 -2
- package/src/notifications/deferred-emit.ts +135 -0
- package/src/notifications/emit-signal.ts +9 -1
- package/src/notifications/home-feed-side-effect.ts +60 -30
- package/src/oauth/connect-orchestrator.ts +3 -0
- package/src/oauth/credential-token-resolver.ts +2 -0
- package/src/oauth/manual-token-connection.ts +19 -0
- package/src/oauth/oauth-store.ts +12 -0
- package/src/oauth/seed-providers.ts +22 -0
- package/src/permissions/prompter.ts +5 -2
- package/src/permissions/secret-prompter.ts +4 -1
- package/src/plugins/defaults/injectors.ts +82 -9
- package/src/prompts/__tests__/system-prompt.test.ts +46 -2
- package/src/prompts/normalize-onboarding.ts +40 -0
- package/src/prompts/sections.ts +32 -14
- package/src/prompts/system-prompt.ts +105 -68
- package/src/prompts/template-detection.ts +37 -0
- package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
- package/src/prompts/templates/BOOTSTRAP.md +8 -0
- package/src/prompts/templates/VOICE.md +3 -0
- package/src/prompts/templates/system-sections.ts +53 -3
- package/src/providers/anthropic/client.ts +132 -5
- package/src/providers/fireworks/client.ts +20 -2
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
- package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
- package/src/providers/inference/adapter-factory.ts +15 -1
- package/src/providers/inference/auth.ts +3 -3
- package/src/providers/inference/codex-token-refresh.ts +128 -0
- package/src/providers/inference/resolve-auth.ts +49 -6
- package/src/providers/model-catalog.ts +48 -1
- package/src/providers/openai/chat-completions-provider.ts +57 -20
- package/src/providers/openai/responses-provider.ts +9 -3
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/types.ts +25 -0
- package/src/runtime/__tests__/agent-wake.test.ts +214 -0
- package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
- package/src/runtime/agent-wake.ts +151 -56
- package/src/runtime/auth/route-policy.ts +7 -3
- package/src/runtime/background-job-runner.ts +26 -0
- package/src/runtime/channel-reply-delivery.ts +182 -47
- package/src/runtime/channel-retry-sweep.ts +141 -16
- package/src/runtime/http-types.ts +7 -4
- package/src/runtime/pending-interactions.ts +51 -8
- package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
- package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
- package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
- package/src/runtime/routes/approval-routes.ts +4 -1
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
- package/src/runtime/routes/content-source-routes.ts +78 -0
- package/src/runtime/routes/conversation-cli-routes.ts +146 -1
- package/src/runtime/routes/conversation-query-routes.ts +60 -1
- package/src/runtime/routes/conversation-routes.ts +281 -76
- package/src/runtime/routes/document-comments-routes.ts +287 -0
- package/src/runtime/routes/documents-routes.ts +33 -0
- package/src/runtime/routes/home-feed-routes.ts +6 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-browser-routes.ts +8 -1
- package/src/runtime/routes/identity-routes.ts +21 -0
- package/src/runtime/routes/inbound-message-handler.ts +288 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
- package/src/runtime/routes/index.ts +12 -4
- package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
- package/src/runtime/routes/integrations/a2a.ts +60 -1
- package/src/runtime/routes/log-export-routes.ts +39 -0
- package/src/runtime/routes/memory-v2-routes.ts +217 -0
- package/src/runtime/routes/notification-routes.ts +19 -2
- package/src/runtime/routes/question-routes.ts +4 -1
- package/src/runtime/routes/sanity-routes.ts +159 -0
- package/src/runtime/routes/slack-channel-routes.ts +187 -0
- package/src/runtime/services/conversation-serializer.ts +30 -4
- package/src/schedule/integration-status.ts +3 -1
- package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
- package/src/security/oauth2-device-code.ts +307 -0
- package/src/security/oauth2.ts +26 -9
- package/src/security/secure-keys.ts +5 -0
- package/src/skills/catalog-install.ts +6 -2
- package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
- package/src/tools/browser/browser-execution.ts +93 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
- package/src/tools/browser/cdp-client/factory.ts +87 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
- package/src/tools/browser/cdp-client/types.ts +36 -0
- package/src/tools/browser/pinned-tabs.ts +90 -0
- package/src/tools/document/document-comment-tool.test.ts +379 -0
- package/src/tools/document/document-comment-tool.ts +156 -0
- package/src/tools/document/document-tool.ts +128 -2
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
- package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
- package/src/tools/network/domain-normalize.ts +17 -0
- package/src/tools/network/web-fetch.ts +213 -64
- package/src/tools/network/web-search.ts +191 -66
- package/src/tools/terminal/safe-env.ts +3 -2
- package/src/tools/tool-approval-handler.ts +19 -12
- package/src/tools/types.ts +4 -0
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/types/onboarding-context.ts +4 -0
- package/src/util/__tests__/favicon.test.ts +84 -0
- package/src/util/favicon.ts +40 -0
- package/src/util/platform.ts +0 -5
- package/src/workspace/git-service.ts +75 -4
- package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/config/bundled-skills/document/SKILL.md +0 -54
- package/src/config/bundled-skills/document/TOOLS.json +0 -106
- package/src/daemon/seed-files.ts +0 -18
- package/src/runtime/routes/interface-routes.ts +0 -43
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
- /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies that every resolution path through
|
|
3
|
+
* `runtime/pending-interactions.ts` publishes an `interaction_resolved`
|
|
4
|
+
* envelope on the event hub with the right state.
|
|
5
|
+
*
|
|
6
|
+
* Each test registers an interaction directly and calls `resolve()` or
|
|
7
|
+
* `removeByConversation()` so we exercise the tracker in isolation
|
|
8
|
+
* without spinning up a Conversation, prompter, or proxy.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import type { ServerMessage } from "../daemon/message-protocol.js";
|
|
14
|
+
|
|
15
|
+
// Capture every broadcast emitted by the tracker. The real hub is replaced
|
|
16
|
+
// with a thin recorder so we can assert payloads deterministically.
|
|
17
|
+
const publishedMessages: ServerMessage[] = [];
|
|
18
|
+
|
|
19
|
+
mock.module("../runtime/assistant-event-hub.js", () => ({
|
|
20
|
+
broadcastMessage: (msg: ServerMessage) => {
|
|
21
|
+
publishedMessages.push(msg);
|
|
22
|
+
},
|
|
23
|
+
capabilityForMessageType: () => undefined,
|
|
24
|
+
assistantEventHub: {
|
|
25
|
+
publish: async () => {},
|
|
26
|
+
subscribe: () => ({ dispose: () => {}, active: true }),
|
|
27
|
+
},
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const pendingInteractions = await import("../runtime/pending-interactions.js");
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
publishedMessages.length = 0;
|
|
34
|
+
pendingInteractions.clear();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
pendingInteractions.clear();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function lastResolvedEvent() {
|
|
42
|
+
const evt = publishedMessages.find((m) => m.type === "interaction_resolved");
|
|
43
|
+
expect(evt).toBeDefined();
|
|
44
|
+
return evt as Extract<ServerMessage, { type: "interaction_resolved" }>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("pendingInteractions.resolve emits interaction_resolved", () => {
|
|
48
|
+
test("default state is 'cancelled'", () => {
|
|
49
|
+
pendingInteractions.register("req-1", {
|
|
50
|
+
conversationId: "conv-1",
|
|
51
|
+
kind: "confirmation",
|
|
52
|
+
});
|
|
53
|
+
const returned = pendingInteractions.resolve("req-1");
|
|
54
|
+
expect(returned).toBeDefined();
|
|
55
|
+
const evt = lastResolvedEvent();
|
|
56
|
+
expect(evt.requestId).toBe("req-1");
|
|
57
|
+
expect(evt.conversationKey).toBe("conv-1");
|
|
58
|
+
expect(evt.conversationId).toBe("conv-1");
|
|
59
|
+
expect(evt.state).toBe("cancelled");
|
|
60
|
+
expect(evt.kind).toBe("confirmation");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("approved state propagates", () => {
|
|
64
|
+
pendingInteractions.register("req-approve", {
|
|
65
|
+
conversationId: "conv-a",
|
|
66
|
+
kind: "confirmation",
|
|
67
|
+
});
|
|
68
|
+
pendingInteractions.resolve("req-approve", "approved");
|
|
69
|
+
expect(lastResolvedEvent().state).toBe("approved");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("rejected state propagates", () => {
|
|
73
|
+
pendingInteractions.register("req-reject", {
|
|
74
|
+
conversationId: "conv-b",
|
|
75
|
+
kind: "confirmation",
|
|
76
|
+
});
|
|
77
|
+
pendingInteractions.resolve("req-reject", "rejected");
|
|
78
|
+
expect(lastResolvedEvent().state).toBe("rejected");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("answered state propagates (secret response)", () => {
|
|
82
|
+
pendingInteractions.register("req-secret", {
|
|
83
|
+
conversationId: "conv-c",
|
|
84
|
+
kind: "secret",
|
|
85
|
+
});
|
|
86
|
+
pendingInteractions.resolve("req-secret", "answered");
|
|
87
|
+
const evt = lastResolvedEvent();
|
|
88
|
+
expect(evt.state).toBe("answered");
|
|
89
|
+
expect(evt.kind).toBe("secret");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("superseded state propagates", () => {
|
|
93
|
+
pendingInteractions.register("req-super", {
|
|
94
|
+
conversationId: "conv-d",
|
|
95
|
+
kind: "confirmation",
|
|
96
|
+
});
|
|
97
|
+
pendingInteractions.resolve("req-super", "superseded");
|
|
98
|
+
expect(lastResolvedEvent().state).toBe("superseded");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("no event is emitted when the requestId is unknown", () => {
|
|
102
|
+
pendingInteractions.resolve("never-registered", "approved");
|
|
103
|
+
expect(publishedMessages).toHaveLength(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("a single resolve emits exactly one event", () => {
|
|
107
|
+
pendingInteractions.register("req-once", {
|
|
108
|
+
conversationId: "conv-e",
|
|
109
|
+
kind: "host_bash",
|
|
110
|
+
});
|
|
111
|
+
pendingInteractions.resolve("req-once", "answered");
|
|
112
|
+
// Second resolve is a no-op because the entry was already consumed.
|
|
113
|
+
pendingInteractions.resolve("req-once", "answered");
|
|
114
|
+
const events = publishedMessages.filter(
|
|
115
|
+
(m) => m.type === "interaction_resolved",
|
|
116
|
+
);
|
|
117
|
+
expect(events).toHaveLength(1);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("clears the registered timer on resolve", () => {
|
|
121
|
+
let fired = false;
|
|
122
|
+
const timer = setTimeout(() => {
|
|
123
|
+
fired = true;
|
|
124
|
+
}, 10_000);
|
|
125
|
+
pendingInteractions.register("req-timer", {
|
|
126
|
+
conversationId: "conv-f",
|
|
127
|
+
kind: "confirmation",
|
|
128
|
+
timer,
|
|
129
|
+
});
|
|
130
|
+
pendingInteractions.resolve("req-timer", "approved");
|
|
131
|
+
clearTimeout(timer);
|
|
132
|
+
expect(fired).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("removeByConversation emits interaction_resolved per entry", () => {
|
|
137
|
+
test("emits superseded for every non-host interaction in the conversation", () => {
|
|
138
|
+
pendingInteractions.register("conf-1", {
|
|
139
|
+
conversationId: "conv-x",
|
|
140
|
+
kind: "confirmation",
|
|
141
|
+
});
|
|
142
|
+
pendingInteractions.register("secret-1", {
|
|
143
|
+
conversationId: "conv-x",
|
|
144
|
+
kind: "secret",
|
|
145
|
+
});
|
|
146
|
+
pendingInteractions.register("question-1", {
|
|
147
|
+
conversationId: "conv-x",
|
|
148
|
+
kind: "question",
|
|
149
|
+
});
|
|
150
|
+
pendingInteractions.register("host-bash-1", {
|
|
151
|
+
conversationId: "conv-x",
|
|
152
|
+
kind: "host_bash",
|
|
153
|
+
});
|
|
154
|
+
pendingInteractions.register("conf-other", {
|
|
155
|
+
conversationId: "conv-y",
|
|
156
|
+
kind: "confirmation",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
pendingInteractions.removeByConversation("conv-x");
|
|
160
|
+
|
|
161
|
+
const events = publishedMessages.filter(
|
|
162
|
+
(m) => m.type === "interaction_resolved",
|
|
163
|
+
) as Extract<ServerMessage, { type: "interaction_resolved" }>[];
|
|
164
|
+
expect(events).toHaveLength(3);
|
|
165
|
+
expect(events.every((e) => e.state === "superseded")).toBe(true);
|
|
166
|
+
const requestIds = new Set(events.map((e) => e.requestId));
|
|
167
|
+
expect(requestIds).toEqual(new Set(["conf-1", "secret-1", "question-1"]));
|
|
168
|
+
|
|
169
|
+
// host_bash entries survive auto-deny — no event for them.
|
|
170
|
+
expect(pendingInteractions.get("host-bash-1")).toBeDefined();
|
|
171
|
+
// Unrelated conversation is untouched.
|
|
172
|
+
expect(pendingInteractions.get("conf-other")).toBeDefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("explicit state arg overrides the default 'superseded'", () => {
|
|
176
|
+
pendingInteractions.register("conf-2", {
|
|
177
|
+
conversationId: "conv-z",
|
|
178
|
+
kind: "confirmation",
|
|
179
|
+
});
|
|
180
|
+
pendingInteractions.removeByConversation("conv-z", "cancelled");
|
|
181
|
+
const events = publishedMessages.filter(
|
|
182
|
+
(m) => m.type === "interaction_resolved",
|
|
183
|
+
);
|
|
184
|
+
expect(events).toHaveLength(1);
|
|
185
|
+
expect(
|
|
186
|
+
(events[0] as Extract<ServerMessage, { type: "interaction_resolved" }>)
|
|
187
|
+
.state,
|
|
188
|
+
).toBe("cancelled");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
getDataDir,
|
|
9
9
|
getDbPath,
|
|
10
10
|
getHistoryPath,
|
|
11
|
-
getInterfacesDir,
|
|
12
11
|
getLogPath,
|
|
13
12
|
getPidPath,
|
|
14
13
|
getSandboxRootDir,
|
|
@@ -67,7 +66,6 @@ describe("path characterization", () => {
|
|
|
67
66
|
expect(getDbPath()).toBe(join(data, "db", "assistant.db"));
|
|
68
67
|
expect(getLogPath()).toBe(join(data, "logs", "vellum.log"));
|
|
69
68
|
expect(getHistoryPath()).toBe(join(data, "history"));
|
|
70
|
-
expect(getInterfacesDir()).toBe(join(data, "interfaces"));
|
|
71
69
|
expect(getSandboxRootDir()).toBe(join(data, "sandbox"));
|
|
72
70
|
expect(getSandboxWorkingDir()).toBe(ws);
|
|
73
71
|
|
|
@@ -117,7 +115,6 @@ describe("path characterization", () => {
|
|
|
117
115
|
expect(existsSync(join(data, "memory"))).toBe(true);
|
|
118
116
|
expect(existsSync(join(data, "memory", "knowledge"))).toBe(true);
|
|
119
117
|
expect(existsSync(join(data, "apps"))).toBe(true);
|
|
120
|
-
expect(existsSync(join(data, "interfaces"))).toBe(true);
|
|
121
118
|
|
|
122
119
|
rmSync(wsDir, { recursive: true, force: true });
|
|
123
120
|
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for PluginSourceWatcher — filesystem watcher that detects plugin
|
|
3
|
+
* directory changes and triggers debounced reregistration.
|
|
4
|
+
*
|
|
5
|
+
* Key regression: the watcher must survive (and recover from) the Linux/Bun
|
|
6
|
+
* recursive-watch limitation where subdirectories created after watch starts
|
|
7
|
+
* are silently dropped. We test that close→reopen + rescan catches these.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Mocks — must be set up before importing the module under test
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const TEST_PLUGINS_DIR = "/tmp/test-plugins";
|
|
17
|
+
|
|
18
|
+
let capturedWatchCallback:
|
|
19
|
+
| ((eventType: string, filename: string | null) => void)
|
|
20
|
+
| null = null;
|
|
21
|
+
let mockWatchShouldThrow = false;
|
|
22
|
+
const mockWatcher = { close: mock(() => {}) };
|
|
23
|
+
|
|
24
|
+
const mockWatch = mock(
|
|
25
|
+
(
|
|
26
|
+
_path: string,
|
|
27
|
+
_opts: Record<string, unknown>,
|
|
28
|
+
callback: (eventType: string, filename: string | null) => void,
|
|
29
|
+
) => {
|
|
30
|
+
if (mockWatchShouldThrow) throw new Error("watch failed");
|
|
31
|
+
capturedWatchCallback = callback;
|
|
32
|
+
return mockWatcher;
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const mockRereadirSync = mock((_path: string): string[] => []);
|
|
37
|
+
|
|
38
|
+
let mockGetRegisteredPluginImpl: (name: string) => unknown | undefined = (
|
|
39
|
+
_name,
|
|
40
|
+
) => undefined;
|
|
41
|
+
const mockGetRegisteredPlugin = mock((name: string) =>
|
|
42
|
+
mockGetRegisteredPluginImpl(name),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
let mockReregisterExternalPluginImpl = mock(async (_name: string) => {});
|
|
46
|
+
const mockReregisterExternalPlugin = mock(async (name: string) =>
|
|
47
|
+
mockReregisterExternalPluginImpl(name),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
mock.module("node:fs", () => {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
52
|
+
const actualFs = require("node:fs");
|
|
53
|
+
return {
|
|
54
|
+
...actualFs,
|
|
55
|
+
watch: mockWatch,
|
|
56
|
+
readdirSync: mockRereadirSync,
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
mock.module("../plugins/registry.js", () => ({
|
|
61
|
+
getRegisteredPlugin: mockGetRegisteredPlugin,
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
mock.module("../util/platform.js", () => ({
|
|
65
|
+
getWorkspacePluginsDir: mock(() => TEST_PLUGINS_DIR),
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
mock.module("../daemon/external-plugins-bootstrap.js", () => ({
|
|
69
|
+
reregisterExternalPlugin: mockReregisterExternalPlugin,
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
mock.module("../util/logger.js", () => ({
|
|
73
|
+
getLogger: mock(() => ({
|
|
74
|
+
info: mock(() => {}),
|
|
75
|
+
warn: mock(() => {}),
|
|
76
|
+
})),
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Import after mocks
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
import { PluginSourceWatcher } from "../daemon/plugin-source-watcher.js";
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Tests
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
describe("PluginSourceWatcher", () => {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
PluginSourceWatcher.resetForTests();
|
|
92
|
+
capturedWatchCallback = null;
|
|
93
|
+
mockWatchShouldThrow = false;
|
|
94
|
+
mockWatcher.close.mockClear();
|
|
95
|
+
mockWatch.mockClear();
|
|
96
|
+
mockRereadirSync.mockClear();
|
|
97
|
+
mockGetRegisteredPlugin.mockClear();
|
|
98
|
+
mockReregisterExternalPlugin.mockClear();
|
|
99
|
+
mockGetRegisteredPluginImpl = (_name: string) => undefined;
|
|
100
|
+
mockReregisterExternalPluginImpl = mock(async (_name: string) => {});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
105
|
+
watcher.stop();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("start() creates a recursive watcher on the plugins directory", () => {
|
|
109
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
110
|
+
watcher.start();
|
|
111
|
+
expect(capturedWatchCallback).not.toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("plugin directory creation triggers rebuild", async () => {
|
|
115
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
116
|
+
watcher.start();
|
|
117
|
+
|
|
118
|
+
capturedWatchCallback!("change", "my-plugin");
|
|
119
|
+
|
|
120
|
+
// Wait for debounce
|
|
121
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
122
|
+
expect(mockReregisterExternalPlugin).toHaveBeenCalledWith("my-plugin");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("nested file change within plugin triggers rebuild", async () => {
|
|
126
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
127
|
+
watcher.start();
|
|
128
|
+
|
|
129
|
+
capturedWatchCallback!("change", "my-plugin/src/index.ts");
|
|
130
|
+
|
|
131
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
132
|
+
expect(mockReregisterExternalPlugin).toHaveBeenCalledWith("my-plugin");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("deeply nested file change triggers rebuild", async () => {
|
|
136
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
137
|
+
watcher.start();
|
|
138
|
+
|
|
139
|
+
capturedWatchCallback!("change", "my-plugin/src/handlers/util/helper.ts");
|
|
140
|
+
|
|
141
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
142
|
+
expect(mockReregisterExternalPlugin).toHaveBeenCalledWith("my-plugin");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("dotfiles in plugins root are ignored", async () => {
|
|
146
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
147
|
+
watcher.start();
|
|
148
|
+
|
|
149
|
+
capturedWatchCallback!("change", ".DS_Store");
|
|
150
|
+
|
|
151
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
152
|
+
expect(mockReregisterExternalPlugin).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("null filename is ignored", async () => {
|
|
156
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
157
|
+
watcher.start();
|
|
158
|
+
|
|
159
|
+
capturedWatchCallback!("change", null);
|
|
160
|
+
|
|
161
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
162
|
+
expect(mockReregisterExternalPlugin).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("rapid changes to same plugin are debounced into single rebuild", async () => {
|
|
166
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
167
|
+
watcher.start();
|
|
168
|
+
|
|
169
|
+
capturedWatchCallback!("change", "my-plugin/src/index.ts");
|
|
170
|
+
capturedWatchCallback!("change", "my-plugin/src/handlers.ts");
|
|
171
|
+
capturedWatchCallback!("change", "my-plugin/package.json");
|
|
172
|
+
|
|
173
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
174
|
+
expect(mockReregisterExternalPlugin).toHaveBeenCalledTimes(1);
|
|
175
|
+
expect(mockReregisterExternalPlugin).toHaveBeenCalledWith("my-plugin");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("changes to different plugins trigger separate rebuilds (debounced)", async () => {
|
|
179
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
180
|
+
watcher.start();
|
|
181
|
+
|
|
182
|
+
capturedWatchCallback!("change", "plugin-a/src/index.ts");
|
|
183
|
+
capturedWatchCallback!("change", "plugin-b/src/index.ts");
|
|
184
|
+
|
|
185
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
186
|
+
expect(mockReregisterExternalPlugin).toHaveBeenCalledTimes(2);
|
|
187
|
+
expect(mockReregisterExternalPlugin).toHaveBeenNthCalledWith(1, "plugin-a");
|
|
188
|
+
expect(mockReregisterExternalPlugin).toHaveBeenNthCalledWith(2, "plugin-b");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("stop() closes watcher and cancels pending rebuilds", () => {
|
|
192
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
193
|
+
watcher.start();
|
|
194
|
+
|
|
195
|
+
capturedWatchCallback!("change", "my-plugin/src/index.ts");
|
|
196
|
+
watcher.stop();
|
|
197
|
+
|
|
198
|
+
expect(mockWatcher.close).toHaveBeenCalledTimes(1);
|
|
199
|
+
// No rebuild should fire after stop
|
|
200
|
+
expect(mockReregisterExternalPlugin).not.toHaveBeenCalled();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("ensureStarted() initializes watcher after start() if watch coverage was lost", () => {
|
|
204
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
205
|
+
watcher.start();
|
|
206
|
+
expect(capturedWatchCallback).not.toBeNull();
|
|
207
|
+
|
|
208
|
+
// Simulate lost coverage while the daemon lifecycle is still started
|
|
209
|
+
// (e.g. a previous watch attempt failed after startup).
|
|
210
|
+
(watcher as unknown as { watcher: unknown }).watcher = null;
|
|
211
|
+
capturedWatchCallback = null;
|
|
212
|
+
|
|
213
|
+
watcher.ensureStarted();
|
|
214
|
+
expect(capturedWatchCallback).not.toBeNull();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("ensureStarted() is a no-op when watcher is already running", () => {
|
|
218
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
219
|
+
watcher.start();
|
|
220
|
+
const callCountAfterStart = mockWatch.mock.calls.length;
|
|
221
|
+
|
|
222
|
+
watcher.ensureStarted();
|
|
223
|
+
expect(mockWatch.mock.calls.length).toBe(callCountAfterStart);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("watcher restart keeps the previous watcher active if replacement fails", async () => {
|
|
227
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
228
|
+
watcher.start();
|
|
229
|
+
const firstCallback = capturedWatchCallback;
|
|
230
|
+
|
|
231
|
+
mockWatchShouldThrow = true;
|
|
232
|
+
capturedWatchCallback!("change", "my-plugin/src/index.ts");
|
|
233
|
+
|
|
234
|
+
await new Promise((r) => setTimeout(r, 600));
|
|
235
|
+
|
|
236
|
+
expect(mockWatcher.close).not.toHaveBeenCalled();
|
|
237
|
+
expect((watcher as unknown as { watcher: unknown }).watcher).toBe(
|
|
238
|
+
mockWatcher,
|
|
239
|
+
);
|
|
240
|
+
expect(capturedWatchCallback).toBe(firstCallback);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("singleton instance is shared across calls", () => {
|
|
244
|
+
const watcher1 = PluginSourceWatcher.getInstance();
|
|
245
|
+
const watcher2 = PluginSourceWatcher.getInstance();
|
|
246
|
+
expect(watcher1).toBe(watcher2);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("resetForTests() clears the singleton", () => {
|
|
250
|
+
const watcher1 = PluginSourceWatcher.getInstance();
|
|
251
|
+
watcher1.start();
|
|
252
|
+
|
|
253
|
+
PluginSourceWatcher.resetForTests();
|
|
254
|
+
const watcher2 = PluginSourceWatcher.getInstance();
|
|
255
|
+
|
|
256
|
+
expect(watcher1).not.toBe(watcher2);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* REGRESSION: When an event arrives during a close→reopen swap, rescan
|
|
261
|
+
* must catch any plugin not yet in the registry.
|
|
262
|
+
*
|
|
263
|
+
* Scenario:
|
|
264
|
+
* 1. Plugin "new-plugin" directory is created
|
|
265
|
+
* 2. Event fires, triggering a watcher restart (close + reopen)
|
|
266
|
+
* 3. Before the old watcher's callback is fully fired, another plugin
|
|
267
|
+
* "late-plugin" is created
|
|
268
|
+
* 4. The new watcher doesn't yet know about "late-plugin"
|
|
269
|
+
* 5. After the reopen, rescan walks the directory and finds "late-plugin"
|
|
270
|
+
* not in the registry, and schedules its rebuild
|
|
271
|
+
*/
|
|
272
|
+
test("watcher restart + rescan catches plugins created during close→reopen", async () => {
|
|
273
|
+
const watcher = PluginSourceWatcher.getInstance();
|
|
274
|
+
watcher.start();
|
|
275
|
+
|
|
276
|
+
// Track which plugins are "registered" at each point
|
|
277
|
+
const registeredPlugins = new Set<string>();
|
|
278
|
+
mockGetRegisteredPluginImpl = (name: string) =>
|
|
279
|
+
registeredPlugins.has(name) ? { name } : undefined;
|
|
280
|
+
|
|
281
|
+
// Simulate multiple plugins on disk
|
|
282
|
+
mockRereadirSync.mockImplementation(() => [
|
|
283
|
+
"new-plugin",
|
|
284
|
+
"late-plugin",
|
|
285
|
+
".DS_Store",
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
// Fire an event on new-plugin (this will trigger a watcher restart)
|
|
289
|
+
capturedWatchCallback!("change", "new-plugin/src/index.ts");
|
|
290
|
+
|
|
291
|
+
// Wait for the direct rebuild debounce, the watcher-restart debounce,
|
|
292
|
+
// and the rescan-triggered rebuild debounce.
|
|
293
|
+
await new Promise((r) => setTimeout(r, 1100));
|
|
294
|
+
|
|
295
|
+
// At this point, rescan should have run and discovered late-plugin,
|
|
296
|
+
// even though no direct fs.watch event was delivered for it.
|
|
297
|
+
const calls = mockReregisterExternalPlugin.mock.calls.map((c) => c[0]);
|
|
298
|
+
expect(calls).toContain("new-plugin");
|
|
299
|
+
expect(calls).toContain("late-plugin");
|
|
300
|
+
expect(calls).not.toContain(".DS_Store");
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -95,7 +95,6 @@ type PersistUserMessageMock = ReturnType<
|
|
|
95
95
|
type RunAgentLoopMock = ReturnType<
|
|
96
96
|
typeof mock<(...args: unknown[]) => Promise<void>>
|
|
97
97
|
>;
|
|
98
|
-
type NoticeMock = ReturnType<typeof mock<(notice: string | undefined) => void>>;
|
|
99
98
|
interface TestConversation {
|
|
100
99
|
conversationId: string;
|
|
101
100
|
trustContext: unknown;
|
|
@@ -123,13 +122,10 @@ interface TestConversation {
|
|
|
123
122
|
estimatedCost: number;
|
|
124
123
|
};
|
|
125
124
|
persistUserMessage: PersistUserMessageMock;
|
|
126
|
-
setSlackRuntimeContextNotice: NoticeMock;
|
|
127
125
|
runAgentLoop: RunAgentLoopMock;
|
|
128
126
|
updateClient: (sender: (...args: unknown[]) => void) => void;
|
|
129
127
|
getCurrentSender: () => ((...args: unknown[]) => void) | undefined;
|
|
130
128
|
__loopDeferred: Deferred<void>;
|
|
131
|
-
__noticeCalls: Array<string | undefined>;
|
|
132
|
-
__loopNotices: Array<string | undefined>;
|
|
133
129
|
__clientSenders: Array<((...args: unknown[]) => void) | undefined>;
|
|
134
130
|
}
|
|
135
131
|
|
|
@@ -171,11 +167,8 @@ async function waitForRunAgentLoopCall(): Promise<void> {
|
|
|
171
167
|
function makeConversation(): TestConversation {
|
|
172
168
|
let turnChannelContext: TurnChannelContext | null = null;
|
|
173
169
|
let turnInterfaceContext: TurnInterfaceContext | null = null;
|
|
174
|
-
let slackNotice: string | undefined;
|
|
175
170
|
let currentSender: ((...args: unknown[]) => void) | undefined;
|
|
176
|
-
const noticeCalls: Array<string | undefined> = [];
|
|
177
171
|
const loopDeferred = createDeferred<void>();
|
|
178
|
-
const loopNotices: Array<string | undefined> = [];
|
|
179
172
|
const clientSenders: Array<((...args: unknown[]) => void) | undefined> = [];
|
|
180
173
|
const messages: unknown[] = [];
|
|
181
174
|
|
|
@@ -223,12 +216,7 @@ function makeConversation(): TestConversation {
|
|
|
223
216
|
_metadata?: Record<string, unknown>,
|
|
224
217
|
) => "persisted-user-message-id",
|
|
225
218
|
),
|
|
226
|
-
setSlackRuntimeContextNotice: mock((notice: string | undefined) => {
|
|
227
|
-
slackNotice = notice;
|
|
228
|
-
noticeCalls.push(notice);
|
|
229
|
-
}),
|
|
230
219
|
runAgentLoop: mock(async (..._args: unknown[]) => {
|
|
231
|
-
loopNotices.push(slackNotice);
|
|
232
220
|
await loopDeferred.promise;
|
|
233
221
|
}),
|
|
234
222
|
updateClient: (sender: (...args: unknown[]) => void) => {
|
|
@@ -237,8 +225,6 @@ function makeConversation(): TestConversation {
|
|
|
237
225
|
},
|
|
238
226
|
getCurrentSender: () => currentSender,
|
|
239
227
|
__loopDeferred: loopDeferred,
|
|
240
|
-
__noticeCalls: noticeCalls,
|
|
241
|
-
__loopNotices: loopNotices,
|
|
242
228
|
__clientSenders: clientSenders,
|
|
243
229
|
};
|
|
244
230
|
|
|
@@ -252,15 +238,13 @@ describe("processMessageInBackground Slack option propagation", () => {
|
|
|
252
238
|
broadcastMessages.length = 0;
|
|
253
239
|
});
|
|
254
240
|
|
|
255
|
-
test("passes Slack inbound metadata to persistence
|
|
241
|
+
test("passes Slack inbound metadata to persistence during background processing", async () => {
|
|
256
242
|
const slackInbound = {
|
|
257
243
|
channelId: "C0123CHANNEL",
|
|
258
244
|
channelTs: "1700000001.111111",
|
|
259
245
|
threadTs: "1700000000.000001",
|
|
260
246
|
displayName: "Alice",
|
|
261
247
|
};
|
|
262
|
-
const notice =
|
|
263
|
-
"Slack context note: this turn joined an existing thread. 2 earlier messages were backfilled.";
|
|
264
248
|
|
|
265
249
|
const result = await processMessageInBackground(
|
|
266
250
|
"conv-background-slack",
|
|
@@ -268,7 +252,6 @@ describe("processMessageInBackground Slack option propagation", () => {
|
|
|
268
252
|
undefined,
|
|
269
253
|
{
|
|
270
254
|
slackInbound,
|
|
271
|
-
slackRuntimeContextNotice: notice,
|
|
272
255
|
},
|
|
273
256
|
"slack",
|
|
274
257
|
"slack",
|
|
@@ -280,43 +263,10 @@ describe("processMessageInBackground Slack option propagation", () => {
|
|
|
280
263
|
slackInbound,
|
|
281
264
|
});
|
|
282
265
|
expect(activeConversation.runAgentLoop).toHaveBeenCalledTimes(1);
|
|
283
|
-
expect(activeConversation.__loopNotices).toEqual([notice]);
|
|
284
266
|
|
|
285
267
|
activeConversation.__loopDeferred.resolve();
|
|
286
268
|
await activeConversation.__loopDeferred.promise;
|
|
287
269
|
await Promise.resolve();
|
|
288
|
-
|
|
289
|
-
expect(activeConversation.__noticeCalls).toEqual([notice, undefined]);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test("clears the Slack runtime notice after normal message processing", async () => {
|
|
293
|
-
const notice =
|
|
294
|
-
"Slack context note: this turn joined an existing thread. 2 earlier messages were backfilled.";
|
|
295
|
-
|
|
296
|
-
const processing = processMessage(
|
|
297
|
-
"conv-background-slack",
|
|
298
|
-
"Reply from Slack",
|
|
299
|
-
undefined,
|
|
300
|
-
{
|
|
301
|
-
slackRuntimeContextNotice: notice,
|
|
302
|
-
isInteractive: true,
|
|
303
|
-
},
|
|
304
|
-
"slack",
|
|
305
|
-
"slack",
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
await waitForRunAgentLoopCall();
|
|
309
|
-
|
|
310
|
-
expect(activeConversation.runAgentLoop).toHaveBeenCalledTimes(1);
|
|
311
|
-
expect(activeConversation.__loopNotices).toEqual([notice]);
|
|
312
|
-
|
|
313
|
-
activeConversation.__loopDeferred.resolve();
|
|
314
|
-
await expect(processing).resolves.toEqual({
|
|
315
|
-
messageId: "persisted-user-message-id",
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
expect(activeConversation.__noticeCalls).toEqual([notice, undefined]);
|
|
319
|
-
expect(activeConversation.__clientSenders).toHaveLength(2);
|
|
320
270
|
});
|
|
321
271
|
|
|
322
272
|
test("observes live agent events without replacing the broadcast emitter", async () => {
|
|
@@ -196,7 +196,6 @@ function makeTestConversation() {
|
|
|
196
196
|
metadata,
|
|
197
197
|
displayContent,
|
|
198
198
|
),
|
|
199
|
-
setSlackRuntimeContextNotice: () => {},
|
|
200
199
|
runAgentLoop,
|
|
201
200
|
updateClient: () => {},
|
|
202
201
|
getCurrentSender: () => undefined,
|
|
@@ -361,6 +360,7 @@ describe("processMessage displayContent", () => {
|
|
|
361
360
|
expect(persistedBlocks).toEqual([
|
|
362
361
|
{
|
|
363
362
|
type: "file",
|
|
363
|
+
_attachmentId: "att-1",
|
|
364
364
|
source: {
|
|
365
365
|
type: "base64",
|
|
366
366
|
media_type: "application/pdf",
|
|
@@ -370,21 +370,26 @@ describe("processMessage displayContent", () => {
|
|
|
370
370
|
},
|
|
371
371
|
]);
|
|
372
372
|
expect(addMessageCalls[0]!.content).not.toContain("<external_content");
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
373
|
+
const inMemoryMessage = conversation.getMessages()[0]!;
|
|
374
|
+
expect(inMemoryMessage.role).toBe("user");
|
|
375
|
+
expect(inMemoryMessage.content[0]).toEqual({
|
|
376
|
+
type: "text",
|
|
377
|
+
text: modelContent,
|
|
378
|
+
});
|
|
379
|
+
const inMemoryFileBlock = inMemoryMessage.content[1] as unknown as Record<
|
|
380
|
+
string,
|
|
381
|
+
unknown
|
|
382
|
+
>;
|
|
383
|
+
expect(inMemoryFileBlock._attachmentId).toBe("att-1");
|
|
384
|
+
expect(inMemoryFileBlock).toMatchObject({
|
|
385
|
+
type: "file",
|
|
386
|
+
source: {
|
|
387
|
+
type: "base64",
|
|
388
|
+
media_type: "application/pdf",
|
|
389
|
+
data: Buffer.from("pdf bytes").toString("base64"),
|
|
390
|
+
filename: "attachment.pdf",
|
|
391
|
+
},
|
|
392
|
+
extracted_text: undefined,
|
|
388
393
|
});
|
|
389
394
|
});
|
|
390
395
|
|