@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321
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/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
- package/src/calls/voice-escalation-judge.ts +6 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +22 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +7 -6
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +7 -1
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -265,14 +265,14 @@ export function getByConversation(
|
|
|
265
265
|
* /v1/host-transfer-result after completing the operation, get a 404, and the
|
|
266
266
|
* proxy timer would fire with a spurious timeout error.
|
|
267
267
|
*
|
|
268
|
-
* `question` interactions are also skipped: a new message
|
|
269
|
-
* ask_question
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* without settling the prompt's
|
|
273
|
-
* fallback like secrets do) and would
|
|
274
|
-
*
|
|
275
|
-
* open both tools concurrently.
|
|
268
|
+
* `question` interactions are also skipped: a new message settles an open
|
|
269
|
+
* ask_question at the enqueue path (see `daemon/handlers/conversations.ts`),
|
|
270
|
+
* either by answering it with the typed text or by steering to the message,
|
|
271
|
+
* which aborts the parked turn and settles the question via its abort signal.
|
|
272
|
+
* Clearing the entry here instead would drop it without settling the prompt's
|
|
273
|
+
* Promise (questions carry no `rpcResolve` fallback like secrets do) and would
|
|
274
|
+
* strip both paths of the entry they need, which can co-occur with a
|
|
275
|
+
* confirmation since one model response can open both tools concurrently.
|
|
276
276
|
*/
|
|
277
277
|
export function removeByConversation(
|
|
278
278
|
conversationId: string,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared resolution core for pending `ask_question` interactions.
|
|
3
3
|
*
|
|
4
4
|
* A pending `question` interaction (registered by {@link QuestionPrompter}) can
|
|
5
|
-
* be resolved from
|
|
5
|
+
* be resolved from three places:
|
|
6
6
|
*
|
|
7
7
|
* - `POST /v1/question-response` — an app/web client submits the user's
|
|
8
8
|
* batched selection (see `routes/question-routes.ts`).
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
* or bare-text answer routes through the guardian reply router to the
|
|
11
11
|
* `pending_question` resolver, which builds the submission (see
|
|
12
12
|
* `approvals/guardian-request-resolvers.ts`).
|
|
13
|
+
* - the enqueue path: a message typed into the chat while the card is open
|
|
14
|
+
* answers the parked question as free text (see
|
|
15
|
+
* `daemon/handlers/conversations.ts`).
|
|
13
16
|
*
|
|
14
|
-
*
|
|
17
|
+
* All funnel through {@link resolvePendingQuestion} so the validate → consume
|
|
15
18
|
* → `rpcResolve` sequence has a single implementation. The helper is
|
|
16
19
|
* transport-agnostic: it returns a discriminated outcome rather than throwing
|
|
17
20
|
* route errors, so each caller maps the outcome to its own surface (HTTP status
|
|
@@ -28,12 +31,20 @@ import {
|
|
|
28
31
|
import * as pendingInteractions from "./pending-interactions.js";
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
|
-
* How to resolve a pending question:
|
|
32
|
-
*
|
|
34
|
+
* How to resolve a pending question:
|
|
35
|
+
*
|
|
36
|
+
* - `submit`: a batched submission, one entry per question.
|
|
37
|
+
* - `close`: the card was dismissed; every question is reported as
|
|
38
|
+
* `skipped`.
|
|
39
|
+
* - `chat_reply`: the user typed a message into the chat while the card was
|
|
40
|
+
* open. The text answers the first question as free text and the rest stay
|
|
41
|
+
* `skipped`; the result is flagged so the tool tells the model where the
|
|
42
|
+
* answer came from.
|
|
33
43
|
*/
|
|
34
44
|
export type QuestionResolutionInput =
|
|
35
45
|
| { kind: "submit"; submissions: QuestionBatchSubmission[] }
|
|
36
|
-
| { kind: "close" }
|
|
46
|
+
| { kind: "close" }
|
|
47
|
+
| { kind: "chat_reply"; text: string };
|
|
37
48
|
|
|
38
49
|
/**
|
|
39
50
|
* Outcome of {@link resolvePendingQuestion}.
|
|
@@ -57,9 +68,10 @@ export type QuestionResolutionOutcome =
|
|
|
57
68
|
| { status: "invalid"; message: string };
|
|
58
69
|
|
|
59
70
|
/**
|
|
60
|
-
* Resolve a pending `question` interaction with a batched submission
|
|
61
|
-
*
|
|
62
|
-
* outcome leaves the pending prompt (and its timer) intact for a
|
|
71
|
+
* Resolve a pending `question` interaction with a batched submission, a close,
|
|
72
|
+
* or a chat reply. Validation runs BEFORE the interaction is consumed, so an
|
|
73
|
+
* `invalid` outcome leaves the pending prompt (and its timer) intact for a
|
|
74
|
+
* retry.
|
|
63
75
|
*/
|
|
64
76
|
export function resolvePendingQuestion(
|
|
65
77
|
requestId: string,
|
|
@@ -81,6 +93,8 @@ export function resolvePendingQuestion(
|
|
|
81
93
|
})),
|
|
82
94
|
overall: "closed",
|
|
83
95
|
};
|
|
96
|
+
} else if (input.kind === "chat_reply") {
|
|
97
|
+
result = buildChatReplyResult(input.text, interaction);
|
|
84
98
|
} else {
|
|
85
99
|
result = buildCompletedResult(input.submissions, interaction);
|
|
86
100
|
}
|
|
@@ -136,6 +150,38 @@ function buildCompletedResult(
|
|
|
136
150
|
return { entries, overall: "completed" };
|
|
137
151
|
}
|
|
138
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Build the result for a chat reply: the typed text answers the first question
|
|
155
|
+
* as free text, every other question stays `skipped`.
|
|
156
|
+
*
|
|
157
|
+
* The daemon holds no partial per-question state (the card keeps the user's
|
|
158
|
+
* in-progress answers locally until the whole batch is posted), so the first
|
|
159
|
+
* question is by construction the first unanswered one. Which question the
|
|
160
|
+
* text really speaks to is the model's call, not the daemon's: the flag on the
|
|
161
|
+
* result tells it the answer came from the chat rather than the card, and it
|
|
162
|
+
* maps the reply onto the batch from there.
|
|
163
|
+
*/
|
|
164
|
+
function buildChatReplyResult(
|
|
165
|
+
text: string,
|
|
166
|
+
interaction: ReturnType<typeof pendingInteractions.get>,
|
|
167
|
+
): QuestionPromptResult {
|
|
168
|
+
const { orderedIds } = readBatchMetadata(interaction);
|
|
169
|
+
if (orderedIds.length === 0) {
|
|
170
|
+
throw new QuestionBatchValidationError(
|
|
171
|
+
"No registered question ids for this batch",
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
entries: orderedIds.map((id, index) =>
|
|
176
|
+
index === 0
|
|
177
|
+
? { questionId: id, decision: "free_text" as const, text }
|
|
178
|
+
: { questionId: id, decision: "skipped" as const },
|
|
179
|
+
),
|
|
180
|
+
overall: "completed",
|
|
181
|
+
answeredInChat: true,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
139
185
|
/**
|
|
140
186
|
* Pull the prompter-stashed batch bookkeeping off a pending interaction.
|
|
141
187
|
* Returns empty defaults if the metadata is absent.
|
|
@@ -1302,3 +1302,149 @@ describe("GET /v1/conversations/sections", () => {
|
|
|
1302
1302
|
expect(sections.some((s) => s.kind === "pinned")).toBe(false);
|
|
1303
1303
|
});
|
|
1304
1304
|
});
|
|
1305
|
+
|
|
1306
|
+
describe("GET /v1/conversations, conversationType=all", () => {
|
|
1307
|
+
beforeEach(() => {
|
|
1308
|
+
clearConversations();
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
function setLastMessageAt(id: string, at: number): void {
|
|
1312
|
+
rawRun(
|
|
1313
|
+
"test:setLastMessageAt",
|
|
1314
|
+
"UPDATE conversations SET last_message_at = ? WHERE id = ?",
|
|
1315
|
+
at,
|
|
1316
|
+
id,
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
test("returns the standard listing and the background umbrella together", async () => {
|
|
1321
|
+
createConversation("foreground-1");
|
|
1322
|
+
createConversation({ title: "bg-1", conversationType: "background" });
|
|
1323
|
+
createConversation({ title: "sched-1", conversationType: "scheduled" });
|
|
1324
|
+
|
|
1325
|
+
const result = (await invoke({ conversationType: "all" })) as ListResponse;
|
|
1326
|
+
|
|
1327
|
+
expect(result.conversations.map((c) => c.title).sort()).toEqual([
|
|
1328
|
+
"bg-1",
|
|
1329
|
+
"foreground-1",
|
|
1330
|
+
"sched-1",
|
|
1331
|
+
]);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
test("with archiveStatus=all one page spans live and done rows of every type", async () => {
|
|
1335
|
+
createConversation("foreground-1");
|
|
1336
|
+
createConversation({ title: "bg-1", conversationType: "background" });
|
|
1337
|
+
seedArchived("archived-1");
|
|
1338
|
+
const archivedBackground = createConversation({
|
|
1339
|
+
title: "archived-bg",
|
|
1340
|
+
conversationType: "background",
|
|
1341
|
+
});
|
|
1342
|
+
rawRun(
|
|
1343
|
+
"test:archiveConversation",
|
|
1344
|
+
"UPDATE conversations SET archived_at = ? WHERE id = ?",
|
|
1345
|
+
Date.now(),
|
|
1346
|
+
archivedBackground.id,
|
|
1347
|
+
);
|
|
1348
|
+
|
|
1349
|
+
const result = (await invoke({
|
|
1350
|
+
conversationType: "all",
|
|
1351
|
+
archiveStatus: "all",
|
|
1352
|
+
})) as ListResponse;
|
|
1353
|
+
|
|
1354
|
+
expect(result.conversations.map((c) => c.title).sort()).toEqual([
|
|
1355
|
+
"archived-1",
|
|
1356
|
+
"archived-bg",
|
|
1357
|
+
"bg-1",
|
|
1358
|
+
"foreground-1",
|
|
1359
|
+
]);
|
|
1360
|
+
expect(result.hasMore).toBe(false);
|
|
1361
|
+
});
|
|
1362
|
+
|
|
1363
|
+
test("orders by last message time descending across the types it merges", async () => {
|
|
1364
|
+
const oldest = createConversation("oldest-foreground");
|
|
1365
|
+
const middle = createConversation({
|
|
1366
|
+
title: "middle-background",
|
|
1367
|
+
conversationType: "background",
|
|
1368
|
+
});
|
|
1369
|
+
const newest = createConversation("newest-foreground");
|
|
1370
|
+
setLastMessageAt(oldest.id, 1000);
|
|
1371
|
+
setLastMessageAt(middle.id, 2000);
|
|
1372
|
+
setLastMessageAt(newest.id, 3000);
|
|
1373
|
+
|
|
1374
|
+
const result = (await invoke({ conversationType: "all" })) as ListResponse;
|
|
1375
|
+
|
|
1376
|
+
expect(result.conversations.map((c) => c.title)).toEqual([
|
|
1377
|
+
"newest-foreground",
|
|
1378
|
+
"middle-background",
|
|
1379
|
+
"oldest-foreground",
|
|
1380
|
+
]);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
test("excludes legacy private rows and subagent runs", async () => {
|
|
1384
|
+
createConversation("foreground-1");
|
|
1385
|
+
const legacyPrivate = createConversation("legacy-private");
|
|
1386
|
+
rawRun(
|
|
1387
|
+
"test:legacyPrivateRow",
|
|
1388
|
+
"UPDATE conversations SET conversation_type = 'private', group_id = 'system:background' WHERE id = ?",
|
|
1389
|
+
legacyPrivate.id,
|
|
1390
|
+
);
|
|
1391
|
+
const subagent = createConversation({
|
|
1392
|
+
title: "subagent-run",
|
|
1393
|
+
conversationType: "background",
|
|
1394
|
+
});
|
|
1395
|
+
rawRun(
|
|
1396
|
+
"test:subagentRun",
|
|
1397
|
+
"UPDATE conversations SET source = 'subagent' WHERE id = ?",
|
|
1398
|
+
subagent.id,
|
|
1399
|
+
);
|
|
1400
|
+
|
|
1401
|
+
const result = (await invoke({ conversationType: "all" })) as ListResponse;
|
|
1402
|
+
|
|
1403
|
+
expect(result.conversations.map((c) => c.title)).toEqual(["foreground-1"]);
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
test("never appends pinned rows outside the page", async () => {
|
|
1407
|
+
// Pinned injection is the compatibility shim for the default sidebar
|
|
1408
|
+
// read. A whole-history page already contains the pinned row in recency
|
|
1409
|
+
// order, so appending it would duplicate it and put hasMore out of step
|
|
1410
|
+
// with the count.
|
|
1411
|
+
const pinned = createConversation("pinned-1");
|
|
1412
|
+
rawRun(
|
|
1413
|
+
"test:pinConversation",
|
|
1414
|
+
"UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned' WHERE id = ?",
|
|
1415
|
+
pinned.id,
|
|
1416
|
+
);
|
|
1417
|
+
const newer = createConversation("newer-1");
|
|
1418
|
+
setLastMessageAt(pinned.id, 1000);
|
|
1419
|
+
setLastMessageAt(newer.id, 2000);
|
|
1420
|
+
|
|
1421
|
+
const result = (await invoke({
|
|
1422
|
+
conversationType: "all",
|
|
1423
|
+
limit: "1",
|
|
1424
|
+
})) as ListResponse;
|
|
1425
|
+
|
|
1426
|
+
expect(result.conversations.map((c) => c.title)).toEqual(["newer-1"]);
|
|
1427
|
+
expect(result.hasMore).toBe(true);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
test("hasMore describes the combined total, so pagination drains it", async () => {
|
|
1431
|
+
createConversation("foreground-1");
|
|
1432
|
+
createConversation({ title: "bg-1", conversationType: "background" });
|
|
1433
|
+
createConversation({ title: "sched-1", conversationType: "scheduled" });
|
|
1434
|
+
|
|
1435
|
+
const first = (await invoke({
|
|
1436
|
+
conversationType: "all",
|
|
1437
|
+
limit: "2",
|
|
1438
|
+
})) as ListResponse;
|
|
1439
|
+
expect(first.conversations).toHaveLength(2);
|
|
1440
|
+
expect(first.hasMore).toBe(true);
|
|
1441
|
+
|
|
1442
|
+
const second = (await invoke({
|
|
1443
|
+
conversationType: "all",
|
|
1444
|
+
limit: "2",
|
|
1445
|
+
offset: String(first.nextOffset),
|
|
1446
|
+
})) as ListResponse;
|
|
1447
|
+
expect(second.conversations).toHaveLength(1);
|
|
1448
|
+
expect(second.hasMore).toBe(false);
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { DictationProfile } from "../../../daemon/dictation-profile-store.js";
|
|
4
|
+
|
|
5
|
+
let profile: DictationProfile;
|
|
6
|
+
let nextToolInput: { mode?: string; text?: string } | null;
|
|
7
|
+
let providerAvailable: boolean;
|
|
8
|
+
let providerFails: boolean;
|
|
9
|
+
let providerResolutions: number;
|
|
10
|
+
let requestedProperties: Record<string, unknown> | undefined;
|
|
11
|
+
|
|
12
|
+
mock.module("../../../providers/provider-send-message.js", () => ({
|
|
13
|
+
getConfiguredProvider: async () => {
|
|
14
|
+
providerResolutions += 1;
|
|
15
|
+
if (!providerAvailable) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
sendMessage: async (
|
|
20
|
+
_messages: unknown,
|
|
21
|
+
options: {
|
|
22
|
+
tools: Array<{
|
|
23
|
+
input_schema: { properties: Record<string, unknown> };
|
|
24
|
+
}>;
|
|
25
|
+
},
|
|
26
|
+
) => {
|
|
27
|
+
if (providerFails) {
|
|
28
|
+
throw new Error("Provider unavailable");
|
|
29
|
+
}
|
|
30
|
+
requestedProperties = options.tools[0]?.input_schema.properties;
|
|
31
|
+
return {
|
|
32
|
+
content: nextToolInput
|
|
33
|
+
? [{ type: "tool_use", input: nextToolInput }]
|
|
34
|
+
: [{ type: "text", text: "Rewritten text." }],
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
extractToolUse: (response: { content: Array<{ type: string }> }) =>
|
|
40
|
+
response.content.find((block) => block.type === "tool_use"),
|
|
41
|
+
userMessage: (text: string) => ({ role: "user", content: text }),
|
|
42
|
+
createTimeout: () => ({ signal: undefined, cleanup: () => undefined }),
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
mock.module("../../../daemon/dictation-profile-store.js", () => ({
|
|
46
|
+
resolveProfile: () => ({ profile, source: "default" }),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
const { ROUTES, computeMaxTokens } = await import("../diagnostics-routes.js");
|
|
50
|
+
const route = ROUTES.find((entry) => entry.operationId === "dictation_post")!;
|
|
51
|
+
|
|
52
|
+
async function dictate(
|
|
53
|
+
transcription: string,
|
|
54
|
+
context: { cursorInTextField?: boolean; selectedText?: string } = {
|
|
55
|
+
cursorInTextField: true,
|
|
56
|
+
},
|
|
57
|
+
) {
|
|
58
|
+
return (await route.handler({
|
|
59
|
+
body: { transcription, context },
|
|
60
|
+
} as unknown as Parameters<typeof route.handler>[0])) as {
|
|
61
|
+
text: string;
|
|
62
|
+
mode: string;
|
|
63
|
+
actionPlan?: string;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
profile = { id: "general", name: "General" };
|
|
69
|
+
nextToolInput = { mode: "dictation", text: "We should ship it." };
|
|
70
|
+
providerAvailable = true;
|
|
71
|
+
providerFails = false;
|
|
72
|
+
providerResolutions = 0;
|
|
73
|
+
requestedProperties = undefined;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("dictation preserves recognized words", () => {
|
|
77
|
+
test.each([
|
|
78
|
+
"I think we probably shouldn't ship it, at least not yet.",
|
|
79
|
+
"Um, I don't, I don't want to say yes. Maybe next week?",
|
|
80
|
+
"First onions, then tomatoes. Actually, no tomatoes.",
|
|
81
|
+
" Please don't send that.\nI mean it. ",
|
|
82
|
+
"send me the draft, but don't send it to anyone else",
|
|
83
|
+
])(
|
|
84
|
+
"preserves text-field dictation despite an attempted model rewrite: %s",
|
|
85
|
+
async (words) => {
|
|
86
|
+
expect(await dictate(words)).toMatchObject({
|
|
87
|
+
mode: "dictation",
|
|
88
|
+
text: words,
|
|
89
|
+
});
|
|
90
|
+
expect(providerResolutions).toBe(1);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
test("a style profile cannot automatically rewrite ordinary dictation", async () => {
|
|
95
|
+
profile.stylePrompt = "Make every sentence confident and professional.";
|
|
96
|
+
const words = "I guess maybe we shouldn't do this.";
|
|
97
|
+
|
|
98
|
+
expect(await dictate(words)).toMatchObject({ text: words });
|
|
99
|
+
expect(providerResolutions).toBe(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("whitespace-only selection is ordinary dictation", async () => {
|
|
103
|
+
expect(
|
|
104
|
+
await dictate("Don't change my words.", {
|
|
105
|
+
cursorInTextField: true,
|
|
106
|
+
selectedText: " \n ",
|
|
107
|
+
}),
|
|
108
|
+
).toMatchObject({ mode: "dictation", text: "Don't change my words." });
|
|
109
|
+
expect(providerResolutions).toBe(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("keeps explicitly configured snippet and dictionary replacements", async () => {
|
|
113
|
+
profile.snippets = [
|
|
114
|
+
{ trigger: "my sign off", expansion: "Thanks, user one" },
|
|
115
|
+
];
|
|
116
|
+
profile.dictionary = [{ spoken: "user one", written: "Example User" }];
|
|
117
|
+
|
|
118
|
+
expect(await dictate("Maybe later. my sign off")).toMatchObject({
|
|
119
|
+
mode: "dictation",
|
|
120
|
+
text: "Maybe later. Thanks, Example User",
|
|
121
|
+
});
|
|
122
|
+
expect(providerResolutions).toBe(1);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("classification outside a text field cannot supply replacement text", async () => {
|
|
126
|
+
const words = "I think we probably shouldn't ship it.";
|
|
127
|
+
|
|
128
|
+
expect(await dictate(words, {})).toMatchObject({
|
|
129
|
+
mode: "dictation",
|
|
130
|
+
text: words,
|
|
131
|
+
});
|
|
132
|
+
expect(providerResolutions).toBe(1);
|
|
133
|
+
expect(Object.keys(requestedProperties ?? {})).toEqual(["mode"]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test.each([true, false])(
|
|
137
|
+
"action classification preserves the original command with cursorInTextField=%s",
|
|
138
|
+
async (cursorInTextField) => {
|
|
139
|
+
nextToolInput = { mode: "action", text: "Send the files." };
|
|
140
|
+
profile.snippets = [{ trigger: "files", expansion: "all files" }];
|
|
141
|
+
const words = "Send the files, but not the private ones.";
|
|
142
|
+
|
|
143
|
+
expect(await dictate(words, { cursorInTextField })).toMatchObject({
|
|
144
|
+
mode: "action",
|
|
145
|
+
text: words,
|
|
146
|
+
actionPlan: `User wants to: ${words}`,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
test.each([
|
|
152
|
+
"missing-provider",
|
|
153
|
+
"provider-error",
|
|
154
|
+
"missing-tool",
|
|
155
|
+
"invalid-mode",
|
|
156
|
+
])("classification fallback preserves words when %s", async (failure) => {
|
|
157
|
+
providerAvailable = failure !== "missing-provider";
|
|
158
|
+
providerFails = failure === "provider-error";
|
|
159
|
+
nextToolInput = failure === "missing-tool" ? null : { mode: "invalid" };
|
|
160
|
+
const words = "I don't think that's what I meant, you know?";
|
|
161
|
+
|
|
162
|
+
expect(await dictate(words, {})).toMatchObject({
|
|
163
|
+
mode: "dictation",
|
|
164
|
+
text: words,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("selected-text output budget", () => {
|
|
170
|
+
test("reserves space for the edit and tool-call JSON", () => {
|
|
171
|
+
expect(computeMaxTokens(900)).toBeGreaterThan(600);
|
|
172
|
+
expect(computeMaxTokens(5)).toBe(512);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Also covers the search-query parser: `is:archived` and its synonyms in the
|
|
10
10
|
* `q` string flip the conversation arm to `includeArchived: true`, and the
|
|
11
|
-
* filter token is stripped from the term before any backend is called.
|
|
11
|
+
* filter token is stripped from the term before any backend is called. Under
|
|
12
|
+
* the `sidebar-done` flag the unfiltered default is `true` instead and
|
|
13
|
+
* `is:unarchived` is the opt-out, so the flag-on twins assert the mirror
|
|
14
|
+
* image of the default ones.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
@@ -30,9 +33,13 @@ const searchConversationsMock = mock(
|
|
|
30
33
|
},
|
|
31
34
|
);
|
|
32
35
|
|
|
36
|
+
let sidebarDoneEnabled = false;
|
|
37
|
+
|
|
33
38
|
const actualContactStore = await import("../../../contacts/contact-store.js");
|
|
34
39
|
const actualConvQueries =
|
|
35
40
|
await import("../../../persistence/conversation-queries.js");
|
|
41
|
+
const actualSidebarDoneGate =
|
|
42
|
+
await import("../../../config/sidebar-done-gate.js");
|
|
36
43
|
|
|
37
44
|
mock.module("../../../contacts/contact-store.js", () => ({
|
|
38
45
|
...actualContactStore,
|
|
@@ -42,6 +49,10 @@ mock.module("../../../persistence/conversation-queries.js", () => ({
|
|
|
42
49
|
...actualConvQueries,
|
|
43
50
|
searchConversations: searchConversationsMock,
|
|
44
51
|
}));
|
|
52
|
+
mock.module("../../../config/sidebar-done-gate.js", () => ({
|
|
53
|
+
...actualSidebarDoneGate,
|
|
54
|
+
isSidebarDoneEnabled: () => sidebarDoneEnabled,
|
|
55
|
+
}));
|
|
45
56
|
|
|
46
57
|
const { ROUTES } = await import("../global-search-routes.js");
|
|
47
58
|
|
|
@@ -69,6 +80,7 @@ afterEach(() => {
|
|
|
69
80
|
searchConversationsCalls = [];
|
|
70
81
|
searchConversationsResult = [];
|
|
71
82
|
searchConversationsMock.mockClear();
|
|
83
|
+
sidebarDoneEnabled = false;
|
|
72
84
|
});
|
|
73
85
|
|
|
74
86
|
describe("global-search contacts recency source", () => {
|
|
@@ -268,3 +280,64 @@ describe("global-search q parser", () => {
|
|
|
268
280
|
expect(result.queryTokens).toEqual(["alpha", "beta"]);
|
|
269
281
|
});
|
|
270
282
|
});
|
|
283
|
+
|
|
284
|
+
describe("global-search q parser under sidebar-done", () => {
|
|
285
|
+
test("plain term reaches done chats by default", async () => {
|
|
286
|
+
sidebarDoneEnabled = true;
|
|
287
|
+
|
|
288
|
+
await handler({
|
|
289
|
+
queryParams: { q: "shema", categories: "conversations" },
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(searchConversationsCalls[0]?.query).toBe("shema");
|
|
293
|
+
expect(searchConversationsCalls[0]?.opts).toMatchObject({
|
|
294
|
+
includeArchived: true,
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("is:unarchived (and archive:no / archive:false) narrows back to live chats", async () => {
|
|
299
|
+
for (const filter of ["is:unarchived", "archive:no", "archive:false"]) {
|
|
300
|
+
sidebarDoneEnabled = true;
|
|
301
|
+
searchConversationsCalls = [];
|
|
302
|
+
searchConversationsMock.mockClear();
|
|
303
|
+
|
|
304
|
+
await handler({
|
|
305
|
+
queryParams: {
|
|
306
|
+
q: `${filter} shema`,
|
|
307
|
+
categories: "conversations",
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(searchConversationsCalls[0]?.query).toBe("shema");
|
|
312
|
+
expect(searchConversationsCalls[0]?.opts).toMatchObject({
|
|
313
|
+
includeArchived: false,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("is:archived stays a no-op the term is still stripped of", async () => {
|
|
319
|
+
sidebarDoneEnabled = true;
|
|
320
|
+
|
|
321
|
+
await handler({
|
|
322
|
+
queryParams: { q: "is:archived shema", categories: "conversations" },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
expect(searchConversationsCalls[0]?.query).toBe("shema");
|
|
326
|
+
expect(searchConversationsCalls[0]?.opts).toMatchObject({
|
|
327
|
+
includeArchived: true,
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("unknown filter tokens stay in the term and do not narrow the default", async () => {
|
|
332
|
+
sidebarDoneEnabled = true;
|
|
333
|
+
|
|
334
|
+
await handler({
|
|
335
|
+
queryParams: { q: "is:starred shema", categories: "conversations" },
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(searchConversationsCalls[0]?.query).toBe("is:starred shema");
|
|
339
|
+
expect(searchConversationsCalls[0]?.opts).toMatchObject({
|
|
340
|
+
includeArchived: true,
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
@@ -405,6 +405,66 @@ describe("POST inference/provider-connections (create)", () => {
|
|
|
405
405
|
expect(result.auth).toEqual({ type: "platform" });
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
+
test("stores an openai-compatible row pointed at opencode.ai as provider opencode", async () => {
|
|
409
|
+
const created = (await call(
|
|
410
|
+
findHandler("inference_provider_connections_create"),
|
|
411
|
+
{
|
|
412
|
+
body: {
|
|
413
|
+
name: "opencode-go",
|
|
414
|
+
provider: "openai-compatible",
|
|
415
|
+
credential: "vault/opencode/key",
|
|
416
|
+
base_url: "https://opencode.ai/zen/go/v1",
|
|
417
|
+
models: [{ id: "mimo-v2.6-flash" }],
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
)) as { provider: string; baseUrl: string | null };
|
|
421
|
+
expect(created.provider).toBe("opencode");
|
|
422
|
+
expect(created.baseUrl).toBe("https://opencode.ai/zen/go/v1");
|
|
423
|
+
|
|
424
|
+
const lookalike = (await call(
|
|
425
|
+
findHandler("inference_provider_connections_create"),
|
|
426
|
+
{
|
|
427
|
+
body: {
|
|
428
|
+
name: "lookalike",
|
|
429
|
+
provider: "openai-compatible",
|
|
430
|
+
base_url: "https://opencode.ai.evil.example/v1",
|
|
431
|
+
models: [{ id: "m" }],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
)) as { provider: string };
|
|
435
|
+
expect(lookalike.provider).toBe("openai-compatible");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("rejects a keyless openai-compatible row pointed at opencode.ai", async () => {
|
|
439
|
+
await expect(
|
|
440
|
+
call(findHandler("inference_provider_connections_create"), {
|
|
441
|
+
body: {
|
|
442
|
+
name: "opencode-keyless",
|
|
443
|
+
provider: "openai-compatible",
|
|
444
|
+
base_url: "https://opencode.ai/zen/v1",
|
|
445
|
+
models: [{ id: "m" }],
|
|
446
|
+
},
|
|
447
|
+
}),
|
|
448
|
+
).rejects.toThrow(/Provider "opencode" requires an API key/);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("rejects repointing an existing openai-compatible row at opencode.ai", async () => {
|
|
452
|
+
await call(findHandler("inference_provider_connections_create"), {
|
|
453
|
+
body: {
|
|
454
|
+
name: "my-proxy",
|
|
455
|
+
provider: "openai-compatible",
|
|
456
|
+
base_url: "http://localhost:8080/v1",
|
|
457
|
+
models: [{ id: "m" }],
|
|
458
|
+
},
|
|
459
|
+
});
|
|
460
|
+
await expect(
|
|
461
|
+
call(findHandler("inference_provider_connections_update"), {
|
|
462
|
+
pathParams: { name: "my-proxy" },
|
|
463
|
+
body: { base_url: "https://opencode.ai/zen/v1" },
|
|
464
|
+
}),
|
|
465
|
+
).rejects.toThrow(/needs an "opencode" connection/);
|
|
466
|
+
});
|
|
467
|
+
|
|
408
468
|
test("rejects a whitespace-only label", async () => {
|
|
409
469
|
await expect(
|
|
410
470
|
call(findHandler("inference_provider_connections_create"), {
|
|
@@ -4,8 +4,9 @@ import { CALL_SITE_DEFAULTS } from "../../../config/call-site-defaults.js";
|
|
|
4
4
|
import {
|
|
5
5
|
type LLMCallSite,
|
|
6
6
|
LLMCallSiteEnum,
|
|
7
|
+
LLMSchema,
|
|
7
8
|
} from "../../../config/schemas/llm.js";
|
|
8
|
-
import { ROUTES } from "../llm-call-sites-routes.js";
|
|
9
|
+
import { buildCallSiteCatalog, ROUTES } from "../llm-call-sites-routes.js";
|
|
9
10
|
|
|
10
11
|
const route = ROUTES.find((r) => r.operationId === "llm_call_sites_list")!;
|
|
11
12
|
|
|
@@ -89,6 +90,20 @@ describe("llm-call-sites-routes", () => {
|
|
|
89
90
|
}
|
|
90
91
|
});
|
|
91
92
|
|
|
93
|
+
test("omits an unavailable managed-only shipped default under BYOK", () => {
|
|
94
|
+
const managed = buildCallSiteCatalog(LLMSchema.parse({})).find(
|
|
95
|
+
({ id }) => id === "voiceEscalationJudge",
|
|
96
|
+
);
|
|
97
|
+
expect(managed?.defaultProfile).toBe("jev-managed");
|
|
98
|
+
expect(managed?.shippedDefaultProfile).toBe("jev-managed");
|
|
99
|
+
|
|
100
|
+
const byok = buildCallSiteCatalog(
|
|
101
|
+
LLMSchema.parse({ defaultProvider: { provider: "anthropic" } }),
|
|
102
|
+
).find(({ id }) => id === "voiceEscalationJudge");
|
|
103
|
+
expect(byok?.defaultProfile).toBeUndefined();
|
|
104
|
+
expect(byok?.shippedDefaultProfile).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
92
107
|
test("domains have non-empty id and displayName", async () => {
|
|
93
108
|
const result = (await route.handler({})) as {
|
|
94
109
|
domains: Array<{ id: string; displayName: string }>;
|