@vellumai/assistant 0.12.2 → 0.12.3-staging.1
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/.env.example +2 -0
- package/AGENTS.md +1 -1
- package/docs/architecture/integrations.md +7 -0
- package/docs/architecture/memory.md +17 -2
- package/docs/credential-execution-service.md +1 -1
- package/docs/desktop-browser-cli.md +7 -3
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/openapi.yaml +54 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +1 -0
- package/scripts/sync-llm-catalog.ts +3 -0
- package/scripts/voice-ttft-spike.ts +2 -2
- package/src/__tests__/agent-loop.test.ts +259 -0
- package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
- package/src/__tests__/anthropic-provider.test.ts +55 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/attachments-store.test.ts +22 -3
- package/src/__tests__/channel-approval.test.ts +9 -14
- package/src/__tests__/channel-reply-delivery.test.ts +56 -0
- package/src/__tests__/chat-credential-redaction.test.ts +23 -0
- package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
- package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +257 -6
- package/src/__tests__/conversation-attachments.test.ts +106 -0
- package/src/__tests__/conversation-fork-crud.test.ts +191 -0
- package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
- package/src/__tests__/credential-execution-client.test.ts +70 -53
- package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
- package/src/__tests__/events-tail-route.test.ts +33 -0
- package/src/__tests__/history-repair.test.ts +53 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
- package/src/__tests__/llm-catalog-parity.test.ts +27 -4
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-commands-routes.test.ts +114 -101
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/persist-media-references.test.ts +50 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
- package/src/__tests__/schedule-routes.test.ts +20 -0
- package/src/__tests__/scheduler-result-notification.test.ts +23 -4
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
- package/src/__tests__/secret-routes-scrub.test.ts +22 -27
- package/src/__tests__/secret-scanner.test.ts +20 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
- package/src/__tests__/secure-keys.test.ts +7 -3
- package/src/__tests__/server-tool-pairing.test.ts +107 -0
- package/src/__tests__/skills.test.ts +5 -4
- package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/tool-result-follow-up.test.ts +99 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
- package/src/__tests__/unicode.test.ts +36 -0
- package/src/agent/loop.ts +37 -12
- package/src/agent/tool-result-follow-up.ts +70 -0
- package/src/api/attachment-provenance.test.ts +68 -0
- package/src/api/computer-use-tool.test.ts +55 -0
- package/src/api/computer-use-tool.ts +27 -0
- package/src/api/events/assistant-outbound-attachment.ts +3 -0
- package/src/api/events/desktop-activity-changed.ts +10 -0
- package/src/api/events/question-request.ts +1 -0
- package/src/api/index.ts +11 -0
- package/src/api/responses/conversation-message.ts +3 -0
- package/src/approvals/approval-primitive.ts +5 -2
- package/src/approvals/scoped-approval-grants.ts +6 -2
- package/src/browser/virtual-desktop-target.ts +1 -2
- package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
- package/src/cli/commands/__tests__/plugins.test.ts +20 -1
- package/src/cli/commands/__tests__/schedules.test.ts +14 -0
- package/src/cli/commands/bash.help.ts +4 -3
- package/src/cli/commands/browser.help.ts +5 -1
- package/src/cli/commands/credentials.help.ts +3 -3
- package/src/cli/commands/oauth/index.help.ts +10 -0
- package/src/cli/commands/oauth/providers.ts +12 -0
- package/src/cli/commands/oauth/request.test.ts +179 -2
- package/src/cli/commands/oauth/request.ts +41 -10
- package/src/cli/commands/plugins.ts +12 -5
- package/src/cli/commands/schedules.ts +2 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
- package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
- package/src/cli/lib/bundled-marketplace.json +855 -0
- package/src/cli/lib/bundled-plugin-packages.json +783 -1
- package/src/cli/lib/inspect-plugin.ts +11 -4
- package/src/cli/lib/upgrade-plugin.ts +19 -11
- package/src/cli/output.ts +18 -4
- package/src/config/__tests__/default-provider.test.ts +4 -0
- package/src/config/bundled-skills/schedule/SKILL.md +6 -6
- package/src/config/loader.ts +9 -17
- package/src/config/platform-identity.ts +8 -8
- package/src/config/profile-text-generation.test.ts +51 -0
- package/src/config/profile-text-generation.ts +51 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
- package/src/config/schemas/llm.ts +7 -2
- package/src/config/schemas/mcp.ts +5 -1
- package/src/config/schemas/memory-v3.ts +12 -0
- package/src/credential-execution/ces-runtime.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +36 -134
- package/src/credential-execution/process-manager.test.ts +20 -24
- package/src/credential-execution/process-manager.ts +24 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
- package/src/daemon/assistant-attachments.ts +20 -12
- package/src/daemon/chat-credential-redaction.ts +16 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
- package/src/daemon/conversation-agent-loop.ts +12 -0
- package/src/daemon/conversation-attachments.ts +93 -12
- package/src/daemon/conversation-tool-setup.ts +71 -3
- package/src/daemon/conversation-turn-finalize.ts +26 -13
- package/src/daemon/conversation.ts +72 -17
- package/src/daemon/daemon-control.ts +3 -7
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/mcp-reload-service.ts +1 -1
- package/src/daemon/message-types/shared.ts +1 -0
- package/src/daemon/orphan-reaper.ts +4 -3
- package/src/daemon/persist-media-references.ts +20 -3
- package/src/daemon/tool-setup-types.ts +6 -0
- package/src/daemon/wake-conversation-ops.ts +50 -15
- package/src/desktop/desktop-automation-lease.test.ts +230 -0
- package/src/desktop/desktop-automation-lease.ts +90 -6
- package/src/desktop/desktop-help.ts +15 -0
- package/src/desktop/desktop-stream-bridge.test.ts +65 -10
- package/src/desktop/desktop-stream-bridge.ts +3 -3
- package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
- package/src/desktop/desktop-wallpaper-worker.ts +17 -0
- package/src/desktop/desktop-wallpaper.test.ts +48 -7
- package/src/desktop/desktop-wallpaper.ts +34 -102
- package/src/desktop/virtual-desktop-feature.ts +1 -1
- package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
- package/src/mcp/__tests__/startup.test.ts +30 -7
- package/src/mcp/__tests__/tool-caps.test.ts +107 -0
- package/src/mcp/manager.ts +168 -101
- package/src/mcp/startup.ts +33 -13
- package/src/mcp/tool-caps.ts +176 -0
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +70 -0
- package/src/notifications/copy-composer.ts +11 -3
- package/src/notifications/schedule-result-producer.ts +4 -6
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
- package/src/oauth/identity-verifier.ts +24 -0
- package/src/oauth/oauth-store.ts +11 -1
- package/src/oauth/provider-serializer.ts +1 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/permissions/confirmation-guardian-request.test.ts +16 -0
- package/src/permissions/confirmation-guardian-request.ts +2 -3
- package/src/permissions/question-prompter.test.ts +31 -0
- package/src/permissions/question-prompter.ts +2 -0
- package/src/persistence/attachments-store.ts +84 -44
- package/src/persistence/conversation-crud.ts +111 -20
- package/src/persistence/conversation-plugin-facade.ts +30 -0
- package/src/persistence/conversation-tool-surface.ts +135 -0
- package/src/persistence/conversation-types.test.ts +32 -0
- package/src/persistence/conversation-types.ts +26 -4
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
- package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/oauth.ts +1 -0
- package/src/persistence/steps.ts +24 -3
- package/src/plugin-api/conversation-turn.ts +31 -7
- package/src/plugin-api/index.ts +11 -1
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
- package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
- package/src/plugins/defaults/memory/AGENTS.md +39 -5
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
- package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
- package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
- package/src/plugins/defaults/memory/buffer-file.ts +354 -0
- package/src/plugins/defaults/memory/buffer-format.ts +40 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
- package/src/plugins/defaults/memory/context-search/format.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
- package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
- package/src/plugins/defaults/memory/host-utils.ts +0 -10
- package/src/plugins/defaults/memory/injectors.ts +4 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
- package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
- package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
- package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
- package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
- package/src/plugins/defaults/memory/v3/card.ts +2 -1
- package/src/plugins/defaults/memory/v3/injector.ts +212 -178
- package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
- package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
- package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
- package/src/plugins/defaults/memory/v3/sections.ts +2 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
- package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
- package/src/prompts/delegation-gate.ts +57 -0
- package/src/prompts/system-prompt.ts +14 -32
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/jev/client.test.ts +260 -0
- package/src/providers/jev/client.ts +518 -0
- package/src/providers/model-catalog.ts +58 -3
- package/src/providers/server-tool-pairing.ts +16 -7
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +94 -1
- package/src/runtime/agent-wake.ts +31 -4
- package/src/runtime/guardian-action-service.ts +2 -17
- package/src/runtime/guardian-reply-router.ts +1 -8
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
- package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -9
- package/src/runtime/routes/conversation-routes.ts +29 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +8 -5
- package/src/runtime/routes/guardian-approval-interception.ts +24 -0
- package/src/runtime/routes/inbound-message-handler.ts +2 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
- package/src/runtime/routes/inference-profiles-routes.ts +9 -0
- package/src/runtime/routes/oauth-commands-routes.ts +57 -54
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
- package/src/runtime/routes/oauth-request-hints.ts +149 -0
- package/src/runtime/routes/schedule-routes.ts +2 -0
- package/src/runtime/routes/secret-routes.ts +99 -119
- package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
- package/src/schedule/run-script.ts +2 -2
- package/src/schedule/scheduler.ts +47 -11
- package/src/schedule/tool-surface-readiness.ts +65 -0
- package/src/schedule/worker-mcp.ts +85 -0
- package/src/schedule/worker.ts +9 -31
- package/src/security/secure-keys.ts +60 -48
- package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
- package/src/tools/ask-question/ask-question-tool.ts +119 -23
- package/src/tools/browser/browser-execution.ts +7 -1
- package/src/tools/host-terminal/host-shell.ts +12 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
- package/src/tools/shared/shell-output.test.ts +10 -0
- package/src/tools/shared/shell-output.ts +14 -2
- package/src/tools/skills/sandbox-runner.ts +13 -2
- package/src/tools/skills/scaffold-managed.ts +2 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
- package/src/tools/terminal/safe-env.ts +35 -24
- package/src/tools/terminal/sanitized-bash.ts +15 -2
- package/src/tools/terminal/shell-launch.test.ts +162 -0
- package/src/tools/terminal/shell.test.ts +29 -0
- package/src/tools/terminal/shell.ts +13 -7
- package/src/util/browser-human-verification.ts +14 -0
- package/src/util/host-process.test.ts +17 -1
- package/src/util/host-process.ts +24 -0
- package/src/util/unicode.ts +29 -0
|
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, test } from "bun:test";
|
|
|
5
5
|
import { eq, like } from "drizzle-orm";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
getAttachmentContent,
|
|
8
9
|
getAttachmentsForMessage,
|
|
9
10
|
linkAttachmentToMessage,
|
|
10
11
|
uploadAttachment,
|
|
@@ -17,10 +18,17 @@ import {
|
|
|
17
18
|
import {
|
|
18
19
|
addMessage,
|
|
19
20
|
createConversation,
|
|
21
|
+
deleteConversation,
|
|
20
22
|
forkConversation,
|
|
21
23
|
getMessages,
|
|
24
|
+
listConversationAttachments,
|
|
25
|
+
updateMessageMetadata,
|
|
22
26
|
} from "../persistence/conversation-crud.js";
|
|
23
27
|
import { getConversationDirPath } from "../persistence/conversation-disk-view.js";
|
|
28
|
+
import {
|
|
29
|
+
COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY,
|
|
30
|
+
computerUseScreenshotAttachmentIdsFromMetadata,
|
|
31
|
+
} from "../persistence/conversation-types.js";
|
|
24
32
|
import {
|
|
25
33
|
getDb,
|
|
26
34
|
getLogsDb,
|
|
@@ -63,6 +71,7 @@ import {
|
|
|
63
71
|
recordInjected as recordV3Injected,
|
|
64
72
|
} from "../plugins/defaults/memory/v3/ever-injected-store.js";
|
|
65
73
|
import { ensureMemoryV3InjectedSectionsSchema } from "../plugins/defaults/memory/v3/plugin-schema.js";
|
|
74
|
+
import { handleListMessages } from "../runtime/routes/conversation-routes.js";
|
|
66
75
|
|
|
67
76
|
await initializeDb();
|
|
68
77
|
|
|
@@ -748,6 +757,188 @@ describe("forkConversation", () => {
|
|
|
748
757
|
);
|
|
749
758
|
});
|
|
750
759
|
|
|
760
|
+
test("remaps automatic screenshot refs and widens fork provenance", async () => {
|
|
761
|
+
const screenshotBase64 =
|
|
762
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
|
|
763
|
+
const source = createConversation("Computer use thread");
|
|
764
|
+
await addMessage(source.id, "user", "Open the example", {
|
|
765
|
+
skipIndexing: true,
|
|
766
|
+
});
|
|
767
|
+
await addMessage(
|
|
768
|
+
source.id,
|
|
769
|
+
"assistant",
|
|
770
|
+
JSON.stringify([
|
|
771
|
+
{
|
|
772
|
+
type: "tool_use",
|
|
773
|
+
id: "computer-use-call",
|
|
774
|
+
name: "computer_use_click",
|
|
775
|
+
input: { x: 10, y: 20 },
|
|
776
|
+
},
|
|
777
|
+
]),
|
|
778
|
+
{ skipIndexing: true },
|
|
779
|
+
);
|
|
780
|
+
const screenshot = await uploadAttachment(
|
|
781
|
+
"computer-use-click.png",
|
|
782
|
+
"image/png",
|
|
783
|
+
screenshotBase64,
|
|
784
|
+
);
|
|
785
|
+
const toolResult = await addMessage(
|
|
786
|
+
source.id,
|
|
787
|
+
"user",
|
|
788
|
+
JSON.stringify([
|
|
789
|
+
{
|
|
790
|
+
type: "tool_result",
|
|
791
|
+
tool_use_id: "computer-use-call",
|
|
792
|
+
content: "Clicked",
|
|
793
|
+
contentBlocks: [
|
|
794
|
+
{
|
|
795
|
+
type: "image",
|
|
796
|
+
source: {
|
|
797
|
+
type: "workspace_ref",
|
|
798
|
+
media_type: "image/png",
|
|
799
|
+
attachmentId: screenshot.id,
|
|
800
|
+
sizeBytes: Buffer.from(screenshotBase64, "base64").byteLength,
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
],
|
|
804
|
+
},
|
|
805
|
+
]),
|
|
806
|
+
{ skipIndexing: true },
|
|
807
|
+
);
|
|
808
|
+
linkAttachmentToMessage(toolResult.id, screenshot.id, 0);
|
|
809
|
+
|
|
810
|
+
const reply = await addMessage(
|
|
811
|
+
source.id,
|
|
812
|
+
"assistant",
|
|
813
|
+
JSON.stringify([{ type: "text", text: "Done." }]),
|
|
814
|
+
{ skipIndexing: true },
|
|
815
|
+
);
|
|
816
|
+
linkAttachmentToMessage(reply.id, screenshot.id, 0);
|
|
817
|
+
const explicit = await uploadAttachment(
|
|
818
|
+
"report.pdf",
|
|
819
|
+
"application/pdf",
|
|
820
|
+
"JVBERg==",
|
|
821
|
+
);
|
|
822
|
+
linkAttachmentToMessage(reply.id, explicit.id, 1);
|
|
823
|
+
updateMessageMetadata(reply.id, {
|
|
824
|
+
[COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY]: [screenshot.id],
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
const fork = forkConversation({ conversationId: source.id });
|
|
828
|
+
const forkRows = getMessages(fork.id);
|
|
829
|
+
const forkToolResult = forkRows.find(
|
|
830
|
+
(row) =>
|
|
831
|
+
row.role === "user" &&
|
|
832
|
+
JSON.stringify(row.content).includes("tool_result"),
|
|
833
|
+
);
|
|
834
|
+
const forkReply = forkRows.find(
|
|
835
|
+
(row) =>
|
|
836
|
+
row.role === "assistant" &&
|
|
837
|
+
JSON.stringify(row.content).includes("Done."),
|
|
838
|
+
);
|
|
839
|
+
expect(forkToolResult).toBeDefined();
|
|
840
|
+
expect(forkReply).toBeDefined();
|
|
841
|
+
|
|
842
|
+
const forkReplyAttachments = getAttachmentsForMessage(forkReply!.id);
|
|
843
|
+
const clonedScreenshot = forkReplyAttachments.find(
|
|
844
|
+
(attachment) => attachment.originalFilename === "computer-use-click.png",
|
|
845
|
+
);
|
|
846
|
+
const clonedExplicit = forkReplyAttachments.find(
|
|
847
|
+
(attachment) => attachment.originalFilename === "report.pdf",
|
|
848
|
+
);
|
|
849
|
+
expect(clonedScreenshot?.id).toBeDefined();
|
|
850
|
+
expect(clonedScreenshot?.id).not.toBe(screenshot.id);
|
|
851
|
+
expect(clonedExplicit?.id).toBeDefined();
|
|
852
|
+
expect(JSON.stringify(forkToolResult!.content)).toContain(
|
|
853
|
+
`"attachmentId":"${clonedScreenshot!.id}"`,
|
|
854
|
+
);
|
|
855
|
+
expect(JSON.stringify(forkToolResult!.content)).not.toContain(
|
|
856
|
+
`"attachmentId":"${screenshot.id}"`,
|
|
857
|
+
);
|
|
858
|
+
const persistedSourceToolResult = getMessages(source.id).find(
|
|
859
|
+
(row) => row.id === toolResult.id,
|
|
860
|
+
);
|
|
861
|
+
expect(JSON.stringify(persistedSourceToolResult?.content)).toContain(
|
|
862
|
+
`"attachmentId":"${screenshot.id}"`,
|
|
863
|
+
);
|
|
864
|
+
expect(JSON.stringify(persistedSourceToolResult?.content)).not.toContain(
|
|
865
|
+
`"attachmentId":"${clonedScreenshot!.id}"`,
|
|
866
|
+
);
|
|
867
|
+
expect(
|
|
868
|
+
getAttachmentsForMessage(forkToolResult!.id).map(
|
|
869
|
+
(attachment) => attachment.id,
|
|
870
|
+
),
|
|
871
|
+
).toEqual([clonedScreenshot!.id]);
|
|
872
|
+
|
|
873
|
+
deleteConversation(source.id);
|
|
874
|
+
|
|
875
|
+
const forkJsonl = readFileSync(
|
|
876
|
+
join(getConversationDirPath(fork.id, fork.createdAt), "messages.jsonl"),
|
|
877
|
+
"utf-8",
|
|
878
|
+
);
|
|
879
|
+
expect(forkJsonl).toContain(
|
|
880
|
+
'"toolResults":[{"content":"Clicked"}],"attachments":["computer-use-click.png"]',
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
const forkMarkerIds = computerUseScreenshotAttachmentIdsFromMetadata(
|
|
884
|
+
parseMetadata(forkReply!.metadata) as Record<string, unknown>,
|
|
885
|
+
);
|
|
886
|
+
expect(forkMarkerIds).toEqual([screenshot.id, clonedScreenshot!.id]);
|
|
887
|
+
expect(forkMarkerIds).not.toContain(clonedExplicit!.id);
|
|
888
|
+
|
|
889
|
+
const history = (await handleListMessages({
|
|
890
|
+
queryParams: { conversationId: fork.id },
|
|
891
|
+
})) as {
|
|
892
|
+
messages: Array<{
|
|
893
|
+
toolCalls?: Array<{ imageAttachmentIds?: string[] }>;
|
|
894
|
+
attachments: Array<{
|
|
895
|
+
id: string;
|
|
896
|
+
computerUseScreenshot?: boolean;
|
|
897
|
+
}>;
|
|
898
|
+
}>;
|
|
899
|
+
};
|
|
900
|
+
expect(
|
|
901
|
+
history.messages.flatMap((message) =>
|
|
902
|
+
(message.toolCalls ?? []).flatMap(
|
|
903
|
+
(toolCall) => toolCall.imageAttachmentIds ?? [],
|
|
904
|
+
),
|
|
905
|
+
),
|
|
906
|
+
).toEqual([clonedScreenshot!.id]);
|
|
907
|
+
const hydratedScreenshot = history.messages
|
|
908
|
+
.flatMap((message) => message.attachments)
|
|
909
|
+
.find((attachment) => attachment.id === clonedScreenshot!.id);
|
|
910
|
+
const hydratedExplicit = history.messages
|
|
911
|
+
.flatMap((message) => message.attachments)
|
|
912
|
+
.find((attachment) => attachment.id === clonedExplicit!.id);
|
|
913
|
+
expect(hydratedScreenshot?.computerUseScreenshot).toBe(true);
|
|
914
|
+
expect(hydratedExplicit?.computerUseScreenshot).toBeUndefined();
|
|
915
|
+
expect(getAttachmentContent(screenshot.id)).toBeNull();
|
|
916
|
+
expect(getAttachmentContent(clonedScreenshot!.id)?.toString("base64")).toBe(
|
|
917
|
+
screenshotBase64,
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
const firstFilesPage = listConversationAttachments(fork.id, {
|
|
921
|
+
limit: 1,
|
|
922
|
+
offset: 0,
|
|
923
|
+
});
|
|
924
|
+
const secondFilesPage = listConversationAttachments(fork.id, {
|
|
925
|
+
limit: 1,
|
|
926
|
+
offset: 1,
|
|
927
|
+
});
|
|
928
|
+
expect(firstFilesPage.total).toBe(2);
|
|
929
|
+
expect(secondFilesPage.total).toBe(2);
|
|
930
|
+
expect(
|
|
931
|
+
[...firstFilesPage.attachments, ...secondFilesPage.attachments].map(
|
|
932
|
+
(attachment) => attachment.id,
|
|
933
|
+
),
|
|
934
|
+
).toEqual([clonedScreenshot!.id, clonedExplicit!.id]);
|
|
935
|
+
expect(
|
|
936
|
+
[...firstFilesPage.attachments, ...secondFilesPage.attachments].every(
|
|
937
|
+
(attachment) => attachment.messageId === forkReply!.id,
|
|
938
|
+
),
|
|
939
|
+
).toBe(true);
|
|
940
|
+
});
|
|
941
|
+
|
|
751
942
|
test("inherits the source conversation's inference profile", async () => {
|
|
752
943
|
const source = createConversation("Pinned profile thread");
|
|
753
944
|
await addMessage(source.id, "user", "Use the balanced profile", {
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Conversation.buildCurrentSystemPrompt` captures the delegation-section
|
|
3
|
+
* state the prompt it builds renders (`renderedDelegateIndependentTasks`),
|
|
4
|
+
* which the wire-surface recorder persists for a retrospective fork to
|
|
5
|
+
* replay. The loop sends the prompt built before the run and never rebuilds
|
|
6
|
+
* it mid-run, so the captured state has to be the one that build used: not a
|
|
7
|
+
* re-derivation at send time, and not the state of a build that failed while
|
|
8
|
+
* the loop kept its previous prompt.
|
|
9
|
+
*/
|
|
10
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { CompactionCircuit } from "../agent/compaction-circuit.js";
|
|
13
|
+
import type { AgentEvent } from "../agent/loop.js";
|
|
14
|
+
import type { AssistantEvent } from "../api/index.js";
|
|
15
|
+
import type { Message, ProviderResponse } from "../providers/types.js";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Mocks — must precede Conversation import
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
mock.module("../providers/registry.js", () => ({
|
|
22
|
+
getProvider: () => ({ name: "mock-provider" }),
|
|
23
|
+
initializeProviders: async () => {},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
27
|
+
isAssistantFeatureFlagEnabled: () => true,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// The builder under the conversation's control: records the options each
|
|
31
|
+
// build receives and can be made to throw, which is what a wake's prompt
|
|
32
|
+
// sync swallows before running on the previous prompt.
|
|
33
|
+
let builtWith: Array<Record<string, unknown> | undefined> = [];
|
|
34
|
+
let throwOnBuild = false;
|
|
35
|
+
mock.module("../prompts/system-prompt.js", () => ({
|
|
36
|
+
buildSystemPrompt: (options?: Record<string, unknown>) => {
|
|
37
|
+
if (throwOnBuild) {
|
|
38
|
+
throw new Error("prompt build failed");
|
|
39
|
+
}
|
|
40
|
+
builtWith.push(options);
|
|
41
|
+
return "system prompt";
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
mock.module("../config/skills.js", () => ({
|
|
46
|
+
loadSkillCatalog: () => [],
|
|
47
|
+
loadSkillBySelector: () => ({ skill: null }),
|
|
48
|
+
ensureSkillIcon: async () => null,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
mock.module("../config/skill-state.js", () => ({
|
|
52
|
+
resolveSkillStates: () => [],
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
mock.module("../permissions/trust-store.js", () => ({
|
|
56
|
+
addRule: () => {},
|
|
57
|
+
findHighestPriorityRule: () => null,
|
|
58
|
+
clearCache: () => {},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
mock.module("../security/secret-allowlist.js", () => ({
|
|
62
|
+
resetAllowlist: () => {},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
mock.module("../persistence/conversation-crud.js", () => ({
|
|
66
|
+
setConversationProcessingStartedAt: () => {},
|
|
67
|
+
isConversationProcessing: () => false,
|
|
68
|
+
setConversationOriginChannelIfUnset: () => {},
|
|
69
|
+
updateConversationContextWindow: () => {},
|
|
70
|
+
deleteMessageById: () => {},
|
|
71
|
+
provenanceFromTrustContext: () => ({
|
|
72
|
+
source: "user",
|
|
73
|
+
trustContext: undefined,
|
|
74
|
+
}),
|
|
75
|
+
getConversationOriginInterface: () => null,
|
|
76
|
+
getConversationOriginChannel: () => null,
|
|
77
|
+
getMessages: () => [],
|
|
78
|
+
getConversation: () => ({
|
|
79
|
+
id: "conv-1",
|
|
80
|
+
contextSummary: null,
|
|
81
|
+
contextCompactedMessageCount: 0,
|
|
82
|
+
totalInputTokens: 0,
|
|
83
|
+
totalOutputTokens: 0,
|
|
84
|
+
totalEstimatedCost: 0,
|
|
85
|
+
}),
|
|
86
|
+
createConversation: () => ({ id: "conv-1" }),
|
|
87
|
+
addMessage: () => ({ id: `msg-${Date.now()}` }),
|
|
88
|
+
updateConversationUsage: () => {},
|
|
89
|
+
updateConversationTitle: () => {},
|
|
90
|
+
reserveMessage: mock(async () => ({ id: "msg-reserve" })),
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
mock.module("../persistence/conversation-queries.js", () => ({
|
|
94
|
+
listConversations: () => [],
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
mock.module("../persistence/attachments-store.js", () => ({
|
|
98
|
+
uploadAttachment: () => ({ id: `att-${Date.now()}` }),
|
|
99
|
+
linkAttachmentToMessage: () => {},
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
|
|
103
|
+
ContextWindowManager: class {
|
|
104
|
+
estimateInputTokens() {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
get tokenCountInputs() {
|
|
108
|
+
return { systemPrompt: "", tools: undefined };
|
|
109
|
+
}
|
|
110
|
+
constructor() {}
|
|
111
|
+
updateConfig() {}
|
|
112
|
+
shouldCompact() {
|
|
113
|
+
return { needed: false, estimatedTokens: 0 };
|
|
114
|
+
}
|
|
115
|
+
async maybeCompact() {
|
|
116
|
+
return { compacted: false };
|
|
117
|
+
}
|
|
118
|
+
resetOverflowRecovery() {}
|
|
119
|
+
},
|
|
120
|
+
createContextSummaryMessage: () => ({
|
|
121
|
+
role: "user",
|
|
122
|
+
content: [{ type: "text", text: "summary" }],
|
|
123
|
+
}),
|
|
124
|
+
getSummaryFromContextMessage: () => null,
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
mock.module("../persistence/llm-usage-store.js", () => ({
|
|
128
|
+
recordUsageEvent: () => ({ id: "mock-id", createdAt: Date.now() }),
|
|
129
|
+
listUsageEvents: () => [],
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
mock.module("../agent/loop.js", () => ({
|
|
133
|
+
AgentLoop: class {
|
|
134
|
+
compactionCircuit = new CompactionCircuit("test-conv");
|
|
135
|
+
constructor() {}
|
|
136
|
+
getToolTokenBudget() {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
getResolvedTools() {
|
|
140
|
+
return [];
|
|
141
|
+
}
|
|
142
|
+
getActiveModel() {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
setSystemPrompt() {}
|
|
146
|
+
async run(_options: {
|
|
147
|
+
messages: Message[];
|
|
148
|
+
onEvent: (event: AgentEvent) => void;
|
|
149
|
+
}): Promise<Message[]> {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
}));
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Import Conversation AFTER mocks
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
import { Conversation } from "../daemon/conversation.js";
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Helpers
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
function makeProvider() {
|
|
166
|
+
return {
|
|
167
|
+
name: "mock",
|
|
168
|
+
async sendMessage(): Promise<ProviderResponse> {
|
|
169
|
+
return {
|
|
170
|
+
content: [],
|
|
171
|
+
model: "mock",
|
|
172
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
173
|
+
stopReason: "end_turn",
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let conversationSeq = 0;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A conversation whose prompt is built, or, given a prompt other than the
|
|
183
|
+
* builder's, one running on a verbatim system-prompt override.
|
|
184
|
+
*/
|
|
185
|
+
function makeConversation(systemPrompt = "system prompt"): Conversation {
|
|
186
|
+
conversationSeq += 1;
|
|
187
|
+
return new Conversation(
|
|
188
|
+
`conv-rendered-delegation-${conversationSeq}`,
|
|
189
|
+
makeProvider(),
|
|
190
|
+
systemPrompt,
|
|
191
|
+
(_msg: AssistantEvent) => {},
|
|
192
|
+
"/tmp",
|
|
193
|
+
{ maxTokens: 4096 },
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function lastBuildDelegation(): unknown {
|
|
198
|
+
return builtWith.at(-1)?.delegateIndependentTasks;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
builtWith = [];
|
|
203
|
+
throwOnBuild = false;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Tests
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
describe("Conversation.buildCurrentSystemPrompt delegation-state capture", () => {
|
|
211
|
+
test("captures the state the build rendered and hands the builder the same value", () => {
|
|
212
|
+
const conversation = makeConversation();
|
|
213
|
+
expect(conversation.renderedDelegateIndependentTasks).toBeUndefined();
|
|
214
|
+
|
|
215
|
+
conversation.buildCurrentSystemPrompt();
|
|
216
|
+
|
|
217
|
+
// An unrestricted turn can spawn, so the section renders on.
|
|
218
|
+
expect(conversation.renderedDelegateIndependentTasks).toBe(true);
|
|
219
|
+
expect(lastBuildDelegation()).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("a change to the gate's inputs after the build does not touch the captured state until the next build", () => {
|
|
223
|
+
const conversation = makeConversation();
|
|
224
|
+
conversation.buildCurrentSystemPrompt();
|
|
225
|
+
expect(conversation.renderedDelegateIndependentTasks).toBe(true);
|
|
226
|
+
|
|
227
|
+
// The loop is still sending the prompt built above, so the recorder must
|
|
228
|
+
// see that prompt's state, whatever the gate would derive now.
|
|
229
|
+
conversation.setSubagentAllowedTools(new Set(["remember"]));
|
|
230
|
+
expect(conversation.renderedDelegateIndependentTasks).toBe(true);
|
|
231
|
+
|
|
232
|
+
conversation.buildCurrentSystemPrompt();
|
|
233
|
+
expect(conversation.renderedDelegateIndependentTasks).toBe(false);
|
|
234
|
+
expect(lastBuildDelegation()).toBe(false);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("a failed build leaves the previously captured state in place", () => {
|
|
238
|
+
const conversation = makeConversation();
|
|
239
|
+
conversation.buildCurrentSystemPrompt();
|
|
240
|
+
expect(conversation.renderedDelegateIndependentTasks).toBe(true);
|
|
241
|
+
|
|
242
|
+
// A wake's prompt sync swallows this and runs on the previous prompt,
|
|
243
|
+
// whose section is still on, even though a rebuild would now render it
|
|
244
|
+
// off.
|
|
245
|
+
conversation.setSubagentAllowedTools(new Set(["remember"]));
|
|
246
|
+
throwOnBuild = true;
|
|
247
|
+
expect(() => conversation.buildCurrentSystemPrompt()).toThrow(
|
|
248
|
+
"prompt build failed",
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
expect(conversation.renderedDelegateIndependentTasks).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("a verbatim system-prompt override captures unknown", () => {
|
|
255
|
+
const conversation = makeConversation("override prompt");
|
|
256
|
+
builtWith = [];
|
|
257
|
+
|
|
258
|
+
expect(conversation.buildCurrentSystemPrompt()).toBe("override prompt");
|
|
259
|
+
|
|
260
|
+
expect(conversation.renderedDelegateIndependentTasks).toBeNull();
|
|
261
|
+
// The override is returned as is; nothing is built.
|
|
262
|
+
expect(builtWith).toEqual([]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("a replaying wake's recorded state wins over the derivation", () => {
|
|
266
|
+
const conversation = makeConversation();
|
|
267
|
+
// Replaying a channel-sourced conversation that recorded the section off,
|
|
268
|
+
// on a wake whose own scope could spawn.
|
|
269
|
+
conversation.delegateIndependentTasksReplay = false;
|
|
270
|
+
|
|
271
|
+
conversation.buildCurrentSystemPrompt();
|
|
272
|
+
|
|
273
|
+
expect(conversation.renderedDelegateIndependentTasks).toBe(false);
|
|
274
|
+
expect(lastBuildDelegation()).toBe(false);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -20,6 +20,7 @@ import { tmpdir } from "node:os";
|
|
|
20
20
|
import { join, resolve } from "node:path";
|
|
21
21
|
import { describe, expect, test } from "bun:test";
|
|
22
22
|
|
|
23
|
+
import { resolveIpcEndpoint } from "@vellumai/ipc-server-utils";
|
|
23
24
|
import { CES_PROTOCOL_VERSION } from "@vellumai/service-contracts/credential-rpc";
|
|
24
25
|
|
|
25
26
|
import {
|
|
@@ -79,44 +80,83 @@ function createMockTransport(): CesTransport & {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
// ---------------------------------------------------------------------------
|
|
82
|
-
//
|
|
83
|
+
// CES discovery — CES_BOOTSTRAP_SOCKET_DIR, not workspace
|
|
83
84
|
// ---------------------------------------------------------------------------
|
|
84
85
|
|
|
86
|
+
function withBootstrapDir(dir: string): () => void {
|
|
87
|
+
const savedDir = process.env["CES_BOOTSTRAP_SOCKET_DIR"];
|
|
88
|
+
process.env["CES_BOOTSTRAP_SOCKET_DIR"] = dir;
|
|
89
|
+
return () => {
|
|
90
|
+
if (savedDir !== undefined) {
|
|
91
|
+
process.env["CES_BOOTSTRAP_SOCKET_DIR"] = savedDir;
|
|
92
|
+
} else {
|
|
93
|
+
delete process.env["CES_BOOTSTRAP_SOCKET_DIR"];
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
85
98
|
describe("managed CES discovery", () => {
|
|
86
|
-
test("returns unavailable when bootstrap socket does not exist",
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const saved = process.env["CES_BOOTSTRAP_SOCKET"];
|
|
99
|
+
test("returns unavailable when bootstrap socket does not exist", () => {
|
|
100
|
+
const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-missing-"));
|
|
101
|
+
const restore = withBootstrapDir(bootstrapDir);
|
|
90
102
|
try {
|
|
91
|
-
|
|
92
|
-
process.env["CES_BOOTSTRAP_SOCKET"] = "/tmp/ces-test-nonexistent.sock";
|
|
93
|
-
const result = await discoverManagedCes();
|
|
103
|
+
const result = discoverManagedCes();
|
|
94
104
|
expect(result.mode).toBe("unavailable");
|
|
95
105
|
expect((result as { reason: string }).reason).toContain(
|
|
96
106
|
"CES bootstrap socket not found",
|
|
97
107
|
);
|
|
98
108
|
} finally {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} else {
|
|
102
|
-
delete process.env["CES_BOOTSTRAP_SOCKET"];
|
|
103
|
-
}
|
|
109
|
+
restore();
|
|
110
|
+
rmSync(bootstrapDir, { recursive: true, force: true });
|
|
104
111
|
}
|
|
105
112
|
});
|
|
106
113
|
|
|
107
|
-
test("never returns a fallback or in-process mode",
|
|
108
|
-
const
|
|
114
|
+
test("never returns a fallback or in-process mode", () => {
|
|
115
|
+
const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-missing-"));
|
|
116
|
+
const restore = withBootstrapDir(bootstrapDir);
|
|
109
117
|
try {
|
|
110
|
-
|
|
111
|
-
const result = await discoverManagedCes();
|
|
112
|
-
// Must be "managed" or "unavailable" — no fallback modes.
|
|
118
|
+
const result = discoverManagedCes();
|
|
113
119
|
expect(["managed", "unavailable"]).toContain(result.mode);
|
|
114
120
|
} finally {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
restore();
|
|
122
|
+
rmSync(bootstrapDir, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("CES bootstrap socket discovery", () => {
|
|
128
|
+
test("looks for the socket under CES_BOOTSTRAP_SOCKET_DIR", () => {
|
|
129
|
+
const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-bootstrap-"));
|
|
130
|
+
const restore = withBootstrapDir(bootstrapDir);
|
|
131
|
+
try {
|
|
132
|
+
const socketPath = resolveIpcEndpoint("ces", {
|
|
133
|
+
workspaceDir: bootstrapDir,
|
|
134
|
+
}).path;
|
|
135
|
+
const result = discoverManagedCes();
|
|
136
|
+
expect(result.mode).toBe("unavailable");
|
|
137
|
+
expect((result as { reason: string }).reason).toContain(socketPath);
|
|
138
|
+
} finally {
|
|
139
|
+
restore();
|
|
140
|
+
rmSync(bootstrapDir, { recursive: true, force: true });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("does not discover CES from VELLUM_WORKSPACE_DIR", () => {
|
|
145
|
+
const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-bootstrap-"));
|
|
146
|
+
const restore = withBootstrapDir(bootstrapDir);
|
|
147
|
+
try {
|
|
148
|
+
const result = discoverManagedCes();
|
|
149
|
+
const workspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
150
|
+
expect(workspaceDir).toBeDefined();
|
|
151
|
+
const workspaceSocket = resolveIpcEndpoint("ces", {
|
|
152
|
+
workspaceDir: workspaceDir!,
|
|
153
|
+
}).path;
|
|
154
|
+
expect((result as { reason: string }).reason).not.toContain(
|
|
155
|
+
workspaceSocket,
|
|
156
|
+
);
|
|
157
|
+
} finally {
|
|
158
|
+
restore();
|
|
159
|
+
rmSync(bootstrapDir, { recursive: true, force: true });
|
|
120
160
|
}
|
|
121
161
|
});
|
|
122
162
|
});
|
|
@@ -126,30 +166,9 @@ describe("managed CES discovery", () => {
|
|
|
126
166
|
// ---------------------------------------------------------------------------
|
|
127
167
|
|
|
128
168
|
describe("discoverCesWithRetry", () => {
|
|
129
|
-
function withManagedEnv(socketPath: string): () => void {
|
|
130
|
-
const savedSocket = process.env["CES_BOOTSTRAP_SOCKET"];
|
|
131
|
-
const savedSocketDir = process.env["CES_BOOTSTRAP_SOCKET_DIR"];
|
|
132
|
-
const savedContainerized = process.env["IS_CONTAINERIZED"];
|
|
133
|
-
delete process.env["CES_BOOTSTRAP_SOCKET_DIR"];
|
|
134
|
-
process.env["CES_BOOTSTRAP_SOCKET"] = socketPath;
|
|
135
|
-
process.env["IS_CONTAINERIZED"] = "true";
|
|
136
|
-
return () => {
|
|
137
|
-
const restore = (key: string, value: string | undefined) => {
|
|
138
|
-
if (value !== undefined) {
|
|
139
|
-
process.env[key] = value;
|
|
140
|
-
} else {
|
|
141
|
-
delete process.env[key];
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
restore("CES_BOOTSTRAP_SOCKET", savedSocket);
|
|
145
|
-
restore("CES_BOOTSTRAP_SOCKET_DIR", savedSocketDir);
|
|
146
|
-
restore("IS_CONTAINERIZED", savedContainerized);
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
169
|
test("returns unavailable after polling when the socket never appears", async () => {
|
|
151
|
-
const
|
|
152
|
-
const restore =
|
|
170
|
+
const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-retry-missing-"));
|
|
171
|
+
const restore = withBootstrapDir(bootstrapDir);
|
|
153
172
|
try {
|
|
154
173
|
const start = Date.now();
|
|
155
174
|
const result = await discoverCesWithRetry({
|
|
@@ -157,22 +176,19 @@ describe("discoverCesWithRetry", () => {
|
|
|
157
176
|
intervalMs: 20,
|
|
158
177
|
});
|
|
159
178
|
expect(result.mode).toBe("unavailable");
|
|
160
|
-
// It must actually have waited rather than failing on the first probe.
|
|
161
179
|
expect(Date.now() - start).toBeGreaterThanOrEqual(150);
|
|
162
180
|
} finally {
|
|
163
181
|
restore();
|
|
182
|
+
rmSync(bootstrapDir, { recursive: true, force: true });
|
|
164
183
|
}
|
|
165
184
|
});
|
|
166
185
|
|
|
167
186
|
test("resolves to managed once the socket is re-bound mid-poll", async () => {
|
|
168
187
|
const dir = mkdtempSync(join(tmpdir(), "ces-retry-"));
|
|
169
|
-
const socketPath =
|
|
170
|
-
const restore =
|
|
188
|
+
const socketPath = resolveIpcEndpoint("ces", { workspaceDir: dir }).path;
|
|
189
|
+
const restore = withBootstrapDir(dir);
|
|
190
|
+
const timer = setTimeout(() => writeFileSync(socketPath, ""), 80);
|
|
171
191
|
try {
|
|
172
|
-
// Simulate the sidecar re-binding its bootstrap socket shortly after
|
|
173
|
-
// the assistant begins probing.
|
|
174
|
-
setTimeout(() => writeFileSync(socketPath, ""), 80);
|
|
175
|
-
|
|
176
192
|
const result = await discoverCesWithRetry({
|
|
177
193
|
timeoutMs: 2_000,
|
|
178
194
|
intervalMs: 20,
|
|
@@ -180,6 +196,7 @@ describe("discoverCesWithRetry", () => {
|
|
|
180
196
|
expect(result.mode).toBe("managed");
|
|
181
197
|
expect((result as { socketPath: string }).socketPath).toBe(socketPath);
|
|
182
198
|
} finally {
|
|
199
|
+
clearTimeout(timer);
|
|
183
200
|
restore();
|
|
184
201
|
rmSync(dir, { recursive: true, force: true });
|
|
185
202
|
}
|