@vellumai/assistant 0.8.8 → 0.8.9-dev.202606091926.ebb2d62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/docs/activation-funnel-telemetry.md +310 -0
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +273 -121
- package/package.json +2 -2
- package/scripts/sync-llm-catalog.ts +2 -0
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +6 -5
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/build-persisted-content.test.ts +75 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +146 -0
- package/src/__tests__/compaction-events.test.ts +7 -7
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +265 -125
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +7 -5
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +158 -234
- package/src/__tests__/conversation-agent-loop.test.ts +342 -623
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -5
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +6 -1
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +44 -4
- package/src/__tests__/conversation-process-callsite.test.ts +15 -15
- package/src/__tests__/conversation-provider-retry-repair.test.ts +66 -65
- package/src/__tests__/conversation-queue.test.ts +10 -10
- package/src/__tests__/conversation-runtime-assembly.test.ts +1059 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +7 -5
- package/src/__tests__/conversation-slash-unknown.test.ts +7 -5
- package/src/__tests__/conversation-speed-override.test.ts +11 -10
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +48 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +24 -21
- package/src/__tests__/conversation-workspace-injection.test.ts +69 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +15 -12
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +5 -4
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +17 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +93 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +5 -1
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +156 -38
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +91 -81
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-skill-contribution.test.ts +15 -14
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +245 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +31 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +5 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/tool-start-timestamp.test.ts +218 -0
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +188 -28
- package/src/acp/resolve-agent.ts +119 -42
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +403 -251
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/responses/conversation-message.ts +72 -13
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/approvals/guardian-request-resolvers.ts +27 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +790 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +381 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +270 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +233 -32
- package/src/cli/lib/install-from-github.ts +797 -60
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +416 -0
- package/src/cli/lib/plugin-marketplace.ts +215 -0
- package/src/cli/lib/search-plugins.ts +201 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +9 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +80 -41
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -3
- package/src/config/feature-flag-registry.json +10 -56
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/loader.ts +5 -22
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -21
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +17 -3
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/compactor.ts +22 -4
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/context/strip-injections.ts +8 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +120 -42
- package/src/daemon/conversation-agent-loop.ts +308 -1297
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +22 -141
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +361 -393
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +165 -11
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +444 -62
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +36 -46
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/server.ts +2 -0
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +6 -0
- package/src/memory/embedding-backend.ts +5 -22
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -63
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- 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 +9 -2
- 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/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +40 -11
- package/src/plugin-api/types.ts +108 -0
- package/src/plugins/defaults/compaction/compact.ts +106 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +250 -42
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +95 -25
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +156 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +285 -0
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +71 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +147 -134
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +456 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +662 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +91 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +74 -19
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +592 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -24
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +91 -0
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +76 -0
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +555 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +266 -0
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +71 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +16 -8
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +156 -0
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +382 -0
- package/src/plugins/defaults/memory-v3-shadow/types.ts +92 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +4 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/external-api.ts +12 -2
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +71 -4
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/model-catalog.ts +57 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +57 -7
- package/src/providers/types.ts +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +466 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +185 -32
- package/src/runtime/routes/acp-routes.ts +328 -30
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +30 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +65 -305
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/services/published-app-updater.ts +30 -8
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +9 -19
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +173 -202
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +32 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/memory/v3/__tests__/assign.test.ts +0 -242
- package/src/memory/v3/__tests__/capabilities.test.ts +0 -118
- package/src/memory/v3/__tests__/core.test.ts +0 -39
- package/src/memory/v3/__tests__/health.test.ts +0 -219
- package/src/memory/v3/__tests__/maintain-job.test.ts +0 -288
- package/src/memory/v3/__tests__/needle.test.ts +0 -107
- package/src/memory/v3/__tests__/orchestrate.test.ts +0 -436
- package/src/memory/v3/__tests__/provider-blocks.test.ts +0 -13
- package/src/memory/v3/__tests__/reconcile.test.ts +0 -274
- package/src/memory/v3/__tests__/render-injection.test.ts +0 -61
- package/src/memory/v3/__tests__/router.test.ts +0 -332
- package/src/memory/v3/__tests__/selector.test.ts +0 -470
- package/src/memory/v3/__tests__/shadow-plugin.test.ts +0 -432
- package/src/memory/v3/__tests__/snapshot.test.ts +0 -168
- package/src/memory/v3/__tests__/tree.test.ts +0 -192
- package/src/memory/v3/assign.ts +0 -268
- package/src/memory/v3/capabilities.ts +0 -124
- package/src/memory/v3/core.ts +0 -26
- package/src/memory/v3/health.ts +0 -0
- package/src/memory/v3/maintain-job.ts +0 -314
- package/src/memory/v3/needle.ts +0 -115
- package/src/memory/v3/orchestrate.ts +0 -126
- package/src/memory/v3/page-content.ts +0 -34
- package/src/memory/v3/provider-blocks.ts +0 -26
- package/src/memory/v3/reconcile.ts +0 -523
- package/src/memory/v3/router.ts +0 -190
- package/src/memory/v3/selection-log-store.ts +0 -84
- package/src/memory/v3/selector.ts +0 -226
- package/src/memory/v3/shadow-plugin.ts +0 -411
- package/src/memory/v3/snapshot.ts +0 -209
- package/src/memory/v3/tree.ts +0 -174
- package/src/memory/v3/types.ts +0 -59
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +0 -216
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- package/src/util/map-limit.ts +0 -27
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `orchestrate.ts` (section-lane pipeline).
|
|
3
|
+
*
|
|
4
|
+
* The orchestrator composes three deterministic candidate lanes — the
|
|
5
|
+
* section-grain BM25 needle, the dense lane, and link-graph edge expansion —
|
|
6
|
+
* into ONE unified pool, runs a SINGLE forced-tool select over the pool, then
|
|
7
|
+
* carries forward the working set. Synthetic capability pages (skills / CLI
|
|
8
|
+
* commands) are indexed like any other page, so they enter the pool through the
|
|
9
|
+
* lanes (e.g. the needle) rather than being always-added.
|
|
10
|
+
*
|
|
11
|
+
* The select provider is stubbed (no network); a single stub answers the one
|
|
12
|
+
* `select_pages` call per turn by reading the numbered `<candidates>` block.
|
|
13
|
+
* The dense lane is stubbed at the module boundary. The needle and edge graph
|
|
14
|
+
* are real, built from tiny inline fixtures so the pool union is exercised
|
|
15
|
+
* end-to-end.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
19
|
+
|
|
20
|
+
import type { PageIndexEntry } from "../../../../memory/v2/page-index.js";
|
|
21
|
+
import type {
|
|
22
|
+
Message,
|
|
23
|
+
Provider,
|
|
24
|
+
ProviderResponse,
|
|
25
|
+
} from "../../../../providers/types.js";
|
|
26
|
+
import type { EdgeGraph } from "../edge.js";
|
|
27
|
+
import { buildEdgeGraph } from "../edge.js";
|
|
28
|
+
import { buildSectionNeedle } from "../section-needle.js";
|
|
29
|
+
import { buildSectionIndex } from "../sections.js";
|
|
30
|
+
import type { MemoryRoutingTurn, SectionIndex, Slug } from "../types.js";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Module stubs installed BEFORE the orchestrator import so pool-select and the
|
|
34
|
+
// dense lane observe them at load time.
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
let providerStub: Provider | null = null;
|
|
38
|
+
|
|
39
|
+
mock.module("../../../../providers/provider-send-message.js", () => ({
|
|
40
|
+
getConfiguredProvider: async () => providerStub,
|
|
41
|
+
extractToolUse: (response: ProviderResponse) =>
|
|
42
|
+
response.content.find((b) => b.type === "tool_use"),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
mock.module("../../../../util/logger.js", () => ({
|
|
46
|
+
getLogger: () =>
|
|
47
|
+
new Proxy({} as Record<string, unknown>, {
|
|
48
|
+
get: (_t, prop) => (prop === "child" ? () => ({}) : () => {}),
|
|
49
|
+
}),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// The dense lane is stubbed: each test sets `denseHits` to control which
|
|
53
|
+
// articles (+ matched ordinals) the dense lane returns. The stub DELEGATES to
|
|
54
|
+
// the real `denseLane` unless this file's tests are running (`denseMockActive`),
|
|
55
|
+
// so the process-global `mock.module` cannot leak fake behavior into
|
|
56
|
+
// dense.test.ts (which exercises the real lane). Spread the real module so
|
|
57
|
+
// every other export (`OVERSAMPLE`) stays present.
|
|
58
|
+
const realDense = { ...(await import("../dense.js")) };
|
|
59
|
+
let denseMockActive = false;
|
|
60
|
+
let denseHits: Array<{ article: Slug; section: number }> = [];
|
|
61
|
+
mock.module("../dense.js", () => ({
|
|
62
|
+
...realDense,
|
|
63
|
+
denseLane: async (...args: Parameters<typeof realDense.denseLane>) =>
|
|
64
|
+
denseMockActive ? denseHits : realDense.denseLane(...args),
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
const { orchestrate, DEFAULT_NEEDLE_K, DEFAULT_DENSE_K } =
|
|
68
|
+
await import("../orchestrate.js");
|
|
69
|
+
const { WorkingSet } = await import("../working-set.js");
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Fixtures: a tiny corpus of pages with bodies + `links:` frontmatter.
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
const PAGES: Record<Slug, string> = {
|
|
76
|
+
"topic-a": "lead for topic a\n## Details\napple banana about topic a",
|
|
77
|
+
"topic-b": "lead for topic b\n## More\ncherry date about topic b",
|
|
78
|
+
"topic-c": "lead for topic c\n## Notes\nelderberry fig about topic c",
|
|
79
|
+
"topic-d": "lead for topic d\n## Notes\ngrape about topic d",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Raw page = frontmatter (with `links:`) + body. */
|
|
83
|
+
const RAW: Record<Slug, string> = {
|
|
84
|
+
"topic-a": `---\nlinks:\n - "topic-d — the curated edge from a to d"\n---\n${PAGES["topic-a"]}`,
|
|
85
|
+
"topic-b": `---\nedges: []\n---\n${PAGES["topic-b"]}`,
|
|
86
|
+
"topic-c": `---\nedges: []\n---\n${PAGES["topic-c"]}`,
|
|
87
|
+
"topic-d": `---\nedges: []\n---\n${PAGES["topic-d"]}`,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const SLUGS = Object.keys(PAGES);
|
|
91
|
+
|
|
92
|
+
function makeEntries(): PageIndexEntry[] {
|
|
93
|
+
return SLUGS.map((slug, i) => ({
|
|
94
|
+
id: i + 1,
|
|
95
|
+
slug,
|
|
96
|
+
summary: `summary of ${slug}`,
|
|
97
|
+
edges: [],
|
|
98
|
+
leaves: [],
|
|
99
|
+
modifiedAt: 0,
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function buildLanes(): Promise<{
|
|
104
|
+
sectionIndex: SectionIndex;
|
|
105
|
+
needle: ReturnType<typeof buildSectionNeedle>;
|
|
106
|
+
edgeGraph: EdgeGraph;
|
|
107
|
+
}> {
|
|
108
|
+
const sectionIndex = await buildSectionIndex(SLUGS, async (s) => PAGES[s]!);
|
|
109
|
+
const needle = buildSectionNeedle(sectionIndex);
|
|
110
|
+
const edgeGraph = await buildEdgeGraph(makeEntries(), async (s) => RAW[s]!);
|
|
111
|
+
return { sectionIndex, needle, edgeGraph };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const config = {} as never;
|
|
115
|
+
|
|
116
|
+
function makeTurn(
|
|
117
|
+
turnNumber: number,
|
|
118
|
+
currentMessage: string,
|
|
119
|
+
): MemoryRoutingTurn {
|
|
120
|
+
return {
|
|
121
|
+
conversationId: "conv-xyz",
|
|
122
|
+
turnNumber,
|
|
123
|
+
currentMessage,
|
|
124
|
+
recentContext: "prior context",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function toolUseResponse(input: Record<string, unknown>): ProviderResponse {
|
|
129
|
+
return {
|
|
130
|
+
model: "stub-model",
|
|
131
|
+
stopReason: "tool_use",
|
|
132
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
133
|
+
content: [{ type: "tool_use", id: "tu-1", name: "select_pages", input }],
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Parse the numbered `<candidates>` block back into an ordered slug list. */
|
|
138
|
+
function candidateSlugs(messages: Message[]): Slug[] {
|
|
139
|
+
for (const msg of messages) {
|
|
140
|
+
for (const block of msg.content) {
|
|
141
|
+
if (block.type !== "text") continue;
|
|
142
|
+
const m = /<candidates>\n([\s\S]*?)\n<\/candidates>/.exec(block.text);
|
|
143
|
+
if (!m) continue;
|
|
144
|
+
return m[1]
|
|
145
|
+
.split("\n")
|
|
146
|
+
.map((line) => /^\[\d+\] (\S+) —/.exec(line)?.[1])
|
|
147
|
+
.filter((s): s is string => !!s);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Provider that selects the pool candidates whose slug is in `keep` (mapping
|
|
155
|
+
* each back to its 1-based id), pinning those in `pin`. Captures the rendered
|
|
156
|
+
* candidate list for pool assertions.
|
|
157
|
+
*/
|
|
158
|
+
let lastPool: Slug[] = [];
|
|
159
|
+
let selectCalls = 0;
|
|
160
|
+
function selectProvider(keep: Slug[], pin: Slug[] = []): Provider {
|
|
161
|
+
return {
|
|
162
|
+
name: "stub",
|
|
163
|
+
sendMessage: async (messages) => {
|
|
164
|
+
selectCalls++;
|
|
165
|
+
const pool = candidateSlugs(messages);
|
|
166
|
+
lastPool = pool;
|
|
167
|
+
const ids: number[] = [];
|
|
168
|
+
const pinned_ids: number[] = [];
|
|
169
|
+
pool.forEach((slug, i) => {
|
|
170
|
+
if (keep.includes(slug)) ids.push(i + 1);
|
|
171
|
+
if (pin.includes(slug)) pinned_ids.push(i + 1);
|
|
172
|
+
});
|
|
173
|
+
return toolUseResponse({ ids, pinned_ids });
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
beforeEach(() => {
|
|
179
|
+
denseMockActive = true;
|
|
180
|
+
providerStub = null;
|
|
181
|
+
denseHits = [];
|
|
182
|
+
lastPool = [];
|
|
183
|
+
selectCalls = 0;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
afterAll(() => {
|
|
187
|
+
denseMockActive = false;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Pool composition: the candidate pool is the union of the lanes. Synthetic
|
|
192
|
+
// capability pages are no longer always-added — they enter through a lane (see
|
|
193
|
+
// the dedicated test below).
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
describe("orchestrate — candidate pool composition", () => {
|
|
197
|
+
test("pool unions needle ∪ dense ∪ edge; one select runs", async () => {
|
|
198
|
+
const lanes = await buildLanes();
|
|
199
|
+
// "apple" hits topic-a (needle). Dense returns topic-b. topic-a links to
|
|
200
|
+
// topic-d (edge).
|
|
201
|
+
denseHits = [{ article: "topic-b", section: 0 }];
|
|
202
|
+
providerStub = selectProvider([]); // selection is irrelevant to pool union
|
|
203
|
+
|
|
204
|
+
await orchestrate(makeTurn(1, "apple"), {
|
|
205
|
+
sectionIndex: lanes.sectionIndex,
|
|
206
|
+
needle: lanes.needle,
|
|
207
|
+
denseConfig: config,
|
|
208
|
+
edgeGraph: lanes.edgeGraph,
|
|
209
|
+
workingSet: new WorkingSet(),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(selectCalls).toBe(1);
|
|
213
|
+
expect(new Set(lastPool)).toEqual(
|
|
214
|
+
new Set(["topic-a", "topic-b", "topic-d"]),
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a synthetic capability page enters the pool via the needle lane", async () => {
|
|
219
|
+
// A capability slug indexed with body content (the section index treats it
|
|
220
|
+
// like any other page) is ranked by the real needle when the query matches
|
|
221
|
+
// its text — this is how synthetic pages reach the pool now that they are no
|
|
222
|
+
// longer always-added.
|
|
223
|
+
const CAP: Slug = "skills/example";
|
|
224
|
+
const sectionIndex = await buildSectionIndex([...SLUGS, CAP], async (s) =>
|
|
225
|
+
s === CAP
|
|
226
|
+
? "# Skill: example\nuse the kumquat skill to do the thing"
|
|
227
|
+
: PAGES[s]!,
|
|
228
|
+
);
|
|
229
|
+
const needle = buildSectionNeedle(sectionIndex);
|
|
230
|
+
const edgeGraph = await buildEdgeGraph(makeEntries(), async (s) => RAW[s]!);
|
|
231
|
+
|
|
232
|
+
providerStub = selectProvider([]); // selection irrelevant to pool union
|
|
233
|
+
await orchestrate(makeTurn(1, "kumquat"), {
|
|
234
|
+
sectionIndex,
|
|
235
|
+
needle,
|
|
236
|
+
denseConfig: config,
|
|
237
|
+
edgeGraph,
|
|
238
|
+
workingSet: new WorkingSet(),
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// The needle ranked the capability page on the "kumquat" term, so it is in
|
|
242
|
+
// the candidate pool.
|
|
243
|
+
expect(lastPool).toContain(CAP);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("edge curated link description becomes the edge candidate's descriptor", async () => {
|
|
247
|
+
const lanes = await buildLanes();
|
|
248
|
+
let descriptorLine = "";
|
|
249
|
+
providerStub = {
|
|
250
|
+
name: "stub",
|
|
251
|
+
sendMessage: async (messages) => {
|
|
252
|
+
for (const msg of messages) {
|
|
253
|
+
for (const block of msg.content) {
|
|
254
|
+
if (block.type !== "text") continue;
|
|
255
|
+
const m = /\[\d+\] topic-d — ([^\n]+)/.exec(block.text);
|
|
256
|
+
if (m) descriptorLine = m[1]!;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return toolUseResponse({ ids: [] });
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
await orchestrate(makeTurn(1, "apple"), {
|
|
264
|
+
sectionIndex: lanes.sectionIndex,
|
|
265
|
+
needle: lanes.needle,
|
|
266
|
+
denseConfig: config,
|
|
267
|
+
edgeGraph: lanes.edgeGraph,
|
|
268
|
+
workingSet: new WorkingSet(),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
expect(descriptorLine).toContain("the curated edge from a to d");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("needleK and denseK default to their constants", async () => {
|
|
275
|
+
const lanes = await buildLanes();
|
|
276
|
+
let needleK = -1;
|
|
277
|
+
const needle = {
|
|
278
|
+
query: (_t: string, k: number) => {
|
|
279
|
+
needleK = k;
|
|
280
|
+
return [];
|
|
281
|
+
},
|
|
282
|
+
bestSection: () => -1,
|
|
283
|
+
};
|
|
284
|
+
providerStub = selectProvider([]);
|
|
285
|
+
await orchestrate(makeTurn(1, "x"), {
|
|
286
|
+
sectionIndex: lanes.sectionIndex,
|
|
287
|
+
needle,
|
|
288
|
+
denseConfig: config,
|
|
289
|
+
edgeGraph: lanes.edgeGraph,
|
|
290
|
+
workingSet: new WorkingSet(),
|
|
291
|
+
});
|
|
292
|
+
expect(needleK).toBe(DEFAULT_NEEDLE_K);
|
|
293
|
+
expect(DEFAULT_DENSE_K).toBe(100);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("sectionBySlug is populated from matched lane sections", async () => {
|
|
297
|
+
const lanes = await buildLanes();
|
|
298
|
+
denseHits = [];
|
|
299
|
+
providerStub = selectProvider(["topic-a"]);
|
|
300
|
+
const result = await orchestrate(makeTurn(1, "apple"), {
|
|
301
|
+
sectionIndex: lanes.sectionIndex,
|
|
302
|
+
needle: lanes.needle,
|
|
303
|
+
denseConfig: config,
|
|
304
|
+
edgeGraph: lanes.edgeGraph,
|
|
305
|
+
workingSet: new WorkingSet(),
|
|
306
|
+
});
|
|
307
|
+
// topic-a matched "apple" in its `## Details` section.
|
|
308
|
+
expect(result.sectionBySlug.get("topic-a")?.article).toBe("topic-a");
|
|
309
|
+
expect(result.sectionBySlug.get("topic-a")?.text).toContain("apple");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
// Edge-only injection: a page surfaced ONLY by the edge lane (the query did not
|
|
315
|
+
// lexically hit it) records NO matched section, so injection falls back to the
|
|
316
|
+
// FULL page (the curated `links` description — not the often-empty lead the
|
|
317
|
+
// query never hit — is what made the candidate relevant). The best-section text
|
|
318
|
+
// is kept only as the select-pool descriptor fallback.
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
describe("orchestrate — edge-only injection", () => {
|
|
322
|
+
test("an edge-only page records NO sectionBySlug entry (→ full-page inject)", async () => {
|
|
323
|
+
const lanes = await buildLanes();
|
|
324
|
+
// "apple" hits topic-a (needle); topic-a links to topic-d (edge-only — the
|
|
325
|
+
// query never hits topic-d). Select topic-d so it is in the result.
|
|
326
|
+
denseHits = [];
|
|
327
|
+
providerStub = selectProvider(["topic-d"]);
|
|
328
|
+
const result = await orchestrate(makeTurn(1, "apple"), {
|
|
329
|
+
sectionIndex: lanes.sectionIndex,
|
|
330
|
+
needle: lanes.needle,
|
|
331
|
+
denseConfig: config,
|
|
332
|
+
edgeGraph: lanes.edgeGraph,
|
|
333
|
+
workingSet: new WorkingSet(),
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// topic-d was selected and injected, but with NO matched section — so
|
|
337
|
+
// `renderV3SectionContent(slug, undefined)` falls back to the full page.
|
|
338
|
+
expect(result.finalInjection).toContain("topic-d");
|
|
339
|
+
expect(result.sectionBySlug.has("topic-d")).toBe(false);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("an edge-only page still carries the curated links description as its descriptor", async () => {
|
|
343
|
+
const lanes = await buildLanes();
|
|
344
|
+
let descriptorLine = "";
|
|
345
|
+
providerStub = {
|
|
346
|
+
name: "stub",
|
|
347
|
+
sendMessage: async (messages) => {
|
|
348
|
+
for (const msg of messages) {
|
|
349
|
+
for (const block of msg.content) {
|
|
350
|
+
if (block.type !== "text") continue;
|
|
351
|
+
const m = /\[\d+\] topic-d — ([^\n]+)/.exec(block.text);
|
|
352
|
+
if (m) descriptorLine = m[1]!;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return toolUseResponse({ ids: [] });
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
await orchestrate(makeTurn(1, "apple"), {
|
|
360
|
+
sectionIndex: lanes.sectionIndex,
|
|
361
|
+
needle: lanes.needle,
|
|
362
|
+
denseConfig: config,
|
|
363
|
+
edgeGraph: lanes.edgeGraph,
|
|
364
|
+
workingSet: new WorkingSet(),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// The curated `links` description (not the lead text) is the descriptor.
|
|
368
|
+
expect(descriptorLine).toContain("the curated edge from a to d");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("an edge-only page with no curated description falls back to bestSection text as the descriptor", async () => {
|
|
372
|
+
// A bare `links:` entry (no ` — `) carries NO description, so the edge
|
|
373
|
+
// candidate's descriptor falls back to the page's best section against the
|
|
374
|
+
// query. The query never hits dst-page, so bestSection returns its lead;
|
|
375
|
+
// that lead text becomes the descriptor (and the page is still injected in
|
|
376
|
+
// full, with no sectionBySlug entry).
|
|
377
|
+
const pages: Record<Slug, string> = {
|
|
378
|
+
"src-page": "lead for src\n## Body\nalpha bravo about src",
|
|
379
|
+
"dst-page": "lead content for dst page\n## Extra\nnothing relevant here",
|
|
380
|
+
};
|
|
381
|
+
const raw: Record<Slug, string> = {
|
|
382
|
+
// bare slug — no ` — ` separator → undefined curated description.
|
|
383
|
+
"src-page": `---\nlinks:\n - "dst-page"\n---\n${pages["src-page"]}`,
|
|
384
|
+
"dst-page": `---\nedges: []\n---\n${pages["dst-page"]}`,
|
|
385
|
+
};
|
|
386
|
+
const slugs = Object.keys(pages);
|
|
387
|
+
const entries: PageIndexEntry[] = slugs.map((slug, i) => ({
|
|
388
|
+
id: i + 1,
|
|
389
|
+
slug,
|
|
390
|
+
summary: `summary of ${slug}`,
|
|
391
|
+
edges: [],
|
|
392
|
+
leaves: [],
|
|
393
|
+
modifiedAt: 0,
|
|
394
|
+
}));
|
|
395
|
+
const sectionIndex = await buildSectionIndex(slugs, async (s) => pages[s]!);
|
|
396
|
+
const needle = buildSectionNeedle(sectionIndex);
|
|
397
|
+
const edgeGraph = await buildEdgeGraph(entries, async (s) => raw[s]!);
|
|
398
|
+
|
|
399
|
+
let descriptorLine = "";
|
|
400
|
+
providerStub = {
|
|
401
|
+
name: "stub",
|
|
402
|
+
sendMessage: async (messages) => {
|
|
403
|
+
for (const msg of messages) {
|
|
404
|
+
for (const block of msg.content) {
|
|
405
|
+
if (block.type !== "text") continue;
|
|
406
|
+
const m = /\[\d+\] dst-page — ([^\n]+)/.exec(block.text);
|
|
407
|
+
if (m) descriptorLine = m[1]!;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return toolUseResponse({ ids: [] });
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// "alpha" hits src-page (needle); src-page links to dst-page (edge-only, no
|
|
415
|
+
// curated description).
|
|
416
|
+
const result = await orchestrate(makeTurn(1, "alpha"), {
|
|
417
|
+
sectionIndex,
|
|
418
|
+
needle,
|
|
419
|
+
denseConfig: config,
|
|
420
|
+
edgeGraph,
|
|
421
|
+
workingSet: new WorkingSet(),
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
// Descriptor fell back to dst-page's lead text; still no matched section.
|
|
425
|
+
expect(descriptorLine).toContain("lead content for dst page");
|
|
426
|
+
expect(result.sectionBySlug.has("dst-page")).toBe(false);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// ---------------------------------------------------------------------------
|
|
431
|
+
// Dense liveness: a dense hit whose article is no longer in the live section
|
|
432
|
+
// index (its page was deleted; its Qdrant points linger) is dropped from the
|
|
433
|
+
// pool, so a deleted page can never be surfaced via the dense lane.
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
describe("orchestrate — dense liveness filter", () => {
|
|
437
|
+
test("a dense hit absent from sectionIndex.byArticle is dropped from the pool", async () => {
|
|
438
|
+
const lanes = await buildLanes();
|
|
439
|
+
// Dense returns a live page (topic-b) AND a deleted page (gone-page) whose
|
|
440
|
+
// points still linger in Qdrant but which is absent from the section index.
|
|
441
|
+
denseHits = [
|
|
442
|
+
{ article: "topic-b", section: 0 },
|
|
443
|
+
{ article: "gone-page", section: 0 },
|
|
444
|
+
];
|
|
445
|
+
providerStub = selectProvider([]); // selection irrelevant to pool membership
|
|
446
|
+
const result = await orchestrate(makeTurn(1, "apple"), {
|
|
447
|
+
sectionIndex: lanes.sectionIndex,
|
|
448
|
+
needle: lanes.needle,
|
|
449
|
+
denseConfig: config,
|
|
450
|
+
edgeGraph: lanes.edgeGraph,
|
|
451
|
+
workingSet: new WorkingSet(),
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// The live dense hit is pooled; the deleted page is dropped entirely.
|
|
455
|
+
expect(lastPool).toContain("topic-b");
|
|
456
|
+
expect(lastPool).not.toContain("gone-page");
|
|
457
|
+
expect(result.laneBySlug.has("gone-page")).toBe(false);
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// ---------------------------------------------------------------------------
|
|
462
|
+
// Lane provenance: each pooled slug records the candgen lane that FIRST
|
|
463
|
+
// surfaced it (needle → dense → edge precedence), exposed via
|
|
464
|
+
// `result.laneBySlug` so the selection telemetry can attribute true sources.
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
describe("orchestrate — lane provenance (laneBySlug)", () => {
|
|
468
|
+
test("laneBySlug tags each pooled slug with its surfacing lane", async () => {
|
|
469
|
+
const lanes = await buildLanes();
|
|
470
|
+
// "apple" hits topic-a (needle). Dense returns topic-b. topic-a links to
|
|
471
|
+
// topic-d (edge). So each lane contributes exactly one distinct slug.
|
|
472
|
+
denseHits = [{ article: "topic-b", section: 0 }];
|
|
473
|
+
providerStub = selectProvider([]); // selection irrelevant to pool provenance
|
|
474
|
+
const result = await orchestrate(makeTurn(1, "apple"), {
|
|
475
|
+
sectionIndex: lanes.sectionIndex,
|
|
476
|
+
needle: lanes.needle,
|
|
477
|
+
denseConfig: config,
|
|
478
|
+
edgeGraph: lanes.edgeGraph,
|
|
479
|
+
workingSet: new WorkingSet(),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
expect(result.laneBySlug.get("topic-a")).toBe("needle");
|
|
483
|
+
expect(result.laneBySlug.get("topic-b")).toBe("dense");
|
|
484
|
+
expect(result.laneBySlug.get("topic-d")).toBe("edge");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("a slug surfaced by needle AND dense keeps the needle lane (first wins)", async () => {
|
|
488
|
+
const lanes = await buildLanes();
|
|
489
|
+
// topic-a is surfaced by the needle on "apple"; dense ALSO returns topic-a.
|
|
490
|
+
// Needle runs first (step 2a before 2b), so the recorded lane stays needle.
|
|
491
|
+
denseHits = [{ article: "topic-a", section: 0 }];
|
|
492
|
+
providerStub = selectProvider([]);
|
|
493
|
+
const result = await orchestrate(makeTurn(1, "apple"), {
|
|
494
|
+
sectionIndex: lanes.sectionIndex,
|
|
495
|
+
needle: lanes.needle,
|
|
496
|
+
denseConfig: config,
|
|
497
|
+
edgeGraph: lanes.edgeGraph,
|
|
498
|
+
workingSet: new WorkingSet(),
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
expect(result.laneBySlug.get("topic-a")).toBe("needle");
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Carry-forward: same working-set semantics as the prior tree pipeline.
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
describe("orchestrate — carry-forward", () => {
|
|
510
|
+
test("a page selected+pinned in turn 1 carries into turn 2 without re-selection", async () => {
|
|
511
|
+
const lanes = await buildLanes();
|
|
512
|
+
const workingSet = new WorkingSet();
|
|
513
|
+
|
|
514
|
+
// Turn 1 selects+pins topic-a.
|
|
515
|
+
providerStub = selectProvider(["topic-a"], ["topic-a"]);
|
|
516
|
+
await orchestrate(makeTurn(1, "apple"), {
|
|
517
|
+
sectionIndex: lanes.sectionIndex,
|
|
518
|
+
needle: lanes.needle,
|
|
519
|
+
denseConfig: config,
|
|
520
|
+
edgeGraph: lanes.edgeGraph,
|
|
521
|
+
workingSet,
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
// Turn 2 selects a DIFFERENT page and never re-selects topic-a.
|
|
525
|
+
denseHits = [{ article: "topic-b", section: 0 }];
|
|
526
|
+
providerStub = selectProvider(["topic-b"]);
|
|
527
|
+
const t2 = await orchestrate(makeTurn(2, "cherry"), {
|
|
528
|
+
sectionIndex: lanes.sectionIndex,
|
|
529
|
+
needle: lanes.needle,
|
|
530
|
+
denseConfig: config,
|
|
531
|
+
edgeGraph: lanes.edgeGraph,
|
|
532
|
+
workingSet,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
expect(t2.currentSelections.map((s) => s.slug)).not.toContain("topic-a");
|
|
536
|
+
expect(t2.finalInjection).toContain("topic-a");
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
test("carry-forward survives a turn whose selections fill the cap", async () => {
|
|
540
|
+
const lanes = await buildLanes();
|
|
541
|
+
// Cap of 1: under a naive record-then-cap order this turn's own selection
|
|
542
|
+
// would evict the carried page before injection. Snapshotting the carry
|
|
543
|
+
// BEFORE recording this turn keeps the earlier page in the injection.
|
|
544
|
+
const workingSet = new WorkingSet(1);
|
|
545
|
+
|
|
546
|
+
providerStub = selectProvider(["topic-a"]); // turn 1 → topic-a
|
|
547
|
+
await orchestrate(makeTurn(1, "apple"), {
|
|
548
|
+
sectionIndex: lanes.sectionIndex,
|
|
549
|
+
needle: lanes.needle,
|
|
550
|
+
denseConfig: config,
|
|
551
|
+
edgeGraph: lanes.edgeGraph,
|
|
552
|
+
workingSet,
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
denseHits = [{ article: "topic-b", section: 0 }];
|
|
556
|
+
providerStub = selectProvider(["topic-b"]); // turn 2 → topic-b
|
|
557
|
+
const t2 = await orchestrate(makeTurn(2, "cherry"), {
|
|
558
|
+
sectionIndex: lanes.sectionIndex,
|
|
559
|
+
needle: lanes.needle,
|
|
560
|
+
denseConfig: config,
|
|
561
|
+
edgeGraph: lanes.edgeGraph,
|
|
562
|
+
workingSet,
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
expect(t2.currentSelections.map((s) => s.slug)).toEqual(["topic-b"]);
|
|
566
|
+
expect(t2.finalInjection).toContain("topic-a"); // carried despite the cap
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
test("a stale non-pinned page ages out of the carry-forward window", async () => {
|
|
570
|
+
const lanes = await buildLanes();
|
|
571
|
+
// window 2: a non-pinned entry unseen for >2 turns evicts.
|
|
572
|
+
const workingSet = new WorkingSet(150, 2);
|
|
573
|
+
|
|
574
|
+
providerStub = selectProvider(["topic-a"]); // turn 1 selects topic-a
|
|
575
|
+
const t1 = await orchestrate(makeTurn(1, "apple"), {
|
|
576
|
+
sectionIndex: lanes.sectionIndex,
|
|
577
|
+
needle: lanes.needle,
|
|
578
|
+
denseConfig: config,
|
|
579
|
+
edgeGraph: lanes.edgeGraph,
|
|
580
|
+
workingSet,
|
|
581
|
+
});
|
|
582
|
+
expect(t1.finalInjection).toContain("topic-a");
|
|
583
|
+
|
|
584
|
+
// Turns 2–4 never re-select topic-a. By turn 4 (4-1=3 > 2) it ages out.
|
|
585
|
+
providerStub = selectProvider([]);
|
|
586
|
+
await orchestrate(makeTurn(2, "x"), {
|
|
587
|
+
sectionIndex: lanes.sectionIndex,
|
|
588
|
+
needle: lanes.needle,
|
|
589
|
+
denseConfig: config,
|
|
590
|
+
edgeGraph: lanes.edgeGraph,
|
|
591
|
+
workingSet,
|
|
592
|
+
});
|
|
593
|
+
await orchestrate(makeTurn(3, "x"), {
|
|
594
|
+
sectionIndex: lanes.sectionIndex,
|
|
595
|
+
needle: lanes.needle,
|
|
596
|
+
denseConfig: config,
|
|
597
|
+
edgeGraph: lanes.edgeGraph,
|
|
598
|
+
workingSet,
|
|
599
|
+
});
|
|
600
|
+
const t4 = await orchestrate(makeTurn(4, "x"), {
|
|
601
|
+
sectionIndex: lanes.sectionIndex,
|
|
602
|
+
needle: lanes.needle,
|
|
603
|
+
denseConfig: config,
|
|
604
|
+
edgeGraph: lanes.edgeGraph,
|
|
605
|
+
workingSet,
|
|
606
|
+
});
|
|
607
|
+
expect(t4.finalInjection).not.toContain("topic-a");
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("pinned current-turn selections land in the working set", async () => {
|
|
611
|
+
const lanes = await buildLanes();
|
|
612
|
+
const ws = new WorkingSet();
|
|
613
|
+
providerStub = selectProvider(["topic-a"], ["topic-a"]);
|
|
614
|
+
await orchestrate(makeTurn(1, "apple"), {
|
|
615
|
+
sectionIndex: lanes.sectionIndex,
|
|
616
|
+
needle: lanes.needle,
|
|
617
|
+
denseConfig: config,
|
|
618
|
+
edgeGraph: lanes.edgeGraph,
|
|
619
|
+
workingSet: ws,
|
|
620
|
+
});
|
|
621
|
+
expect(ws.union().has("topic-a")).toBe(true);
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// Degradation: an empty pool and a provider-unavailable path are recall-safe.
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
|
|
629
|
+
describe("orchestrate — degradation", () => {
|
|
630
|
+
test("an empty pool yields no selections and an empty injection", async () => {
|
|
631
|
+
const lanes = await buildLanes();
|
|
632
|
+
providerStub = selectProvider([]);
|
|
633
|
+
const result = await orchestrate(makeTurn(1, "zzzzz no-match"), {
|
|
634
|
+
sectionIndex: lanes.sectionIndex,
|
|
635
|
+
needle: { query: () => [], bestSection: () => -1 },
|
|
636
|
+
denseConfig: config,
|
|
637
|
+
edgeGraph: lanes.edgeGraph,
|
|
638
|
+
workingSet: new WorkingSet(),
|
|
639
|
+
});
|
|
640
|
+
expect(result.currentSelections).toEqual([]);
|
|
641
|
+
expect(result.finalInjection).toEqual([]);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test("omitted ids keeps ALL pooled candidates (recall-safe)", async () => {
|
|
645
|
+
const lanes = await buildLanes();
|
|
646
|
+
denseHits = [{ article: "topic-b", section: 0 }];
|
|
647
|
+
providerStub = {
|
|
648
|
+
name: "stub",
|
|
649
|
+
sendMessage: async () => toolUseResponse({}), // omitted ids → keep all
|
|
650
|
+
};
|
|
651
|
+
const result = await orchestrate(makeTurn(1, "apple"), {
|
|
652
|
+
sectionIndex: lanes.sectionIndex,
|
|
653
|
+
needle: lanes.needle,
|
|
654
|
+
denseConfig: config,
|
|
655
|
+
edgeGraph: lanes.edgeGraph,
|
|
656
|
+
workingSet: new WorkingSet(),
|
|
657
|
+
});
|
|
658
|
+
expect(new Set(result.finalInjection)).toEqual(
|
|
659
|
+
new Set(["topic-a", "topic-b", "topic-d"]),
|
|
660
|
+
);
|
|
661
|
+
});
|
|
662
|
+
});
|