@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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for {@link getPluginCatalog}.
|
|
3
|
+
*
|
|
4
|
+
* The underlying `loadPluginCatalog` is mocked so we exercise the cache's
|
|
5
|
+
* TTL window, refresh-on-expiry, and stale-on-error fallback in isolation —
|
|
6
|
+
* no network and no dependence on the real GitHub-backed loader.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
afterEach,
|
|
11
|
+
beforeEach,
|
|
12
|
+
describe,
|
|
13
|
+
expect,
|
|
14
|
+
mock,
|
|
15
|
+
setSystemTime,
|
|
16
|
+
test,
|
|
17
|
+
} from "bun:test";
|
|
18
|
+
|
|
19
|
+
import type { PluginCatalog, SearchPluginsDeps } from "../search-plugins.js";
|
|
20
|
+
import { PluginCatalogUnavailableError } from "../search-plugins.js";
|
|
21
|
+
|
|
22
|
+
// Mock the loader the cache wraps. `loadSpy` records every call and returns
|
|
23
|
+
// (or throws) whatever the test configures. The real error class is passed
|
|
24
|
+
// through so propagation preserves its identity.
|
|
25
|
+
const loadSpy = mock(
|
|
26
|
+
async (
|
|
27
|
+
_opts: { ref: string },
|
|
28
|
+
_deps: SearchPluginsDeps,
|
|
29
|
+
): Promise<PluginCatalog> => {
|
|
30
|
+
throw new Error("loadSpy default impl not configured");
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
mock.module("../search-plugins.js", () => ({
|
|
35
|
+
PluginCatalogUnavailableError,
|
|
36
|
+
loadPluginCatalog: loadSpy,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
PLUGIN_CATALOG_CACHE_TTL_MS,
|
|
41
|
+
getPluginCatalog,
|
|
42
|
+
invalidatePluginCatalogCache,
|
|
43
|
+
} = await import("../plugin-catalog-cache.js");
|
|
44
|
+
|
|
45
|
+
const deps: SearchPluginsDeps = {
|
|
46
|
+
fetch: (async () => new Response("", { status: 200 })) as never,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// A non-zero base time. Bun treats `setSystemTime(new Date(0))` as "reset to
|
|
50
|
+
// the real clock", so the fake clock must start from a positive epoch.
|
|
51
|
+
const BASE_TIME_MS = 1_700_000_000_000;
|
|
52
|
+
|
|
53
|
+
function catalog(ref: string, names: string[]): PluginCatalog {
|
|
54
|
+
return {
|
|
55
|
+
ref,
|
|
56
|
+
matches: names.map((name) => ({
|
|
57
|
+
name,
|
|
58
|
+
path: `experimental/plugins/${name}`,
|
|
59
|
+
source: { kind: "first-party" as const },
|
|
60
|
+
})),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("getPluginCatalog", () => {
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
loadSpy.mockClear();
|
|
67
|
+
invalidatePluginCatalogCache();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
// Restore real time in case a test installed a fake clock.
|
|
72
|
+
setSystemTime();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("loads once and serves the cached catalog within the TTL window", async () => {
|
|
76
|
+
// GIVEN a loader that returns a catalog
|
|
77
|
+
loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["a"]));
|
|
78
|
+
|
|
79
|
+
// WHEN we request the same ref twice in quick succession
|
|
80
|
+
const first = await getPluginCatalog("main", deps);
|
|
81
|
+
const second = await getPluginCatalog("main", deps);
|
|
82
|
+
|
|
83
|
+
// THEN the loader is consulted exactly once — the second call is cached
|
|
84
|
+
expect(loadSpy).toHaveBeenCalledTimes(1);
|
|
85
|
+
expect(first).toBe(second);
|
|
86
|
+
expect(first.matches.map((m) => m.name)).toEqual(["a"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("caches per ref independently", async () => {
|
|
90
|
+
// GIVEN a loader that echoes the requested ref
|
|
91
|
+
loadSpy.mockImplementation(async ({ ref }) => catalog(ref, [ref]));
|
|
92
|
+
|
|
93
|
+
// WHEN we request two different refs
|
|
94
|
+
const main = await getPluginCatalog("main", deps);
|
|
95
|
+
const branch = await getPluginCatalog("feature", deps);
|
|
96
|
+
|
|
97
|
+
// THEN each ref triggers its own load and is keyed separately
|
|
98
|
+
expect(loadSpy).toHaveBeenCalledTimes(2);
|
|
99
|
+
expect(main.ref).toBe("main");
|
|
100
|
+
expect(branch.ref).toBe("feature");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("refreshes after the TTL elapses", async () => {
|
|
104
|
+
// GIVEN a fresh load at t0
|
|
105
|
+
setSystemTime(new Date(BASE_TIME_MS));
|
|
106
|
+
loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["v1"]));
|
|
107
|
+
await getPluginCatalog("main", deps);
|
|
108
|
+
|
|
109
|
+
// AND the loader now returns updated data
|
|
110
|
+
loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["v2"]));
|
|
111
|
+
|
|
112
|
+
// WHEN the TTL has elapsed and we request again
|
|
113
|
+
setSystemTime(new Date(BASE_TIME_MS + PLUGIN_CATALOG_CACHE_TTL_MS + 1));
|
|
114
|
+
const refreshed = await getPluginCatalog("main", deps);
|
|
115
|
+
|
|
116
|
+
// THEN the loader is consulted again and the new catalog is returned
|
|
117
|
+
expect(loadSpy).toHaveBeenCalledTimes(2);
|
|
118
|
+
expect(refreshed.matches.map((m) => m.name)).toEqual(["v2"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("serves the stale catalog when a refresh fails", async () => {
|
|
122
|
+
// GIVEN a good catalog cached at t0
|
|
123
|
+
setSystemTime(new Date(BASE_TIME_MS));
|
|
124
|
+
loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["good"]));
|
|
125
|
+
const fresh = await getPluginCatalog("main", deps);
|
|
126
|
+
|
|
127
|
+
// AND the upstream later fails (rate-limited)
|
|
128
|
+
loadSpy.mockImplementation(async () => {
|
|
129
|
+
throw new PluginCatalogUnavailableError("rate limited", 403);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// WHEN the TTL has elapsed and the refresh fails
|
|
133
|
+
setSystemTime(new Date(BASE_TIME_MS + PLUGIN_CATALOG_CACHE_TTL_MS + 1));
|
|
134
|
+
const stale = await getPluginCatalog("main", deps);
|
|
135
|
+
|
|
136
|
+
// THEN the last good catalog is served instead of throwing
|
|
137
|
+
expect(stale).toBe(fresh);
|
|
138
|
+
expect(stale.matches.map((m) => m.name)).toEqual(["good"]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("resets the TTL window after serving stale so it doesn't re-fetch every call", async () => {
|
|
142
|
+
// GIVEN a cached catalog and a now-failing upstream
|
|
143
|
+
setSystemTime(new Date(BASE_TIME_MS));
|
|
144
|
+
loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["good"]));
|
|
145
|
+
await getPluginCatalog("main", deps);
|
|
146
|
+
loadSpy.mockImplementation(async () => {
|
|
147
|
+
throw new PluginCatalogUnavailableError("rate limited", 403);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// WHEN the TTL elapses (one failed refresh) and we immediately ask again
|
|
151
|
+
setSystemTime(new Date(BASE_TIME_MS + PLUGIN_CATALOG_CACHE_TTL_MS + 1));
|
|
152
|
+
await getPluginCatalog("main", deps);
|
|
153
|
+
const callsAfterFirstStale = loadSpy.mock.calls.length;
|
|
154
|
+
await getPluginCatalog("main", deps);
|
|
155
|
+
|
|
156
|
+
// THEN the second request is served from the reset window without
|
|
157
|
+
// re-entering the failing loader
|
|
158
|
+
expect(loadSpy.mock.calls.length).toBe(callsAfterFirstStale);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("propagates a hard error even with a cache, and does not serve stale", async () => {
|
|
162
|
+
// GIVEN a good catalog cached at t0
|
|
163
|
+
setSystemTime(new Date(BASE_TIME_MS));
|
|
164
|
+
loadSpy.mockImplementation(async ({ ref }) => catalog(ref, ["good"]));
|
|
165
|
+
await getPluginCatalog("main", deps);
|
|
166
|
+
|
|
167
|
+
// AND the upstream later fails with a HARD error (e.g. the prefix is
|
|
168
|
+
// gone), NOT a transient PluginCatalogUnavailableError
|
|
169
|
+
loadSpy.mockImplementation(async () => {
|
|
170
|
+
throw new Error("GitHub contents listing failed: HTTP 404");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// WHEN the TTL has elapsed and the refresh hard-fails
|
|
174
|
+
setSystemTime(new Date(BASE_TIME_MS + PLUGIN_CATALOG_CACHE_TTL_MS + 1));
|
|
175
|
+
const err = await getPluginCatalog("main", deps).catch((e: unknown) => e);
|
|
176
|
+
|
|
177
|
+
// THEN the hard error propagates instead of masking the misconfiguration
|
|
178
|
+
// with stale data
|
|
179
|
+
expect(err).toBeInstanceOf(Error);
|
|
180
|
+
expect(err).not.toBeInstanceOf(PluginCatalogUnavailableError);
|
|
181
|
+
expect((err as Error).message).toMatch(/HTTP 404/);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("propagates the error when there is no cache to fall back on", async () => {
|
|
185
|
+
// GIVEN no prior successful load and a failing upstream
|
|
186
|
+
loadSpy.mockImplementation(async () => {
|
|
187
|
+
throw new PluginCatalogUnavailableError("rate limited", 403);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// WHEN we request a cold catalog
|
|
191
|
+
const err = await getPluginCatalog("main", deps).catch((e: unknown) => e);
|
|
192
|
+
|
|
193
|
+
// THEN the failure propagates so the caller can map it to 503
|
|
194
|
+
expect(err).toBeInstanceOf(PluginCatalogUnavailableError);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for {@link getPluginDetails}.
|
|
3
|
+
*
|
|
4
|
+
* Network is replaced with an in-memory GitHub Contents API fixture passed via
|
|
5
|
+
* the `fetch` dependency, and the installed-copy path is exercised against a
|
|
6
|
+
* real temp directory passed via `workspacePluginsDir` — no globals are
|
|
7
|
+
* monkey-patched. The fixture answers three URL shapes:
|
|
8
|
+
* - the marketplace manifest file (raw JSON body),
|
|
9
|
+
* - directory listings (`/contents/<path>` → entry array or 404),
|
|
10
|
+
* - raw file downloads (a listing entry's `download_url`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
17
|
+
|
|
18
|
+
import type { FetchLike } from "../install-from-github.js";
|
|
19
|
+
import {
|
|
20
|
+
getPluginDetails,
|
|
21
|
+
PluginDetailsNotFoundError,
|
|
22
|
+
} from "../plugin-details.js";
|
|
23
|
+
|
|
24
|
+
interface ContentEntry {
|
|
25
|
+
name: string;
|
|
26
|
+
path: string;
|
|
27
|
+
type: "file" | "dir" | "symlink" | "submodule";
|
|
28
|
+
download_url: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function fileEntry(name: string, downloadUrl: string): ContentEntry {
|
|
32
|
+
return { name, path: name, type: "file", download_url: downloadUrl };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface FixtureConfig {
|
|
36
|
+
/** Manifest object served at `experimental/plugins/marketplace.json`. Omit for 404. */
|
|
37
|
+
marketplace?: unknown;
|
|
38
|
+
/** Directory listings keyed by `<owner>/<repo>[/<path>]`. Missing key → 404. */
|
|
39
|
+
listings?: Record<string, ContentEntry[]>;
|
|
40
|
+
/** Raw file bodies keyed by `download_url`. Missing key → 404. */
|
|
41
|
+
raw?: Record<string, string>;
|
|
42
|
+
/** URL substrings that should reject with a network error. */
|
|
43
|
+
failOn?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a `fetch` that routes GitHub Contents API requests against in-memory
|
|
48
|
+
* fixtures. Anything unrecognised returns 500 so test bugs surface loudly.
|
|
49
|
+
*/
|
|
50
|
+
function makeFetch(config: FixtureConfig): FetchLike {
|
|
51
|
+
return (async (input: RequestInfo | URL) => {
|
|
52
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
53
|
+
|
|
54
|
+
for (const needle of config.failOn ?? []) {
|
|
55
|
+
if (url.includes(needle)) throw new Error(`network down: ${needle}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (url.includes("marketplace.json")) {
|
|
59
|
+
if (config.marketplace === undefined) {
|
|
60
|
+
return new Response("not found", { status: 404 });
|
|
61
|
+
}
|
|
62
|
+
return new Response(JSON.stringify(config.marketplace), { status: 200 });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (config.raw && url in config.raw) {
|
|
66
|
+
return new Response(config.raw[url], { status: 200 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (url.includes("/contents")) {
|
|
70
|
+
const key = listingKey(url);
|
|
71
|
+
const entries = config.listings?.[key];
|
|
72
|
+
if (!entries) return new Response("not found", { status: 404 });
|
|
73
|
+
return new Response(JSON.stringify(entries), {
|
|
74
|
+
status: 200,
|
|
75
|
+
headers: { "content-type": "application/json" },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new Response("unexpected url: " + url, { status: 500 });
|
|
80
|
+
}) as FetchLike;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Derive the `<owner>/<repo>[/<path>]` listing key from a contents URL. */
|
|
84
|
+
function listingKey(url: string): string {
|
|
85
|
+
const afterRepos = url.split("/repos/")[1] ?? "";
|
|
86
|
+
const [ownerRepo, rest = ""] = splitOnce(afterRepos, "/contents");
|
|
87
|
+
const pathPart = rest.split("?")[0] ?? ""; // leading "/" or ""
|
|
88
|
+
return `${ownerRepo}${pathPart}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function splitOnce(s: string, sep: string): [string, string] {
|
|
92
|
+
const i = s.indexOf(sep);
|
|
93
|
+
if (i === -1) return [s, ""];
|
|
94
|
+
return [s.slice(0, i), s.slice(i + sep.length)];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let workspace: string;
|
|
98
|
+
|
|
99
|
+
beforeEach(() => {
|
|
100
|
+
workspace = mkdtempSync(join(tmpdir(), "plugin-details-"));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
afterEach(() => {
|
|
104
|
+
rmSync(workspace, { recursive: true, force: true });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("getPluginDetails", () => {
|
|
108
|
+
test("resolves an external plugin: manifest metadata + repo README/package.json", async () => {
|
|
109
|
+
// GIVEN a marketplace entry for an external, not-installed plugin
|
|
110
|
+
const fetch = makeFetch({
|
|
111
|
+
marketplace: {
|
|
112
|
+
name: "vellum",
|
|
113
|
+
plugins: [
|
|
114
|
+
{
|
|
115
|
+
name: "caveman",
|
|
116
|
+
source: {
|
|
117
|
+
source: "github",
|
|
118
|
+
repo: "example-org/caveman",
|
|
119
|
+
ref: "v1",
|
|
120
|
+
},
|
|
121
|
+
description: "manifest description",
|
|
122
|
+
homepage: "https://example.com/caveman",
|
|
123
|
+
license: "MIT",
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
// AND no first-party directory for the name (404), but the external repo
|
|
128
|
+
// root lists a README and package.json
|
|
129
|
+
listings: {
|
|
130
|
+
"example-org/caveman": [
|
|
131
|
+
fileEntry("README.md", "raw://caveman/readme"),
|
|
132
|
+
fileEntry("package.json", "raw://caveman/pkg"),
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
raw: {
|
|
136
|
+
"raw://caveman/readme": "# Caveman\n\nGrug brain plugin.",
|
|
137
|
+
"raw://caveman/pkg": JSON.stringify({
|
|
138
|
+
version: "1.8.2",
|
|
139
|
+
description: "package description",
|
|
140
|
+
homepage: "https://pkg.example.com",
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// WHEN we resolve the detail view at a ref
|
|
146
|
+
const details = await getPluginDetails(
|
|
147
|
+
{ name: "caveman", ref: "v1" },
|
|
148
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// THEN the source is the external repo and the README comes from it
|
|
152
|
+
expect(details.source).toEqual({
|
|
153
|
+
kind: "github",
|
|
154
|
+
repo: "example-org/caveman",
|
|
155
|
+
ref: "v1",
|
|
156
|
+
});
|
|
157
|
+
expect(details.installed).toBe(false);
|
|
158
|
+
expect(details.readme).toContain("Grug brain plugin");
|
|
159
|
+
// AND manifest fields win over the repo package.json for description/homepage
|
|
160
|
+
expect(details.description).toBe("manifest description");
|
|
161
|
+
expect(details.homepage).toBe("https://example.com/caveman");
|
|
162
|
+
expect(details.license).toBe("MIT");
|
|
163
|
+
// AND version falls back to the repo package.json (manifest has none)
|
|
164
|
+
expect(details.version).toBe("1.8.2");
|
|
165
|
+
expect(details.ref).toBe("v1");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("reads an external plugin at its pinned source ref, not the catalog ref", async () => {
|
|
169
|
+
// GIVEN a marketplace entry pinned to a ref that differs from the catalog
|
|
170
|
+
// ref the detail view is resolved at
|
|
171
|
+
const marketplace = {
|
|
172
|
+
name: "vellum",
|
|
173
|
+
plugins: [
|
|
174
|
+
{
|
|
175
|
+
name: "caveman",
|
|
176
|
+
source: {
|
|
177
|
+
source: "github",
|
|
178
|
+
repo: "example-org/caveman",
|
|
179
|
+
ref: "v1.8.2",
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
// AND a fetch that records the ref query param of every contents request
|
|
185
|
+
const contentsRefs = new Map<string, string>();
|
|
186
|
+
const fetch = (async (input: RequestInfo | URL) => {
|
|
187
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
188
|
+
if (url.includes("marketplace.json")) {
|
|
189
|
+
return new Response(JSON.stringify(marketplace), { status: 200 });
|
|
190
|
+
}
|
|
191
|
+
if (url.includes("/contents")) {
|
|
192
|
+
const ref = new URL(url).searchParams.get("ref") ?? "";
|
|
193
|
+
const key = url.includes("example-org/caveman")
|
|
194
|
+
? "external"
|
|
195
|
+
: "first-party";
|
|
196
|
+
contentsRefs.set(key, ref);
|
|
197
|
+
if (key === "external") {
|
|
198
|
+
return new Response(
|
|
199
|
+
JSON.stringify([fileEntry("README.md", "raw://caveman/readme")]),
|
|
200
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return new Response("not found", { status: 404 });
|
|
204
|
+
}
|
|
205
|
+
if (url === "raw://caveman/readme") {
|
|
206
|
+
return new Response("# Caveman", { status: 200 });
|
|
207
|
+
}
|
|
208
|
+
return new Response("unexpected url: " + url, { status: 500 });
|
|
209
|
+
}) as FetchLike;
|
|
210
|
+
|
|
211
|
+
// WHEN we resolve the detail view at the catalog ref `main`
|
|
212
|
+
const details = await getPluginDetails(
|
|
213
|
+
{ name: "caveman", ref: "main" },
|
|
214
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
// THEN the external repo is read at the plugin's pinned ref, not `main`
|
|
218
|
+
expect(contentsRefs.get("external")).toBe("v1.8.2");
|
|
219
|
+
// AND the first-party collision probe still uses the catalog ref
|
|
220
|
+
expect(contentsRefs.get("first-party")).toBe("main");
|
|
221
|
+
// AND the README from the pinned ref is surfaced
|
|
222
|
+
expect(details.readme).toContain("Caveman");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("first-party directory wins a name also claimed by the marketplace", async () => {
|
|
226
|
+
// GIVEN both a marketplace entry AND a first-party in-repo directory for
|
|
227
|
+
// the same name
|
|
228
|
+
const fetch = makeFetch({
|
|
229
|
+
marketplace: {
|
|
230
|
+
name: "vellum",
|
|
231
|
+
plugins: [
|
|
232
|
+
{
|
|
233
|
+
name: "simple-memory",
|
|
234
|
+
source: {
|
|
235
|
+
source: "github",
|
|
236
|
+
repo: "example-org/simple-memory",
|
|
237
|
+
ref: "v9",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
listings: {
|
|
243
|
+
"vellum-ai/vellum-assistant/experimental/plugins/simple-memory": [
|
|
244
|
+
fileEntry("README.md", "raw://fp/readme"),
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
raw: { "raw://fp/readme": "# Simple Memory (first-party)" },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// WHEN we resolve the detail view
|
|
251
|
+
const details = await getPluginDetails(
|
|
252
|
+
{ name: "simple-memory" },
|
|
253
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// THEN the first-party source is used, not the external one
|
|
257
|
+
expect(details.source).toEqual({ kind: "first-party" });
|
|
258
|
+
expect(details.readme).toContain("first-party");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("prefers an installed copy's README and package.json over the repo", async () => {
|
|
262
|
+
// GIVEN an installed copy on disk with its own README + package.json
|
|
263
|
+
const target = join(workspace, "caveman");
|
|
264
|
+
mkdirSync(target, { recursive: true });
|
|
265
|
+
writeFileSync(join(target, "README.md"), "# Installed Caveman");
|
|
266
|
+
writeFileSync(
|
|
267
|
+
join(target, "package.json"),
|
|
268
|
+
JSON.stringify({ version: "2.0.0", license: "Apache-2.0" }),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// AND a marketplace entry + external repo that would otherwise be used
|
|
272
|
+
const fetch = makeFetch({
|
|
273
|
+
marketplace: {
|
|
274
|
+
name: "vellum",
|
|
275
|
+
plugins: [
|
|
276
|
+
{
|
|
277
|
+
name: "caveman",
|
|
278
|
+
source: {
|
|
279
|
+
source: "github",
|
|
280
|
+
repo: "example-org/caveman",
|
|
281
|
+
ref: "v1",
|
|
282
|
+
},
|
|
283
|
+
description: "manifest description",
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
},
|
|
287
|
+
listings: {
|
|
288
|
+
"example-org/caveman": [fileEntry("README.md", "raw://caveman/readme")],
|
|
289
|
+
},
|
|
290
|
+
raw: { "raw://caveman/readme": "# Remote Caveman" },
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// WHEN we resolve the detail view
|
|
294
|
+
const details = await getPluginDetails(
|
|
295
|
+
{ name: "caveman" },
|
|
296
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
// THEN the installed README + version/license win; manifest fills the gap
|
|
300
|
+
expect(details.installed).toBe(true);
|
|
301
|
+
expect(details.readme).toBe("# Installed Caveman");
|
|
302
|
+
expect(details.version).toBe("2.0.0");
|
|
303
|
+
expect(details.license).toBe("Apache-2.0");
|
|
304
|
+
expect(details.description).toBe("manifest description");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("throws PluginDetailsNotFoundError when nothing claims the name", async () => {
|
|
308
|
+
// GIVEN no install, no first-party directory, and an empty marketplace
|
|
309
|
+
const fetch = makeFetch({
|
|
310
|
+
marketplace: { name: "vellum", plugins: [] },
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// WHEN / THEN resolving an unknown name rejects with the not-found error
|
|
314
|
+
await expect(
|
|
315
|
+
getPluginDetails(
|
|
316
|
+
{ name: "ghost" },
|
|
317
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
318
|
+
),
|
|
319
|
+
).rejects.toBeInstanceOf(PluginDetailsNotFoundError);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("degrades to manifest metadata when the repo listing fails", async () => {
|
|
323
|
+
// GIVEN a marketplace entry whose external repo listing errors out
|
|
324
|
+
const fetch = makeFetch({
|
|
325
|
+
marketplace: {
|
|
326
|
+
name: "vellum",
|
|
327
|
+
plugins: [
|
|
328
|
+
{
|
|
329
|
+
name: "caveman",
|
|
330
|
+
source: {
|
|
331
|
+
source: "github",
|
|
332
|
+
repo: "example-org/caveman",
|
|
333
|
+
ref: "v1",
|
|
334
|
+
},
|
|
335
|
+
description: "manifest description",
|
|
336
|
+
license: "MIT",
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
failOn: ["example-org/caveman"],
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// WHEN we resolve the detail view
|
|
344
|
+
const details = await getPluginDetails(
|
|
345
|
+
{ name: "caveman", ref: "v1" },
|
|
346
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// THEN it still renders manifest metadata with a null README rather than throwing
|
|
350
|
+
expect(details.readme).toBeNull();
|
|
351
|
+
expect(details.description).toBe("manifest description");
|
|
352
|
+
expect(details.license).toBe("MIT");
|
|
353
|
+
expect(details.source).toEqual({
|
|
354
|
+
kind: "github",
|
|
355
|
+
repo: "example-org/caveman",
|
|
356
|
+
ref: "v1",
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("rejects an invalid (path-traversal) plugin name", async () => {
|
|
361
|
+
// GIVEN a name that fails the install-name sanitizer
|
|
362
|
+
const fetch = makeFetch({ marketplace: { name: "vellum", plugins: [] } });
|
|
363
|
+
|
|
364
|
+
// WHEN / THEN resolution throws before any lookup
|
|
365
|
+
await expect(
|
|
366
|
+
getPluginDetails(
|
|
367
|
+
{ name: "../escape" },
|
|
368
|
+
{ fetch, workspacePluginsDir: workspace },
|
|
369
|
+
),
|
|
370
|
+
).rejects.toThrow();
|
|
371
|
+
});
|
|
372
|
+
});
|