@vellumai/assistant 0.6.5 → 0.6.6
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/AGENTS.md +9 -1
- package/ARCHITECTURE.md +15 -17
- package/Dockerfile +6 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/docs/architecture/integrations.md +32 -39
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +7 -6
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/plugins.md +761 -0
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +212 -68
- package/package.json +1 -1
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +7 -2
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/catalog-cache.test.ts +69 -0
- package/src/__tests__/checker.test.ts +459 -171
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema.test.ts +22 -9
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-window-manager.test.ts +355 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
- package/src/__tests__/conversation-agent-loop.test.ts +30 -141
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
- package/src/__tests__/conversation-process-callsite.test.ts +3 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
- package/src/__tests__/conversation-queue.test.ts +29 -14
- package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
- package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-slash-queue.test.ts +7 -2
- package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
- package/src/__tests__/conversation-speed-override.test.ts +6 -1
- package/src/__tests__/conversation-title-service.test.ts +116 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-usage.test.ts +1 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +2 -2
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +0 -26
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/migration-import-from-url.test.ts +5 -68
- package/src/__tests__/model-intents.test.ts +4 -2
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +41 -76
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +44 -12
- package/src/__tests__/proxy-approval-callback.test.ts +69 -8
- package/src/__tests__/reaction-persistence.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/suggestion-routes.test.ts +103 -4
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-policy.test.ts +21 -3
- package/src/agent/loop.ts +340 -102
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
- package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -9
- package/src/cli/commands/conversations.ts +55 -7
- package/src/cli/commands/image-generation.ts +33 -34
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/program.ts +25 -29
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-tool-registry.ts +0 -15
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +19 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/llm.ts +2 -3
- package/src/config/schemas/security.ts +6 -6
- package/src/config/schemas/tts.ts +11 -0
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +94 -5
- package/src/context/__tests__/compact-prompt.test.ts +27 -9
- package/src/context/prompts/compact.md +26 -12
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +190 -16
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/config-watcher.ts +0 -2
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
- package/src/daemon/conversation-agent-loop.ts +984 -683
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +37 -19
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +14 -7
- package/src/daemon/conversation-runtime-assembly.ts +532 -411
- package/src/daemon/conversation-tool-setup.ts +41 -4
- package/src/daemon/conversation.ts +80 -35
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/handlers/config-model.ts +11 -0
- package/src/daemon/handlers/skills.ts +5 -1
- package/src/daemon/lifecycle.ts +33 -68
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +49 -0
- package/src/daemon/message-types/messages.ts +12 -0
- package/src/daemon/server.ts +5 -3
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -56
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +24 -1
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/emit-feed-event.ts +7 -0
- package/src/home/feed-types.ts +41 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/ipc/__tests__/socket-path.test.ts +11 -50
- package/src/ipc/cli-client.ts +1 -1
- package/src/ipc/cli-server.ts +3 -3
- package/src/ipc/gateway-client.ts +4 -1
- package/src/ipc/routes/browser-context.ts +2 -0
- package/src/ipc/routes/browser.ts +1 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +14 -0
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/socket-path.ts +14 -38
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/conversation-crud.ts +48 -18
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +25 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/extraction.ts +10 -2
- package/src/memory/graph/graph-search.test.ts +1 -0
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/retriever.ts +10 -3
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/pkb/pkb-index.test.ts +1 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +65 -4
- package/src/memory/pkb/pkb-search.ts +40 -18
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +25 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
- package/src/messaging/providers/slack/render-transcript.ts +58 -0
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +30 -14
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -108
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +293 -18
- package/src/permissions/approval-policy.ts +110 -58
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +414 -2
- package/src/permissions/bash-risk-classifier.ts +303 -60
- package/src/permissions/checker.ts +157 -29
- package/src/permissions/command-registry.test.ts +239 -0
- package/src/permissions/command-registry.ts +234 -54
- package/src/permissions/defaults.ts +5 -4
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +61 -4
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/types.ts +2 -0
- package/src/permissions/workspace-policy.ts +8 -3
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/providers/model-catalog.ts +52 -29
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
- package/src/providers/speech-to-text/xai-realtime.ts +39 -14
- package/src/runtime/AGENTS.md +25 -16
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/http-server.ts +77 -8
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
- package/src/runtime/routes/approval-routes.ts +17 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/conversation-routes.ts +223 -116
- package/src/runtime/routes/inbound-message-handler.ts +88 -13
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +0 -3
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/skill-route-registry.ts +75 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +48 -8
- package/src/skills/catalog-cache.ts +12 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
- package/src/tools/browser/browser-execution.ts +88 -19
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/executor.ts +126 -74
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/permission-checker.ts +98 -49
- package/src/tools/policy-context.ts +4 -0
- package/src/tools/registry.ts +140 -3
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/types.ts +28 -2
- package/src/util/platform.ts +7 -2
- package/src/util/pricing.ts +26 -3
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- package/src/tools/watch/watch-state.ts +0 -142
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the plugin registry (PR 13).
|
|
3
|
+
*
|
|
4
|
+
* Covers successful registration, required-field and duplicate-name
|
|
5
|
+
* validation, capability-version negotiation error messaging, injector
|
|
6
|
+
* ordering, and middleware collection order.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ASSISTANT_API_VERSIONS,
|
|
13
|
+
getInjectors,
|
|
14
|
+
getMiddlewaresFor,
|
|
15
|
+
getRegisteredPlugins,
|
|
16
|
+
registerPlugin,
|
|
17
|
+
resetPluginRegistryForTests,
|
|
18
|
+
} from "../plugins/registry.js";
|
|
19
|
+
import {
|
|
20
|
+
type CompactionArgs,
|
|
21
|
+
type CompactionResult,
|
|
22
|
+
type Injector,
|
|
23
|
+
type Middleware,
|
|
24
|
+
type Plugin,
|
|
25
|
+
PluginExecutionError,
|
|
26
|
+
} from "../plugins/types.js";
|
|
27
|
+
|
|
28
|
+
/** Build a minimal, valid plugin with the given name and optional extras. */
|
|
29
|
+
function buildPlugin(
|
|
30
|
+
name: string,
|
|
31
|
+
extras: Partial<Omit<Plugin, "manifest">> = {},
|
|
32
|
+
requiresOverride?: Record<string, string>,
|
|
33
|
+
): Plugin {
|
|
34
|
+
return {
|
|
35
|
+
manifest: {
|
|
36
|
+
name,
|
|
37
|
+
version: "0.0.1",
|
|
38
|
+
requires: requiresOverride ?? { pluginRuntime: "v1" },
|
|
39
|
+
},
|
|
40
|
+
...extras,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("plugin registry", () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
resetPluginRegistryForTests();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("registers a minimal plugin successfully", () => {
|
|
50
|
+
const plugin = buildPlugin("alpha");
|
|
51
|
+
registerPlugin(plugin);
|
|
52
|
+
|
|
53
|
+
const registered = getRegisteredPlugins();
|
|
54
|
+
expect(registered).toHaveLength(1);
|
|
55
|
+
expect(registered[0]?.manifest.name).toBe("alpha");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("throws on duplicate-name registration", () => {
|
|
59
|
+
registerPlugin(buildPlugin("alpha"));
|
|
60
|
+
expect(() => registerPlugin(buildPlugin("alpha"))).toThrow(
|
|
61
|
+
PluginExecutionError,
|
|
62
|
+
);
|
|
63
|
+
expect(() => registerPlugin(buildPlugin("alpha"))).toThrow(
|
|
64
|
+
"already registered",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("throws when manifest is missing", () => {
|
|
69
|
+
// Cast through `unknown` to simulate a JS caller passing a malformed plugin.
|
|
70
|
+
expect(() => registerPlugin({} as unknown as Plugin)).toThrow(
|
|
71
|
+
PluginExecutionError,
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("throws when manifest.name is missing", () => {
|
|
76
|
+
const bad = {
|
|
77
|
+
manifest: {
|
|
78
|
+
version: "0.0.1",
|
|
79
|
+
requires: { pluginRuntime: "v1" },
|
|
80
|
+
},
|
|
81
|
+
} as unknown as Plugin;
|
|
82
|
+
expect(() => registerPlugin(bad)).toThrow(/manifest\.name is required/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("throws on non-kebab-case plugin names (path-traversal guard)", () => {
|
|
86
|
+
// Plugin names flow into filesystem paths (`plugins-data/<name>/`), so the
|
|
87
|
+
// registry must reject anything that could escape the storage directory
|
|
88
|
+
// or otherwise deviate from the kebab-case contract.
|
|
89
|
+
const cases = [
|
|
90
|
+
"../evil",
|
|
91
|
+
"../../etc",
|
|
92
|
+
"evil/with/slashes",
|
|
93
|
+
"has space",
|
|
94
|
+
"Has-Uppercase",
|
|
95
|
+
"-leading-hyphen",
|
|
96
|
+
"trailing-hyphen-",
|
|
97
|
+
"double--hyphen",
|
|
98
|
+
".",
|
|
99
|
+
"",
|
|
100
|
+
];
|
|
101
|
+
for (const name of cases) {
|
|
102
|
+
const plugin = buildPlugin(name || "x");
|
|
103
|
+
// Override the name post-build so the empty-string case exercises the
|
|
104
|
+
// same code path as the others (buildPlugin uses the literal value).
|
|
105
|
+
(plugin.manifest as { name: string }).name = name;
|
|
106
|
+
expect(() => registerPlugin(plugin)).toThrow(PluginExecutionError);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("accepts valid kebab-case plugin names", () => {
|
|
111
|
+
for (const name of ["a", "abc", "a-b", "a1-b2", "foo-bar-baz"]) {
|
|
112
|
+
resetPluginRegistryForTests();
|
|
113
|
+
expect(() => registerPlugin(buildPlugin(name))).not.toThrow();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("throws when manifest.version is missing", () => {
|
|
118
|
+
const bad = {
|
|
119
|
+
manifest: {
|
|
120
|
+
name: "missing-version",
|
|
121
|
+
requires: { pluginRuntime: "v1" },
|
|
122
|
+
},
|
|
123
|
+
} as unknown as Plugin;
|
|
124
|
+
expect(() => registerPlugin(bad)).toThrow(/manifest\.version is required/);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("throws when manifest.requires is missing", () => {
|
|
128
|
+
const bad = {
|
|
129
|
+
manifest: { name: "missing-requires", version: "0.0.1" },
|
|
130
|
+
} as unknown as Plugin;
|
|
131
|
+
expect(() => registerPlugin(bad)).toThrow(/manifest\.requires is required/);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("throws when requires.pluginRuntime is missing", () => {
|
|
135
|
+
const plugin = buildPlugin(
|
|
136
|
+
"no-runtime",
|
|
137
|
+
{},
|
|
138
|
+
// Valid shape but no pluginRuntime entry.
|
|
139
|
+
{ memoryApi: "v1" },
|
|
140
|
+
);
|
|
141
|
+
expect(() => registerPlugin(plugin)).toThrow(PluginExecutionError);
|
|
142
|
+
expect(() => registerPlugin(plugin)).toThrow(/pluginRuntime/);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("throws with version-mismatch message when a required version is not exposed", () => {
|
|
146
|
+
// The assistant seeds memoryApi with ["v1"]. Requesting v2 must fail.
|
|
147
|
+
const plugin = buildPlugin(
|
|
148
|
+
"too-new",
|
|
149
|
+
{},
|
|
150
|
+
{
|
|
151
|
+
pluginRuntime: "v1",
|
|
152
|
+
memoryApi: "v2",
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(() => registerPlugin(plugin)).toThrow(PluginExecutionError);
|
|
157
|
+
|
|
158
|
+
// Sanity-check the assistant actually exposes only v1 for memoryApi so
|
|
159
|
+
// this test fails loudly if the capability table ever adds v2.
|
|
160
|
+
expect(ASSISTANT_API_VERSIONS.memoryApi).toEqual(["v1"]);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
registerPlugin(plugin);
|
|
164
|
+
throw new Error("expected registerPlugin to throw");
|
|
165
|
+
} catch (err) {
|
|
166
|
+
expect(err).toBeInstanceOf(PluginExecutionError);
|
|
167
|
+
const msg = (err as PluginExecutionError).message;
|
|
168
|
+
// Error message must reference plugin name, API, required version,
|
|
169
|
+
// and the versions the assistant exposes.
|
|
170
|
+
expect(msg).toContain("too-new");
|
|
171
|
+
expect(msg).toContain("memoryApi");
|
|
172
|
+
expect(msg).toContain("v2");
|
|
173
|
+
expect(msg).toContain("v1");
|
|
174
|
+
expect((err as PluginExecutionError).pluginName).toBe("too-new");
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("throws with clear message when a required capability is unknown", () => {
|
|
179
|
+
const plugin = buildPlugin(
|
|
180
|
+
"asks-for-mystery",
|
|
181
|
+
{},
|
|
182
|
+
{
|
|
183
|
+
pluginRuntime: "v1",
|
|
184
|
+
thisDoesNotExist: "v1",
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
expect(() => registerPlugin(plugin)).toThrow(PluginExecutionError);
|
|
188
|
+
try {
|
|
189
|
+
registerPlugin(plugin);
|
|
190
|
+
throw new Error("expected registerPlugin to throw");
|
|
191
|
+
} catch (err) {
|
|
192
|
+
const msg = (err as PluginExecutionError).message;
|
|
193
|
+
expect(msg).toContain("asks-for-mystery");
|
|
194
|
+
expect(msg).toContain("thisDoesNotExist");
|
|
195
|
+
expect(msg).toContain("(none)");
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("getInjectors returns injectors sorted by order ascending", () => {
|
|
200
|
+
const high: Injector = {
|
|
201
|
+
name: "high-order",
|
|
202
|
+
order: 20,
|
|
203
|
+
async produce() {
|
|
204
|
+
return null;
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const low: Injector = {
|
|
208
|
+
name: "low-order",
|
|
209
|
+
order: 10,
|
|
210
|
+
async produce() {
|
|
211
|
+
return null;
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Register the higher-order plugin first so registration order alone
|
|
216
|
+
// would produce the wrong sequence — the test proves sort-by-order wins.
|
|
217
|
+
registerPlugin(buildPlugin("high", { injectors: [high] }));
|
|
218
|
+
registerPlugin(buildPlugin("low", { injectors: [low] }));
|
|
219
|
+
|
|
220
|
+
const injectors = getInjectors();
|
|
221
|
+
expect(injectors.map((i) => i.name)).toEqual(["low-order", "high-order"]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("getMiddlewaresFor returns middleware in registration order", () => {
|
|
225
|
+
const firstMw: Middleware<CompactionArgs, CompactionResult> = async (
|
|
226
|
+
args,
|
|
227
|
+
next,
|
|
228
|
+
) => next(args);
|
|
229
|
+
const secondMw: Middleware<CompactionArgs, CompactionResult> = async (
|
|
230
|
+
args,
|
|
231
|
+
next,
|
|
232
|
+
) => next(args);
|
|
233
|
+
|
|
234
|
+
registerPlugin(
|
|
235
|
+
buildPlugin("plugin-first", { middleware: { compaction: firstMw } }),
|
|
236
|
+
);
|
|
237
|
+
registerPlugin(
|
|
238
|
+
buildPlugin("plugin-second", { middleware: { compaction: secondMw } }),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const middlewares = getMiddlewaresFor("compaction");
|
|
242
|
+
expect(middlewares).toHaveLength(2);
|
|
243
|
+
// Identity comparison proves the middleware instances come back in
|
|
244
|
+
// registration order — outer→inner composition semantics belong to the
|
|
245
|
+
// pipeline runner (PR 12), not the registry.
|
|
246
|
+
expect(middlewares[0]).toBe(firstMw);
|
|
247
|
+
expect(middlewares[1]).toBe(secondMw);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("getMiddlewaresFor skips plugins without a middleware for the pipeline", () => {
|
|
251
|
+
const mw: Middleware<CompactionArgs, CompactionResult> = async (
|
|
252
|
+
args,
|
|
253
|
+
next,
|
|
254
|
+
) => next(args);
|
|
255
|
+
registerPlugin(buildPlugin("bare"));
|
|
256
|
+
registerPlugin(buildPlugin("has-mw", { middleware: { compaction: mw } }));
|
|
257
|
+
|
|
258
|
+
const middlewares = getMiddlewaresFor("compaction");
|
|
259
|
+
expect(middlewares).toHaveLength(1);
|
|
260
|
+
expect(middlewares[0]).toBe(mw);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("getRegisteredPlugins reflects registration order", () => {
|
|
264
|
+
registerPlugin(buildPlugin("one"));
|
|
265
|
+
registerPlugin(buildPlugin("two"));
|
|
266
|
+
registerPlugin(buildPlugin("three"));
|
|
267
|
+
expect(getRegisteredPlugins().map((p) => p.manifest.name)).toEqual([
|
|
268
|
+
"one",
|
|
269
|
+
"two",
|
|
270
|
+
"three",
|
|
271
|
+
]);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for plugin HTTP-route contributions (PR 32).
|
|
3
|
+
*
|
|
4
|
+
* A plugin may declare a `routes` array on its {@link Plugin} shape; after
|
|
5
|
+
* `init()` succeeds, bootstrap wires each entry into the skill-route registry
|
|
6
|
+
* via {@link registerSkillRoute}, retains the opaque {@link SkillRouteHandle}
|
|
7
|
+
* it receives back, and on shutdown calls {@link unregisterSkillRoute} with
|
|
8
|
+
* that exact handle. Pattern-text matching was removed deliberately: two
|
|
9
|
+
* owners (e.g. a plugin and a skill) can legitimately register the same
|
|
10
|
+
* regex, and keying on `source + flags` would let one owner's teardown
|
|
11
|
+
* silently evict another owner's route, violating the "no traffic hits a
|
|
12
|
+
* plugin handler during onShutdown" invariant.
|
|
13
|
+
*
|
|
14
|
+
* The registry doesn't own HTTP itself — the tests here exercise:
|
|
15
|
+
*
|
|
16
|
+
* 1. Bootstrap → `registerSkillRoute` → `matchSkillRoute` returns the plugin's
|
|
17
|
+
* handler, and the handler responds as expected.
|
|
18
|
+
* 2. Shutdown → `unregisterSkillRoute` drops the entry, and subsequent
|
|
19
|
+
* `matchSkillRoute` lookups return `null`.
|
|
20
|
+
* 3. Plugins without `routes` (or with an empty array) bootstrap cleanly.
|
|
21
|
+
* 4. When two plugins register regex patterns with identical `source+flags`,
|
|
22
|
+
* each plugin's shutdown only removes its own route — the other plugin's
|
|
23
|
+
* route stays live until its own teardown runs.
|
|
24
|
+
*
|
|
25
|
+
* Uses `mock.module` to stub credential resolution — bootstrap otherwise
|
|
26
|
+
* tries to hit the real secure-key backend. `resetPluginRegistryForTests()`
|
|
27
|
+
* isolates plugin-registry state and `resetSkillRoutesForTests()` isolates
|
|
28
|
+
* skill-route-registry state between cases.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { rm } from "node:fs/promises";
|
|
32
|
+
import { tmpdir } from "node:os";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
35
|
+
|
|
36
|
+
// Stub the credential store before importing bootstrap so the module binds to
|
|
37
|
+
// the mock. Plugins in these tests don't declare `requiresCredential`, but
|
|
38
|
+
// the mock keeps the test hermetic regardless of what the backend would do.
|
|
39
|
+
const getSecureKeyAsyncMock = mock(
|
|
40
|
+
async (_account: string): Promise<string | undefined> => undefined,
|
|
41
|
+
);
|
|
42
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
43
|
+
getSecureKeyAsync: getSecureKeyAsyncMock,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
import type { AssistantConfig } from "../config/schema.js";
|
|
47
|
+
import {
|
|
48
|
+
bootstrapPlugins,
|
|
49
|
+
type DaemonContext,
|
|
50
|
+
} from "../daemon/external-plugins-bootstrap.js";
|
|
51
|
+
import { runShutdownHooks } from "../daemon/shutdown-registry.js";
|
|
52
|
+
import {
|
|
53
|
+
registerPlugin,
|
|
54
|
+
resetPluginRegistryForTests,
|
|
55
|
+
} from "../plugins/registry.js";
|
|
56
|
+
import type { Plugin } from "../plugins/types.js";
|
|
57
|
+
import {
|
|
58
|
+
matchSkillRoute,
|
|
59
|
+
resetSkillRoutesForTests,
|
|
60
|
+
type SkillRoute,
|
|
61
|
+
} from "../runtime/skill-route-registry.js";
|
|
62
|
+
|
|
63
|
+
// Redirect plugin storage creation into a per-process temp tree so the test
|
|
64
|
+
// never touches a developer's real ~/.vellum.
|
|
65
|
+
const TEST_INSTANCE_DIR = join(
|
|
66
|
+
tmpdir(),
|
|
67
|
+
`vellum-plugin-route-contrib-test-${process.pid}`,
|
|
68
|
+
);
|
|
69
|
+
process.env.BASE_DATA_DIR = TEST_INSTANCE_DIR;
|
|
70
|
+
|
|
71
|
+
const fakeConfig = {} as unknown as AssistantConfig;
|
|
72
|
+
const fakeCtx: DaemonContext = {
|
|
73
|
+
config: fakeConfig,
|
|
74
|
+
assistantVersion: "9.9.9-test",
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Build a minimal valid plugin with optional route contributions. */
|
|
78
|
+
function buildPlugin(
|
|
79
|
+
name: string,
|
|
80
|
+
extras: Partial<Omit<Plugin, "manifest">> = {},
|
|
81
|
+
): Plugin {
|
|
82
|
+
return {
|
|
83
|
+
manifest: {
|
|
84
|
+
name,
|
|
85
|
+
version: "0.0.1",
|
|
86
|
+
requires: { pluginRuntime: "v1" },
|
|
87
|
+
},
|
|
88
|
+
...extras,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe("plugin route contributions", () => {
|
|
93
|
+
const echoPattern = /^\/_plugin\/echo$/;
|
|
94
|
+
|
|
95
|
+
beforeEach(async () => {
|
|
96
|
+
resetPluginRegistryForTests();
|
|
97
|
+
resetSkillRoutesForTests();
|
|
98
|
+
getSecureKeyAsyncMock.mockReset();
|
|
99
|
+
getSecureKeyAsyncMock.mockImplementation(async () => undefined);
|
|
100
|
+
await rm(TEST_INSTANCE_DIR, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("bootstrap registers a plugin's routes and the HTTP handler responds", async () => {
|
|
104
|
+
let initFired = false;
|
|
105
|
+
const route: SkillRoute = {
|
|
106
|
+
pattern: echoPattern,
|
|
107
|
+
methods: ["GET"],
|
|
108
|
+
handler: async () =>
|
|
109
|
+
new Response("echo", {
|
|
110
|
+
status: 200,
|
|
111
|
+
headers: { "content-type": "text/plain" },
|
|
112
|
+
}),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
registerPlugin(
|
|
116
|
+
buildPlugin("echo-plugin", {
|
|
117
|
+
async init() {
|
|
118
|
+
initFired = true;
|
|
119
|
+
},
|
|
120
|
+
routes: [route],
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await bootstrapPlugins(fakeCtx);
|
|
125
|
+
|
|
126
|
+
// init() must have run — route registration is gated on init success.
|
|
127
|
+
expect(initFired).toBe(true);
|
|
128
|
+
|
|
129
|
+
// matchSkillRoute resolves against the same registry the HTTP server
|
|
130
|
+
// hits at request dispatch time, so a match here proves the plugin's
|
|
131
|
+
// handler is reachable from production code paths.
|
|
132
|
+
const matched = matchSkillRoute("/_plugin/echo", "GET");
|
|
133
|
+
expect(matched).not.toBeNull();
|
|
134
|
+
expect(matched!.kind).toBe("match");
|
|
135
|
+
|
|
136
|
+
// Invoke the handler through the matched record to prove the response
|
|
137
|
+
// actually comes from the plugin — not some default.
|
|
138
|
+
if (matched!.kind !== "match") throw new Error("unreachable");
|
|
139
|
+
const req = new Request("http://host/_plugin/echo");
|
|
140
|
+
const res = await matched!.route.handler(req, matched!.match);
|
|
141
|
+
expect(res.status).toBe(200);
|
|
142
|
+
expect(await res.text()).toBe("echo");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("shutdown unregisters the plugin's routes", async () => {
|
|
146
|
+
const route: SkillRoute = {
|
|
147
|
+
pattern: echoPattern,
|
|
148
|
+
methods: ["GET"],
|
|
149
|
+
handler: async () => new Response("echo", { status: 200 }),
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
registerPlugin(buildPlugin("echo-plugin", { routes: [route] }));
|
|
153
|
+
|
|
154
|
+
await bootstrapPlugins(fakeCtx);
|
|
155
|
+
|
|
156
|
+
// Sanity: route is live after bootstrap.
|
|
157
|
+
expect(matchSkillRoute("/_plugin/echo", "GET")).not.toBeNull();
|
|
158
|
+
|
|
159
|
+
// Shutdown runs the reverse-order teardown hook registered by bootstrap.
|
|
160
|
+
await runShutdownHooks("test-shutdown");
|
|
161
|
+
|
|
162
|
+
// Route is gone — matchSkillRoute returns null because no pattern
|
|
163
|
+
// matches the path at all anymore.
|
|
164
|
+
expect(matchSkillRoute("/_plugin/echo", "GET")).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("plugin with no routes bootstraps and shuts down cleanly", async () => {
|
|
168
|
+
// Declaring no `routes` field is the common case; bootstrap must skip
|
|
169
|
+
// route handling entirely (the guard is `if plugin.routes && length > 0`).
|
|
170
|
+
registerPlugin(buildPlugin("no-routes-plugin", { async init() {} }));
|
|
171
|
+
|
|
172
|
+
await bootstrapPlugins(fakeCtx);
|
|
173
|
+
await runShutdownHooks("test-shutdown");
|
|
174
|
+
|
|
175
|
+
// Nothing to verify beyond "neither throws" — an empty `routes` must not
|
|
176
|
+
// regress existing no-op bootstrap semantics.
|
|
177
|
+
expect(true).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("shutdown tolerates a route whose registry entry was wiped externally", async () => {
|
|
181
|
+
// Guard against the case where a stale handle no longer points at a live
|
|
182
|
+
// registry entry (e.g. the registry was cleared externally). The shutdown
|
|
183
|
+
// hook must not crash — unregisterSkillRoute returns false, and
|
|
184
|
+
// bootstrap's try/catch around the call swallows the signal. This
|
|
185
|
+
// exercises the defensive path so a partial-crash recovery still runs
|
|
186
|
+
// every plugin's onShutdown in reverse order.
|
|
187
|
+
let shutdownFired = false;
|
|
188
|
+
registerPlugin(
|
|
189
|
+
buildPlugin("echo-plugin", {
|
|
190
|
+
routes: [
|
|
191
|
+
{
|
|
192
|
+
pattern: echoPattern,
|
|
193
|
+
methods: ["GET"],
|
|
194
|
+
handler: async () => new Response("echo", { status: 200 }),
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
async onShutdown() {
|
|
198
|
+
shutdownFired = true;
|
|
199
|
+
},
|
|
200
|
+
}),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await bootstrapPlugins(fakeCtx);
|
|
204
|
+
|
|
205
|
+
// Simulate an external wipe before the shutdown hook runs — e.g. a
|
|
206
|
+
// different subsystem calling `resetSkillRoutesForTests` or a hot-reload
|
|
207
|
+
// flow clearing the registry. The plugin's retained handle is now stale.
|
|
208
|
+
resetSkillRoutesForTests();
|
|
209
|
+
|
|
210
|
+
await runShutdownHooks("test-shutdown");
|
|
211
|
+
|
|
212
|
+
// onShutdown still ran despite the stale handle — proving the
|
|
213
|
+
// route-unregister step does not short-circuit plugin teardown.
|
|
214
|
+
expect(shutdownFired).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("shutdown of one plugin does not evict a sibling's same-pattern route", async () => {
|
|
218
|
+
// Regression for the reviewer-flagged invariant: keying unregistration on
|
|
219
|
+
// `pattern.source + flags` would let plugin-A's teardown drop plugin-B's
|
|
220
|
+
// route when both declared regex with identical text. With handle-keyed
|
|
221
|
+
// identity, each plugin's teardown removes only its own registrations.
|
|
222
|
+
//
|
|
223
|
+
// We simulate this by wiring two plugins that both contribute a route
|
|
224
|
+
// matching `/^\/_plugin\/echo$/`. Teardown runs in reverse registration
|
|
225
|
+
// order, so plugin-B is torn down first; plugin-A's route must still
|
|
226
|
+
// match afterwards, and only disappear once plugin-A's own teardown
|
|
227
|
+
// runs.
|
|
228
|
+
let pluginAShutdown = false;
|
|
229
|
+
let pluginBShutdown = false;
|
|
230
|
+
|
|
231
|
+
registerPlugin(
|
|
232
|
+
buildPlugin("plugin-a", {
|
|
233
|
+
routes: [
|
|
234
|
+
{
|
|
235
|
+
pattern: /^\/_plugin\/echo$/,
|
|
236
|
+
methods: ["GET"],
|
|
237
|
+
handler: async () => new Response("a", { status: 200 }),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
async onShutdown() {
|
|
241
|
+
// When plugin-A's teardown runs, plugin-B has already been torn
|
|
242
|
+
// down — but plugin-A's route must still be live because
|
|
243
|
+
// `onShutdown` fires *after* the route-unregister step for this
|
|
244
|
+
// plugin but *before* it leaves teardownPlugin. We check liveness
|
|
245
|
+
// from plugin-B's teardown instead (see below).
|
|
246
|
+
pluginAShutdown = true;
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
registerPlugin(
|
|
252
|
+
buildPlugin("plugin-b", {
|
|
253
|
+
routes: [
|
|
254
|
+
{
|
|
255
|
+
pattern: /^\/_plugin\/echo$/,
|
|
256
|
+
methods: ["GET"],
|
|
257
|
+
handler: async () => new Response("b", { status: 200 }),
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
async onShutdown() {
|
|
261
|
+
// Plugin-B's routes have already been unregistered by the time
|
|
262
|
+
// this fires, but plugin-A's route is still live — it only gets
|
|
263
|
+
// torn down after this hook returns and the loop moves on to
|
|
264
|
+
// plugin-A. Confirm the registry still has a matching route.
|
|
265
|
+
pluginBShutdown = true;
|
|
266
|
+
const matched = matchSkillRoute("/_plugin/echo", "GET");
|
|
267
|
+
expect(matched).not.toBeNull();
|
|
268
|
+
expect(matched!.kind).toBe("match");
|
|
269
|
+
},
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
await bootstrapPlugins(fakeCtx);
|
|
274
|
+
|
|
275
|
+
// Both plugins' routes landed in the registry; matching returns one of
|
|
276
|
+
// them (order defined by registration, but we only care that *some*
|
|
277
|
+
// route matches before shutdown starts).
|
|
278
|
+
expect(matchSkillRoute("/_plugin/echo", "GET")).not.toBeNull();
|
|
279
|
+
|
|
280
|
+
await runShutdownHooks("test-shutdown");
|
|
281
|
+
|
|
282
|
+
expect(pluginBShutdown).toBe(true);
|
|
283
|
+
expect(pluginAShutdown).toBe(true);
|
|
284
|
+
|
|
285
|
+
// After both plugins shut down, no routes remain.
|
|
286
|
+
expect(matchSkillRoute("/_plugin/echo", "GET")).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
});
|