@vellumai/assistant 0.11.5 → 0.11.6-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/AGENTS.md +5 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +70 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +16 -1
- package/node_modules/@vellumai/gateway-client/src/index.ts +6 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +121 -61
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/ingress.ts +103 -0
- package/openapi.yaml +421 -15
- package/package.json +1 -1
- package/scripts/sync-web-search-catalog.ts +6 -0
- package/src/__tests__/app-pin-store.test.ts +149 -0
- package/src/__tests__/channel-availability-routes.test.ts +23 -1
- package/src/__tests__/channel-readiness-discord.test.ts +231 -0
- package/src/__tests__/channel-readiness-service.test.ts +126 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +141 -0
- package/src/__tests__/channel-reply-delivery.test.ts +4 -4
- package/src/__tests__/client-os-metadata-persistence.test.ts +23 -10
- package/src/__tests__/conversation-delete-watch-timeline.test.ts +231 -0
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-seed-composer.test.ts +8 -0
- package/src/__tests__/conversation-slash-commands.test.ts +8 -0
- package/src/__tests__/disk-pressure-policy.test.ts +6 -0
- package/src/__tests__/gemini-provider.test.ts +138 -0
- package/src/__tests__/history-repair.test.ts +105 -3
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +45 -0
- package/src/__tests__/migration-import-from-path.test.ts +349 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +102 -0
- package/src/__tests__/oauth-commands-routes.test.ts +89 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +7 -6
- package/src/__tests__/openai-provider.test.ts +18 -0
- package/src/__tests__/openai-responses-provider.test.ts +18 -0
- package/src/__tests__/platform-callback-registration.test.ts +184 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +71 -3
- package/src/__tests__/pricing.test.ts +2 -2
- package/src/__tests__/public-ingress-urls.test.ts +36 -0
- package/src/__tests__/resolve-trust-class.test.ts +0 -48
- package/src/__tests__/sanitize-config-for-transfer.test.ts +28 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +49 -0
- package/src/__tests__/settings-routes.test.ts +85 -3
- package/src/__tests__/web-search-catalog-parity.test.ts +8 -0
- package/src/agent/history-repair/history-repair.ts +45 -14
- package/src/agent/loop.ts +4 -1
- package/src/api/constants/profile-config-validation.ts +60 -0
- package/src/api/events/tool-result.ts +6 -1
- package/src/api/events/watch-retro-completed.ts +52 -0
- package/src/api/index.ts +11 -0
- package/src/apps/app-pin-reconciler.ts +92 -0
- package/src/apps/app-pin-store.ts +125 -0
- package/src/channels/gateway-channel-socket-health.ts +32 -0
- package/src/channels/gateway-discord-admission.ts +32 -0
- package/src/channels/types.ts +20 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/inference-profiles.test.ts +16 -4
- package/src/cli/commands/__tests__/inference-providers.test.ts +67 -2
- package/src/cli/commands/channels/__tests__/channels.test.ts +85 -0
- package/src/cli/commands/channels/index.ts +45 -31
- package/src/cli/commands/inference-profiles.ts +56 -3
- package/src/cli/commands/inference-providers.ts +28 -2
- package/src/cli/commands/oauth/index.help.ts +7 -1
- package/src/cli/commands/oauth/request.test.ts +290 -0
- package/src/cli/commands/oauth/request.ts +57 -41
- package/src/cli/lib/bundled-marketplace.json +14 -1
- package/src/cli/lib/open-browser.test.ts +67 -0
- package/src/cli/lib/open-browser.ts +24 -5
- package/src/config/__tests__/profile-materialization.test.ts +26 -0
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +6 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/profile-materialization.ts +29 -0
- package/src/config/sanitize-for-transfer.ts +16 -0
- package/src/config/schemas/llm.ts +7 -0
- package/src/config/schemas/services.ts +6 -0
- package/src/context/outbound-sanitize.ts +6 -0
- package/src/daemon/__tests__/lifecycle-watch-timeline-sweep.test.ts +98 -0
- package/src/daemon/conversation-error.ts +24 -2
- package/src/daemon/conversation-slash.ts +6 -15
- package/src/daemon/daemon-control.ts +1 -0
- package/src/daemon/disk-pressure-policy.ts +7 -1
- package/src/daemon/handlers/__tests__/config-ingress-tunnel-records.test.ts +208 -0
- package/src/daemon/handlers/config-ingress.ts +115 -5
- package/src/daemon/lifecycle.ts +26 -0
- package/src/daemon/message-types/web-activity.ts +3 -2
- package/src/daemon/trust-context.ts +0 -37
- package/src/inbound/__tests__/tunnel-probe.test.ts +448 -0
- package/src/inbound/platform-callback-registration.ts +28 -2
- package/src/inbound/public-ingress-urls.ts +12 -0
- package/src/inbound/tunnel-probe.ts +261 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +25 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +8 -2
- package/src/live-voice/__tests__/live-voice-session-manager.test.ts +212 -10
- package/src/live-voice/__tests__/live-voice-session-telemetry.test.ts +5 -2
- package/src/live-voice/live-voice-connection.ts +46 -8
- package/src/live-voice/live-voice-manager.ts +25 -0
- package/src/live-voice/live-voice-session-manager.ts +318 -2
- package/src/live-voice/live-voice-session.ts +52 -2
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +126 -68
- package/src/messaging/providers/channel-transport.ts +64 -47
- package/src/messaging/providers/discord/send.test.ts +46 -1
- package/src/messaging/providers/discord/send.ts +51 -0
- package/src/messaging/providers/discord/transport.ts +26 -3
- package/src/messaging/providers/index.ts +22 -47
- package/src/messaging/providers/slack/send.test.ts +83 -26
- package/src/messaging/providers/slack/send.ts +120 -51
- package/src/messaging/providers/slack/stream-tasks.test.ts +26 -0
- package/src/messaging/providers/slack/stream-tasks.ts +39 -0
- package/src/messaging/providers/slack/transport.ts +24 -22
- package/src/messaging/providers/telegram-bot/send.test.ts +109 -12
- package/src/messaging/providers/telegram-bot/send.ts +43 -0
- package/src/messaging/providers/telegram-bot/transport.ts +25 -8
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +30 -7
- package/src/notifications/adapters/telegram.ts +48 -1
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/conversation-seed-composer.ts +7 -2
- package/src/oauth/byo-connection.test.ts +63 -0
- package/src/oauth/byo-connection.ts +16 -15
- package/src/oauth/connection.test.ts +111 -0
- package/src/oauth/connection.ts +142 -1
- package/src/oauth/platform-connection.test.ts +34 -0
- package/src/oauth/platform-connection.ts +28 -5
- package/src/oauth/seed-providers.ts +15 -1
- package/src/permissions/types.ts +3 -1
- package/src/persistence/conversation-crud.ts +46 -0
- package/src/persistence/conversation-types.ts +11 -9
- package/src/persistence/db-async-query.ts +2 -1
- package/src/persistence/db-maintenance.ts +15 -0
- package/src/persistence/embeddings/qdrant-manager.ts +1 -0
- package/src/persistence/migrations/367-create-watch-timeline-entries.ts +46 -0
- package/src/persistence/migrations/368-watch-timeline-screenshot-blob.ts +33 -0
- package/src/persistence/migrations/369-create-app-pins.ts +37 -0
- package/src/persistence/migrations/__tests__/367-create-watch-timeline-entries.test.ts +98 -0
- package/src/persistence/migrations/__tests__/368-watch-timeline-screenshot-blob.test.ts +98 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/infrastructure.ts +17 -0
- package/src/persistence/schema/watch.ts +29 -0
- package/src/persistence/steps.ts +6 -0
- package/src/plugins/mtime-cache.ts +11 -0
- package/src/providers/__tests__/retry-network-error.test.ts +84 -0
- package/src/providers/connection-resolution.ts +23 -1
- package/src/providers/content-blocks.ts +9 -0
- package/src/providers/fetch-provider-catalog.ts +19 -0
- package/src/providers/gemini/client.ts +13 -5
- package/src/providers/inference/__tests__/endpoint-probe.test.ts +92 -0
- package/src/providers/inference/__tests__/profile-config-validation.test.ts +39 -0
- package/src/providers/inference/__tests__/profile-probe-classify.test.ts +66 -0
- package/src/providers/inference/adapter-factory.ts +0 -9
- package/src/providers/inference/credential-rotation.ts +61 -0
- package/src/providers/inference/endpoint-probe.ts +115 -0
- package/src/providers/inference/profile-probe.ts +256 -0
- package/src/providers/model-catalog.ts +170 -125
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +17 -1
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +42 -60
- package/src/providers/openai/__tests__/connection-error-wrap.test.ts +44 -0
- package/src/providers/openai/__tests__/orphan-tool-result-guard.test.ts +34 -2
- package/src/providers/openai/api-error-normalization.ts +16 -2
- package/src/providers/openai/chat-completions-provider.ts +75 -29
- package/src/providers/openai/responses-provider.ts +5 -2
- package/src/providers/openrouter/client.ts +0 -1
- package/src/providers/provider-send-message.ts +11 -0
- package/src/providers/retry.ts +6 -0
- package/src/providers/search-provider-catalog.ts +20 -0
- package/src/providers/vercel-ai-gateway/client.ts +0 -1
- package/src/runtime/AGENTS.md +1 -0
- package/src/runtime/__tests__/desktop-presence.test.ts +27 -4
- package/src/runtime/__tests__/host-observe.test.ts +302 -0
- package/src/runtime/channel-readiness-service.ts +214 -14
- package/src/runtime/channel-readiness-types.ts +49 -2
- package/src/runtime/channel-reply-delivery.ts +2 -2
- package/src/runtime/desktop-presence.ts +24 -21
- package/src/runtime/host-observe.ts +246 -0
- package/src/runtime/http-server.ts +181 -1
- package/src/runtime/migrations/__tests__/staged-import-path.test.ts +104 -0
- package/src/runtime/migrations/staged-import-path.ts +116 -0
- package/src/runtime/routes/__tests__/app-pin-routes.test.ts +383 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +80 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +118 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/ingress-status-routes.test.ts +508 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -56
- package/src/runtime/routes/__tests__/watch-routes-guardian-cache.test.ts +139 -0
- package/src/runtime/routes/__tests__/watch-routes.test.ts +598 -0
- package/src/runtime/routes/app-management-routes.ts +140 -29
- package/src/runtime/routes/channel-availability-routes.ts +1 -0
- package/src/runtime/routes/channel-readiness-routes.ts +14 -2
- package/src/runtime/routes/conversation-query-routes.ts +10 -0
- package/src/runtime/routes/guardian-approval-interception.ts +24 -33
- package/src/runtime/routes/host-cu-routes.ts +18 -0
- package/src/runtime/routes/identity-routes.ts +2 -0
- package/src/runtime/routes/inbound-message-handler.ts +10 -7
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +166 -308
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +158 -335
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +232 -31
- package/src/runtime/routes/inference-provider-connection-routes.ts +24 -4
- package/src/runtime/routes/ingress-status-routes.ts +180 -0
- package/src/runtime/routes/live-voice-routes.test.ts +40 -1
- package/src/runtime/routes/live-voice-routes.ts +34 -0
- package/src/runtime/routes/migration-routes.ts +218 -10
- package/src/runtime/routes/oauth-commands-routes.ts +23 -16
- package/src/runtime/routes/plugins-routes.ts +12 -28
- package/src/runtime/routes/question-routes.ts +6 -0
- package/src/runtime/routes/secret-routes.ts +7 -27
- package/src/runtime/routes/settings-routes.ts +9 -6
- package/src/runtime/routes/watch-routes.ts +807 -0
- package/src/runtime/slack-reply-session.test.ts +230 -121
- package/src/runtime/slack-reply-session.ts +113 -81
- package/src/runtime/{slack-task-progress.test.ts → task-progress.test.ts} +1 -28
- package/src/runtime/{slack-task-progress.ts → task-progress.ts} +30 -51
- package/src/security/__tests__/untrusted-content.test.ts +42 -0
- package/src/security/untrusted-content.ts +28 -9
- package/src/telemetry/__tests__/live-voice-funnel.test.ts +108 -0
- package/src/telemetry/live-voice-funnel.ts +75 -8
- package/src/tools/credentials/store.ts +18 -6
- package/src/tools/network/__tests__/firecrawl-compat.test.ts +77 -0
- package/src/tools/network/__tests__/web-fetch-fastcrw.test.ts +169 -0
- package/src/tools/network/__tests__/web-search.test.ts +97 -2
- package/src/tools/network/firecrawl-compat.ts +90 -0
- package/src/tools/network/web-fetch.ts +142 -62
- package/src/tools/network/web-search.ts +141 -55
- package/src/tools/types.ts +2 -1
- package/src/util/oauth-request-body.test.ts +74 -0
- package/src/util/oauth-request-body.ts +60 -0
- package/src/util/worker-process.ts +1 -0
- package/src/watch/__tests__/watch-retro.test.ts +665 -0
- package/src/watch/__tests__/watch-session-manager.test.ts +566 -0
- package/src/watch/__tests__/watch-timeline.test.ts +670 -0
- package/src/watch/watch-retro.ts +480 -0
- package/src/watch/watch-session-manager.ts +575 -0
- package/src/watch/watch-timeline.ts +848 -0
|
@@ -13,6 +13,15 @@ const sendCalls: Array<{
|
|
|
13
13
|
/** When true, sendTelegramReply throws if an approval argument is present. */
|
|
14
14
|
let rejectRichDelivery = false;
|
|
15
15
|
|
|
16
|
+
const editCalls: Array<{
|
|
17
|
+
chatId: string;
|
|
18
|
+
messageId: string;
|
|
19
|
+
text: string;
|
|
20
|
+
}> = [];
|
|
21
|
+
|
|
22
|
+
/** When set, editTelegramMessage rejects with this message. */
|
|
23
|
+
let editFailure: string | undefined;
|
|
24
|
+
|
|
16
25
|
mock.module("../messaging/providers/telegram-bot/send.js", () => ({
|
|
17
26
|
sendTelegramReply: async (
|
|
18
27
|
chatId: string,
|
|
@@ -37,6 +46,16 @@ mock.module("../messaging/providers/telegram-bot/send.js", () => ({
|
|
|
37
46
|
totalCount: 0,
|
|
38
47
|
}),
|
|
39
48
|
sendTelegramTypingIndicator: async () => true,
|
|
49
|
+
editTelegramMessage: async (
|
|
50
|
+
chatId: string,
|
|
51
|
+
messageId: string,
|
|
52
|
+
text: string,
|
|
53
|
+
) => {
|
|
54
|
+
if (editFailure) {
|
|
55
|
+
throw new Error(editFailure);
|
|
56
|
+
}
|
|
57
|
+
editCalls.push({ chatId, messageId, text });
|
|
58
|
+
},
|
|
40
59
|
}));
|
|
41
60
|
|
|
42
61
|
import { TelegramAdapter } from "../notifications/adapters/telegram.js";
|
|
@@ -72,7 +91,9 @@ function makeDestination(
|
|
|
72
91
|
describe("TelegramAdapter", () => {
|
|
73
92
|
beforeEach(() => {
|
|
74
93
|
sendCalls.length = 0;
|
|
94
|
+
editCalls.length = 0;
|
|
75
95
|
rejectRichDelivery = false;
|
|
96
|
+
editFailure = undefined;
|
|
76
97
|
});
|
|
77
98
|
|
|
78
99
|
test("prefers deliveryText and does not append deterministic label", async () => {
|
|
@@ -301,4 +322,85 @@ describe("TelegramAdapter", () => {
|
|
|
301
322
|
);
|
|
302
323
|
expect(call.text).toContain("XYZW");
|
|
303
324
|
});
|
|
325
|
+
|
|
326
|
+
describe("update", () => {
|
|
327
|
+
test("edits the delivered message in place and keeps its id", async () => {
|
|
328
|
+
const adapter = new TelegramAdapter();
|
|
329
|
+
|
|
330
|
+
const result = await adapter.update(
|
|
331
|
+
{
|
|
332
|
+
deliveryId: "del-1",
|
|
333
|
+
destination: "chat-123",
|
|
334
|
+
messageId: "5150",
|
|
335
|
+
conversationId: null,
|
|
336
|
+
},
|
|
337
|
+
{ body: "Approved by Alice" },
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
expect(result.success).toBe(true);
|
|
341
|
+
expect(editCalls).toEqual([
|
|
342
|
+
{ chatId: "chat-123", messageId: "5150", text: "Approved by Alice" },
|
|
343
|
+
]);
|
|
344
|
+
// An edit addresses one message and leaves it in place, so the delivery
|
|
345
|
+
// row's id must still identify the card afterwards.
|
|
346
|
+
expect(result.messageId).toBe("5150");
|
|
347
|
+
// Revising a card must never post a second one beside it.
|
|
348
|
+
expect(sendCalls).toHaveLength(0);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("falls back to the title when no body is supplied", async () => {
|
|
352
|
+
const adapter = new TelegramAdapter();
|
|
353
|
+
|
|
354
|
+
await adapter.update(
|
|
355
|
+
{
|
|
356
|
+
deliveryId: "del-1",
|
|
357
|
+
destination: "chat-123",
|
|
358
|
+
messageId: "5150",
|
|
359
|
+
conversationId: null,
|
|
360
|
+
},
|
|
361
|
+
{ title: "Expired" },
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
expect(editCalls[0]?.text).toBe("Expired");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("refuses a delivery that captured no message id", async () => {
|
|
368
|
+
const adapter = new TelegramAdapter();
|
|
369
|
+
|
|
370
|
+
const result = await adapter.update(
|
|
371
|
+
{
|
|
372
|
+
deliveryId: "del-1",
|
|
373
|
+
destination: "chat-123",
|
|
374
|
+
messageId: null,
|
|
375
|
+
conversationId: null,
|
|
376
|
+
},
|
|
377
|
+
{ body: "Approved" },
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(result.success).toBe(false);
|
|
381
|
+
expect(result.error).toContain("missing_message_id");
|
|
382
|
+
expect(editCalls).toHaveLength(0);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("reports a failed edit rather than posting a replacement", async () => {
|
|
386
|
+
const adapter = new TelegramAdapter();
|
|
387
|
+
editFailure = "Telegram API error: message to edit not found";
|
|
388
|
+
|
|
389
|
+
const result = await adapter.update(
|
|
390
|
+
{
|
|
391
|
+
deliveryId: "del-1",
|
|
392
|
+
destination: "chat-123",
|
|
393
|
+
messageId: "5150",
|
|
394
|
+
conversationId: null,
|
|
395
|
+
},
|
|
396
|
+
{ body: "Approved" },
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
expect(result.success).toBe(false);
|
|
400
|
+
expect(result.error).toContain("message to edit not found");
|
|
401
|
+
// The original would otherwise sit beside the replacement, which reads
|
|
402
|
+
// as the assistant answering twice.
|
|
403
|
+
expect(sendCalls).toHaveLength(0);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
304
406
|
});
|
|
@@ -656,6 +656,95 @@ describe("POST oauth/request", () => {
|
|
|
656
656
|
expect(result.body).toEqual({ hello: "world" });
|
|
657
657
|
});
|
|
658
658
|
|
|
659
|
+
test("passes a pre-parsed string body through to the connection unchanged", async () => {
|
|
660
|
+
const multipart =
|
|
661
|
+
"--boundary\r\nContent-Type: application/json\r\n\r\n{}\r\n--boundary--\r\n";
|
|
662
|
+
|
|
663
|
+
await getRoute("POST", "oauth/request").handler(
|
|
664
|
+
makeArgs({
|
|
665
|
+
body: {
|
|
666
|
+
provider: "google",
|
|
667
|
+
url: "https://api.google.com/upload/drive/v3/files",
|
|
668
|
+
method: "POST",
|
|
669
|
+
headers: { "Content-Type": "multipart/related; boundary=boundary" },
|
|
670
|
+
parsed_data: multipart,
|
|
671
|
+
},
|
|
672
|
+
}),
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
expect(mockResolveRequests).toHaveLength(1);
|
|
676
|
+
const req = mockResolveRequests[0] as {
|
|
677
|
+
body: unknown;
|
|
678
|
+
headers: Record<string, string>;
|
|
679
|
+
};
|
|
680
|
+
expect(req.body).toBe(multipart);
|
|
681
|
+
expect(req.headers["Content-Type"]).toBe(
|
|
682
|
+
"multipart/related; boundary=boundary",
|
|
683
|
+
);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("keeps a raw data string raw under a non-JSON Content-Type", async () => {
|
|
687
|
+
await getRoute("POST", "oauth/request").handler(
|
|
688
|
+
makeArgs({
|
|
689
|
+
body: {
|
|
690
|
+
provider: "google",
|
|
691
|
+
url: "https://api.google.com/upload/drive/v3/files",
|
|
692
|
+
method: "POST",
|
|
693
|
+
headers: { "content-type": "text/csv" },
|
|
694
|
+
data: '{"looks":"like json"}',
|
|
695
|
+
},
|
|
696
|
+
}),
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
const req = mockResolveRequests[0] as { body: unknown };
|
|
700
|
+
expect(req.body).toBe('{"looks":"like json"}');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test("parses a raw data string as JSON under a JSON Content-Type", async () => {
|
|
704
|
+
await getRoute("POST", "oauth/request").handler(
|
|
705
|
+
makeArgs({
|
|
706
|
+
body: {
|
|
707
|
+
provider: "google",
|
|
708
|
+
url: "https://api.google.com/v1/sheets",
|
|
709
|
+
method: "POST",
|
|
710
|
+
headers: { "Content-Type": "application/json" },
|
|
711
|
+
data: '{"title":"Sheet"}',
|
|
712
|
+
},
|
|
713
|
+
}),
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
const req = mockResolveRequests[0] as { body: unknown };
|
|
717
|
+
expect(req.body).toEqual({ title: "Sheet" });
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test("base64-encodes binary response bodies for the JSON envelope", async () => {
|
|
721
|
+
const binary = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0xff, 0x00]);
|
|
722
|
+
mockResolveResponse = {
|
|
723
|
+
status: 200,
|
|
724
|
+
headers: { "content-type": "application/octet-stream" },
|
|
725
|
+
body: binary,
|
|
726
|
+
};
|
|
727
|
+
const result = (await getRoute("POST", "oauth/request").handler(
|
|
728
|
+
makeArgs({
|
|
729
|
+
body: {
|
|
730
|
+
provider: "google",
|
|
731
|
+
url: "https://api.google.com/drive/v3/files/file-123?alt=media",
|
|
732
|
+
},
|
|
733
|
+
}),
|
|
734
|
+
)) as {
|
|
735
|
+
ok: boolean;
|
|
736
|
+
status: number;
|
|
737
|
+
body: unknown;
|
|
738
|
+
bodyEncoding?: string;
|
|
739
|
+
};
|
|
740
|
+
expect(result.ok).toBe(true);
|
|
741
|
+
expect(result.body).toBe(binary.toString("base64"));
|
|
742
|
+
expect(result.bodyEncoding).toBe("base64");
|
|
743
|
+
expect(Buffer.from(String(result.body), "base64").equals(binary)).toBe(
|
|
744
|
+
true,
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
|
|
659
748
|
test("rejects absolute URL host outside provider base host when no injection templates exist", async () => {
|
|
660
749
|
await expect(
|
|
661
750
|
getRoute("POST", "oauth/request").handler(
|
|
@@ -14,16 +14,15 @@ await initializeDb();
|
|
|
14
14
|
seedOAuthProviders();
|
|
15
15
|
|
|
16
16
|
describe("oauth provider profiles (DB-seeded)", () => {
|
|
17
|
-
test("google provider row includes Drive in default scopes", () => {
|
|
17
|
+
test("google provider row includes Drive, Sheets, and Slides in default scopes", () => {
|
|
18
18
|
const provider = getProvider("google");
|
|
19
19
|
|
|
20
20
|
expect(provider).toBeDefined();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
21
|
+
const scopes = JSON.parse(provider!.defaultScopes) as string[];
|
|
22
|
+
expect(scopes).toContain("https://www.googleapis.com/auth/drive");
|
|
24
23
|
});
|
|
25
24
|
|
|
26
|
-
test("google provider row contains bearer injection templates for
|
|
25
|
+
test("google provider row contains bearer injection templates for 8 Google API hosts", () => {
|
|
27
26
|
const provider = getProvider("google");
|
|
28
27
|
|
|
29
28
|
expect(provider).toBeDefined();
|
|
@@ -36,7 +35,7 @@ describe("oauth provider profiles (DB-seeded)", () => {
|
|
|
36
35
|
valuePrefix: string;
|
|
37
36
|
}>;
|
|
38
37
|
|
|
39
|
-
expect(templates).toHaveLength(
|
|
38
|
+
expect(templates).toHaveLength(8);
|
|
40
39
|
|
|
41
40
|
const byHost = new Map(templates.map((t) => [t.hostPattern, t]));
|
|
42
41
|
|
|
@@ -45,6 +44,8 @@ describe("oauth provider profiles (DB-seeded)", () => {
|
|
|
45
44
|
"www.googleapis.com",
|
|
46
45
|
"people.googleapis.com",
|
|
47
46
|
"docs.googleapis.com",
|
|
47
|
+
"sheets.googleapis.com",
|
|
48
|
+
"slides.googleapis.com",
|
|
48
49
|
"tasks.googleapis.com",
|
|
49
50
|
"calendar.googleapis.com",
|
|
50
51
|
]) {
|
|
@@ -82,9 +82,27 @@ class FakeAPIError extends Error {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
// Simulate the SDK's transport-failure and caller-abort subclasses; the
|
|
86
|
+
// error normalizer distinguishes them by class, so the mock must define both
|
|
87
|
+
// for suites that share this process.
|
|
88
|
+
class FakeAPIConnectionError extends FakeAPIError {
|
|
89
|
+
constructor() {
|
|
90
|
+
super(undefined as unknown as number, "Connection error.");
|
|
91
|
+
this.name = "APIConnectionError";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
class FakeAPIUserAbortError extends FakeAPIError {
|
|
95
|
+
constructor() {
|
|
96
|
+
super(undefined as unknown as number, "Request was aborted.");
|
|
97
|
+
this.name = "APIUserAbortError";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
85
101
|
mock.module("openai", () => ({
|
|
86
102
|
default: class MockOpenAI {
|
|
87
103
|
static APIError = FakeAPIError;
|
|
104
|
+
static APIConnectionError = FakeAPIConnectionError;
|
|
105
|
+
static APIUserAbortError = FakeAPIUserAbortError;
|
|
88
106
|
constructor(opts: Record<string, unknown>) {
|
|
89
107
|
lastConstructorOptions = opts;
|
|
90
108
|
}
|
|
@@ -50,9 +50,27 @@ class FakeAPIError extends Error {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Simulate the SDK's transport-failure and caller-abort subclasses; the
|
|
54
|
+
// error normalizer distinguishes them by class, so the mock must define both
|
|
55
|
+
// for suites that share this process.
|
|
56
|
+
class FakeAPIConnectionError extends FakeAPIError {
|
|
57
|
+
constructor() {
|
|
58
|
+
super(undefined as unknown as number, "Connection error.");
|
|
59
|
+
this.name = "APIConnectionError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
class FakeAPIUserAbortError extends FakeAPIError {
|
|
63
|
+
constructor() {
|
|
64
|
+
super(undefined as unknown as number, "Request was aborted.");
|
|
65
|
+
this.name = "APIUserAbortError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
mock.module("openai", () => ({
|
|
54
70
|
default: class MockOpenAI {
|
|
55
71
|
static APIError = FakeAPIError;
|
|
72
|
+
static APIConnectionError = FakeAPIConnectionError;
|
|
73
|
+
static APIUserAbortError = FakeAPIUserAbortError;
|
|
56
74
|
constructor(opts: Record<string, unknown>) {
|
|
57
75
|
lastConstructorOptions = opts;
|
|
58
76
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { setIngressPublicBaseUrl } from "../config/env.js";
|
|
3
4
|
import { credentialKey } from "../security/credential-key.js";
|
|
4
5
|
|
|
5
6
|
let mockIsPlatform = true;
|
|
6
7
|
let mockPlatformBaseUrl = "";
|
|
7
8
|
let mockPlatformAssistantId = "";
|
|
8
9
|
let mockSecureKeys: Record<string, string> = {};
|
|
10
|
+
let mockConfig: { ingress?: { publicBaseUrl?: string; enabled?: boolean } } =
|
|
11
|
+
{};
|
|
9
12
|
|
|
10
13
|
// Bun shares mocked modules across test files in a combined run, so each mock
|
|
11
14
|
// spreads the real module and overrides only what this file drives. Replacing
|
|
@@ -30,6 +33,12 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
30
33
|
getSecureKeyAsync: async (key: string) => mockSecureKeys[key] ?? undefined,
|
|
31
34
|
}));
|
|
32
35
|
|
|
36
|
+
const actualLoader = await import("../config/loader.js");
|
|
37
|
+
mock.module("../config/loader.js", () => ({
|
|
38
|
+
...actualLoader,
|
|
39
|
+
getConfig: () => mockConfig,
|
|
40
|
+
}));
|
|
41
|
+
|
|
33
42
|
const originalFetch = globalThis.fetch;
|
|
34
43
|
const originalEnvCredential = process.env.ASSISTANT_API_KEY;
|
|
35
44
|
|
|
@@ -48,6 +57,8 @@ describe("platform callback registration", () => {
|
|
|
48
57
|
mockPlatformBaseUrl = "";
|
|
49
58
|
mockPlatformAssistantId = "";
|
|
50
59
|
mockSecureKeys = {};
|
|
60
|
+
mockConfig = {};
|
|
61
|
+
setIngressPublicBaseUrl(undefined);
|
|
51
62
|
delete process.env.ASSISTANT_API_KEY;
|
|
52
63
|
globalThis.fetch = originalFetch;
|
|
53
64
|
});
|
|
@@ -155,6 +166,177 @@ describe("platform callback registration", () => {
|
|
|
155
166
|
registerCallbackRoute("webhooks/telegram", "telegram"),
|
|
156
167
|
).resolves.toBe("https://platform.example.com/v1/gateway/callbacks/x/");
|
|
157
168
|
});
|
|
169
|
+
|
|
170
|
+
test("self-hosted registerCallbackRoute sends configured callback_base_url", async () => {
|
|
171
|
+
mockIsPlatform = false;
|
|
172
|
+
mockConfig = {
|
|
173
|
+
ingress: { publicBaseUrl: "https://my-assistant.example.com" },
|
|
174
|
+
};
|
|
175
|
+
mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
|
|
176
|
+
"https://platform.example.com";
|
|
177
|
+
mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
|
|
178
|
+
"22222222-3333-4444-8555-666666666666";
|
|
179
|
+
mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
|
|
180
|
+
"ast-self-hosted-key";
|
|
181
|
+
|
|
182
|
+
globalThis.fetch = mock(
|
|
183
|
+
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
184
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
185
|
+
assistant_id: "22222222-3333-4444-8555-666666666666",
|
|
186
|
+
callback_path: "webhooks/telegram",
|
|
187
|
+
type: "telegram",
|
|
188
|
+
callback_base_url: "https://my-assistant.example.com",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return new Response(
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
callback_url:
|
|
194
|
+
"https://my-assistant.example.com/v1/gateway/callbacks/x/",
|
|
195
|
+
callback_path:
|
|
196
|
+
"22222222-3333-4444-8555-666666666666/webhooks/telegram",
|
|
197
|
+
type: "telegram",
|
|
198
|
+
assistant_id: "22222222-3333-4444-8555-666666666666",
|
|
199
|
+
}),
|
|
200
|
+
{
|
|
201
|
+
status: 201,
|
|
202
|
+
headers: { "content-type": "application/json" },
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
) as unknown as typeof fetch;
|
|
207
|
+
|
|
208
|
+
await expect(
|
|
209
|
+
registerCallbackRoute("webhooks/telegram", "telegram"),
|
|
210
|
+
).resolves.toBe(
|
|
211
|
+
"https://my-assistant.example.com/v1/gateway/callbacks/x/",
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("self-hosted registerCallbackRoute sends detected module-level callback_base_url", async () => {
|
|
216
|
+
mockIsPlatform = false;
|
|
217
|
+
mockConfig = {};
|
|
218
|
+
setIngressPublicBaseUrl("https://detected.example.com/");
|
|
219
|
+
mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
|
|
220
|
+
"https://platform.example.com";
|
|
221
|
+
mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
|
|
222
|
+
"22222222-3333-4444-8555-666666666666";
|
|
223
|
+
mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
|
|
224
|
+
"ast-self-hosted-key";
|
|
225
|
+
|
|
226
|
+
globalThis.fetch = mock(
|
|
227
|
+
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
228
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
229
|
+
assistant_id: "22222222-3333-4444-8555-666666666666",
|
|
230
|
+
callback_path: "webhooks/telegram",
|
|
231
|
+
type: "telegram",
|
|
232
|
+
callback_base_url: "https://detected.example.com",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return new Response(
|
|
236
|
+
JSON.stringify({
|
|
237
|
+
callback_url:
|
|
238
|
+
"https://detected.example.com/v1/gateway/callbacks/x/",
|
|
239
|
+
callback_path:
|
|
240
|
+
"22222222-3333-4444-8555-666666666666/webhooks/telegram",
|
|
241
|
+
type: "telegram",
|
|
242
|
+
assistant_id: "22222222-3333-4444-8555-666666666666",
|
|
243
|
+
}),
|
|
244
|
+
{
|
|
245
|
+
status: 201,
|
|
246
|
+
headers: { "content-type": "application/json" },
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
) as unknown as typeof fetch;
|
|
251
|
+
|
|
252
|
+
await expect(
|
|
253
|
+
registerCallbackRoute("webhooks/telegram", "telegram"),
|
|
254
|
+
).resolves.toBe("https://detected.example.com/v1/gateway/callbacks/x/");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("self-hosted registerCallbackRoute omits callback_base_url when no ingress is available", async () => {
|
|
258
|
+
mockIsPlatform = false;
|
|
259
|
+
mockConfig = {};
|
|
260
|
+
mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
|
|
261
|
+
"https://platform.example.com";
|
|
262
|
+
mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
|
|
263
|
+
"22222222-3333-4444-8555-666666666666";
|
|
264
|
+
mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
|
|
265
|
+
"ast-self-hosted-key";
|
|
266
|
+
|
|
267
|
+
globalThis.fetch = mock(
|
|
268
|
+
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
269
|
+
const body = JSON.parse(String(init?.body)) as Record<string, string>;
|
|
270
|
+
expect(body).toEqual({
|
|
271
|
+
assistant_id: "22222222-3333-4444-8555-666666666666",
|
|
272
|
+
callback_path: "webhooks/telegram",
|
|
273
|
+
type: "telegram",
|
|
274
|
+
});
|
|
275
|
+
expect(body).not.toHaveProperty("callback_base_url");
|
|
276
|
+
|
|
277
|
+
return new Response(
|
|
278
|
+
JSON.stringify({
|
|
279
|
+
callback_url:
|
|
280
|
+
"https://platform.example.com/v1/gateway/callbacks/x/",
|
|
281
|
+
callback_path:
|
|
282
|
+
"22222222-3333-4444-8555-666666666666/webhooks/telegram",
|
|
283
|
+
type: "telegram",
|
|
284
|
+
assistant_id: "22222222-3333-4444-8555-666666666666",
|
|
285
|
+
}),
|
|
286
|
+
{
|
|
287
|
+
status: 201,
|
|
288
|
+
headers: { "content-type": "application/json" },
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
},
|
|
292
|
+
) as unknown as typeof fetch;
|
|
293
|
+
|
|
294
|
+
await expect(
|
|
295
|
+
registerCallbackRoute("webhooks/telegram", "telegram"),
|
|
296
|
+
).resolves.toBe("https://platform.example.com/v1/gateway/callbacks/x/");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("platform-managed registerCallbackRoute omits callback_base_url even when ingress exists", async () => {
|
|
300
|
+
mockIsPlatform = true;
|
|
301
|
+
mockConfig = { ingress: { publicBaseUrl: "https://velay.example.com" } };
|
|
302
|
+
mockSecureKeys[credentialKey("vellum", "platform_base_url")] =
|
|
303
|
+
"https://platform.example.com";
|
|
304
|
+
mockSecureKeys[credentialKey("vellum", "platform_assistant_id")] =
|
|
305
|
+
"11111111-2222-4333-8444-555555555555";
|
|
306
|
+
mockSecureKeys[credentialKey("vellum", "assistant_api_key")] =
|
|
307
|
+
"ast-managed-key";
|
|
308
|
+
|
|
309
|
+
globalThis.fetch = mock(
|
|
310
|
+
async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
311
|
+
const body = JSON.parse(String(init?.body)) as Record<string, string>;
|
|
312
|
+
expect(body).not.toHaveProperty("callback_base_url");
|
|
313
|
+
expect(body).toEqual({
|
|
314
|
+
assistant_id: "11111111-2222-4333-8444-555555555555",
|
|
315
|
+
callback_path: "webhooks/telegram",
|
|
316
|
+
type: "telegram",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
return new Response(
|
|
320
|
+
JSON.stringify({
|
|
321
|
+
callback_url:
|
|
322
|
+
"https://platform.example.com/v1/gateway/callbacks/x/",
|
|
323
|
+
callback_path:
|
|
324
|
+
"11111111-2222-4333-8444-555555555555/webhooks/telegram",
|
|
325
|
+
type: "telegram",
|
|
326
|
+
assistant_id: "11111111-2222-4333-8444-555555555555",
|
|
327
|
+
}),
|
|
328
|
+
{
|
|
329
|
+
status: 201,
|
|
330
|
+
headers: { "content-type": "application/json" },
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
},
|
|
334
|
+
) as unknown as typeof fetch;
|
|
335
|
+
|
|
336
|
+
await expect(
|
|
337
|
+
registerCallbackRoute("webhooks/telegram", "telegram"),
|
|
338
|
+
).resolves.toBe("https://platform.example.com/v1/gateway/callbacks/x/");
|
|
339
|
+
});
|
|
158
340
|
});
|
|
159
341
|
|
|
160
342
|
/**
|
|
@@ -194,6 +376,8 @@ describe("resolveCallbackUrl resolution order", () => {
|
|
|
194
376
|
mockPlatformBaseUrl = "";
|
|
195
377
|
mockPlatformAssistantId = "";
|
|
196
378
|
mockSecureKeys = {};
|
|
379
|
+
mockConfig = {};
|
|
380
|
+
setIngressPublicBaseUrl(undefined);
|
|
197
381
|
delete process.env.ASSISTANT_API_KEY;
|
|
198
382
|
registerCalls = 0;
|
|
199
383
|
globalThis.fetch = mock(async () => {
|
|
@@ -13,9 +13,14 @@ import {
|
|
|
13
13
|
} from "bun:test";
|
|
14
14
|
|
|
15
15
|
import { credentialKey } from "@vellumai/credential-storage";
|
|
16
|
+
import { eq } from "drizzle-orm";
|
|
16
17
|
|
|
18
|
+
import * as conversationStore from "../daemon/conversation-store.js";
|
|
17
19
|
import * as scrub from "../daemon/credential-transcript-scrub.js";
|
|
18
20
|
import * as manualToken from "../oauth/manual-token-connection.js";
|
|
21
|
+
import { getDb } from "../persistence/db-connection.js";
|
|
22
|
+
import { initializeDb } from "../persistence/db-init.js";
|
|
23
|
+
import { providerConnections } from "../persistence/schema/inference.js";
|
|
19
24
|
import { resolveCredential } from "../plugin-api/resolve-credential.js";
|
|
20
25
|
import {
|
|
21
26
|
CredentialStoreError,
|
|
@@ -54,6 +59,9 @@ let setSpy: ReturnType<typeof spyOn>;
|
|
|
54
59
|
let getSpy: ReturnType<typeof spyOn>;
|
|
55
60
|
let scrubSpy: ReturnType<typeof spyOn>;
|
|
56
61
|
let syncSpy: ReturnType<typeof spyOn>;
|
|
62
|
+
let evictSpy: ReturnType<typeof spyOn>;
|
|
63
|
+
|
|
64
|
+
await initializeDb();
|
|
57
65
|
|
|
58
66
|
beforeEach(() => {
|
|
59
67
|
if (existsSync(TEST_DIR)) {
|
|
@@ -89,6 +97,14 @@ beforeEach(() => {
|
|
|
89
97
|
syncSpy = spyOn(manualToken, "syncManualTokenConnection").mockImplementation(
|
|
90
98
|
async () => {},
|
|
91
99
|
);
|
|
100
|
+
evictSpy = spyOn(
|
|
101
|
+
conversationStore,
|
|
102
|
+
"evictConversationsForReload",
|
|
103
|
+
).mockImplementation(() => {});
|
|
104
|
+
getDb()
|
|
105
|
+
.delete(providerConnections)
|
|
106
|
+
.where(eq(providerConnections.name, "openrouter-connection"))
|
|
107
|
+
.run();
|
|
92
108
|
});
|
|
93
109
|
|
|
94
110
|
afterEach(() => {
|
|
@@ -96,6 +112,7 @@ afterEach(() => {
|
|
|
96
112
|
getSpy.mockRestore();
|
|
97
113
|
scrubSpy.mockRestore();
|
|
98
114
|
syncSpy.mockRestore();
|
|
115
|
+
evictSpy.mockRestore();
|
|
99
116
|
});
|
|
100
117
|
|
|
101
118
|
afterAll(() => {
|
|
@@ -198,14 +215,65 @@ describe("storeCredential", () => {
|
|
|
198
215
|
expect(syncSpy).toHaveBeenCalledWith("acme");
|
|
199
216
|
});
|
|
200
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Verifies that storeCredentialValue refreshes providers for dependent connections.
|
|
220
|
+
*/
|
|
221
|
+
test("refreshes providers when a stored credential backs a connection", async () => {
|
|
222
|
+
// GIVEN a provider connection references the credential being stored.
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
getDb()
|
|
225
|
+
.insert(providerConnections)
|
|
226
|
+
.values({
|
|
227
|
+
name: "openrouter-connection",
|
|
228
|
+
provider: "openai",
|
|
229
|
+
auth: JSON.stringify({
|
|
230
|
+
type: "api_key",
|
|
231
|
+
credential: credentialKey("openrouter", "api_key"),
|
|
232
|
+
}),
|
|
233
|
+
createdAt: now,
|
|
234
|
+
updatedAt: now,
|
|
235
|
+
})
|
|
236
|
+
.run();
|
|
237
|
+
|
|
238
|
+
// AND the credential is non-managed.
|
|
239
|
+
// WHEN the credential is stored.
|
|
240
|
+
await storeCredentialValue({
|
|
241
|
+
service: "openrouter",
|
|
242
|
+
field: "api_key",
|
|
243
|
+
value: "openrouter-key",
|
|
244
|
+
skipTranscriptScrub: true,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// THEN the provider refresh runs once.
|
|
248
|
+
expect(evictSpy).toHaveBeenCalledTimes(1);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Verifies that storeCredentialValue skips refresh for an unused credential.
|
|
253
|
+
*/
|
|
254
|
+
test("does not refresh providers when a stored credential backs no connection", async () => {
|
|
255
|
+
// GIVEN no provider connection references the credential being stored.
|
|
256
|
+
// AND the credential is non-managed.
|
|
257
|
+
// WHEN the credential is stored.
|
|
258
|
+
await storeCredentialValue({
|
|
259
|
+
service: "acme",
|
|
260
|
+
field: "api_key",
|
|
261
|
+
value: "acme-key",
|
|
262
|
+
skipTranscriptScrub: true,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// THEN the provider refresh does not run.
|
|
266
|
+
expect(evictSpy).toHaveBeenCalledTimes(0);
|
|
267
|
+
});
|
|
268
|
+
|
|
201
269
|
describe("plugin scoping", () => {
|
|
202
270
|
test("fails closed with no plugin in context", async () => {
|
|
203
271
|
// A plugin's module body is evaluated outside any context. An unscoped
|
|
204
272
|
// branch there would let top-level code overwrite the user's own
|
|
205
273
|
// credentials, so the write is refused instead.
|
|
206
|
-
await expect(
|
|
207
|
-
/
|
|
208
|
-
);
|
|
274
|
+
await expect(
|
|
275
|
+
storeCredential("acme/api_key", "sk-secret"),
|
|
276
|
+
).rejects.toThrow(/requires an active plugin execution context/);
|
|
209
277
|
expect(setSpy).not.toHaveBeenCalled();
|
|
210
278
|
expect(getCredentialMetadata("acme", "api_key")).toBeUndefined();
|
|
211
279
|
});
|
|
@@ -834,8 +834,8 @@ describe("Anthropic models on OpenRouter", () => {
|
|
|
834
834
|
});
|
|
835
835
|
|
|
836
836
|
expect(result.pricingStatus).toBe("priced");
|
|
837
|
-
// 0.05M x $0.
|
|
838
|
-
expect(result.estimatedCostUsd).toBeCloseTo(0.
|
|
837
|
+
// 0.05M x $0.2 direct + 0.05M x $0.25 write + 0.05M x $0.02 read
|
|
838
|
+
expect(result.estimatedCostUsd).toBeCloseTo(0.01 + 0.0125 + 0.001, 10);
|
|
839
839
|
});
|
|
840
840
|
|
|
841
841
|
test("returns unpriced for unknown non-Anthropic OpenRouter model", () => {
|