@vellumai/assistant 0.11.3 → 0.11.4-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +140 -38
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +2 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +55 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +82 -11
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +9 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +13 -3
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-intents.ts +26 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +63 -0
- package/src/tts/reasoning-tag-filter.ts +110 -0
- package/src/workspace/byok-default-profile-ensure.ts +61 -18
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -25,7 +25,7 @@ import { createConversation } from "../../../persistence/conversation-crud.js";
|
|
|
25
25
|
import { getDb } from "../../../persistence/db-connection.js";
|
|
26
26
|
import { initializeDb } from "../../../persistence/db-init.js";
|
|
27
27
|
import { createGroup } from "../../../persistence/group-crud.js";
|
|
28
|
-
import { rawRun } from "../../../persistence/raw-query.js";
|
|
28
|
+
import { rawExec, rawRun } from "../../../persistence/raw-query.js";
|
|
29
29
|
import {
|
|
30
30
|
conversationAssistantAttentionState,
|
|
31
31
|
conversationAttentionEvents,
|
|
@@ -782,3 +782,221 @@ describe("GET /v1/conversations/unread-count", () => {
|
|
|
782
782
|
expect(invokeUnreadCount()).toEqual({ count: 1 });
|
|
783
783
|
});
|
|
784
784
|
});
|
|
785
|
+
|
|
786
|
+
describe("GET /v1/conversations/sections", () => {
|
|
787
|
+
const sectionsHandler = findHandler(
|
|
788
|
+
CONVERSATION_LIST_ROUTES,
|
|
789
|
+
"getConversationSections",
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
interface SectionRow {
|
|
793
|
+
kind: "pinned" | "group" | "channel" | "chats";
|
|
794
|
+
groupId?: string;
|
|
795
|
+
name?: string;
|
|
796
|
+
icon?: string | null;
|
|
797
|
+
sortPosition?: number;
|
|
798
|
+
channelId?: string;
|
|
799
|
+
total: number;
|
|
800
|
+
unread: number;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function invokeSections(): SectionRow[] {
|
|
804
|
+
return (sectionsHandler({}) as { sections: SectionRow[] }).sections;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function fileIntoGroup(conversationId: string, groupId: string): void {
|
|
808
|
+
rawRun(
|
|
809
|
+
"test:fileIntoGroup",
|
|
810
|
+
"UPDATE conversations SET group_id = ? WHERE id = ?",
|
|
811
|
+
groupId,
|
|
812
|
+
conversationId,
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function stampChannel(conversationId: string, channel: string): void {
|
|
817
|
+
rawRun(
|
|
818
|
+
"test:stampChannel",
|
|
819
|
+
"UPDATE conversations SET origin_channel = ? WHERE id = ?",
|
|
820
|
+
channel,
|
|
821
|
+
conversationId,
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function pin(conversationId: string): void {
|
|
826
|
+
rawRun(
|
|
827
|
+
"test:pinConversation",
|
|
828
|
+
"UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
|
|
829
|
+
conversationId,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function seedUnseen(conversationId: string): void {
|
|
834
|
+
projectAssistantMessage({
|
|
835
|
+
conversationId,
|
|
836
|
+
messageId: `msg-${conversationId}`,
|
|
837
|
+
messageAt: Date.now(),
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
beforeEach(() => {
|
|
842
|
+
clearConversations();
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test("an empty conversation table yields Chats alone, at zero", () => {
|
|
846
|
+
// Chats is the leftover bucket and renders regardless, so its counts
|
|
847
|
+
// are part of the contract even when nothing exists.
|
|
848
|
+
expect(invokeSections()).toEqual([{ kind: "chats", total: 0, unread: 0 }]);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
test("every section kind appears with its own totals", () => {
|
|
852
|
+
const pinned = createConversation({ title: "pinned-1" });
|
|
853
|
+
pin(pinned.id);
|
|
854
|
+
const group = createGroup("Sections Spread Group");
|
|
855
|
+
fileIntoGroup(createConversation({ title: "g-1" }).id, group.id);
|
|
856
|
+
fileIntoGroup(createConversation({ title: "g-2" }).id, group.id);
|
|
857
|
+
stampChannel(createConversation({ title: "slack-1" }).id, "slack");
|
|
858
|
+
createConversation({ title: "native-unstamped" });
|
|
859
|
+
stampChannel(createConversation({ title: "native-stamped" }).id, "vellum");
|
|
860
|
+
|
|
861
|
+
const sections = invokeSections();
|
|
862
|
+
|
|
863
|
+
expect(sections).toContainEqual({ kind: "pinned", total: 1, unread: 0 });
|
|
864
|
+
expect(sections).toContainEqual({
|
|
865
|
+
kind: "group",
|
|
866
|
+
groupId: group.id,
|
|
867
|
+
name: "Sections Spread Group",
|
|
868
|
+
icon: null,
|
|
869
|
+
sortPosition: group.sortPosition,
|
|
870
|
+
total: 2,
|
|
871
|
+
unread: 0,
|
|
872
|
+
});
|
|
873
|
+
expect(sections).toContainEqual({
|
|
874
|
+
kind: "channel",
|
|
875
|
+
channelId: "slack",
|
|
876
|
+
total: 1,
|
|
877
|
+
unread: 0,
|
|
878
|
+
});
|
|
879
|
+
// NULL origin_channel reads as native, exactly as the list filter reads
|
|
880
|
+
// it, so the stamped and unstamped native rows share the Chats bucket.
|
|
881
|
+
expect(sections).toContainEqual({ kind: "chats", total: 2, unread: 0 });
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
test("unread counts follow the seen state per section", () => {
|
|
885
|
+
const pinned = createConversation({ title: "pinned-seen" });
|
|
886
|
+
pin(pinned.id);
|
|
887
|
+
seedUnseen(pinned.id);
|
|
888
|
+
recordConversationSeenSignal({
|
|
889
|
+
conversationId: pinned.id,
|
|
890
|
+
sourceChannel: "vellum",
|
|
891
|
+
signalType: "macos_conversation_opened",
|
|
892
|
+
confidence: "explicit",
|
|
893
|
+
source: "test",
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
const group = createGroup("Sections Unread Group");
|
|
897
|
+
const unreadInGroup = createConversation({ title: "g-unread" });
|
|
898
|
+
fileIntoGroup(unreadInGroup.id, group.id);
|
|
899
|
+
seedUnseen(unreadInGroup.id);
|
|
900
|
+
// A member with no attention projection at all reads as seen.
|
|
901
|
+
fileIntoGroup(createConversation({ title: "g-quiet" }).id, group.id);
|
|
902
|
+
|
|
903
|
+
const chatsUnread = createConversation({ title: "chat-unread" });
|
|
904
|
+
seedUnseen(chatsUnread.id);
|
|
905
|
+
|
|
906
|
+
const sections = invokeSections();
|
|
907
|
+
|
|
908
|
+
expect(sections).toContainEqual({ kind: "pinned", total: 1, unread: 0 });
|
|
909
|
+
expect(sections).toContainEqual({
|
|
910
|
+
kind: "group",
|
|
911
|
+
groupId: group.id,
|
|
912
|
+
name: "Sections Unread Group",
|
|
913
|
+
icon: null,
|
|
914
|
+
sortPosition: group.sortPosition,
|
|
915
|
+
total: 2,
|
|
916
|
+
unread: 1,
|
|
917
|
+
});
|
|
918
|
+
expect(sections).toContainEqual({ kind: "chats", total: 1, unread: 1 });
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test("a background row filed into a group counts toward total but never unread", () => {
|
|
922
|
+
// Mirrors the unread-count contract: the custom-group visibility arm
|
|
923
|
+
// admits the row into the section, and the not-background unread rule
|
|
924
|
+
// keeps it out of the badge.
|
|
925
|
+
const group = createGroup("Sections Background Group");
|
|
926
|
+
const bg = createConversation({
|
|
927
|
+
title: "bg-in-group",
|
|
928
|
+
conversationType: "background",
|
|
929
|
+
});
|
|
930
|
+
fileIntoGroup(bg.id, group.id);
|
|
931
|
+
seedUnseen(bg.id);
|
|
932
|
+
|
|
933
|
+
const sections = invokeSections();
|
|
934
|
+
|
|
935
|
+
expect(sections).toContainEqual({
|
|
936
|
+
kind: "group",
|
|
937
|
+
groupId: group.id,
|
|
938
|
+
name: "Sections Background Group",
|
|
939
|
+
icon: null,
|
|
940
|
+
sortPosition: group.sortPosition,
|
|
941
|
+
total: 1,
|
|
942
|
+
unread: 0,
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("an empty custom group gets no section", () => {
|
|
947
|
+
const group = createGroup("Sections Empty Group");
|
|
948
|
+
|
|
949
|
+
const sections = invokeSections();
|
|
950
|
+
|
|
951
|
+
expect(sections.some((s) => s.groupId === group.id)).toBe(false);
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test("archived rows count toward no section", () => {
|
|
955
|
+
const group = createGroup("Sections Archived Group");
|
|
956
|
+
fileIntoGroup(seedArchived("archived-in-group"), group.id);
|
|
957
|
+
|
|
958
|
+
const sections = invokeSections();
|
|
959
|
+
|
|
960
|
+
expect(sections.some((s) => s.groupId === group.id)).toBe(false);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test("a dangling group id is skipped, not surfaced", () => {
|
|
964
|
+
/* Live write paths cannot create this state: group_id carries a foreign
|
|
965
|
+
key, and the placement write sanitizes unknown groups precisely to
|
|
966
|
+
avoid violating it. A restored snapshot can, though: in-place restore
|
|
967
|
+
swaps the SQLite file without running migrations in-process (the same
|
|
968
|
+
window that lets legacy private rows exist transiently), so the
|
|
969
|
+
fixture creates the state the way reality does, with enforcement off. */
|
|
970
|
+
rawExec("PRAGMA foreign_keys = OFF");
|
|
971
|
+
try {
|
|
972
|
+
fileIntoGroup(
|
|
973
|
+
createConversation({ title: "dangling" }).id,
|
|
974
|
+
"00000000-0000-4000-8000-00000000dead",
|
|
975
|
+
);
|
|
976
|
+
} finally {
|
|
977
|
+
rawExec("PRAGMA foreign_keys = ON");
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const sections = invokeSections();
|
|
981
|
+
|
|
982
|
+
expect(sections.some((s) => s.kind === "group")).toBe(false);
|
|
983
|
+
// Grouped, so it does not leak into the ungrouped Chats bucket either.
|
|
984
|
+
expect(sections).toContainEqual({ kind: "chats", total: 0, unread: 0 });
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
test("an unsurfaced background row pinned by raw column writes stays invisible", () => {
|
|
988
|
+
// Standard-listing visibility admits a pinned background row only when
|
|
989
|
+
// it is surfaced (the pinned group fails the custom-group arm on its
|
|
990
|
+
// system: prefix), so it appears in neither the Pinned list nor the
|
|
991
|
+
// Pinned count.
|
|
992
|
+
const bg = createConversation({
|
|
993
|
+
title: "bg-pinned",
|
|
994
|
+
conversationType: "background",
|
|
995
|
+
});
|
|
996
|
+
pin(bg.id);
|
|
997
|
+
|
|
998
|
+
const sections = invokeSections();
|
|
999
|
+
|
|
1000
|
+
expect(sections.some((s) => s.kind === "pinned")).toBe(false);
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
@@ -1712,3 +1712,55 @@ describe("config invariant flag enrichment", () => {
|
|
|
1712
1712
|
}
|
|
1713
1713
|
});
|
|
1714
1714
|
});
|
|
1715
|
+
|
|
1716
|
+
describe("provider membership at the write choke point", () => {
|
|
1717
|
+
const configPatchRoute = ROUTES.find(
|
|
1718
|
+
(r) => r.operationId === "config_patch",
|
|
1719
|
+
)!;
|
|
1720
|
+
|
|
1721
|
+
test("a pre-existing unknown provider does not block unrelated writes", async () => {
|
|
1722
|
+
// Read tolerance means a stored entry-name provider is legal on disk;
|
|
1723
|
+
// re-validating it on every write would make all later settings saves
|
|
1724
|
+
// fail with no in-product repair path.
|
|
1725
|
+
rawConfigFixture = {
|
|
1726
|
+
llm: {
|
|
1727
|
+
profiles: {
|
|
1728
|
+
work: {
|
|
1729
|
+
source: "user",
|
|
1730
|
+
provider: "anthropic-work",
|
|
1731
|
+
model: "claude-opus-4-8",
|
|
1732
|
+
},
|
|
1733
|
+
},
|
|
1734
|
+
},
|
|
1735
|
+
};
|
|
1736
|
+
seedRawConfig();
|
|
1737
|
+
|
|
1738
|
+
await configPatchRoute.handler({
|
|
1739
|
+
body: { llm: { callSites: { mainAgent: { maxTokens: 2048 } } } },
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
const llm = loadRawConfig().llm as {
|
|
1743
|
+
callSites?: Record<string, { maxTokens?: number }>;
|
|
1744
|
+
profiles?: Record<string, { provider?: string }>;
|
|
1745
|
+
};
|
|
1746
|
+
expect(llm.callSites?.mainAgent?.maxTokens).toBe(2048);
|
|
1747
|
+
expect(llm.profiles?.work?.provider).toBe("anthropic-work");
|
|
1748
|
+
});
|
|
1749
|
+
|
|
1750
|
+
test("introducing an unknown provider is rejected", async () => {
|
|
1751
|
+
rawConfigFixture = { llm: {} };
|
|
1752
|
+
seedRawConfig();
|
|
1753
|
+
|
|
1754
|
+
await expect(
|
|
1755
|
+
configPatchRoute.handler({
|
|
1756
|
+
body: {
|
|
1757
|
+
llm: {
|
|
1758
|
+
profiles: {
|
|
1759
|
+
rogue: { source: "user", provider: "not-a-provider", model: "m" },
|
|
1760
|
+
},
|
|
1761
|
+
},
|
|
1762
|
+
},
|
|
1763
|
+
}),
|
|
1764
|
+
).rejects.toThrow(/Invalid provider/);
|
|
1765
|
+
});
|
|
1766
|
+
});
|
|
@@ -432,6 +432,31 @@ describe("PUT config/llm/default-provider", () => {
|
|
|
432
432
|
expect(availability(result)).toEqual({ status: "ok" });
|
|
433
433
|
});
|
|
434
434
|
|
|
435
|
+
test("chatgpt persists and resolves the canonical subscription row", async () => {
|
|
436
|
+
seedConnection({
|
|
437
|
+
name: "chatgpt-subscription",
|
|
438
|
+
provider: "chatgpt",
|
|
439
|
+
auth: {
|
|
440
|
+
type: "oauth_subscription",
|
|
441
|
+
credential: "credential/chatgpt/access_token",
|
|
442
|
+
},
|
|
443
|
+
});
|
|
444
|
+
secureKeyResults["credential/chatgpt/access_token"] = {
|
|
445
|
+
value: "token",
|
|
446
|
+
unreachable: false,
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const result = (await put({ provider: "chatgpt" })) as Record<
|
|
450
|
+
string,
|
|
451
|
+
unknown
|
|
452
|
+
>;
|
|
453
|
+
|
|
454
|
+
expect(persistedDefaultProvider()).toEqual({ provider: "chatgpt" });
|
|
455
|
+
expect(result.provider).toBe("chatgpt");
|
|
456
|
+
expect(result.resolvedConnectionName).toBe("chatgpt-subscription");
|
|
457
|
+
expect(availability(result)).toEqual({ status: "ok" });
|
|
458
|
+
});
|
|
459
|
+
|
|
435
460
|
test("persists an explicit connectionName", async () => {
|
|
436
461
|
await put({ provider: "openai", connectionName: "work-openai" });
|
|
437
462
|
expect(persistedDefaultProvider()).toEqual({
|
|
@@ -449,6 +474,42 @@ describe("PUT config/llm/default-provider", () => {
|
|
|
449
474
|
expect(availability(result).status).toBe("missing_connection");
|
|
450
475
|
});
|
|
451
476
|
|
|
477
|
+
test("a noncanonical pin on a routing identity → 400 naming the canonical row", async () => {
|
|
478
|
+
for (const [provider, canonical] of [
|
|
479
|
+
["chatgpt", "chatgpt-subscription"],
|
|
480
|
+
["vellum", "vellum"],
|
|
481
|
+
] as const) {
|
|
482
|
+
const err = await put({
|
|
483
|
+
provider,
|
|
484
|
+
connectionName: "some-other-row",
|
|
485
|
+
}).catch((e: unknown) => e);
|
|
486
|
+
expect(String(err)).toContain(canonical);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("the canonical pin on a routing identity is accepted", async () => {
|
|
491
|
+
seedConnection({
|
|
492
|
+
name: "chatgpt-subscription",
|
|
493
|
+
provider: "chatgpt",
|
|
494
|
+
auth: {
|
|
495
|
+
type: "oauth_subscription",
|
|
496
|
+
credential: "credential/chatgpt/access_token",
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
secureKeyResults["credential/chatgpt/access_token"] = {
|
|
500
|
+
value: "token",
|
|
501
|
+
unreachable: false,
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const result = (await put({
|
|
505
|
+
provider: "chatgpt",
|
|
506
|
+
connectionName: "chatgpt-subscription",
|
|
507
|
+
})) as Record<string, unknown>;
|
|
508
|
+
|
|
509
|
+
expect(result.resolvedConnectionName).toBe("chatgpt-subscription");
|
|
510
|
+
expect(availability(result)).toEqual({ status: "ok" });
|
|
511
|
+
});
|
|
512
|
+
|
|
452
513
|
test("invalid provider → 400 naming the allowed providers", async () => {
|
|
453
514
|
const err = await put({ provider: "not-a-provider" }).catch(
|
|
454
515
|
(e: unknown) => e,
|
|
@@ -150,6 +150,50 @@ describe("POST inference/profiles (create) validation", () => {
|
|
|
150
150
|
).rejects.toBeInstanceOf(BadRequestError);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
test("creates an entry-backed profile, validating the model by the row's kind", async () => {
|
|
154
|
+
seedConnection("anthropic-work", "anthropic", {
|
|
155
|
+
type: "api_key",
|
|
156
|
+
credential: "credential/anthropic-work/api_key",
|
|
157
|
+
});
|
|
158
|
+
const result = (await call("inference_profiles_create", {
|
|
159
|
+
body: {
|
|
160
|
+
name: "work-profile",
|
|
161
|
+
provider: "anthropic-work",
|
|
162
|
+
model: "claude-opus-4-8",
|
|
163
|
+
},
|
|
164
|
+
})) as { entry: Record<string, unknown> };
|
|
165
|
+
expect(result.entry.provider).toBe("anthropic-work");
|
|
166
|
+
expect(result.entry.model).toBe("claude-opus-4-8");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("rejects an entry-backed profile whose model the row's kind cannot serve", async () => {
|
|
170
|
+
seedConnection("anthropic-work", "anthropic", {
|
|
171
|
+
type: "api_key",
|
|
172
|
+
credential: "credential/anthropic-work/api_key",
|
|
173
|
+
});
|
|
174
|
+
await expect(
|
|
175
|
+
call("inference_profiles_create", {
|
|
176
|
+
body: {
|
|
177
|
+
name: "work-profile",
|
|
178
|
+
provider: "anthropic-work",
|
|
179
|
+
model: "gpt-5.5",
|
|
180
|
+
},
|
|
181
|
+
}),
|
|
182
|
+
).rejects.toBeInstanceOf(BadRequestError);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("rejects a provider naming no known vendor or entry", async () => {
|
|
186
|
+
await expect(
|
|
187
|
+
call("inference_profiles_create", {
|
|
188
|
+
body: {
|
|
189
|
+
name: "ghost-profile",
|
|
190
|
+
provider: "no-such-entry",
|
|
191
|
+
model: "claude-opus-4-8",
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
).rejects.toBeInstanceOf(BadRequestError);
|
|
195
|
+
});
|
|
196
|
+
|
|
153
197
|
test("creates a vellum profile for a managed-routable model", async () => {
|
|
154
198
|
seedVellumConnection();
|
|
155
199
|
const result = (await call("inference_profiles_create", {
|
|
@@ -232,6 +232,61 @@ describe("POST inference/provider-connections (create)", () => {
|
|
|
232
232
|
expect((err as BadRequestError).message).toContain("vellum");
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
test("rejects a chatgpt identity row under a non-canonical name", async () => {
|
|
236
|
+
const err = await call(
|
|
237
|
+
findHandler("inference_provider_connections_create"),
|
|
238
|
+
{
|
|
239
|
+
body: {
|
|
240
|
+
name: "my-chatgpt",
|
|
241
|
+
provider: "chatgpt",
|
|
242
|
+
auth: { type: "oauth_subscription", credential: "credential/x" },
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
).catch((e: unknown) => e);
|
|
246
|
+
|
|
247
|
+
expect(err).toBeInstanceOf(BadRequestError);
|
|
248
|
+
expect((err as BadRequestError).message).toContain("chatgpt-subscription");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("rejects key auth on the chatgpt provider, derived or explicit", async () => {
|
|
252
|
+
for (const body of [
|
|
253
|
+
{
|
|
254
|
+
name: "chatgpt-subscription",
|
|
255
|
+
provider: "chatgpt",
|
|
256
|
+
auth: { type: "api_key", credential: "vault/x" },
|
|
257
|
+
},
|
|
258
|
+
// No explicit auth: derivation would fall through to api_key.
|
|
259
|
+
{
|
|
260
|
+
name: "chatgpt-subscription",
|
|
261
|
+
provider: "chatgpt",
|
|
262
|
+
credential: "vault/x",
|
|
263
|
+
},
|
|
264
|
+
]) {
|
|
265
|
+
const err = await call(
|
|
266
|
+
findHandler("inference_provider_connections_create"),
|
|
267
|
+
{ body },
|
|
268
|
+
).catch((e: unknown) => e);
|
|
269
|
+
expect(err).toBeInstanceOf(BadRequestError);
|
|
270
|
+
expect((err as BadRequestError).message).toContain("sign-in");
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("rejects subscription auth on a non-chatgpt provider", async () => {
|
|
275
|
+
const err = await call(
|
|
276
|
+
findHandler("inference_provider_connections_create"),
|
|
277
|
+
{
|
|
278
|
+
body: {
|
|
279
|
+
name: "keyed-oauth",
|
|
280
|
+
provider: "openai",
|
|
281
|
+
auth: { type: "oauth_subscription", credential: "credential/x" },
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
).catch((e: unknown) => e);
|
|
285
|
+
|
|
286
|
+
expect(err).toBeInstanceOf(BadRequestError);
|
|
287
|
+
expect((err as BadRequestError).message).toContain("chatgpt");
|
|
288
|
+
});
|
|
289
|
+
|
|
235
290
|
test("rejects key auth on the vellum provider", async () => {
|
|
236
291
|
const err = await call(
|
|
237
292
|
findHandler("inference_provider_connections_create"),
|
|
@@ -396,7 +451,7 @@ describe("POST inference/provider-connections (create)", () => {
|
|
|
396
451
|
models: [{ id: "my-model" }],
|
|
397
452
|
},
|
|
398
453
|
}),
|
|
399
|
-
).rejects.toThrow(/
|
|
454
|
+
).rejects.toThrow(/reserved as a provider id/);
|
|
400
455
|
|
|
401
456
|
await call(findHandler("inference_provider_connections_create"), {
|
|
402
457
|
body: {
|
|
@@ -677,6 +732,52 @@ describe("PATCH inference/provider-connections/:name (update)", () => {
|
|
|
677
732
|
|
|
678
733
|
// The guard must not trap a legacy row in its mismatched state: an auth
|
|
679
734
|
// change that repairs the pairing is exactly what should be allowed.
|
|
735
|
+
// The canonical subscription row owns the "chatgpt" identity: writing
|
|
736
|
+
// subscription auth to it stamps the provider (the CLI login-chatgpt path
|
|
737
|
+
// PATCHes auth through this route on a row the identity migration
|
|
738
|
+
// deliberately skipped).
|
|
739
|
+
test("stamps the chatgpt identity when subscription auth lands on the canonical row", async () => {
|
|
740
|
+
seedConnection({
|
|
741
|
+
name: "chatgpt-subscription",
|
|
742
|
+
provider: "openai",
|
|
743
|
+
auth: { type: "api_key", credential: "vault/openai/key" },
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
const result = (await call(
|
|
747
|
+
findHandler("inference_provider_connections_update"),
|
|
748
|
+
{
|
|
749
|
+
pathParams: { name: "chatgpt-subscription" },
|
|
750
|
+
body: {
|
|
751
|
+
auth: {
|
|
752
|
+
type: "oauth_subscription",
|
|
753
|
+
credential: "credential/chatgpt/access_token",
|
|
754
|
+
},
|
|
755
|
+
},
|
|
756
|
+
},
|
|
757
|
+
)) as { provider: string; auth: { type: string } };
|
|
758
|
+
expect(result.provider).toBe("chatgpt");
|
|
759
|
+
expect(result.auth.type).toBe("oauth_subscription");
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("does not stamp the identity for non-subscription auth on that name", async () => {
|
|
763
|
+
seedConnection({
|
|
764
|
+
name: "chatgpt-subscription",
|
|
765
|
+
provider: "openai",
|
|
766
|
+
auth: { type: "api_key", credential: "vault/openai/key" },
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
const result = (await call(
|
|
770
|
+
findHandler("inference_provider_connections_update"),
|
|
771
|
+
{
|
|
772
|
+
pathParams: { name: "chatgpt-subscription" },
|
|
773
|
+
body: {
|
|
774
|
+
auth: { type: "api_key", credential: "vault/openai/other-key" },
|
|
775
|
+
},
|
|
776
|
+
},
|
|
777
|
+
)) as { provider: string };
|
|
778
|
+
expect(result.provider).toBe("openai");
|
|
779
|
+
});
|
|
780
|
+
|
|
680
781
|
test("allows repairing a legacy mismatched row with a valid auth change", async () => {
|
|
681
782
|
seedConnection({
|
|
682
783
|
name: "legacy-managed-anthropic",
|
|
@@ -109,6 +109,7 @@ import {
|
|
|
109
109
|
} from "../../../cli/lib/uninstall-plugin.js";
|
|
110
110
|
import {
|
|
111
111
|
PluginMergeBaselineError,
|
|
112
|
+
PluginNotCuratedError,
|
|
112
113
|
PluginNotUpgradableError,
|
|
113
114
|
type PluginUpgradeResult,
|
|
114
115
|
type UpgradePluginDeps,
|
|
@@ -252,6 +253,7 @@ const upgradeSpy = mock(
|
|
|
252
253
|
|
|
253
254
|
mock.module("../../../cli/lib/upgrade-plugin.js", () => ({
|
|
254
255
|
PluginMergeBaselineError,
|
|
256
|
+
PluginNotCuratedError,
|
|
255
257
|
PluginNotUpgradableError,
|
|
256
258
|
upgradePlugin: upgradeSpy,
|
|
257
259
|
}));
|
|
@@ -2127,6 +2129,7 @@ describe("POST /v1/plugins/:name/upgrade", () => {
|
|
|
2127
2129
|
name: "level-up",
|
|
2128
2130
|
dryRun: false,
|
|
2129
2131
|
strategy: undefined,
|
|
2132
|
+
marketplaceOnly: false,
|
|
2130
2133
|
});
|
|
2131
2134
|
});
|
|
2132
2135
|
|
|
@@ -2148,6 +2151,7 @@ describe("POST /v1/plugins/:name/upgrade", () => {
|
|
|
2148
2151
|
name: "level-up",
|
|
2149
2152
|
dryRun: undefined,
|
|
2150
2153
|
strategy: "ours",
|
|
2154
|
+
marketplaceOnly: false,
|
|
2151
2155
|
});
|
|
2152
2156
|
});
|
|
2153
2157
|
|
|
@@ -2175,6 +2179,7 @@ describe("POST /v1/plugins/:name/upgrade", () => {
|
|
|
2175
2179
|
name: "level-up",
|
|
2176
2180
|
dryRun: undefined,
|
|
2177
2181
|
strategy: "assistant",
|
|
2182
|
+
marketplaceOnly: false,
|
|
2178
2183
|
});
|
|
2179
2184
|
});
|
|
2180
2185
|
|
|
@@ -2300,6 +2305,10 @@ describe("POST /v1/plugins/:name/upgrade", () => {
|
|
|
2300
2305
|
expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
|
|
2301
2306
|
name: "level-up",
|
|
2302
2307
|
dryRun: undefined,
|
|
2308
|
+
strategy: undefined,
|
|
2309
|
+
// Unlike dryRun, an absent flag here is a definite "no": the untrusted
|
|
2310
|
+
// direct path stays available unless a caller opts out of it.
|
|
2311
|
+
marketplaceOnly: false,
|
|
2303
2312
|
});
|
|
2304
2313
|
});
|
|
2305
2314
|
|
|
@@ -2343,6 +2352,41 @@ describe("POST /v1/plugins/:name/upgrade", () => {
|
|
|
2343
2352
|
).rejects.toBeInstanceOf(ConflictError);
|
|
2344
2353
|
});
|
|
2345
2354
|
|
|
2355
|
+
test("marketplaceOnly is forwarded so the lib enforces it, not the caller", async () => {
|
|
2356
|
+
// The lib re-inspects the catalog inside the call, so the flag has to
|
|
2357
|
+
// travel with the request: a caller's own pre-check cannot bind a decision
|
|
2358
|
+
// the lib makes afterwards.
|
|
2359
|
+
upgradeSpy.mockImplementation(async () => upgradeResult());
|
|
2360
|
+
|
|
2361
|
+
await invokeUpgrade({
|
|
2362
|
+
pathParams: { name: "level-up" },
|
|
2363
|
+
body: { marketplaceOnly: true },
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
expect(upgradeSpy.mock.calls[0]?.[0]).toEqual({
|
|
2367
|
+
name: "level-up",
|
|
2368
|
+
dryRun: undefined,
|
|
2369
|
+
strategy: undefined,
|
|
2370
|
+
marketplaceOnly: true,
|
|
2371
|
+
});
|
|
2372
|
+
});
|
|
2373
|
+
|
|
2374
|
+
test("PluginNotCuratedError → ConflictError (409)", async () => {
|
|
2375
|
+
// A marketplaceOnly caller asked about an install the catalog does not
|
|
2376
|
+
// claim. Retrying changes nothing: only a human upgrading it without the
|
|
2377
|
+
// flag can move it.
|
|
2378
|
+
upgradeSpy.mockImplementation(async () => {
|
|
2379
|
+
throw new PluginNotCuratedError("level-up");
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
await expect(
|
|
2383
|
+
invokeUpgrade({
|
|
2384
|
+
pathParams: { name: "level-up" },
|
|
2385
|
+
body: { marketplaceOnly: true },
|
|
2386
|
+
}),
|
|
2387
|
+
).rejects.toBeInstanceOf(ConflictError);
|
|
2388
|
+
});
|
|
2389
|
+
|
|
2346
2390
|
test("PluginNotFoundError → NotFoundError (404)", async () => {
|
|
2347
2391
|
upgradeSpy.mockImplementation(async () => {
|
|
2348
2392
|
throw new PluginNotFoundError("level-up", "main", "vellum-ai/level-up");
|
|
@@ -288,6 +288,31 @@ describe("stt-routes", () => {
|
|
|
288
288
|
expect(err.message).toContain("not available");
|
|
289
289
|
});
|
|
290
290
|
|
|
291
|
+
test("surfaces a typed resolver error verbatim instead of the generic copy", async () => {
|
|
292
|
+
// A streaming-only provider IS configured, so the "nothing configured"
|
|
293
|
+
// and "not available" strings would both send the caller looking for a
|
|
294
|
+
// problem that does not exist.
|
|
295
|
+
const reason =
|
|
296
|
+
'Deepgram Flux is streaming-only. Batch transcription requires the deepgram provider: set services.stt.provider to "deepgram".';
|
|
297
|
+
mockResolveError = new SttError("provider-error", reason, {
|
|
298
|
+
userFacing: true,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const { handler } = getRoute("stt/transcribe");
|
|
302
|
+
const err = await expectRouteError(
|
|
303
|
+
() =>
|
|
304
|
+
handler(
|
|
305
|
+
makeArgs({
|
|
306
|
+
audioBase64: toBase64("audio-data"),
|
|
307
|
+
mimeType: "audio/wav",
|
|
308
|
+
}),
|
|
309
|
+
),
|
|
310
|
+
503,
|
|
311
|
+
"SERVICE_UNAVAILABLE",
|
|
312
|
+
);
|
|
313
|
+
expect(err.message).toBe(reason);
|
|
314
|
+
});
|
|
315
|
+
|
|
291
316
|
// -- Timeout --------------------------------------------------------------
|
|
292
317
|
|
|
293
318
|
test("throws 504 when transcription times out", async () => {
|