@vellumai/assistant 0.8.10 → 0.8.11-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +62 -1
- package/docs/workspace-tools.md +196 -0
- package/examples/plugins/echo/README.md +3 -3
- package/knip.json +1 -0
- package/openapi.yaml +460 -128
- package/package.json +2 -1
- package/scripts/build-plugin-api.ts +299 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
- package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
- package/src/__tests__/agent-loop-thinking.test.ts +4 -0
- package/src/__tests__/agent-loop.test.ts +578 -5
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
- package/src/__tests__/approval-cascade.test.ts +1 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
- package/src/__tests__/btw-routes.test.ts +0 -1
- package/src/__tests__/build-persisted-content.test.ts +75 -1
- package/src/__tests__/catalog-install-normalize.test.ts +141 -0
- package/src/__tests__/ces-startup-timeout.test.ts +60 -0
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +264 -124
- package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
- package/src/__tests__/conversation-agent-loop.test.ts +131 -551
- package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
- package/src/__tests__/conversation-fork-crud.test.ts +259 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-lifecycle.test.ts +257 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
- package/src/__tests__/conversation-queue.test.ts +1 -39
- package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
- package/src/__tests__/conversation-skill-tools.test.ts +491 -5
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +1 -0
- package/src/__tests__/conversation-store.test.ts +74 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
- package/src/__tests__/credential-execution-tools.test.ts +23 -11
- package/src/__tests__/credential-security-invariants.test.ts +6 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
- package/src/__tests__/device-id.test.ts +70 -1
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- package/src/__tests__/empty-response-hook.test.ts +242 -66
- package/src/__tests__/external-plugin-loader.test.ts +0 -31
- package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
- package/src/__tests__/history-repair-hook.test.ts +228 -3
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-browser-proxy.test.ts +254 -9
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/image-recovery-hook.test.ts +387 -0
- package/src/__tests__/injector-chain.test.ts +5 -4
- package/src/__tests__/injector-v3-suppression.test.ts +373 -47
- package/src/__tests__/intent-routing.test.ts +7 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/oauth-store.test.ts +0 -85
- package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
- package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
- package/src/__tests__/persist-unsendable-image.test.ts +4 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-bootstrap.test.ts +82 -73
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -8
- package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
- package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
- package/src/__tests__/registry.test.ts +240 -1
- package/src/__tests__/require-fresh-approval.test.ts +3 -0
- package/src/__tests__/schedule-routes.test.ts +116 -1
- package/src/__tests__/schedule-store.test.ts +28 -0
- package/src/__tests__/schedule-tools.test.ts +94 -1
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
- package/src/__tests__/skills.test.ts +202 -0
- package/src/__tests__/slim-skill-category.test.ts +195 -0
- package/src/__tests__/strip-memory-injections.test.ts +33 -39
- package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
- package/src/__tests__/title-generate-hook.test.ts +9 -7
- package/src/__tests__/tool-audit-listener.test.ts +264 -1
- package/src/__tests__/tool-error-hook.test.ts +4 -3
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/tool-start-timestamp.test.ts +218 -0
- package/src/__tests__/tools-get-route.test.ts +202 -0
- package/src/__tests__/workspace-tool-loader.test.ts +319 -0
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
- package/src/agent/loop.ts +569 -319
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/index.ts +10 -0
- package/src/api/responses/conversation-message.ts +135 -27
- package/src/api/responses/memory-v3-selection-log.ts +4 -4
- package/src/approvals/guardian-request-resolvers.ts +26 -0
- package/src/browser-session/backends/host-bridge.ts +29 -0
- package/src/browser-session/index.ts +1 -0
- package/src/browser-session/types.ts +5 -1
- package/src/cli/commands/__tests__/schedules.test.ts +62 -4
- package/src/cli/commands/__tests__/skills.test.ts +53 -0
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/inference-providers.ts +0 -8
- package/src/cli/commands/plugins.ts +2 -2
- package/src/cli/commands/schedules.ts +27 -4
- package/src/cli/commands/skills.ts +187 -146
- package/src/cli/commands/tools.ts +106 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
- package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
- package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
- package/src/cli/lib/install-from-github.ts +187 -117
- package/src/cli/lib/plugin-catalog-cache.ts +9 -9
- package/src/cli/lib/plugin-details.ts +38 -68
- package/src/cli/lib/plugin-marketplace.ts +42 -14
- package/src/cli/lib/search-plugins.ts +29 -129
- package/src/cli/program.ts +2 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
- package/src/config/bundled-skills/app-control/SKILL.md +1 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
- package/src/config/bundled-skills/contacts/SKILL.md +1 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
- package/src/config/bundled-skills/followups/SKILL.md +1 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
- package/src/config/bundled-skills/messaging/SKILL.md +1 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
- package/src/config/bundled-skills/sequences/SKILL.md +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +1 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
- package/src/config/bundled-skills/subagent/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
- package/src/config/env-registry.ts +23 -0
- package/src/config/feature-flag-registry.json +17 -81
- package/src/config/loader.ts +5 -22
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
- package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
- package/src/config/schemas/compaction-logs.ts +79 -0
- package/src/config/schemas/memory-v2.ts +0 -8
- package/src/config/schemas/memory-v3.ts +104 -33
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/config/skills.ts +117 -47
- package/src/context/compactor.ts +11 -0
- package/src/context/strip-injections.ts +38 -4
- package/src/credential-execution/feature-gates.ts +0 -21
- package/src/credential-execution/startup-timeout.ts +32 -4
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
- package/src/daemon/conversation-agent-loop.ts +123 -658
- package/src/daemon/conversation-error.ts +6 -33
- package/src/daemon/conversation-lifecycle.ts +1 -1
- package/src/daemon/conversation-runtime-assembly.ts +183 -22
- package/src/daemon/conversation-skill-tools.ts +137 -8
- package/src/daemon/conversation-slash.ts +0 -14
- package/src/daemon/conversation-store.ts +2 -19
- package/src/daemon/conversation-tool-setup.ts +87 -1
- package/src/daemon/conversation.ts +119 -50
- package/src/daemon/external-plugins-bootstrap.ts +36 -95
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +10 -4
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-browser-proxy.ts +117 -22
- package/src/daemon/lifecycle.ts +34 -5
- package/src/daemon/message-protocol.ts +0 -7
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/skills.ts +17 -0
- package/src/daemon/providers-setup.ts +3 -0
- package/src/daemon/server.ts +3 -3
- package/src/daemon/tool-setup-types.ts +9 -3
- package/src/daemon/trust-context.ts +23 -0
- package/src/daemon/workspace-tools-watcher.ts +324 -0
- package/src/events/tool-audit-listener.ts +78 -16
- package/src/events/tool-metrics-listener.ts +2 -5
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
- package/src/memory/__tests__/conversation-queries.test.ts +176 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
- package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
- package/src/memory/conversation-crud.ts +134 -3
- package/src/memory/conversation-queries.ts +64 -7
- package/src/memory/db-init.ts +14 -0
- package/src/memory/embedding-backend.test.ts +130 -1
- package/src/memory/embedding-backend.ts +79 -106
- package/src/memory/embedding-gemini.ts +5 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
- package/src/memory/graph/conversation-graph-memory.ts +36 -25
- package/src/memory/graph/tool-handlers.ts +3 -0
- package/src/memory/job-handlers/cleanup.ts +3 -1
- package/src/memory/jobs-store.ts +0 -28
- package/src/memory/jobs-worker.ts +10 -22
- package/src/memory/memory-marker.ts +29 -0
- package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
- package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
- package/src/memory/migrations/270-schedule-description.ts +36 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
- package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
- package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
- package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
- package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
- package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +16 -0
- package/src/memory/schema/infrastructure.ts +26 -0
- package/src/memory/skill-loaded-events-store.test.ts +160 -0
- package/src/memory/skill-loaded-events-store.ts +95 -0
- package/src/memory/tool-executed-events-store.test.ts +219 -0
- package/src/memory/tool-executed-events-store.ts +102 -0
- package/src/memory/tool-usage-store.ts +15 -3
- package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
- package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
- package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
- package/src/memory/v2/__tests__/page-store.test.ts +33 -0
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
- package/src/memory/v2/activation-store.ts +50 -1
- package/src/memory/v2/consolidation-job.ts +113 -29
- package/src/memory/v2/injected-block-slugs.ts +79 -0
- package/src/memory/v2/injection.ts +6 -1
- package/src/memory/v2/prompts/consolidation.ts +414 -13
- package/src/memory/v2/router.ts +2 -28
- package/src/memory/v2/static-context.ts +1 -1
- package/src/memory/v2/types.ts +16 -0
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +2 -1
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/oauth/oauth-store.ts +0 -9
- package/src/permissions/checker.test.ts +83 -1
- package/src/permissions/checker.ts +25 -2
- package/src/permissions/gateway-threshold-reader.test.ts +182 -0
- package/src/permissions/gateway-threshold-reader.ts +80 -0
- package/src/platform/client.ts +1 -3
- package/src/platform/feature-gate.ts +3 -12
- package/src/plugin-api/constants.ts +4 -2
- package/src/plugin-api/index.ts +63 -11
- package/src/plugin-api/types.ts +236 -71
- package/src/plugins/defaults/compaction/compact.ts +66 -2
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +303 -1
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
- package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
- package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
- package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
- package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
- package/src/plugins/defaults/history-repair/terminal.ts +39 -2
- package/src/plugins/defaults/image-recovery/detect.ts +25 -0
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
- package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
- package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
- package/src/plugins/defaults/image-recovery/package.json +14 -0
- package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
- package/src/plugins/defaults/index.ts +71 -5
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
- package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
- package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
- package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
- package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
- package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
- package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
- package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
- package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
- package/src/plugins/pipeline.ts +8 -5
- package/src/plugins/types.ts +5 -51
- package/src/providers/cache-control.ts +26 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
- package/src/providers/model-catalog.ts +13 -1
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
- package/src/providers/openai/chat-completions-provider.ts +46 -0
- package/src/providers/openai/responses-provider.ts +45 -0
- package/src/providers/registry.ts +0 -8
- package/src/runtime/__tests__/agent-wake.test.ts +0 -1
- package/src/runtime/agent-wake.ts +13 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
- package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
- package/src/runtime/routes/acp-routes.test.ts +106 -0
- package/src/runtime/routes/acp-routes.ts +248 -2
- package/src/runtime/routes/browser-tabs-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
- package/src/runtime/routes/consolidation-routes.ts +6 -82
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +71 -0
- package/src/runtime/routes/identity-routes.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
- package/src/runtime/routes/plugins-routes.ts +34 -45
- package/src/runtime/routes/schedule-routes.ts +43 -5
- package/src/runtime/routes/settings-routes.ts +140 -15
- package/src/runtime/routes/skills-routes.ts +18 -6
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
- package/src/runtime/services/conversation-serializer.ts +38 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/schedule/schedule-store.ts +19 -0
- package/src/skills/catalog-install.ts +77 -13
- package/src/tasks/task-scheduler.ts +1 -0
- package/src/telemetry/types.ts +66 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
- package/src/telemetry/usage-telemetry-reporter.ts +213 -20
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
- package/src/tools/browser/browser-execution.ts +27 -9
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
- package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
- package/src/tools/browser/cdp-client/factory.ts +217 -17
- package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
- package/src/tools/browser/cdp-client/types.ts +22 -2
- package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
- package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
- package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
- package/src/tools/executor.ts +39 -7
- package/src/tools/registry.ts +387 -5
- package/src/tools/schedule/create.ts +16 -0
- package/src/tools/schedule/list.ts +12 -4
- package/src/tools/schedule/update.ts +12 -0
- package/src/tools/skills/load.ts +11 -6
- package/src/tools/terminal/safe-env.ts +2 -0
- package/src/tools/types.ts +65 -9
- package/src/tools/workspace-tools/loader.ts +673 -0
- package/src/usage/attribution.ts +28 -0
- package/src/util/device-id.ts +17 -3
- package/src/util/platform.ts +16 -0
- package/tsconfig.plugin-api.json +13 -0
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
- package/src/daemon/message-types/browser.ts +0 -10
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
- package/src/plugins/external-api.ts +0 -114
- package/src/plugins/plugin-skill-contributions.ts +0 -292
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for {@link installPlugin}.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* No globals are monkey-patched and no `--test-hook` exports leak into
|
|
11
|
-
* production code.
|
|
4
|
+
* Whitelisted external plugins are shallow-cloned with `git` (replaced by a
|
|
5
|
+
* fake {@link GitRunner} that materializes a tree into the clone dir) and,
|
|
6
|
+
* when a curated adapter stub exists for the name, the stub is overlaid from
|
|
7
|
+
* the GitHub Contents API (replaced by an in-memory fixture passed via the
|
|
8
|
+
* `fetch` dependency). No globals are monkey-patched and no `--test-hook`
|
|
9
|
+
* exports leak into production code.
|
|
12
10
|
*/
|
|
13
11
|
|
|
14
12
|
import {
|
|
@@ -45,13 +43,13 @@ const DOWNLOAD_HOST = "https://files.test/";
|
|
|
45
43
|
* Build a `fetch` that serves the GitHub Contents API from an in-memory tree.
|
|
46
44
|
*
|
|
47
45
|
* `tree` is keyed by full canonical-repo path (e.g.
|
|
48
|
-
* `
|
|
46
|
+
* `plugins/caveman/package.json`) and maps each entry to a
|
|
49
47
|
* file's content (`string`/`Uint8Array`) or `null` for an explicit directory.
|
|
50
48
|
* Directory listings are derived from the key set; file `download_url`s point
|
|
51
49
|
* at {@link DOWNLOAD_HOST} and are served with the stored bytes.
|
|
52
50
|
*
|
|
53
51
|
* The marketplace manifest lookup is answered with `manifest` (or a 404 when
|
|
54
|
-
* omitted, so
|
|
52
|
+
* omitted, so the name resolves to no installable source).
|
|
55
53
|
*/
|
|
56
54
|
function makeContentsFetch(opts: {
|
|
57
55
|
tree: Record<string, Uint8Array | string | null>;
|
|
@@ -60,11 +58,11 @@ function makeContentsFetch(opts: {
|
|
|
60
58
|
* HTTP status to answer the manifest fetch with. Defaults to 200 (when
|
|
61
59
|
* `manifest` is provided) or 404 (when omitted). Set to a transient status
|
|
62
60
|
* (e.g. 500) to simulate a marketplace lookup that fails rather than being
|
|
63
|
-
* absent
|
|
61
|
+
* absent.
|
|
64
62
|
*/
|
|
65
63
|
manifestStatus?: number;
|
|
66
64
|
}): FetchLike {
|
|
67
|
-
const MANIFEST_URL = `https://api.github.com/repos/${CANON_REPO}/contents/
|
|
65
|
+
const MANIFEST_URL = `https://api.github.com/repos/${CANON_REPO}/contents/plugins/marketplace.json`;
|
|
68
66
|
const CONTENTS = `https://api.github.com/repos/${CANON_REPO}/contents/`;
|
|
69
67
|
const { tree } = opts;
|
|
70
68
|
|
|
@@ -136,17 +134,6 @@ function makeContentsFetch(opts: {
|
|
|
136
134
|
}) as FetchLike;
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
/** First-party fixture: keys are relative to `experimental/plugins/`. */
|
|
140
|
-
function fixtureFetch(
|
|
141
|
-
tree: Record<string, Uint8Array | string | null>,
|
|
142
|
-
): FetchLike {
|
|
143
|
-
const full: Record<string, Uint8Array | string | null> = {};
|
|
144
|
-
for (const [key, value] of Object.entries(tree)) {
|
|
145
|
-
full[`experimental/plugins/${key}`] = value;
|
|
146
|
-
}
|
|
147
|
-
return makeContentsFetch({ tree: full });
|
|
148
|
-
}
|
|
149
|
-
|
|
150
137
|
/**
|
|
151
138
|
* Build a fake {@link GitRunner} that simulates a shallow clone.
|
|
152
139
|
*
|
|
@@ -191,21 +178,19 @@ function fakeGitRunner(opts: {
|
|
|
191
178
|
|
|
192
179
|
/** A git runner that fails the test if any git command is invoked. */
|
|
193
180
|
const unusedGitRunner: GitRunner = async (args) => {
|
|
194
|
-
throw new Error(
|
|
195
|
-
`git should not run for a first-party install: ${args.join(" ")}`,
|
|
196
|
-
);
|
|
181
|
+
throw new Error(`git should not run for this install: ${args.join(" ")}`);
|
|
197
182
|
};
|
|
198
183
|
|
|
199
184
|
/**
|
|
200
185
|
* Read the real, committed caveman adapter stub from the repo so the
|
|
201
186
|
* integration test exercises the adapter that ships rather than a fixture
|
|
202
187
|
* copy that could drift from it. Returns every stub file keyed by its
|
|
203
|
-
* Contents-API path (`
|
|
188
|
+
* Contents-API path (`plugins/caveman/<rel>`) so the fetch fake
|
|
204
189
|
* serves the whole stub — package.json, the adapter, and its templates — to
|
|
205
190
|
* the real postinstall runner. Resolves the stub relative to this test file.
|
|
206
191
|
*/
|
|
207
192
|
function readRealCavemanStub(): Record<string, string> {
|
|
208
|
-
const repoRel = "
|
|
193
|
+
const repoRel = "plugins/caveman";
|
|
209
194
|
const stubDir = join(import.meta.dir, "../../../../../", repoRel);
|
|
210
195
|
const tree: Record<string, string> = {};
|
|
211
196
|
const walk = (relDir: string): void => {
|
|
@@ -223,7 +208,24 @@ function readRealCavemanStub(): Record<string, string> {
|
|
|
223
208
|
return tree;
|
|
224
209
|
}
|
|
225
210
|
|
|
226
|
-
|
|
211
|
+
const CAVEMAN_SHA = "63a91ecadbf4c4719a4602a5abb00883f9966034";
|
|
212
|
+
|
|
213
|
+
const CAVEMAN_MANIFEST = {
|
|
214
|
+
name: "vellum-assistant",
|
|
215
|
+
plugins: [
|
|
216
|
+
{
|
|
217
|
+
name: "caveman",
|
|
218
|
+
source: {
|
|
219
|
+
source: "github",
|
|
220
|
+
repo: "JuliusBrussee/caveman",
|
|
221
|
+
ref: CAVEMAN_SHA,
|
|
222
|
+
},
|
|
223
|
+
description: "Ultra-compressed communication mode.",
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
describe("installPlugin — install lifecycle", () => {
|
|
227
229
|
let ws: string;
|
|
228
230
|
let pluginsDir: string;
|
|
229
231
|
|
|
@@ -237,280 +239,76 @@ describe("installPlugin — first-party", () => {
|
|
|
237
239
|
rmSync(ws, { recursive: true, force: true });
|
|
238
240
|
});
|
|
239
241
|
|
|
240
|
-
test("copies a first-party plugin into <workspacePluginsDir>/<name>", async () => {
|
|
241
|
-
const result = await installPlugin(
|
|
242
|
-
{ name: "simple-memory", force: false, ref: "main" },
|
|
243
|
-
{
|
|
244
|
-
fetch: fixtureFetch({
|
|
245
|
-
"simple-memory": null,
|
|
246
|
-
"simple-memory/package.json": '{"name":"simple-memory"}',
|
|
247
|
-
"simple-memory/README.md": "# simple-memory",
|
|
248
|
-
"simple-memory/hooks": null,
|
|
249
|
-
"simple-memory/hooks/init.ts": "export default async () => {};\n",
|
|
250
|
-
"simple-memory/tools": null,
|
|
251
|
-
"simple-memory/tools/ping.ts": "export default {};\n",
|
|
252
|
-
}),
|
|
253
|
-
workspacePluginsDir: pluginsDir,
|
|
254
|
-
},
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
const target = join(pluginsDir, "simple-memory");
|
|
258
|
-
expect(result.target).toBe(target);
|
|
259
|
-
expect(result.fileCount).toBe(4);
|
|
260
|
-
expect(result.ref).toBe("main");
|
|
261
|
-
// First-party installs have no clone, so no commit is resolved.
|
|
262
|
-
expect(result.commit).toBeNull();
|
|
263
|
-
expect(existsSync(join(target, "package.json"))).toBe(true);
|
|
264
|
-
expect(existsSync(join(target, "README.md"))).toBe(true);
|
|
265
|
-
expect(existsSync(join(target, "hooks", "init.ts"))).toBe(true);
|
|
266
|
-
expect(existsSync(join(target, "tools", "ping.ts"))).toBe(true);
|
|
267
|
-
expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
|
|
268
|
-
'{"name":"simple-memory"}',
|
|
269
|
-
);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
test("writes a provenance manifest recording the source and ref", async () => {
|
|
273
|
-
// GIVEN a first-party install
|
|
274
|
-
const target = join(pluginsDir, "simple-memory");
|
|
275
|
-
|
|
276
|
-
// WHEN it completes
|
|
277
|
-
await installPlugin(
|
|
278
|
-
{ name: "simple-memory", force: false, ref: "main" },
|
|
279
|
-
{
|
|
280
|
-
fetch: fixtureFetch({
|
|
281
|
-
"simple-memory/package.json": '{"name":"simple-memory"}',
|
|
282
|
-
}),
|
|
283
|
-
workspacePluginsDir: pluginsDir,
|
|
284
|
-
},
|
|
285
|
-
);
|
|
286
|
-
|
|
287
|
-
// THEN a hidden manifest records the resolved coordinates (no commit for
|
|
288
|
-
// first-party) and is not counted as a plugin file
|
|
289
|
-
const manifest = JSON.parse(
|
|
290
|
-
readFileSync(join(target, ".vellum-plugin.json"), "utf-8"),
|
|
291
|
-
);
|
|
292
|
-
expect(manifest.name).toBe("simple-memory");
|
|
293
|
-
expect(manifest.source.kind).toBe("first-party");
|
|
294
|
-
expect(manifest.source.repo).toBe("vellum-assistant");
|
|
295
|
-
expect(manifest.source.ref).toBe("main");
|
|
296
|
-
expect(manifest.commit).toBeUndefined();
|
|
297
|
-
});
|
|
298
|
-
|
|
299
242
|
test("refuses to overwrite an existing install without --force", async () => {
|
|
300
|
-
|
|
243
|
+
// GIVEN a plugin already installed at <pluginsDir>/caveman
|
|
244
|
+
const target = join(pluginsDir, "caveman");
|
|
301
245
|
mkdirSync(target, { recursive: true });
|
|
302
|
-
writeFileSync(join(target, "
|
|
246
|
+
writeFileSync(join(target, "package.json"), '{"name":"caveman"}');
|
|
303
247
|
|
|
248
|
+
// WHEN we install the same name without --force
|
|
249
|
+
// THEN it refuses rather than clobbering the existing copy, and git is
|
|
250
|
+
// never invoked (the guard trips before any clone)
|
|
251
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
304
252
|
await expect(
|
|
305
253
|
installPlugin(
|
|
306
|
-
{ name: "
|
|
307
|
-
{
|
|
308
|
-
fetch: fixtureFetch({
|
|
309
|
-
"simple-memory/package.json": "{}",
|
|
310
|
-
}),
|
|
311
|
-
workspacePluginsDir: pluginsDir,
|
|
312
|
-
},
|
|
254
|
+
{ name: "caveman", force: false, ref: "main" },
|
|
255
|
+
{ fetch, runGit: unusedGitRunner, workspacePluginsDir: pluginsDir },
|
|
313
256
|
),
|
|
314
257
|
).rejects.toBeInstanceOf(PluginAlreadyInstalledError);
|
|
315
|
-
|
|
316
|
-
// The pre-existing marker is left untouched on refusal.
|
|
317
|
-
expect(readFileSync(join(target, "marker"), "utf-8")).toBe("pre-existing");
|
|
318
258
|
});
|
|
319
259
|
|
|
320
260
|
test("--force replaces an existing install", async () => {
|
|
321
|
-
|
|
261
|
+
// GIVEN a stale copy already on disk
|
|
262
|
+
const target = join(pluginsDir, "caveman");
|
|
322
263
|
mkdirSync(target, { recursive: true });
|
|
323
|
-
writeFileSync(join(target, "
|
|
264
|
+
writeFileSync(join(target, "stale.txt"), "old");
|
|
324
265
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
266
|
+
// AND a clone that materializes the fresh upstream tree
|
|
267
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
268
|
+
const runGit = fakeGitRunner({
|
|
269
|
+
tree: { "package.json": '{"name":"caveman"}', "README.md": "# caveman" },
|
|
270
|
+
commit: CAVEMAN_SHA,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// WHEN we install with --force
|
|
274
|
+
const result = await installPlugin(
|
|
275
|
+
{ name: "caveman", force: true, ref: "main" },
|
|
276
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
333
277
|
);
|
|
334
278
|
|
|
335
|
-
|
|
279
|
+
// THEN the fresh tree replaces the old copy entirely
|
|
280
|
+
expect(result.target).toBe(target);
|
|
281
|
+
expect(existsSync(join(target, "stale.txt"))).toBe(false);
|
|
336
282
|
expect(existsSync(join(target, "package.json"))).toBe(true);
|
|
283
|
+
expect(existsSync(join(target, "README.md"))).toBe(true);
|
|
337
284
|
});
|
|
338
285
|
|
|
339
|
-
test("--force preserves the existing install when the
|
|
340
|
-
//
|
|
341
|
-
|
|
342
|
-
// tree on disk is untouched (all writes go through the staging dir).
|
|
343
|
-
const target = join(pluginsDir, "simple-memory");
|
|
286
|
+
test("--force preserves the existing install when the clone fails", async () => {
|
|
287
|
+
// GIVEN a working copy already on disk
|
|
288
|
+
const target = join(pluginsDir, "caveman");
|
|
344
289
|
mkdirSync(target, { recursive: true });
|
|
345
|
-
writeFileSync(join(target, "
|
|
290
|
+
writeFileSync(join(target, "package.json"), '{"name":"caveman-existing"}');
|
|
346
291
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
),
|
|
356
|
-
).rejects.toThrow(/HTTP 503/);
|
|
357
|
-
|
|
358
|
-
expect(readFileSync(join(target, "marker"), "utf-8")).toBe("pre-existing");
|
|
359
|
-
// And no staging dir leaks into the plugins directory.
|
|
360
|
-
expect(readdirSync(pluginsDir)).toEqual(["simple-memory"]);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
test("404 on the canonical path is reported as not-found", async () => {
|
|
364
|
-
await expect(
|
|
365
|
-
installPlugin(
|
|
366
|
-
{ name: "missing-plugin", force: false, ref: "main" },
|
|
367
|
-
{
|
|
368
|
-
fetch: fixtureFetch({}),
|
|
369
|
-
workspacePluginsDir: pluginsDir,
|
|
370
|
-
},
|
|
371
|
-
),
|
|
372
|
-
).rejects.toBeInstanceOf(PluginNotFoundError);
|
|
373
|
-
|
|
374
|
-
expect(existsSync(join(pluginsDir, "missing-plugin"))).toBe(false);
|
|
375
|
-
// And no staging dir leaks either.
|
|
376
|
-
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
test("HTTP 5xx from GitHub propagates and leaves no staging behind", async () => {
|
|
380
|
-
await expect(
|
|
381
|
-
installPlugin(
|
|
382
|
-
{ name: "demo", force: false, ref: "main" },
|
|
383
|
-
{
|
|
384
|
-
fetch: (async () =>
|
|
385
|
-
new Response("upstream broken", { status: 503 })) as FetchLike,
|
|
386
|
-
workspacePluginsDir: pluginsDir,
|
|
387
|
-
},
|
|
388
|
-
),
|
|
389
|
-
).rejects.toThrow(/HTTP 503/);
|
|
390
|
-
|
|
391
|
-
expect(existsSync(join(pluginsDir, "demo"))).toBe(false);
|
|
392
|
-
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
test("a rate-limited contents listing surfaces a retryable PluginSourceUnavailableError", async () => {
|
|
396
|
-
// GIVEN GitHub's unauthenticated rate limit is exhausted: the contents
|
|
397
|
-
// listing 403s with the remaining-quota header at zero
|
|
398
|
-
const rateLimited: FetchLike = (async (input: RequestInfo | URL) => {
|
|
399
|
-
const url = typeof input === "string" ? input : input.toString();
|
|
400
|
-
if (url.includes("marketplace.json")) {
|
|
401
|
-
return new Response("not found", { status: 404 });
|
|
402
|
-
}
|
|
403
|
-
if (url.includes("/contents/")) {
|
|
404
|
-
return new Response("rate limited", {
|
|
405
|
-
status: 403,
|
|
406
|
-
headers: { "x-ratelimit-remaining": "0" },
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
return new Response("not found", { status: 404 });
|
|
410
|
-
}) as FetchLike;
|
|
411
|
-
|
|
412
|
-
// WHEN we install
|
|
413
|
-
// THEN the failure is classified as transient (retryable), not a hard
|
|
414
|
-
// error, so the route can surface a 503 instead of a 500
|
|
415
|
-
await expect(
|
|
416
|
-
installPlugin(
|
|
417
|
-
{ name: "demo", force: false, ref: "main" },
|
|
418
|
-
{ fetch: rateLimited, workspacePluginsDir: pluginsDir },
|
|
419
|
-
),
|
|
420
|
-
).rejects.toBeInstanceOf(PluginSourceUnavailableError);
|
|
421
|
-
|
|
422
|
-
// AND no staging dir leaks behind on the transient failure.
|
|
423
|
-
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
test("a forbidden contents listing with quota remaining stays a hard error", async () => {
|
|
427
|
-
// GIVEN a 403 that is NOT a rate-limit (quota header present and nonzero):
|
|
428
|
-
// a genuine authorization failure, not a transient one
|
|
429
|
-
const forbidden: FetchLike = (async (input: RequestInfo | URL) => {
|
|
430
|
-
const url = typeof input === "string" ? input : input.toString();
|
|
431
|
-
if (url.includes("marketplace.json")) {
|
|
432
|
-
return new Response("not found", { status: 404 });
|
|
433
|
-
}
|
|
434
|
-
if (url.includes("/contents/")) {
|
|
435
|
-
return new Response("forbidden", {
|
|
436
|
-
status: 403,
|
|
437
|
-
headers: { "x-ratelimit-remaining": "57" },
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
return new Response("not found", { status: 404 });
|
|
441
|
-
}) as FetchLike;
|
|
292
|
+
// AND a clone that fails mid-fetch
|
|
293
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
294
|
+
const runGit = fakeGitRunner({
|
|
295
|
+
tree: {},
|
|
296
|
+
fetchError: Object.assign(new Error("git fetch failed"), {
|
|
297
|
+
stderr: "fatal: unable to access: Could not resolve host: github.com",
|
|
298
|
+
}),
|
|
299
|
+
});
|
|
442
300
|
|
|
443
|
-
// WHEN we install
|
|
444
|
-
// THEN
|
|
445
|
-
// route maps it to 500 rather than inviting an endless retry loop
|
|
301
|
+
// WHEN we install with --force and the fetch fails
|
|
302
|
+
// THEN the previous install is left untouched (staging never swapped in)
|
|
446
303
|
await expect(
|
|
447
304
|
installPlugin(
|
|
448
|
-
{ name: "
|
|
449
|
-
{ fetch
|
|
305
|
+
{ name: "caveman", force: true, ref: "main" },
|
|
306
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
450
307
|
),
|
|
451
|
-
).rejects.
|
|
452
|
-
expect(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
test("respects ref by forwarding it to the contents listing", async () => {
|
|
456
|
-
// The requested ref must reach the first-party contents listing request.
|
|
457
|
-
let listRef: string | undefined;
|
|
458
|
-
const base = fixtureFetch({ "demo/package.json": "{}" });
|
|
459
|
-
const fetch: FetchLike = async (input, init) => {
|
|
460
|
-
const url = typeof input === "string" ? input : input.toString();
|
|
461
|
-
const match = /\/contents\/experimental\/plugins\/demo\?ref=([^&]+)/.exec(
|
|
462
|
-
url,
|
|
463
|
-
);
|
|
464
|
-
if (match) listRef = decodeURIComponent(match[1]!);
|
|
465
|
-
return base(url, init);
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
await installPlugin(
|
|
469
|
-
{ name: "demo", force: false, ref: "feat-branch" },
|
|
470
|
-
{ fetch, workspacePluginsDir: pluginsDir },
|
|
308
|
+
).rejects.toThrow();
|
|
309
|
+
expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
|
|
310
|
+
'{"name":"caveman-existing"}',
|
|
471
311
|
);
|
|
472
|
-
|
|
473
|
-
expect(listRef).toBe("feat-branch");
|
|
474
|
-
expect(existsSync(join(pluginsDir, "demo", "package.json"))).toBe(true);
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
test("rejects untrusted entry names from the GitHub response", async () => {
|
|
478
|
-
// Even though GitHub returns trustworthy data, defense-in-depth requires
|
|
479
|
-
// us to validate every entry name before any filesystem write. A malicious
|
|
480
|
-
// or buggy upstream that hands us `../escape` must not write outside the
|
|
481
|
-
// target.
|
|
482
|
-
const badFetch: FetchLike = (async (input: RequestInfo | URL) => {
|
|
483
|
-
const url = typeof input === "string" ? input : input.toString();
|
|
484
|
-
if (url.includes("marketplace.json")) {
|
|
485
|
-
return new Response("not found", { status: 404 });
|
|
486
|
-
}
|
|
487
|
-
if (url.includes("/contents/")) {
|
|
488
|
-
return new Response(
|
|
489
|
-
JSON.stringify([
|
|
490
|
-
{
|
|
491
|
-
name: "../escape",
|
|
492
|
-
path: "experimental/plugins/demo/../escape",
|
|
493
|
-
type: "file",
|
|
494
|
-
download_url: `${DOWNLOAD_HOST}escape`,
|
|
495
|
-
},
|
|
496
|
-
]),
|
|
497
|
-
{ status: 200 },
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
return new Response("x", { status: 200 });
|
|
501
|
-
}) as FetchLike;
|
|
502
|
-
|
|
503
|
-
await expect(
|
|
504
|
-
installPlugin(
|
|
505
|
-
{ name: "demo", force: false, ref: "main" },
|
|
506
|
-
{ fetch: badFetch, workspacePluginsDir: pluginsDir },
|
|
507
|
-
),
|
|
508
|
-
).rejects.toThrow(/Unsafe entry name/);
|
|
509
|
-
|
|
510
|
-
// Nothing was written outside the target — in fact, the target itself
|
|
511
|
-
// is gone because the failed install rolled back the staging dir.
|
|
512
|
-
expect(existsSync(join(pluginsDir, "..", "escape"))).toBe(false);
|
|
513
|
-
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
514
312
|
});
|
|
515
313
|
});
|
|
516
314
|
|
|
@@ -528,21 +326,6 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
528
326
|
rmSync(ws, { recursive: true, force: true });
|
|
529
327
|
});
|
|
530
328
|
|
|
531
|
-
const CAVEMAN_MANIFEST = {
|
|
532
|
-
name: "vellum-assistant",
|
|
533
|
-
plugins: [
|
|
534
|
-
{
|
|
535
|
-
name: "caveman",
|
|
536
|
-
source: {
|
|
537
|
-
source: "github",
|
|
538
|
-
repo: "JuliusBrussee/caveman",
|
|
539
|
-
ref: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
540
|
-
},
|
|
541
|
-
description: "Ultra-compressed communication mode.",
|
|
542
|
-
},
|
|
543
|
-
],
|
|
544
|
-
};
|
|
545
|
-
|
|
546
329
|
test("installs a whitelisted plugin by shallow-cloning its pinned repo + ref", async () => {
|
|
547
330
|
// GIVEN a marketplace whitelisting caveman at its repo root, pinned to a commit SHA
|
|
548
331
|
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
@@ -588,11 +371,11 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
588
371
|
"https://github.com/JuliusBrussee/caveman.git",
|
|
589
372
|
);
|
|
590
373
|
|
|
591
|
-
// AND a provenance manifest records the
|
|
374
|
+
// AND a provenance manifest records the github source + commit.
|
|
592
375
|
const manifest = JSON.parse(
|
|
593
376
|
readFileSync(join(target, ".vellum-plugin.json"), "utf-8"),
|
|
594
377
|
);
|
|
595
|
-
expect(manifest.source.kind).toBe("
|
|
378
|
+
expect(manifest.source.kind).toBe("github");
|
|
596
379
|
expect(manifest.source.owner).toBe("JuliusBrussee");
|
|
597
380
|
expect(manifest.source.ref).toBe(
|
|
598
381
|
"63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
@@ -664,14 +447,13 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
664
447
|
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
665
448
|
});
|
|
666
449
|
|
|
667
|
-
test("a name absent from the manifest
|
|
668
|
-
// GIVEN a manifest that does NOT whitelist "simple-memory"
|
|
669
|
-
// first-party tree in the canonical repo
|
|
450
|
+
test("a name absent from the manifest is a not-found", async () => {
|
|
451
|
+
// GIVEN a manifest that does NOT whitelist "simple-memory"
|
|
670
452
|
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
671
453
|
|
|
672
|
-
// WHEN we install
|
|
673
|
-
// THEN
|
|
674
|
-
//
|
|
454
|
+
// WHEN we install an unlisted name
|
|
455
|
+
// THEN it is a clean not-found pointing at the marketplace manifest, and
|
|
456
|
+
// git is never invoked (there is no source to clone)
|
|
675
457
|
await expect(
|
|
676
458
|
installPlugin(
|
|
677
459
|
{ name: "simple-memory", ref: "main" },
|
|
@@ -679,13 +461,13 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
679
461
|
),
|
|
680
462
|
).rejects.toMatchObject({
|
|
681
463
|
constructor: PluginNotFoundError,
|
|
682
|
-
message: expect.stringContaining("
|
|
464
|
+
message: expect.stringContaining("plugins/marketplace.json"),
|
|
683
465
|
});
|
|
684
466
|
});
|
|
685
467
|
|
|
686
468
|
test("overlays a curated adapter stub and runs its postinstall transform", async () => {
|
|
687
469
|
// GIVEN caveman is whitelisted externally
|
|
688
|
-
// AND we curate an adapter stub for it at
|
|
470
|
+
// AND we curate an adapter stub for it at plugins/caveman —
|
|
689
471
|
// the real, committed stub (package.json + postinstall.ts + templates), so
|
|
690
472
|
// this test exercises the adapter that ships, not a fixture copy of it
|
|
691
473
|
const fetch = makeContentsFetch({
|
|
@@ -697,7 +479,12 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
697
479
|
// terse-mode ruleset in skills/caveman/SKILL.md
|
|
698
480
|
const runGit = fakeGitRunner({
|
|
699
481
|
tree: {
|
|
700
|
-
"package.json":
|
|
482
|
+
"package.json": JSON.stringify({
|
|
483
|
+
name: "caveman-installer",
|
|
484
|
+
version: "0.1.0",
|
|
485
|
+
description: "Caveman installer.",
|
|
486
|
+
license: "MIT",
|
|
487
|
+
}),
|
|
701
488
|
".claude-plugin/plugin.json": JSON.stringify({
|
|
702
489
|
name: "caveman",
|
|
703
490
|
description: "Ultra-compressed communication mode.",
|
|
@@ -723,6 +510,12 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
723
510
|
expect(pkg.name).toBe("caveman");
|
|
724
511
|
expect(pkg.peerDependencies["@vellumai/plugin-api"]).toBeString();
|
|
725
512
|
expect(pkg.scripts?.postinstall).toBeUndefined();
|
|
513
|
+
// AND every other upstream manifest field is preserved verbatim — only
|
|
514
|
+
// `name` and the peer dep are touched, so the installed plugin reports the
|
|
515
|
+
// upstream `version` (and description, license, …) rather than the stub's
|
|
516
|
+
expect(pkg.version).toBe("0.1.0");
|
|
517
|
+
expect(pkg.description).toBe("Caveman installer.");
|
|
518
|
+
expect(pkg.license).toBe("MIT");
|
|
726
519
|
|
|
727
520
|
// AND a pre-model-call hook is synthesized carrying the upstream ruleset,
|
|
728
521
|
// so caveman actually runs (terse mode injected on the user-facing call)
|
|
@@ -737,9 +530,156 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
737
530
|
expect(hook).not.toContain("description: terse mode");
|
|
738
531
|
});
|
|
739
532
|
|
|
533
|
+
test("falls back to the stub manifest when the upstream ships no package.json", async () => {
|
|
534
|
+
// GIVEN caveman is whitelisted with the real curated adapter stub
|
|
535
|
+
const fetch = makeContentsFetch({
|
|
536
|
+
tree: readRealCavemanStub(),
|
|
537
|
+
manifest: CAVEMAN_MANIFEST,
|
|
538
|
+
});
|
|
539
|
+
// AND the upstream clone has NO package.json — only the Claude Code
|
|
540
|
+
// manifest and the terse-mode ruleset the adapter reads
|
|
541
|
+
const runGit = fakeGitRunner({
|
|
542
|
+
tree: {
|
|
543
|
+
".claude-plugin/plugin.json": JSON.stringify({ name: "caveman" }),
|
|
544
|
+
"skills/caveman/SKILL.md":
|
|
545
|
+
"---\nname: caveman\n---\n\nCAVEMAN MODE. Drop filler words.",
|
|
546
|
+
},
|
|
547
|
+
commit: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// WHEN we install (real postinstall runner)
|
|
551
|
+
await installPlugin(
|
|
552
|
+
{ name: "caveman", ref: "main" },
|
|
553
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
// THEN the overlaid stub is the only manifest available, so it becomes the
|
|
557
|
+
// base: the loader still gets a matching `name` and the peer dep, and the
|
|
558
|
+
// spent stub `postinstall` is dropped so no install machinery is shipped
|
|
559
|
+
const pkg = JSON.parse(
|
|
560
|
+
readFileSync(join(pluginsDir, "caveman", "package.json"), "utf-8"),
|
|
561
|
+
);
|
|
562
|
+
expect(pkg.name).toBe("caveman");
|
|
563
|
+
expect(pkg.peerDependencies["@vellumai/plugin-api"]).toBeString();
|
|
564
|
+
expect(pkg.scripts?.postinstall).toBeUndefined();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("strips a clone-supplied bunfig.toml so its preload can't run before the adapter", async () => {
|
|
568
|
+
// GIVEN caveman is whitelisted with the real, committed adapter stub
|
|
569
|
+
const fetch = makeContentsFetch({
|
|
570
|
+
tree: readRealCavemanStub(),
|
|
571
|
+
manifest: CAVEMAN_MANIFEST,
|
|
572
|
+
});
|
|
573
|
+
// AND the upstream clone is a valid Claude Code plugin that ALSO smuggles a
|
|
574
|
+
// `bunfig.toml` whose `preload` points at attacker code in the clone. Bun
|
|
575
|
+
// loads `$cwd/bunfig.toml` and runs `preload` before the entry point, so an
|
|
576
|
+
// un-stripped config would execute `evil.ts` (writing a `pwned` sentinel)
|
|
577
|
+
// ahead of the curated adapter — arbitrary code execution.
|
|
578
|
+
const runGit = fakeGitRunner({
|
|
579
|
+
tree: {
|
|
580
|
+
"package.json": '{"name":"caveman-installer"}',
|
|
581
|
+
".claude-plugin/plugin.json": JSON.stringify({
|
|
582
|
+
name: "caveman",
|
|
583
|
+
description: "Ultra-compressed communication mode.",
|
|
584
|
+
}),
|
|
585
|
+
"skills/caveman/SKILL.md":
|
|
586
|
+
"---\nname: caveman\ndescription: terse mode\n---\n\nCAVEMAN MODE. Drop filler words. Keep technical substance.",
|
|
587
|
+
"bunfig.toml": 'preload = ["./evil.ts"]\n',
|
|
588
|
+
"evil.ts":
|
|
589
|
+
'import { writeFileSync } from "node:fs";\nwriteFileSync("pwned", "pwned");\n',
|
|
590
|
+
},
|
|
591
|
+
commit: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// WHEN we install with the real postinstall runner (real `bun` subprocess)
|
|
595
|
+
const result = await installPlugin(
|
|
596
|
+
{ name: "caveman", ref: "main" },
|
|
597
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
// THEN the adapter still ran and produced a valid Vellum plugin
|
|
601
|
+
const target = join(pluginsDir, "caveman");
|
|
602
|
+
expect(result.commit).toBe("63a91ecadbf4c4719a4602a5abb00883f9966034");
|
|
603
|
+
expect(
|
|
604
|
+
readFileSync(join(target, "hooks", "pre-model-call.ts"), "utf-8"),
|
|
605
|
+
).toContain("CAVEMAN MODE. Drop filler words.");
|
|
606
|
+
// AND the upstream bunfig.toml was dropped before `bun` ran, so its preload
|
|
607
|
+
// never fired — no sentinel was written — and the config never persists in
|
|
608
|
+
// the installed plugin
|
|
609
|
+
expect(existsSync(join(target, "pwned"))).toBe(false);
|
|
610
|
+
expect(existsSync(join(target, "bunfig.toml"))).toBe(false);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
test("drops a clone-supplied bunfig.toml regardless of filename case", async () => {
|
|
614
|
+
// GIVEN a raw external clone (no adapter stub) that smuggles a Bun config
|
|
615
|
+
// under an uppercase name. The macOS install target is case-insensitive, so
|
|
616
|
+
// Bun would still open a `BUNFIG.TOML`; a case-sensitive skip would miss it.
|
|
617
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
618
|
+
const runGit = fakeGitRunner({
|
|
619
|
+
tree: {
|
|
620
|
+
"package.json": '{"name":"caveman"}',
|
|
621
|
+
"BUNFIG.TOML": 'preload = ["./evil.ts"]\n',
|
|
622
|
+
},
|
|
623
|
+
commit: "63a91ecadbf4c4719a4602a5abb00883f9966034",
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// WHEN we install
|
|
627
|
+
await installPlugin(
|
|
628
|
+
{ name: "caveman", ref: "main" },
|
|
629
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
// THEN the uppercase Bun config was dropped while the plugin's own files
|
|
633
|
+
// were materialized
|
|
634
|
+
const target = join(pluginsDir, "caveman");
|
|
635
|
+
expect(existsSync(join(target, "BUNFIG.TOML"))).toBe(false);
|
|
636
|
+
expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
|
|
637
|
+
'{"name":"caveman"}',
|
|
638
|
+
);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
test("stages the clone outside the served plugins/ directory", async () => {
|
|
642
|
+
// GIVEN caveman is whitelisted externally (raw clone, no adapter stub)
|
|
643
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
644
|
+
// AND a git runner that records the working directory it clones into
|
|
645
|
+
const cloneCwds: string[] = [];
|
|
646
|
+
const runGit: GitRunner = async (args, { cwd }) => {
|
|
647
|
+
if (args[0] === "fetch") {
|
|
648
|
+
cloneCwds.push(cwd);
|
|
649
|
+
mkdirSync(join(cwd, ".git"), { recursive: true });
|
|
650
|
+
writeFileSync(join(cwd, "package.json"), '{"name":"caveman"}');
|
|
651
|
+
return { stdout: "" };
|
|
652
|
+
}
|
|
653
|
+
if (args[0] === "rev-parse") {
|
|
654
|
+
return { stdout: "63a91ecadbf4c4719a4602a5abb00883f9966034\n" };
|
|
655
|
+
}
|
|
656
|
+
return { stdout: "" };
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
// WHEN we install
|
|
660
|
+
const result = await installPlugin(
|
|
661
|
+
{ name: "caveman", ref: "main" },
|
|
662
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
// THEN the half-built clone was staged in a sibling directory, never inside
|
|
666
|
+
// the served plugins/ tree — so the daemon's source watcher and startup
|
|
667
|
+
// loader can't observe the un-adapted clone (wrong name, no peer dep) and
|
|
668
|
+
// log spurious name-mismatch / missing-peer-dependency warnings mid-install
|
|
669
|
+
expect(cloneCwds).toHaveLength(1);
|
|
670
|
+
expect(dirname(cloneCwds[0]!)).toBe(
|
|
671
|
+
join(dirname(pluginsDir), ".plugins-staging"),
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
// AND the finished plugin still lands inside plugins/, with no staging
|
|
675
|
+
// artifact left behind in the served directory
|
|
676
|
+
expect(result.target).toBe(join(pluginsDir, "caveman"));
|
|
677
|
+
expect(readdirSync(pluginsDir)).toEqual(["caveman"]);
|
|
678
|
+
});
|
|
679
|
+
|
|
740
680
|
test("does not run a postinstall for a raw clone without an adapter stub", async () => {
|
|
741
681
|
// GIVEN caveman is whitelisted but we curate NO adapter stub for it
|
|
742
|
-
// (the Contents API has no
|
|
682
|
+
// (the Contents API has no plugins/caveman directory)
|
|
743
683
|
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
744
684
|
const runGit = fakeGitRunner({
|
|
745
685
|
tree: { "package.json": '{"name":"caveman"}', "hooks/init.ts": "//" },
|
|
@@ -767,11 +707,11 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
767
707
|
// GIVEN a whitelisted plugin with a curated stub declaring a postinstall
|
|
768
708
|
const fetch = makeContentsFetch({
|
|
769
709
|
tree: {
|
|
770
|
-
"
|
|
710
|
+
"plugins/caveman/package.json": JSON.stringify({
|
|
771
711
|
name: "caveman",
|
|
772
712
|
scripts: { postinstall: "bun ./postinstall.ts" },
|
|
773
713
|
}),
|
|
774
|
-
"
|
|
714
|
+
"plugins/caveman/postinstall.ts": "// adapter",
|
|
775
715
|
},
|
|
776
716
|
manifest: CAVEMAN_MANIFEST,
|
|
777
717
|
});
|
|
@@ -801,7 +741,7 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
801
741
|
// rather than the supported `bun <script>` adapter convention
|
|
802
742
|
const fetch = makeContentsFetch({
|
|
803
743
|
tree: {
|
|
804
|
-
"
|
|
744
|
+
"plugins/caveman/package.json": JSON.stringify({
|
|
805
745
|
name: "caveman",
|
|
806
746
|
scripts: { postinstall: "rm -rf /" },
|
|
807
747
|
}),
|
|
@@ -825,33 +765,21 @@ describe("installPlugin — marketplace resolution", () => {
|
|
|
825
765
|
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
826
766
|
});
|
|
827
767
|
|
|
828
|
-
test("
|
|
768
|
+
test("a transient marketplace failure surfaces a retryable error", async () => {
|
|
829
769
|
// GIVEN the marketplace lookup fails transiently (e.g. rate-limit / 5xx)
|
|
830
|
-
// rather than being absent, so
|
|
831
|
-
|
|
832
|
-
// with a postinstall + adapter script, but no hooks/tools of its own)
|
|
833
|
-
const fetch = makeContentsFetch({
|
|
834
|
-
tree: {
|
|
835
|
-
"experimental/plugins/caveman/package.json": JSON.stringify({
|
|
836
|
-
name: "caveman",
|
|
837
|
-
scripts: { postinstall: "bun ./postinstall.ts" },
|
|
838
|
-
}),
|
|
839
|
-
"experimental/plugins/caveman/postinstall.ts": "// adapter",
|
|
840
|
-
},
|
|
841
|
-
manifestStatus: 500,
|
|
842
|
-
});
|
|
770
|
+
// rather than being absent, so the name can't be resolved to a source
|
|
771
|
+
const fetch = makeContentsFetch({ tree: {}, manifestStatus: 500 });
|
|
843
772
|
|
|
844
|
-
// WHEN we install the name (git never runs — there is no
|
|
773
|
+
// WHEN we install the name (git never runs — there is no resolved source to
|
|
845
774
|
// clone because the marketplace that would have named it was unreadable)
|
|
846
|
-
// THEN it
|
|
847
|
-
//
|
|
848
|
-
// nothing is left behind on disk
|
|
775
|
+
// THEN it surfaces as the retryable variant so the route maps it to 503,
|
|
776
|
+
// and nothing is left behind on disk
|
|
849
777
|
await expect(
|
|
850
778
|
installPlugin(
|
|
851
779
|
{ name: "caveman", ref: "main" },
|
|
852
780
|
{ fetch, runGit: unusedGitRunner, workspacePluginsDir: pluginsDir },
|
|
853
781
|
),
|
|
854
|
-
).rejects.toBeInstanceOf(
|
|
782
|
+
).rejects.toBeInstanceOf(PluginSourceUnavailableError);
|
|
855
783
|
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
856
784
|
});
|
|
857
785
|
});
|