@vellumai/assistant 0.8.7-dev.202606052232.2ddc989 → 0.8.8
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/bun.lock +2 -2
- package/docs/plugins.md +832 -0
- package/examples/plugins/echo/README.md +60 -61
- package/examples/plugins/echo/package.json +2 -1
- package/examples/plugins/echo/register.ts +143 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +6 -7
- package/openapi.yaml +5 -15
- package/package.json +2 -2
- package/src/__tests__/agent-loop-exit-reason.test.ts +56 -3
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +0 -1
- package/src/__tests__/approval-routes-http.test.ts +1 -4
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/circuit-breaker-pipeline.test.ts +405 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +251 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +3 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +3 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -3
- package/src/__tests__/conversation-agent-loop.test.ts +39 -42
- package/src/__tests__/conversation-clean-command.test.ts +2 -5
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -140
- package/src/__tests__/conversation-runtime-workspace.test.ts +27 -108
- package/src/__tests__/conversation-starter-routes.test.ts +6 -14
- package/src/__tests__/conversation-workspace-cache-state.test.ts +16 -17
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -61
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -7
- package/src/__tests__/db-acp-history.test.ts +0 -101
- package/src/__tests__/dynamic-page-surface.test.ts +0 -31
- package/src/__tests__/file-write-tool.test.ts +0 -63
- package/src/__tests__/gateway-only-guard.test.ts +2 -12
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +4 -2
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -1
- package/src/__tests__/heartbeat-service.test.ts +0 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/injector-background-turn.test.ts +1 -1
- package/src/__tests__/injector-chain.test.ts +6 -34
- package/src/__tests__/injector-disk-pressure.test.ts +34 -77
- package/src/__tests__/injector-document-comments.test.ts +1 -1
- package/src/__tests__/list-messages-hidden-metadata.test.ts +0 -38
- package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
- package/src/__tests__/{overflow-reduction-loop.test.ts → overflow-reduce-pipeline.test.ts} +284 -64
- package/src/__tests__/pipeline-runner.test.ts +554 -0
- package/src/__tests__/plugin-api-shim.test.ts +6 -3
- package/src/__tests__/plugin-bootstrap.test.ts +23 -12
- package/src/__tests__/plugin-registry.test.ts +49 -3
- package/src/__tests__/plugin-types.test.ts +70 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +1 -4
- package/src/__tests__/skill-feature-flags-integration.test.ts +0 -33
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +3 -1
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +3 -1
- package/src/__tests__/subagent-notify-parent.test.ts +3 -1
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +286 -54
- package/src/acp/__tests__/client-handler.test.ts +0 -40
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -137
- package/src/acp/__tests__/session-manager-persistence.test.ts +28 -95
- package/src/acp/agent-process.ts +1 -61
- package/src/acp/client-handler.ts +0 -31
- package/src/acp/prepare-agent-env.ts +29 -83
- package/src/acp/resolve-agent.test.ts +7 -320
- package/src/acp/resolve-agent.ts +18 -182
- package/src/acp/session-manager.ts +73 -495
- package/src/acp/types.ts +0 -8
- package/src/agent/compaction-circuit.ts +102 -60
- package/src/agent/loop.ts +59 -32
- package/src/api/responses/conversation-message.ts +1 -7
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +0 -1
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +0 -10
- package/src/config/acp-defaults.ts +0 -6
- package/src/config/bundled-skills/acp/SKILL.md +31 -83
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +381 -224
- package/src/config/bundled-skills/app-builder/TOOLS.json +0 -29
- package/src/config/bundled-skills/document-editor/SKILL.md +23 -28
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +0 -2
- package/src/config/feature-flag-registry.json +5 -14
- package/src/config/schemas/heartbeat.ts +0 -9
- package/src/context/strip-injections.ts +2 -8
- package/src/context/window-manager.ts +1 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
- package/src/daemon/conversation-agent-loop.ts +279 -62
- package/src/daemon/conversation-runtime-assembly.ts +69 -106
- package/src/daemon/conversation-store.ts +90 -9
- package/src/daemon/conversation-workspace.ts +0 -17
- package/src/daemon/conversation.ts +6 -0
- package/src/daemon/external-plugins-bootstrap.ts +11 -11
- package/src/daemon/handlers/conversations.ts +1 -3
- package/src/daemon/handlers/skills.ts +1 -4
- package/src/daemon/lifecycle.ts +0 -21
- package/src/daemon/server.ts +0 -2
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -3
- package/src/heartbeat/heartbeat-run-store.ts +1 -23
- package/src/heartbeat/heartbeat-service.ts +0 -26
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -15
- package/src/ipc/skill-routes/memory.ts +2 -4
- package/src/memory/conversation-starter-checkpoints.ts +0 -1
- package/src/memory/db-init.ts +0 -2
- package/src/memory/job-handlers/conversation-starters.ts +2 -13
- package/src/memory/jobs-worker.ts +1 -1
- package/src/memory/migrations/index.ts +0 -1
- package/src/memory/schema/acp.ts +0 -4
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +4 -13
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/assign.test.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/live-integration.test.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/maintain-job.test.ts +5 -5
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/orchestrate.test.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/reconcile.test.ts +2 -2
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/render-injection.test.ts +1 -1
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/router.test.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selector.test.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/shadow-plugin.test.ts +12 -12
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/assign.ts +5 -5
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/capabilities.ts +2 -2
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/maintain-job.ts +8 -8
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/page-content.ts +2 -2
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/provider-blocks.ts +1 -1
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/reconcile.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/render-injection.ts +1 -1
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/router.ts +3 -3
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selection-log-store.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selector.ts +4 -4
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/shadow-plugin.ts +90 -28
- package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/tree.ts +1 -1
- package/src/plugin-api/index.ts +5 -0
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
- package/src/plugins/defaults/{memory-v3-shadow → circuit-breaker}/package.json +2 -2
- package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
- package/src/plugins/defaults/compaction/package.json +1 -1
- package/src/plugins/defaults/compaction/register.ts +19 -8
- package/src/plugins/defaults/compaction/terminal.ts +73 -0
- package/src/plugins/defaults/index.ts +5 -3
- package/src/plugins/defaults/{memory-retrieval/injectors.ts → injectors/register.ts} +7 -45
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +7 -11
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +126 -0
- package/src/plugins/defaults/overflow-reduce/package.json +15 -0
- package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
- package/src/plugins/external-api.ts +2 -2
- package/src/plugins/pipeline.ts +293 -6
- package/src/plugins/registry.ts +37 -9
- package/src/plugins/types.ts +336 -32
- package/src/plugins/user-loader.ts +127 -30
- package/src/proactive-artifact/aux-message-injector.ts +1 -1
- package/src/proactive-artifact/job.test.ts +1 -1
- package/src/prompts/__tests__/system-prompt.test.ts +0 -6
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +2 -4
- package/src/runtime/__tests__/agent-wake.test.ts +5 -5
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +3 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +55 -283
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +4 -5
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -4
- package/src/runtime/routes/acp-routes.test.ts +25 -89
- package/src/runtime/routes/acp-routes.ts +29 -81
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +10 -6
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +2 -15
- package/src/runtime/routes/conversation-starter-routes.ts +7 -13
- package/src/runtime/routes/conversations-import-routes.ts +7 -24
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +3 -18
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/memory-v3-routes.ts +6 -16
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -4
- package/src/runtime/routes/work-items-routes.ts +4 -2
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +4 -2
- package/src/subagent/manager.ts +5 -17
- package/src/tools/acp/list-agents.test.ts +1 -7
- package/src/tools/acp/spawn.test.ts +55 -158
- package/src/tools/acp/spawn.ts +72 -47
- package/src/tools/acp/steer.test.ts +8 -105
- package/src/tools/acp/steer.ts +17 -48
- package/src/tools/apps/executors.ts +8 -13
- package/src/tools/filesystem/write.ts +0 -34
- package/src/tools/subagent/spawn.ts +4 -2
- package/src/tools/ui-surface/definitions.ts +4 -25
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +5 -4
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +45 -69
- package/examples/plugins/echo/hooks/post-tool-use.ts +0 -18
- package/examples/plugins/echo/hooks/stop.ts +0 -16
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +0 -18
- package/examples/plugins/echo/src/emit.ts +0 -19
- package/src/__tests__/compaction-circuit.test.ts +0 -258
- package/src/__tests__/compaction-direct.test.ts +0 -132
- package/src/__tests__/conversations-import-system-filter.test.ts +0 -101
- package/src/acp/__tests__/agent-process.test.ts +0 -161
- package/src/acp/__tests__/helpers/acp-history-db.ts +0 -82
- package/src/acp/__tests__/helpers/exec-file-stub.ts +0 -101
- package/src/acp/__tests__/session-manager-resume.test.ts +0 -736
- package/src/acp/auto-install.test.ts +0 -196
- package/src/acp/auto-install.ts +0 -177
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/acp/resume-hint.ts +0 -25
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +0 -48
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +0 -57
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +0 -38
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -62
- package/src/daemon/conversation-registry.ts +0 -159
- package/src/daemon/overflow-reduction-loop.ts +0 -230
- package/src/memory/migrations/272-acp-session-history-cwd.ts +0 -36
- package/src/plugins/defaults/compaction/compact.ts +0 -59
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -75
- package/src/plugins/defaults/memory-v3-shadow/register.ts +0 -26
- package/src/tools/acp/context.ts +0 -20
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/core.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/health.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/needle.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/snapshot.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/tree.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/core.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/README.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/core.json +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/health.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/needle.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/orchestrate.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/snapshot.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/types.ts +0 -0
- /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/working-set.ts +0 -0
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for the user plugin loader.
|
|
2
|
+
* Tests for the user plugin loader (PR 29).
|
|
3
3
|
*
|
|
4
4
|
* Redirects `getWorkspaceDir()` into a per-test temp directory via
|
|
5
5
|
* `VELLUM_WORKSPACE_DIR` so `loadUserPlugins()` walks an isolated tree
|
|
6
6
|
* that we populate on demand.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - A directory with a `package.json` is loaded and registered.
|
|
16
|
-
* - A directory without a `package.json` is skipped silently.
|
|
17
|
-
* - A missing `getWorkspaceDir()/plugins/` directory is a no-op (zero
|
|
18
|
-
* installed user plugins is the default shape of a fresh daemon).
|
|
19
|
-
* - One failing plugin does not prevent a sibling from registering.
|
|
8
|
+
* Covers:
|
|
9
|
+
* - A plugin whose `register.ts` calls `registerPlugin()` at import time
|
|
10
|
+
* ends up in the registry after `loadUserPlugins()` resolves.
|
|
11
|
+
* - A plugin whose `register.ts` throws during import is logged + skipped;
|
|
12
|
+
* other plugins in the same directory still load.
|
|
13
|
+
* - A missing `getWorkspaceDir()/plugins/` directory is a no-op (zero installed
|
|
14
|
+
* user plugins is the default shape of a fresh daemon).
|
|
20
15
|
*/
|
|
21
16
|
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
22
17
|
import { tmpdir } from "node:os";
|
|
@@ -31,6 +26,7 @@ import { loadUserPlugins } from "../plugins/user-loader.js";
|
|
|
31
26
|
|
|
32
27
|
// Isolate every run under its own tempdir so parallel test files (and
|
|
33
28
|
// repeated runs of this file) cannot collide on `<workspaceDir>/plugins/`.
|
|
29
|
+
// Each describe-scope gets a fresh subdirectory.
|
|
34
30
|
const TEST_WORKSPACE_DIR = join(
|
|
35
31
|
tmpdir(),
|
|
36
32
|
`vellum-user-plugin-loader-test-${process.pid}-${Date.now()}`,
|
|
@@ -41,26 +37,30 @@ process.env.VELLUM_WORKSPACE_DIR = TEST_WORKSPACE_DIR;
|
|
|
41
37
|
const PLUGINS_DIR = join(TEST_WORKSPACE_DIR, "plugins");
|
|
42
38
|
|
|
43
39
|
/**
|
|
44
|
-
* Write a directory
|
|
45
|
-
*
|
|
46
|
-
*
|
|
40
|
+
* Write a plugin directory with a `register.ts` (TypeScript source, so bun
|
|
41
|
+
* can import it at test time without a build step) that executes the given
|
|
42
|
+
* body. The body has access to `registerPlugin` via a relative import back
|
|
43
|
+
* into the repo's registry module.
|
|
44
|
+
*
|
|
45
|
+
* `relativeRegistryImport` points from the synthetic plugin file at
|
|
46
|
+
* `<TEST_WORKSPACE_DIR>/plugins/<name>/register.ts` to the real
|
|
47
|
+
* registry source at `<repo>/assistant/src/plugins/registry.ts`. Using a
|
|
48
|
+
* relative path (rather than a project-root alias) keeps the test hermetic
|
|
49
|
+
* and matches how an on-disk user plugin would actually import the
|
|
50
|
+
* registry's public API in a real install.
|
|
47
51
|
*/
|
|
48
|
-
function writePlugin(
|
|
49
|
-
name: string,
|
|
50
|
-
pkg: Record<string, unknown>,
|
|
51
|
-
files: Record<string, string> = {},
|
|
52
|
-
): void {
|
|
52
|
+
function writePlugin(name: string, body: string): void {
|
|
53
53
|
const pluginDir = join(PLUGINS_DIR, name);
|
|
54
54
|
mkdirSync(pluginDir, { recursive: true });
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
// Resolve the absolute path to the registry module so the synthetic
|
|
56
|
+
// register.ts can import it. bun happily resolves `.ts` files at runtime
|
|
57
|
+
// when the test suite itself is running in source mode.
|
|
58
|
+
const registryPath = join(import.meta.dir, "..", "plugins", "registry.ts");
|
|
59
|
+
const registerSource = `
|
|
60
|
+
import { registerPlugin } from ${JSON.stringify(registryPath)};
|
|
61
|
+
${body}
|
|
62
|
+
`;
|
|
63
|
+
writeFileSync(join(pluginDir, "register.ts"), registerSource);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function clearPluginsDir(): void {
|
|
@@ -73,14 +73,17 @@ describe("user plugin loader", () => {
|
|
|
73
73
|
clearPluginsDir();
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
test("loads a plugin
|
|
76
|
+
test("loads a valid plugin whose register.ts calls registerPlugin()", async () => {
|
|
77
77
|
writePlugin(
|
|
78
78
|
"my-plugin",
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
`
|
|
80
|
+
registerPlugin({
|
|
81
|
+
manifest: {
|
|
82
|
+
name: "my-plugin",
|
|
83
|
+
version: "0.0.1",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
`,
|
|
84
87
|
);
|
|
85
88
|
|
|
86
89
|
await loadUserPlugins();
|
|
@@ -88,22 +91,34 @@ describe("user plugin loader", () => {
|
|
|
88
91
|
const registered = getRegisteredPlugins();
|
|
89
92
|
expect(registered).toHaveLength(1);
|
|
90
93
|
expect(registered[0]?.manifest.name).toBe("my-plugin");
|
|
91
|
-
expect(typeof registered[0]?.hooks?.init).toBe("function");
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
test("per-plugin failure is isolated: other plugins still load", async () => {
|
|
95
|
-
// Plugin A
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
// Plugin A throws at import time. The loader must log and move on so
|
|
98
|
+
// Plugin B still ends up registered — one bad user plugin cannot brick
|
|
99
|
+
// the entire user-plugin surface or crash the daemon.
|
|
100
|
+
writePlugin(
|
|
101
|
+
"broken-plugin",
|
|
102
|
+
`
|
|
103
|
+
throw new Error("boom at import time");
|
|
104
|
+
`,
|
|
105
|
+
);
|
|
106
|
+
writePlugin(
|
|
107
|
+
"good-plugin",
|
|
108
|
+
`
|
|
109
|
+
registerPlugin({
|
|
110
|
+
manifest: {
|
|
111
|
+
name: "good-plugin",
|
|
112
|
+
version: "0.0.1",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
`,
|
|
116
|
+
);
|
|
103
117
|
|
|
104
118
|
await loadUserPlugins();
|
|
105
119
|
|
|
106
|
-
const
|
|
120
|
+
const registered = getRegisteredPlugins();
|
|
121
|
+
const names = registered.map((p) => p.manifest.name);
|
|
107
122
|
// Order is not guaranteed (filesystem-dependent) — assert membership.
|
|
108
123
|
expect(names).toContain("good-plugin");
|
|
109
124
|
expect(names).not.toContain("broken-plugin");
|
|
@@ -117,9 +132,92 @@ describe("user plugin loader", () => {
|
|
|
117
132
|
expect(getRegisteredPlugins()).toHaveLength(0);
|
|
118
133
|
});
|
|
119
134
|
|
|
120
|
-
test("
|
|
121
|
-
//
|
|
122
|
-
//
|
|
135
|
+
test("plugin with hanging top-level await is timed out and skipped", async () => {
|
|
136
|
+
// A plugin whose module evaluation never resolves (hanging top-level
|
|
137
|
+
// await) would otherwise block daemon startup indefinitely. The loader
|
|
138
|
+
// must bound the import with a timeout so the hang is isolated the same
|
|
139
|
+
// way a thrown error would be. A neighboring well-behaved plugin must
|
|
140
|
+
// still load.
|
|
141
|
+
writePlugin(
|
|
142
|
+
"hanging-plugin",
|
|
143
|
+
`
|
|
144
|
+
await new Promise(() => {
|
|
145
|
+
// Intentionally never resolves — simulates a plugin whose top-level
|
|
146
|
+
// await blocks forever.
|
|
147
|
+
});
|
|
148
|
+
registerPlugin({
|
|
149
|
+
manifest: {
|
|
150
|
+
name: "hanging-plugin",
|
|
151
|
+
version: "0.0.1",
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
`,
|
|
155
|
+
);
|
|
156
|
+
writePlugin(
|
|
157
|
+
"healthy-plugin",
|
|
158
|
+
`
|
|
159
|
+
registerPlugin({
|
|
160
|
+
manifest: {
|
|
161
|
+
name: "healthy-plugin",
|
|
162
|
+
version: "0.0.1",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Use a short test-only timeout so the suite does not wait the full
|
|
169
|
+
// production 10s for the hung-plugin path.
|
|
170
|
+
await loadUserPlugins({ importTimeoutMs: 250 });
|
|
171
|
+
|
|
172
|
+
const names = getRegisteredPlugins().map((p) => p.manifest.name);
|
|
173
|
+
expect(names).toContain("healthy-plugin");
|
|
174
|
+
expect(names).not.toContain("hanging-plugin");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("plugin whose top-level await resolves AFTER the timeout cannot register late", async () => {
|
|
178
|
+
// Codex/Devin P1 regression: racing `import(moduleUrl)` against a timeout
|
|
179
|
+
// only stops the loader from awaiting the module — it does NOT cancel
|
|
180
|
+
// module evaluation. A plugin whose top-level await eventually resolves
|
|
181
|
+
// continues running in the background and would otherwise call
|
|
182
|
+
// `registerPlugin()` after `loadUserPlugins()` has returned (and after
|
|
183
|
+
// `bootstrapPlugins()` has potentially already walked the registry),
|
|
184
|
+
// leaving the plugin visible to `getMiddlewaresFor()` with its `init()`
|
|
185
|
+
// hook never invoked.
|
|
186
|
+
//
|
|
187
|
+
// The `closeRegistration()` latch must reject that late arrival so the
|
|
188
|
+
// registry stays consistent with the bootstrap invariant.
|
|
189
|
+
writePlugin(
|
|
190
|
+
"slow-late-plugin",
|
|
191
|
+
`
|
|
192
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
193
|
+
registerPlugin({
|
|
194
|
+
manifest: {
|
|
195
|
+
name: "slow-late-plugin",
|
|
196
|
+
version: "0.0.1",
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
`,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Time out well before the plugin's top-level await resolves. The loader
|
|
203
|
+
// returns immediately; the abandoned import keeps evaluating in the
|
|
204
|
+
// background.
|
|
205
|
+
await loadUserPlugins({ importTimeoutMs: 25 });
|
|
206
|
+
|
|
207
|
+
// Wait long enough for the abandoned import's top-level await to resolve
|
|
208
|
+
// and try to `registerPlugin()`. The closed-registration latch must
|
|
209
|
+
// reject the call; the `.catch(() => {})` on the abandoned import must
|
|
210
|
+
// swallow the resulting rejection so the test does not see an
|
|
211
|
+
// unhandled-rejection crash.
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
213
|
+
|
|
214
|
+
const names = getRegisteredPlugins().map((p) => p.manifest.name);
|
|
215
|
+
expect(names).not.toContain("slow-late-plugin");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("subdirectory without register.{ts,js} is silently skipped", async () => {
|
|
219
|
+
// Populate a directory that looks like a plugin but lacks a register
|
|
220
|
+
// file. The loader must skip it without throwing.
|
|
123
221
|
const stubDir = join(PLUGINS_DIR, "not-a-plugin");
|
|
124
222
|
mkdirSync(stubDir, { recursive: true });
|
|
125
223
|
writeFileSync(join(stubDir, "README.md"), "# not actually a plugin\n");
|
|
@@ -128,12 +226,146 @@ describe("user plugin loader", () => {
|
|
|
128
226
|
expect(getRegisteredPlugins()).toHaveLength(0);
|
|
129
227
|
});
|
|
130
228
|
|
|
131
|
-
|
|
132
|
-
|
|
229
|
+
describe("experimental plugin framework branch", () => {
|
|
230
|
+
/**
|
|
231
|
+
* Write a directory-convention plugin (package.json + optional
|
|
232
|
+
* hooks/tools default exports). Mirrors `writePlugin()` above but
|
|
233
|
+
* targets the new experimental loader path.
|
|
234
|
+
*/
|
|
235
|
+
function writeExperimentalPlugin(
|
|
236
|
+
name: string,
|
|
237
|
+
pkg: Record<string, unknown>,
|
|
238
|
+
files: Record<string, string> = {},
|
|
239
|
+
): void {
|
|
240
|
+
const pluginDir = join(PLUGINS_DIR, name);
|
|
241
|
+
mkdirSync(pluginDir, { recursive: true });
|
|
242
|
+
writeFileSync(
|
|
243
|
+
join(pluginDir, "package.json"),
|
|
244
|
+
JSON.stringify(pkg, null, 2),
|
|
245
|
+
);
|
|
246
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
247
|
+
const parts = rel.split("/");
|
|
248
|
+
parts.pop();
|
|
249
|
+
if (parts.length > 0) {
|
|
250
|
+
mkdirSync(join(pluginDir, ...parts), { recursive: true });
|
|
251
|
+
}
|
|
252
|
+
writeFileSync(join(pluginDir, rel), body);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
133
255
|
|
|
134
|
-
|
|
256
|
+
test("loads a plugin via the package.json branch and registers it", async () => {
|
|
257
|
+
writeExperimentalPlugin(
|
|
258
|
+
"experimental-one",
|
|
259
|
+
{ name: "experimental-one", version: "0.1.0" },
|
|
260
|
+
{
|
|
261
|
+
"hooks/init.ts":
|
|
262
|
+
"export default async function init(_ctx: unknown): Promise<void> {}\n",
|
|
263
|
+
},
|
|
264
|
+
);
|
|
135
265
|
|
|
136
|
-
|
|
137
|
-
|
|
266
|
+
await loadUserPlugins();
|
|
267
|
+
|
|
268
|
+
const names = getRegisteredPlugins().map((p) => p.manifest.name);
|
|
269
|
+
expect(names).toContain("experimental-one");
|
|
270
|
+
const registered = getRegisteredPlugins().find(
|
|
271
|
+
(p) => p.manifest.name === "experimental-one",
|
|
272
|
+
);
|
|
273
|
+
expect(typeof registered?.hooks?.init).toBe("function");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("strips npm scope from package.json name", async () => {
|
|
277
|
+
writeExperimentalPlugin("scoped", {
|
|
278
|
+
name: "@vellumai/cool-plugin",
|
|
279
|
+
version: "0.1.0",
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
await loadUserPlugins();
|
|
283
|
+
|
|
284
|
+
const names = getRegisteredPlugins().map((p) => p.manifest.name);
|
|
285
|
+
expect(names).toContain("cool-plugin");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("a broken experimental plugin is logged and skipped without affecting others", async () => {
|
|
289
|
+
// Plugin A has a malformed package.json; Plugin B is a healthy
|
|
290
|
+
// legacy register.ts. The loader must isolate A's failure and still
|
|
291
|
+
// register B — same per-plugin contract as the legacy path.
|
|
292
|
+
const brokenDir = join(PLUGINS_DIR, "broken-experimental");
|
|
293
|
+
mkdirSync(brokenDir, { recursive: true });
|
|
294
|
+
writeFileSync(join(brokenDir, "package.json"), "{ not valid json");
|
|
295
|
+
|
|
296
|
+
writePlugin(
|
|
297
|
+
"healthy-legacy",
|
|
298
|
+
`
|
|
299
|
+
registerPlugin({
|
|
300
|
+
manifest: {
|
|
301
|
+
name: "healthy-legacy",
|
|
302
|
+
version: "0.0.1",
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
`,
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
await loadUserPlugins();
|
|
309
|
+
|
|
310
|
+
const names = getRegisteredPlugins().map((p) => p.manifest.name);
|
|
311
|
+
expect(names).toContain("healthy-legacy");
|
|
312
|
+
expect(names).not.toContain("broken-experimental");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("experimental and legacy plugins coexist in one workspace", async () => {
|
|
316
|
+
writeExperimentalPlugin("new-style", {
|
|
317
|
+
name: "new-style",
|
|
318
|
+
version: "0.1.0",
|
|
319
|
+
});
|
|
320
|
+
writePlugin(
|
|
321
|
+
"old-style",
|
|
322
|
+
`
|
|
323
|
+
registerPlugin({
|
|
324
|
+
manifest: {
|
|
325
|
+
name: "old-style",
|
|
326
|
+
version: "0.0.1",
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
`,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
await loadUserPlugins();
|
|
333
|
+
|
|
334
|
+
const names = getRegisteredPlugins()
|
|
335
|
+
.map((p) => p.manifest.name)
|
|
336
|
+
.sort();
|
|
337
|
+
expect(names).toEqual(["new-style", "old-style"]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("package.json branch wins over a stale register.ts in the same dir", async () => {
|
|
341
|
+
// A migrated plugin may keep its old register.ts on disk while it
|
|
342
|
+
// adopts the new convention. The package.json gate takes the
|
|
343
|
+
// experimental path and the legacy register.ts is never imported,
|
|
344
|
+
// so the plugin must register exactly once.
|
|
345
|
+
const dir = join(PLUGINS_DIR, "migrated");
|
|
346
|
+
mkdirSync(dir, { recursive: true });
|
|
347
|
+
writeFileSync(
|
|
348
|
+
join(dir, "package.json"),
|
|
349
|
+
JSON.stringify({ name: "migrated", version: "0.1.0" }),
|
|
350
|
+
);
|
|
351
|
+
// A register.ts that would double-register if both paths fired.
|
|
352
|
+
const registryPath = join(
|
|
353
|
+
import.meta.dir,
|
|
354
|
+
"..",
|
|
355
|
+
"plugins",
|
|
356
|
+
"registry.ts",
|
|
357
|
+
);
|
|
358
|
+
writeFileSync(
|
|
359
|
+
join(dir, "register.ts"),
|
|
360
|
+
`import { registerPlugin } from ${JSON.stringify(registryPath)};
|
|
361
|
+
registerPlugin({ manifest: { name: "migrated", version: "0.0.1", requires: { pluginRuntime: "v1" } } });
|
|
362
|
+
`,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
await loadUserPlugins();
|
|
366
|
+
|
|
367
|
+
const names = getRegisteredPlugins().map((p) => p.manifest.name);
|
|
368
|
+
expect(names.filter((n) => n === "migrated")).toHaveLength(1);
|
|
369
|
+
});
|
|
138
370
|
});
|
|
139
371
|
});
|
|
@@ -62,43 +62,3 @@ describe("VellumAcpClientHandler.sessionUpdate", () => {
|
|
|
62
62
|
expect(handler.responseText).toBe("");
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
|
-
|
|
66
|
-
describe("VellumAcpClientHandler replay suppression", () => {
|
|
67
|
-
function messageChunk(text: string): SessionNotification {
|
|
68
|
-
return {
|
|
69
|
-
sessionId: ACP_SESSION_ID,
|
|
70
|
-
update: {
|
|
71
|
-
sessionUpdate: "agent_message_chunk",
|
|
72
|
-
content: { type: "text", text },
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
test("updates received while suppressed are dropped", async () => {
|
|
78
|
-
const { handler, sent } = makeHandler();
|
|
79
|
-
|
|
80
|
-
handler.beginReplaySuppression();
|
|
81
|
-
await handler.sessionUpdate(messageChunk("replayed history"));
|
|
82
|
-
|
|
83
|
-
expect(sent).toHaveLength(0);
|
|
84
|
-
expect(handler.responseText).toBe("");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
test("updates after endReplaySuppression() flow normally", async () => {
|
|
88
|
-
const { handler, sent } = makeHandler();
|
|
89
|
-
|
|
90
|
-
handler.beginReplaySuppression();
|
|
91
|
-
await handler.sessionUpdate(messageChunk("replayed history"));
|
|
92
|
-
handler.endReplaySuppression();
|
|
93
|
-
await handler.sessionUpdate(messageChunk("live response"));
|
|
94
|
-
|
|
95
|
-
expect(sent).toHaveLength(1);
|
|
96
|
-
expect(sent[0]).toEqual({
|
|
97
|
-
type: "acp_session_update",
|
|
98
|
-
acpSessionId: ACP_SESSION_ID,
|
|
99
|
-
updateType: "agent_message_chunk",
|
|
100
|
-
content: "live response",
|
|
101
|
-
});
|
|
102
|
-
expect(handler.responseText).toBe("live response");
|
|
103
|
-
});
|
|
104
|
-
});
|
|
@@ -216,22 +216,6 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
216
216
|
expect(prepared.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("vault-FFF");
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
-
test("injects the token for the bunx-rewritten claude adapter (adapterCommand gate)", async () => {
|
|
220
|
-
// The resolver rewrites a missing claude-agent-acp binary to run via
|
|
221
|
-
// `bun x --bun <pkg>` and preserves the canonical identity on
|
|
222
|
-
// `adapterCommand`. Without this gate, bunx-resolved spawns would start
|
|
223
|
-
// with no auth and die as zombies on the first prompt.
|
|
224
|
-
seedVaultToken("vault-bunx");
|
|
225
|
-
|
|
226
|
-
const prepared = await prepareAgentEnv({
|
|
227
|
-
command: "bun",
|
|
228
|
-
args: ["x", "--bun", "@agentclientprotocol/claude-agent-acp"],
|
|
229
|
-
adapterCommand: "claude-agent-acp",
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
expect(prepared.env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("vault-bunx");
|
|
233
|
-
});
|
|
234
|
-
|
|
235
219
|
test("does NOT mutate the caller's agentConfig", async () => {
|
|
236
220
|
seedVaultToken("vault-GGG");
|
|
237
221
|
const original = {
|
|
@@ -250,115 +234,6 @@ describe("prepareAgentEnv — claude-agent-acp gating", () => {
|
|
|
250
234
|
});
|
|
251
235
|
});
|
|
252
236
|
|
|
253
|
-
describe("prepareAgentEnv - gemini gating", () => {
|
|
254
|
-
function seedVaultGeminiKey(key: string): void {
|
|
255
|
-
vaultStore.set("acp/gemini_api_key", key);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
test("injects GEMINI_API_KEY from the vault via the broker when agent.env has no override", async () => {
|
|
259
|
-
seedVaultGeminiKey("vault-gem-AAA");
|
|
260
|
-
|
|
261
|
-
const prepared = await prepareAgentEnv({
|
|
262
|
-
command: "gemini",
|
|
263
|
-
args: ["--acp"],
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
expect(prepared.env?.GEMINI_API_KEY).toBe("vault-gem-AAA");
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test("auto-registers metadata with acp_spawn in allowedTools when none exists", async () => {
|
|
270
|
-
seedVaultGeminiKey("vault-gem-auto-meta");
|
|
271
|
-
|
|
272
|
-
await prepareAgentEnv({ command: "gemini", args: ["--acp"] });
|
|
273
|
-
|
|
274
|
-
const meta = metadataStore.get("acp/gemini_api_key");
|
|
275
|
-
expect(meta).toBeDefined();
|
|
276
|
-
expect(meta!.allowedTools).toContain("acp_spawn");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test("injects the key for the bunx-rewritten gemini CLI (adapterCommand gate)", async () => {
|
|
280
|
-
seedVaultGeminiKey("vault-gem-bunx");
|
|
281
|
-
|
|
282
|
-
const prepared = await prepareAgentEnv({
|
|
283
|
-
command: "bun",
|
|
284
|
-
args: ["x", "--bun", "@google/gemini-cli", "--acp"],
|
|
285
|
-
adapterCommand: "gemini",
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
expect(prepared.env?.GEMINI_API_KEY).toBe("vault-gem-bunx");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test("a vault miss does NOT throw and spawns without GEMINI_API_KEY (key is optional)", async () => {
|
|
292
|
-
const prepared = await prepareAgentEnv({
|
|
293
|
-
command: "gemini",
|
|
294
|
-
args: ["--acp"],
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
expect(prepared.env).not.toHaveProperty("GEMINI_API_KEY");
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("respects explicit tool policy that excludes acp_spawn without failing the spawn", async () => {
|
|
301
|
-
metadataStore.set("acp/gemini_api_key", {
|
|
302
|
-
allowedTools: ["other_tool"],
|
|
303
|
-
});
|
|
304
|
-
seedVaultGeminiKey("vault-gem-restricted");
|
|
305
|
-
|
|
306
|
-
const prepared = await prepareAgentEnv({
|
|
307
|
-
command: "gemini",
|
|
308
|
-
args: ["--acp"],
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
expect(prepared.env).not.toHaveProperty("GEMINI_API_KEY");
|
|
312
|
-
const meta = metadataStore.get("acp/gemini_api_key");
|
|
313
|
-
expect(meta!.allowedTools).toEqual(["other_tool"]);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
test("agent.env override wins over the vault entry and skips the broker (precedence pin)", async () => {
|
|
317
|
-
// Seed a vault value but no metadata: if the override path consulted the
|
|
318
|
-
// broker anyway, ensureAcpCredentialPolicy would create metadata here.
|
|
319
|
-
seedVaultGeminiKey("vault-gem-CCC");
|
|
320
|
-
|
|
321
|
-
const prepared = await prepareAgentEnv({
|
|
322
|
-
command: "gemini",
|
|
323
|
-
args: ["--acp"],
|
|
324
|
-
env: { GEMINI_API_KEY: "config-gem-DDD" },
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
expect(prepared.env?.GEMINI_API_KEY).toBe("config-gem-DDD");
|
|
328
|
-
expect(metadataStore.has("acp/gemini_api_key")).toBe(false);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
test("preserves unrelated env vars on agent.env when injecting from the vault", async () => {
|
|
332
|
-
seedVaultGeminiKey("vault-gem-EEE");
|
|
333
|
-
|
|
334
|
-
const prepared = await prepareAgentEnv({
|
|
335
|
-
command: "gemini",
|
|
336
|
-
args: ["--acp"],
|
|
337
|
-
env: { NO_COLOR: "1" },
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
expect(prepared.env?.GEMINI_API_KEY).toBe("vault-gem-EEE");
|
|
341
|
-
expect(prepared.env?.NO_COLOR).toBe("1");
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
test("does NOT mutate the caller's agentConfig", async () => {
|
|
345
|
-
seedVaultGeminiKey("vault-gem-GGG");
|
|
346
|
-
const original = {
|
|
347
|
-
command: "gemini",
|
|
348
|
-
args: ["--acp"],
|
|
349
|
-
env: { OTHER: "keep" },
|
|
350
|
-
};
|
|
351
|
-
const beforeEnv = { ...original.env };
|
|
352
|
-
|
|
353
|
-
const prepared = await prepareAgentEnv(original);
|
|
354
|
-
|
|
355
|
-
expect(prepared).not.toBe(original);
|
|
356
|
-
expect(prepared.env).not.toBe(original.env);
|
|
357
|
-
expect(original.env).toEqual(beforeEnv);
|
|
358
|
-
expect(original.env).not.toHaveProperty("GEMINI_API_KEY");
|
|
359
|
-
});
|
|
360
|
-
});
|
|
361
|
-
|
|
362
237
|
describe("prepareAgentEnv — non-claude commands", () => {
|
|
363
238
|
test("returns the config unchanged for codex-acp (no required env vars today)", async () => {
|
|
364
239
|
const prepared = await prepareAgentEnv({
|
|
@@ -369,18 +244,6 @@ describe("prepareAgentEnv — non-claude commands", () => {
|
|
|
369
244
|
expect(prepared.env).toEqual({});
|
|
370
245
|
});
|
|
371
246
|
|
|
372
|
-
test("no injection for a bunx-rewritten non-claude adapter", async () => {
|
|
373
|
-
seedVaultToken("vault-should-not-leak");
|
|
374
|
-
|
|
375
|
-
const prepared = await prepareAgentEnv({
|
|
376
|
-
command: "bun",
|
|
377
|
-
args: ["x", "--bun", "@zed-industries/codex-acp"],
|
|
378
|
-
adapterCommand: "codex-acp",
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
expect(prepared.env).toEqual({});
|
|
382
|
-
});
|
|
383
|
-
|
|
384
247
|
test("returns the config unchanged for an unrecognized command basename", async () => {
|
|
385
248
|
seedVaultToken("vault-HHH");
|
|
386
249
|
|