@vellumai/assistant 0.8.8 → 0.8.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +235 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
- package/src/__tests__/conversation-agent-loop.test.ts +297 -586
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -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__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +225 -9
- package/src/acp/resolve-agent.ts +122 -17
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +414 -248
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/bundled-skills/acp/SKILL.md +85 -33
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +15 -6
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +175 -1055
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +404 -57
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- 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 +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- 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 +315 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- 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 +6 -10
- 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 +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- 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 +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +8 -2
- package/src/tools/acp/spawn.test.ts +176 -195
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
|
@@ -34,15 +34,26 @@
|
|
|
34
34
|
|
|
35
35
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
36
36
|
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
type InstallPluginOptions,
|
|
39
|
+
type InstallPluginResult,
|
|
40
|
+
InvalidPluginNameError,
|
|
41
|
+
PluginAlreadyInstalledError,
|
|
42
|
+
PluginNotFoundError,
|
|
43
|
+
PluginSourceUnavailableError,
|
|
44
|
+
} from "../../../cli/lib/install-from-github.js";
|
|
38
45
|
import type { InstalledPluginInfo } from "../../../cli/lib/list-installed-plugins.js";
|
|
46
|
+
import {
|
|
47
|
+
type PluginDetails,
|
|
48
|
+
PluginDetailsNotFoundError,
|
|
49
|
+
type PluginDetailsOptions,
|
|
50
|
+
} from "../../../cli/lib/plugin-details.js";
|
|
39
51
|
import type {
|
|
52
|
+
PluginCatalog,
|
|
40
53
|
PluginSearchMatch,
|
|
41
54
|
SearchPluginsDeps,
|
|
42
|
-
SearchPluginsOptions,
|
|
43
|
-
SearchPluginsResult,
|
|
44
55
|
} from "../../../cli/lib/search-plugins.js";
|
|
45
|
-
import {
|
|
56
|
+
import { PluginCatalogUnavailableError } from "../../../cli/lib/search-plugins.js";
|
|
46
57
|
import {
|
|
47
58
|
PluginNotInstalledError,
|
|
48
59
|
type UninstallPluginOptions,
|
|
@@ -57,22 +68,18 @@ mock.module("../../../cli/lib/list-installed-plugins.js", () => ({
|
|
|
57
68
|
listInstalledPlugins: () => installedFixture,
|
|
58
69
|
}));
|
|
59
70
|
|
|
60
|
-
// Mock
|
|
61
|
-
// returns
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
throw new Error("searchSpy default impl not configured");
|
|
71
|
+
// Mock the catalog cache: `getCatalogSpy` records every invocation and
|
|
72
|
+
// returns the (unfiltered) catalog the route then filters in memory via the
|
|
73
|
+
// real `filterPluginCatalog`. The real `search-plugins.js` module is left
|
|
74
|
+
// unmocked so filtering + error classes behave exactly as in production.
|
|
75
|
+
const getCatalogSpy = mock(
|
|
76
|
+
async (_ref: string, _deps: SearchPluginsDeps): Promise<PluginCatalog> => {
|
|
77
|
+
throw new Error("getCatalogSpy default impl not configured");
|
|
68
78
|
},
|
|
69
79
|
);
|
|
70
80
|
|
|
71
|
-
mock.module("../../../cli/lib/
|
|
72
|
-
|
|
73
|
-
// `instanceof InvalidSearchPatternError`.
|
|
74
|
-
InvalidSearchPatternError,
|
|
75
|
-
searchPlugins: searchSpy,
|
|
81
|
+
mock.module("../../../cli/lib/plugin-catalog-cache.js", () => ({
|
|
82
|
+
getPluginCatalog: getCatalogSpy,
|
|
76
83
|
}));
|
|
77
84
|
|
|
78
85
|
// Mock uninstallPlugin. The handler's error mapping is the wiring under
|
|
@@ -89,16 +96,50 @@ mock.module("../../../cli/lib/uninstall-plugin.js", () => ({
|
|
|
89
96
|
uninstallPlugin: uninstallSpy,
|
|
90
97
|
}));
|
|
91
98
|
|
|
99
|
+
// Mock installPlugin. As with the other libs, the handler's error mapping
|
|
100
|
+
// is the wiring under test; the lib's own behavior is covered separately.
|
|
101
|
+
const installSpy = mock(
|
|
102
|
+
async (_opts: InstallPluginOptions): Promise<InstallPluginResult> => {
|
|
103
|
+
throw new Error("installSpy default impl not configured");
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
92
107
|
// `InvalidPluginNameError` is re-exported from uninstall-plugin.js but
|
|
93
108
|
// the canonical definition lives in install-from-github.js. The
|
|
94
109
|
// handler imports from install-from-github.js, so mock that too so the
|
|
95
|
-
// `instanceof`
|
|
96
|
-
// the
|
|
110
|
+
// `instanceof` checks inside the handler resolve to the same classes as
|
|
111
|
+
// the ones the spies throw. The error classes are passed through real so
|
|
112
|
+
// `instanceof` aligns.
|
|
97
113
|
mock.module("../../../cli/lib/install-from-github.js", () => ({
|
|
114
|
+
DEFAULT_PLUGIN_REF: "main",
|
|
98
115
|
InvalidPluginNameError,
|
|
116
|
+
PluginAlreadyInstalledError,
|
|
117
|
+
PluginNotFoundError,
|
|
118
|
+
PluginSourceUnavailableError,
|
|
119
|
+
installPlugin: installSpy,
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
// Mock getPluginDetails: the detail handler unions disk + manifest + repo
|
|
123
|
+
// in the lib (covered by plugin-details.test.ts); here we isolate the
|
|
124
|
+
// route's error mapping and pass-through.
|
|
125
|
+
const detailsSpy = mock(
|
|
126
|
+
async (_opts: PluginDetailsOptions): Promise<PluginDetails> => {
|
|
127
|
+
throw new Error("detailsSpy default impl not configured");
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
mock.module("../../../cli/lib/plugin-details.js", () => ({
|
|
132
|
+
PluginDetailsNotFoundError,
|
|
133
|
+
getPluginDetails: detailsSpy,
|
|
99
134
|
}));
|
|
100
135
|
|
|
101
|
-
import {
|
|
136
|
+
import {
|
|
137
|
+
BadRequestError,
|
|
138
|
+
ConflictError,
|
|
139
|
+
InternalError,
|
|
140
|
+
NotFoundError,
|
|
141
|
+
ServiceUnavailableError,
|
|
142
|
+
} from "../errors.js";
|
|
102
143
|
import { ROUTES as PLUGINS_ROUTES } from "../plugins-routes.js";
|
|
103
144
|
import type { RouteDefinition, RouteHandlerArgs } from "../types.js";
|
|
104
145
|
|
|
@@ -111,10 +152,12 @@ function findHandler(operationId: string): RouteDefinition["handler"] {
|
|
|
111
152
|
const listHandler = findHandler("plugins_list");
|
|
112
153
|
const searchHandler = findHandler("plugins_search");
|
|
113
154
|
const uninstallHandler = findHandler("plugins_uninstall");
|
|
155
|
+
const getHandler = findHandler("plugins_get");
|
|
156
|
+
const installHandler = findHandler("plugins_install");
|
|
114
157
|
|
|
115
|
-
function invoke(
|
|
116
|
-
|
|
117
|
-
|
|
158
|
+
function invoke(args: RouteHandlerArgs = {}): {
|
|
159
|
+
plugins: Array<Record<string, unknown>>;
|
|
160
|
+
} {
|
|
118
161
|
return listHandler(args) as { plugins: Array<Record<string, unknown>> };
|
|
119
162
|
}
|
|
120
163
|
|
|
@@ -268,7 +311,9 @@ describe("GET /v1/plugins", () => {
|
|
|
268
311
|
|
|
269
312
|
// matches multiple
|
|
270
313
|
expect(
|
|
271
|
-
invoke({ queryParams: { q: "o" } })
|
|
314
|
+
invoke({ queryParams: { q: "o" } })
|
|
315
|
+
.plugins.map((p) => p.id)
|
|
316
|
+
.sort(),
|
|
272
317
|
).toEqual(["calendar-sync", "todo", "weather"].sort());
|
|
273
318
|
|
|
274
319
|
// no match
|
|
@@ -305,81 +350,129 @@ describe("GET /v1/plugins", () => {
|
|
|
305
350
|
// GET /v1/plugins/search
|
|
306
351
|
// ---------------------------------------------------------------------------
|
|
307
352
|
|
|
353
|
+
function catalog(
|
|
354
|
+
ref: string,
|
|
355
|
+
matches: readonly PluginSearchMatch[],
|
|
356
|
+
): PluginCatalog {
|
|
357
|
+
return { ref, matches };
|
|
358
|
+
}
|
|
359
|
+
|
|
308
360
|
describe("GET /v1/plugins/search", () => {
|
|
309
361
|
beforeEach(() => {
|
|
310
|
-
|
|
311
|
-
// Default to a happy-path
|
|
312
|
-
|
|
313
|
-
query: opts.query,
|
|
314
|
-
ref: opts.ref ?? "main",
|
|
315
|
-
matches: [],
|
|
316
|
-
}));
|
|
362
|
+
getCatalogSpy.mockClear();
|
|
363
|
+
// Default to a happy-path empty catalog; individual tests override.
|
|
364
|
+
getCatalogSpy.mockImplementation(async (ref) => catalog(ref, []));
|
|
317
365
|
});
|
|
318
366
|
|
|
319
|
-
test("
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
367
|
+
test("resolves the catalog at the requested ref and filters by ?q=", async () => {
|
|
368
|
+
getCatalogSpy.mockImplementation(async (ref) =>
|
|
369
|
+
catalog(ref, [
|
|
370
|
+
{
|
|
371
|
+
name: "simple-memory",
|
|
372
|
+
path: "experimental/plugins/simple-memory",
|
|
373
|
+
source: { kind: "first-party" },
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "caveman",
|
|
377
|
+
path: "github:JuliusBrussee/caveman@v1.8.2",
|
|
378
|
+
description: "Ultra-compressed communication mode.",
|
|
379
|
+
source: {
|
|
380
|
+
kind: "github",
|
|
381
|
+
repo: "JuliusBrussee/caveman",
|
|
382
|
+
ref: "v1.8.2",
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
]),
|
|
386
|
+
);
|
|
328
387
|
|
|
329
388
|
const result = await invokeSearch({
|
|
330
389
|
queryParams: { q: "^simple", ref: "my-feature-branch" },
|
|
331
390
|
});
|
|
332
391
|
|
|
333
|
-
// The
|
|
334
|
-
expect(
|
|
335
|
-
const [
|
|
336
|
-
expect(
|
|
337
|
-
expect(opts.ref).toBe("my-feature-branch");
|
|
392
|
+
// The cache was consulted once at the requested ref.
|
|
393
|
+
expect(getCatalogSpy).toHaveBeenCalledTimes(1);
|
|
394
|
+
const [ref] = getCatalogSpy.mock.calls[0]!;
|
|
395
|
+
expect(ref).toBe("my-feature-branch");
|
|
338
396
|
|
|
397
|
+
// The query is applied in-memory by the real filter: `^simple` matches
|
|
398
|
+
// only `simple-memory`, and the source discriminator is preserved.
|
|
339
399
|
expect(result).toEqual({
|
|
340
400
|
query: "^simple",
|
|
341
401
|
ref: "my-feature-branch",
|
|
342
402
|
matches: [
|
|
343
|
-
{
|
|
344
|
-
|
|
403
|
+
{
|
|
404
|
+
name: "simple-memory",
|
|
405
|
+
path: "experimental/plugins/simple-memory",
|
|
406
|
+
source: { kind: "first-party" },
|
|
407
|
+
},
|
|
345
408
|
],
|
|
346
409
|
});
|
|
347
410
|
});
|
|
348
411
|
|
|
349
|
-
test("missing ?q=
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
412
|
+
test("missing ?q= matches all (empty-string query) at the default ref", async () => {
|
|
413
|
+
getCatalogSpy.mockImplementation(async (ref) =>
|
|
414
|
+
catalog(ref, [
|
|
415
|
+
{
|
|
416
|
+
name: "caveman",
|
|
417
|
+
path: "github:JuliusBrussee/caveman@v1.8.2",
|
|
418
|
+
source: {
|
|
419
|
+
kind: "github",
|
|
420
|
+
repo: "JuliusBrussee/caveman",
|
|
421
|
+
ref: "v1.8.2",
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
]),
|
|
425
|
+
);
|
|
426
|
+
const result = await invokeSearch();
|
|
427
|
+
const [ref] = getCatalogSpy.mock.calls[0]!;
|
|
428
|
+
// No ref supplied → route resolves the default ref before caching.
|
|
429
|
+
expect(ref).toBe("main");
|
|
430
|
+
expect(result.query).toBe("");
|
|
431
|
+
expect(result.matches.map((m) => m.name)).toEqual(["caveman"]);
|
|
356
432
|
});
|
|
357
433
|
|
|
358
|
-
test("whitespace-only ?ref=
|
|
434
|
+
test("whitespace-only ?ref= falls back to the default ref", async () => {
|
|
359
435
|
await invokeSearch({ queryParams: { q: "x", ref: " " } });
|
|
360
|
-
const [
|
|
361
|
-
expect(
|
|
436
|
+
const [ref] = getCatalogSpy.mock.calls[0]!;
|
|
437
|
+
expect(ref).toBe("main");
|
|
362
438
|
});
|
|
363
439
|
|
|
364
|
-
test("supplies a bound globalThis.fetch as the
|
|
440
|
+
test("supplies a bound globalThis.fetch as the catalog loader's fetch dep", async () => {
|
|
365
441
|
await invokeSearch({ queryParams: { q: "memory" } });
|
|
366
|
-
const [, deps] =
|
|
442
|
+
const [, deps] = getCatalogSpy.mock.calls[0]!;
|
|
367
443
|
expect(typeof deps.fetch).toBe("function");
|
|
368
444
|
});
|
|
369
445
|
|
|
370
|
-
test("InvalidSearchPatternError → BadRequestError (400)", async () => {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
446
|
+
test("InvalidSearchPatternError → BadRequestError (400), before any catalog load", async () => {
|
|
447
|
+
// GIVEN a malformed regex query and a cold cache
|
|
448
|
+
// WHEN the search runs
|
|
449
|
+
// THEN it's a deterministic 400 AND the catalog is never loaded, so a
|
|
450
|
+
// user typo can't waste a GitHub request (which would otherwise surface
|
|
451
|
+
// as 503 on a rate-limited cold cache).
|
|
375
452
|
await expect(
|
|
376
453
|
invokeSearch({ queryParams: { q: "(" } }),
|
|
377
454
|
).rejects.toBeInstanceOf(BadRequestError);
|
|
455
|
+
expect(getCatalogSpy).not.toHaveBeenCalled();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("PluginCatalogUnavailableError → ServiceUnavailableError (503)", async () => {
|
|
459
|
+
getCatalogSpy.mockImplementation(async () => {
|
|
460
|
+
throw new PluginCatalogUnavailableError(
|
|
461
|
+
"GitHub contents listing failed for experimental/plugins @ main: HTTP 403",
|
|
462
|
+
403,
|
|
463
|
+
);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// A rate-limited upstream (with no cache to fall back on) is transient
|
|
467
|
+
// and retryable — the route surfaces it as 503, not a misleading 500.
|
|
468
|
+
await expect(
|
|
469
|
+
invokeSearch({ queryParams: { q: "memory" } }),
|
|
470
|
+
).rejects.toBeInstanceOf(ServiceUnavailableError);
|
|
378
471
|
});
|
|
379
472
|
|
|
380
473
|
test("unknown errors → InternalError with original message preserved", async () => {
|
|
381
|
-
|
|
382
|
-
throw new Error("GitHub contents listing failed: HTTP
|
|
474
|
+
getCatalogSpy.mockImplementation(async () => {
|
|
475
|
+
throw new Error("GitHub contents listing failed: HTTP 404");
|
|
383
476
|
});
|
|
384
477
|
|
|
385
478
|
await expect(
|
|
@@ -394,20 +487,28 @@ describe("GET /v1/plugins/search", () => {
|
|
|
394
487
|
|
|
395
488
|
test("re-packs readonly match array into a mutable copy", async () => {
|
|
396
489
|
const frozenMatches = Object.freeze([
|
|
397
|
-
Object.freeze({
|
|
490
|
+
Object.freeze({
|
|
491
|
+
name: "a",
|
|
492
|
+
path: "experimental/plugins/a",
|
|
493
|
+
source: { kind: "first-party" } as const,
|
|
494
|
+
}),
|
|
398
495
|
]) as readonly PluginSearchMatch[];
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
matches: frozenMatches,
|
|
403
|
-
}));
|
|
496
|
+
getCatalogSpy.mockImplementation(async (ref) =>
|
|
497
|
+
catalog(ref, frozenMatches),
|
|
498
|
+
);
|
|
404
499
|
|
|
405
500
|
const result = await invokeSearch({ queryParams: { q: "a" } });
|
|
406
501
|
// The route returns a non-frozen array we can mutate without
|
|
407
502
|
// touching the lib's internal cache. This matters when serializers
|
|
408
503
|
// (or downstream test fixtures) reach in.
|
|
409
504
|
expect(Object.isFrozen(result.matches)).toBe(false);
|
|
410
|
-
expect(() =>
|
|
505
|
+
expect(() =>
|
|
506
|
+
result.matches.push({
|
|
507
|
+
name: "b",
|
|
508
|
+
path: "x",
|
|
509
|
+
source: { kind: "first-party" },
|
|
510
|
+
}),
|
|
511
|
+
).not.toThrow();
|
|
411
512
|
});
|
|
412
513
|
});
|
|
413
514
|
|
|
@@ -510,3 +611,252 @@ describe("DELETE /v1/plugins/:name", () => {
|
|
|
510
611
|
expect((caught as Error).message).toBe("plugin uninstall failed");
|
|
511
612
|
});
|
|
512
613
|
});
|
|
614
|
+
|
|
615
|
+
function pluginDetails(overrides: Partial<PluginDetails> = {}): PluginDetails {
|
|
616
|
+
return {
|
|
617
|
+
name: overrides.name ?? "caveman",
|
|
618
|
+
installed: overrides.installed ?? false,
|
|
619
|
+
description: overrides.description ?? null,
|
|
620
|
+
homepage: overrides.homepage ?? null,
|
|
621
|
+
license: overrides.license ?? null,
|
|
622
|
+
version: overrides.version ?? null,
|
|
623
|
+
source: overrides.source ?? { kind: "first-party" },
|
|
624
|
+
readme: overrides.readme ?? null,
|
|
625
|
+
ref: overrides.ref ?? "main",
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
async function invokeGet(args: RouteHandlerArgs = {}): Promise<PluginDetails> {
|
|
630
|
+
return (await getHandler(args)) as PluginDetails;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
describe("GET /v1/plugins/:name", () => {
|
|
634
|
+
beforeEach(() => {
|
|
635
|
+
detailsSpy.mockReset();
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("forwards name + ref to getPluginDetails and returns the detail view", async () => {
|
|
639
|
+
const view = pluginDetails({
|
|
640
|
+
name: "caveman",
|
|
641
|
+
installed: true,
|
|
642
|
+
description: "A loud agent plugin",
|
|
643
|
+
homepage: "https://example.com/caveman",
|
|
644
|
+
license: "MIT",
|
|
645
|
+
version: "1.8.2",
|
|
646
|
+
source: { kind: "github", repo: "example-org/caveman", ref: "v1.8.2" },
|
|
647
|
+
readme: "# Caveman\n\nHello.",
|
|
648
|
+
ref: "v1.8.2",
|
|
649
|
+
});
|
|
650
|
+
detailsSpy.mockImplementation(async () => view);
|
|
651
|
+
|
|
652
|
+
const result = await invokeGet({
|
|
653
|
+
pathParams: { name: "caveman" },
|
|
654
|
+
queryParams: { ref: "v1.8.2" },
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
expect(result).toEqual(view);
|
|
658
|
+
expect(detailsSpy.mock.calls[0]?.[0]).toEqual({
|
|
659
|
+
name: "caveman",
|
|
660
|
+
ref: "v1.8.2",
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
test("omits ref (passes undefined) when the query param is absent or blank", async () => {
|
|
665
|
+
detailsSpy.mockImplementation(async () => pluginDetails());
|
|
666
|
+
|
|
667
|
+
await invokeGet({ pathParams: { name: "caveman" }, queryParams: {} });
|
|
668
|
+
expect(detailsSpy.mock.calls[0]?.[0]).toEqual({
|
|
669
|
+
name: "caveman",
|
|
670
|
+
ref: undefined,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
await invokeGet({
|
|
674
|
+
pathParams: { name: "caveman" },
|
|
675
|
+
queryParams: { ref: " " },
|
|
676
|
+
});
|
|
677
|
+
expect(detailsSpy.mock.calls[1]?.[0]).toEqual({
|
|
678
|
+
name: "caveman",
|
|
679
|
+
ref: undefined,
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("InvalidPluginNameError → BadRequestError (400)", async () => {
|
|
684
|
+
detailsSpy.mockImplementation(async () => {
|
|
685
|
+
throw new InvalidPluginNameError("../escape");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
await expect(
|
|
689
|
+
invokeGet({ pathParams: { name: "../escape" } }),
|
|
690
|
+
).rejects.toBeInstanceOf(BadRequestError);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("PluginDetailsNotFoundError → NotFoundError (404)", async () => {
|
|
694
|
+
detailsSpy.mockImplementation(async () => {
|
|
695
|
+
throw new PluginDetailsNotFoundError("ghost", "main");
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
await expect(
|
|
699
|
+
invokeGet({ pathParams: { name: "ghost" } }),
|
|
700
|
+
).rejects.toBeInstanceOf(NotFoundError);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test("unknown errors → InternalError with original message preserved", async () => {
|
|
704
|
+
detailsSpy.mockImplementation(async () => {
|
|
705
|
+
throw new Error("ENOTFOUND api.github.com");
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
let caught: unknown;
|
|
709
|
+
try {
|
|
710
|
+
await invokeGet({ pathParams: { name: "caveman" } });
|
|
711
|
+
} catch (err) {
|
|
712
|
+
caught = err;
|
|
713
|
+
}
|
|
714
|
+
expect(caught).toBeInstanceOf(InternalError);
|
|
715
|
+
expect((caught as Error).message).toContain("ENOTFOUND");
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
async function invokeInstall(args: RouteHandlerArgs = {}): Promise<{
|
|
720
|
+
ok: true;
|
|
721
|
+
name: string;
|
|
722
|
+
target: string;
|
|
723
|
+
fileCount: number;
|
|
724
|
+
ref: string;
|
|
725
|
+
}> {
|
|
726
|
+
return (await installHandler(args)) as {
|
|
727
|
+
ok: true;
|
|
728
|
+
name: string;
|
|
729
|
+
target: string;
|
|
730
|
+
fileCount: number;
|
|
731
|
+
ref: string;
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
describe("POST /v1/plugins/install", () => {
|
|
736
|
+
beforeEach(() => {
|
|
737
|
+
installSpy.mockReset();
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("forwards name/force and shapes the result, pinning ref to the default", async () => {
|
|
741
|
+
installSpy.mockImplementation(async (opts) => ({
|
|
742
|
+
name: opts.name,
|
|
743
|
+
target: `/workspace/.vellum/plugins/${opts.name}`,
|
|
744
|
+
fileCount: 7,
|
|
745
|
+
ref: opts.ref ?? "main",
|
|
746
|
+
commit: null,
|
|
747
|
+
}));
|
|
748
|
+
|
|
749
|
+
const result = await invokeInstall({
|
|
750
|
+
body: { name: "caveman", force: true },
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
expect(result).toEqual({
|
|
754
|
+
ok: true,
|
|
755
|
+
name: "caveman",
|
|
756
|
+
target: "/workspace/.vellum/plugins/caveman",
|
|
757
|
+
fileCount: 7,
|
|
758
|
+
ref: "main",
|
|
759
|
+
});
|
|
760
|
+
expect(installSpy.mock.calls[0]?.[0]).toEqual({
|
|
761
|
+
name: "caveman",
|
|
762
|
+
ref: "main",
|
|
763
|
+
force: true,
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("ignores a caller-supplied ref and pins to the curated default", async () => {
|
|
768
|
+
// Security boundary: installing from an unreviewed ref (a PR branch,
|
|
769
|
+
// fork ref, ...) could load attacker-controlled marketplace/first-party
|
|
770
|
+
// code, so the HTTP route never honors a body `ref` — it always resolves
|
|
771
|
+
// against the curated default ref.
|
|
772
|
+
installSpy.mockImplementation(async (opts) => ({
|
|
773
|
+
name: opts.name,
|
|
774
|
+
target: `/workspace/.vellum/plugins/${opts.name}`,
|
|
775
|
+
fileCount: 7,
|
|
776
|
+
ref: opts.ref ?? "main",
|
|
777
|
+
commit: null,
|
|
778
|
+
}));
|
|
779
|
+
|
|
780
|
+
const result = await invokeInstall({
|
|
781
|
+
body: { name: "caveman", ref: "attacker-pr-branch" },
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
expect(result.ref).toBe("main");
|
|
785
|
+
expect(installSpy.mock.calls[0]?.[0]).toEqual({
|
|
786
|
+
name: "caveman",
|
|
787
|
+
ref: "main",
|
|
788
|
+
force: undefined,
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
test("a missing name short-circuits to BadRequestError without calling the lib", async () => {
|
|
793
|
+
await expect(invokeInstall({ body: {} })).rejects.toBeInstanceOf(
|
|
794
|
+
BadRequestError,
|
|
795
|
+
);
|
|
796
|
+
expect(installSpy).not.toHaveBeenCalled();
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
test("InvalidPluginNameError → BadRequestError (400)", async () => {
|
|
800
|
+
installSpy.mockImplementation(async () => {
|
|
801
|
+
throw new InvalidPluginNameError("../escape");
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
await expect(
|
|
805
|
+
invokeInstall({ body: { name: "../escape" } }),
|
|
806
|
+
).rejects.toBeInstanceOf(BadRequestError);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
test("PluginAlreadyInstalledError → ConflictError (409)", async () => {
|
|
810
|
+
installSpy.mockImplementation(async (opts) => {
|
|
811
|
+
throw new PluginAlreadyInstalledError(
|
|
812
|
+
opts.name,
|
|
813
|
+
`/workspace/.vellum/plugins/${opts.name}`,
|
|
814
|
+
);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
await expect(
|
|
818
|
+
invokeInstall({ body: { name: "caveman" } }),
|
|
819
|
+
).rejects.toBeInstanceOf(ConflictError);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("PluginNotFoundError → NotFoundError (404)", async () => {
|
|
823
|
+
installSpy.mockImplementation(async (opts) => {
|
|
824
|
+
throw new PluginNotFoundError(opts.name, "main", "example-org/ghost");
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
await expect(
|
|
828
|
+
invokeInstall({ body: { name: "ghost" } }),
|
|
829
|
+
).rejects.toBeInstanceOf(NotFoundError);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
test("PluginSourceUnavailableError → ServiceUnavailableError (503)", async () => {
|
|
833
|
+
// A rate-limited or temporarily-down GitHub source is retryable: the
|
|
834
|
+
// route surfaces 503 so the client can back off and try again, rather
|
|
835
|
+
// than a misleading 500 that reads as a permanent failure.
|
|
836
|
+
installSpy.mockImplementation(async () => {
|
|
837
|
+
throw new PluginSourceUnavailableError(
|
|
838
|
+
"GitHub tree listing for JuliusBrussee/caveman @ v1.8.2: HTTP 403",
|
|
839
|
+
403,
|
|
840
|
+
);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
await expect(
|
|
844
|
+
invokeInstall({ body: { name: "caveman" } }),
|
|
845
|
+
).rejects.toBeInstanceOf(ServiceUnavailableError);
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
test("unknown errors → InternalError with original message preserved", async () => {
|
|
849
|
+
installSpy.mockImplementation(async () => {
|
|
850
|
+
throw new Error("ECONNRESET");
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
let caught: unknown;
|
|
854
|
+
try {
|
|
855
|
+
await invokeInstall({ body: { name: "caveman" } });
|
|
856
|
+
} catch (err) {
|
|
857
|
+
caught = err;
|
|
858
|
+
}
|
|
859
|
+
expect(caught).toBeInstanceOf(InternalError);
|
|
860
|
+
expect((caught as Error).message).toContain("ECONNRESET");
|
|
861
|
+
});
|
|
862
|
+
});
|
|
@@ -42,7 +42,7 @@ const findBySurfaceCalls: string[] = [];
|
|
|
42
42
|
const getOrCreateCalls: string[] = [];
|
|
43
43
|
const rawGetCalls: Array<{ sql: string; params: unknown[] }> = [];
|
|
44
44
|
|
|
45
|
-
mock.module("../../../daemon/conversation-
|
|
45
|
+
mock.module("../../../daemon/conversation-registry.js", () => ({
|
|
46
46
|
findConversation: (id: string) => {
|
|
47
47
|
findConvCalls.push(id);
|
|
48
48
|
return memoryById ?? undefined;
|
|
@@ -51,6 +51,9 @@ mock.module("../../../daemon/conversation-store.js", () => ({
|
|
|
51
51
|
findBySurfaceCalls.push(surfaceId);
|
|
52
52
|
return memoryBySurface ?? undefined;
|
|
53
53
|
},
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
mock.module("../../../daemon/conversation-store.js", () => ({
|
|
54
57
|
getOrCreateConversation: async (id: string) => {
|
|
55
58
|
getOrCreateCalls.push(id);
|
|
56
59
|
if (!rehydrated) {
|
|
@@ -183,9 +186,7 @@ describe("triggerSurfaceAction handler", () => {
|
|
|
183
186
|
// SQL must filter the messages table by ui_surface payload pattern.
|
|
184
187
|
expect(rawGetCalls[0]!.sql).toContain("FROM messages");
|
|
185
188
|
expect(rawGetCalls[0]!.sql).toContain("LIKE");
|
|
186
|
-
expect(rawGetCalls[0]!.params).toEqual([
|
|
187
|
-
`%"surfaceId":"surf-evicted"%`,
|
|
188
|
-
]);
|
|
189
|
+
expect(rawGetCalls[0]!.params).toEqual([`%"surfaceId":"surf-evicted"%`]);
|
|
189
190
|
expect(getOrCreateCalls).toEqual(["conv-from-db"]);
|
|
190
191
|
expect(rehydrated.surfaceActionCalls).toEqual([
|
|
191
192
|
{ surfaceId: "surf-evicted", actionId: "act-3", data: undefined },
|
|
@@ -51,7 +51,7 @@ const findBySurfaceCalls: string[] = [];
|
|
|
51
51
|
const getOrCreateCalls: string[] = [];
|
|
52
52
|
const rawGetCalls: Array<{ sql: string; params: unknown[] }> = [];
|
|
53
53
|
|
|
54
|
-
mock.module("../../../daemon/conversation-
|
|
54
|
+
mock.module("../../../daemon/conversation-registry.js", () => ({
|
|
55
55
|
findConversation: (id: string) => {
|
|
56
56
|
findConvCalls.push(id);
|
|
57
57
|
return memoryById ?? undefined;
|
|
@@ -60,6 +60,9 @@ mock.module("../../../daemon/conversation-store.js", () => ({
|
|
|
60
60
|
findBySurfaceCalls.push(surfaceId);
|
|
61
61
|
return undefined;
|
|
62
62
|
},
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
mock.module("../../../daemon/conversation-store.js", () => ({
|
|
63
66
|
getOrCreateConversation: async (id: string) => {
|
|
64
67
|
getOrCreateCalls.push(id);
|
|
65
68
|
if (!rehydrated) {
|