@vellumai/assistant 0.4.31 → 0.4.33
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 +1 -1
- package/docs/architecture/memory.md +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
- package/src/__tests__/access-request-decision.test.ts +83 -1
- package/src/__tests__/actor-token-service.test.ts +0 -1
- package/src/__tests__/anthropic-provider.test.ts +86 -1
- package/src/__tests__/approval-routes-http.test.ts +0 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/call-routes-http.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +0 -1
- package/src/__tests__/channel-invite-transport.test.ts +52 -40
- package/src/__tests__/checker.test.ts +37 -98
- package/src/__tests__/commit-message-enrichment-service.test.ts +4 -23
- package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +6 -5
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/daemon-server-session-init.test.ts +1 -19
- package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
- package/src/__tests__/followup-tools.test.ts +0 -30
- package/src/__tests__/gemini-provider.test.ts +79 -1
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +0 -1
- package/src/__tests__/guardian-outbound-http.test.ts +0 -1
- package/src/__tests__/handlers-telegram-config.test.ts +0 -1
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -4
- package/src/__tests__/ingress-reconcile.test.ts +3 -36
- package/src/__tests__/ipc-snapshot.test.ts +0 -4
- package/src/__tests__/managed-proxy-context.test.ts +163 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
- package/src/__tests__/migration-export-http.test.ts +0 -1
- package/src/__tests__/migration-import-commit-http.test.ts +0 -1
- package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
- package/src/__tests__/migration-validate-http.test.ts +0 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -1
- package/src/__tests__/notification-guardian-path.test.ts +0 -1
- package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
- package/src/__tests__/openai-provider.test.ts +82 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
- package/src/__tests__/recurrence-types.test.ts +0 -15
- package/src/__tests__/relay-server.test.ts +145 -2
- package/src/__tests__/sandbox-host-parity.test.ts +5 -2
- package/src/__tests__/schedule-tools.test.ts +28 -44
- package/src/__tests__/session-init.benchmark.test.ts +0 -2
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/slack-channel-config.test.ts +0 -1
- package/src/__tests__/slack-inbound-verification.test.ts +0 -1
- package/src/__tests__/sms-messaging-provider.test.ts +0 -4
- package/src/__tests__/task-management-tools.test.ts +111 -0
- package/src/__tests__/terminal-tools.test.ts +5 -2
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +66 -74
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -1
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
- package/src/__tests__/trusted-contact-verification.test.ts +0 -1
- package/src/__tests__/twilio-config.test.ts +0 -3
- package/src/__tests__/twilio-routes.test.ts +0 -1
- package/src/__tests__/update-bulletin.test.ts +0 -2
- package/src/__tests__/user-reference.test.ts +47 -1
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-git-service.test.ts +2 -2
- package/src/amazon/session.ts +30 -91
- package/src/calls/call-controller.ts +423 -571
- package/src/calls/finalize-call.ts +20 -0
- package/src/calls/relay-access-wait.ts +340 -0
- package/src/calls/relay-server.ts +271 -956
- package/src/calls/relay-setup-router.ts +307 -0
- package/src/calls/relay-verification.ts +280 -0
- package/src/calls/twilio-config.ts +1 -8
- package/src/calls/voice-control-protocol.ts +184 -0
- package/src/calls/voice-session-bridge.ts +1 -8
- package/src/channels/config.ts +41 -2
- package/src/config/agent-schema.ts +1 -1
- package/src/config/bundled-skills/followups/TOOLS.json +0 -4
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env.ts +0 -14
- package/src/config/feature-flag-registry.json +5 -5
- package/src/config/loader.ts +19 -0
- package/src/config/schema.ts +2 -2
- package/src/config/user-reference.ts +47 -9
- package/src/daemon/handlers/config-channels.ts +11 -10
- package/src/daemon/handlers/contacts.ts +5 -1
- package/src/daemon/handlers/session-history.ts +398 -0
- package/src/daemon/handlers/session-user-message.ts +982 -0
- package/src/daemon/handlers/sessions.ts +9 -1338
- package/src/daemon/ipc-contract/sessions.ts +0 -6
- package/src/daemon/ipc-contract-inventory.json +0 -1
- package/src/daemon/lifecycle.ts +18 -55
- package/src/home-base/app-link-store.ts +0 -7
- package/src/memory/channel-delivery-store.ts +1 -0
- package/src/memory/conversation-attention-store.ts +1 -1
- package/src/memory/conversation-store.ts +0 -51
- package/src/memory/db-init.ts +9 -1
- package/src/memory/delivery-crud.ts +13 -0
- package/src/memory/invite-store.ts +71 -1
- package/src/memory/job-handlers/conflict.ts +24 -0
- package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
- package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/recall-cache.ts +0 -5
- package/src/memory/schema/calls.ts +274 -0
- package/src/memory/schema/contacts.ts +127 -0
- package/src/memory/schema/conversations.ts +129 -0
- package/src/memory/schema/guardian.ts +172 -0
- package/src/memory/schema/index.ts +8 -0
- package/src/memory/schema/infrastructure.ts +205 -0
- package/src/memory/schema/memory-core.ts +196 -0
- package/src/memory/schema/notifications.ts +191 -0
- package/src/memory/schema/tasks.ts +78 -0
- package/src/memory/schema.ts +1 -1385
- package/src/memory/slack-thread-store.ts +0 -69
- package/src/notifications/decisions-store.ts +2 -105
- package/src/notifications/deliveries-store.ts +0 -11
- package/src/notifications/preferences-store.ts +1 -58
- package/src/permissions/checker.ts +6 -17
- package/src/providers/anthropic/client.ts +6 -2
- package/src/providers/gemini/client.ts +13 -2
- package/src/providers/managed-proxy/constants.ts +55 -0
- package/src/providers/managed-proxy/context.ts +77 -0
- package/src/providers/registry.ts +112 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +52 -26
- package/src/runtime/auth/token-service.ts +50 -0
- package/src/runtime/channel-guardian-service.ts +1 -3
- package/src/runtime/channel-invite-transport.ts +121 -34
- package/src/runtime/channel-invite-transports/email.ts +50 -0
- package/src/runtime/channel-invite-transports/slack.ts +81 -0
- package/src/runtime/channel-invite-transports/sms.ts +70 -0
- package/src/runtime/channel-invite-transports/telegram.ts +29 -11
- package/src/runtime/channel-invite-transports/voice.ts +12 -12
- package/src/runtime/http-server.ts +83 -722
- package/src/runtime/http-types.ts +0 -16
- package/src/runtime/invite-redemption-service.ts +193 -0
- package/src/runtime/invite-redemption-templates.ts +6 -6
- package/src/runtime/invite-service.ts +81 -11
- package/src/runtime/middleware/auth.ts +0 -12
- package/src/runtime/routes/access-request-decision.ts +52 -6
- package/src/runtime/routes/app-routes.ts +33 -0
- package/src/runtime/routes/approval-routes.ts +32 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -0
- package/src/runtime/routes/attachment-routes.ts +32 -0
- package/src/runtime/routes/brain-graph-routes.ts +27 -0
- package/src/runtime/routes/call-routes.ts +41 -0
- package/src/runtime/routes/channel-readiness-routes.ts +20 -0
- package/src/runtime/routes/channel-routes.ts +70 -0
- package/src/runtime/routes/contact-routes.ts +96 -6
- package/src/runtime/routes/conversation-attention-routes.ts +15 -0
- package/src/runtime/routes/conversation-routes.ts +190 -193
- package/src/runtime/routes/debug-routes.ts +15 -0
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/global-search-routes.ts +15 -0
- package/src/runtime/routes/guardian-action-routes.ts +22 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +21 -6
- package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
- package/src/runtime/routes/identity-routes.ts +20 -0
- package/src/runtime/routes/inbound-message-handler.ts +9 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +295 -10
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +9 -42
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
- package/src/runtime/routes/integration-routes.ts +83 -0
- package/src/runtime/routes/invite-routes.ts +32 -0
- package/src/runtime/routes/migration-routes.ts +30 -0
- package/src/runtime/routes/pairing-routes.ts +18 -0
- package/src/runtime/routes/secret-routes.ts +20 -0
- package/src/runtime/routes/surface-action-routes.ts +26 -0
- package/src/runtime/routes/trust-rules-routes.ts +31 -0
- package/src/runtime/routes/twilio-routes.ts +79 -0
- package/src/schedule/recurrence-types.ts +1 -11
- package/src/tools/browser/browser-manager.ts +10 -1
- package/src/tools/browser/runtime-check.ts +3 -1
- package/src/tools/followups/followup_create.ts +9 -3
- package/src/tools/mcp/mcp-tool-factory.ts +0 -17
- package/src/tools/memory/definitions.ts +0 -6
- package/src/tools/network/script-proxy/session-manager.ts +38 -3
- package/src/tools/schedule/create.ts +1 -3
- package/src/tools/schedule/update.ts +9 -6
- package/src/tools/shared/shell-output.ts +7 -2
- package/src/twitter/session.ts +29 -77
- package/src/util/cookie-session.ts +114 -0
- package/src/util/platform.ts +0 -4
- package/src/workspace/git-service.ts +10 -4
- package/src/__tests__/conversation-routes.test.ts +0 -99
- package/src/__tests__/task-tools.test.ts +0 -685
- package/src/contacts/startup-migration.ts +0 -21
|
@@ -1,8 +1,35 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock the underlying dependencies of managed-proxy/context.js rather than
|
|
5
|
+
// the context module itself. This avoids global mock bleed: other test files
|
|
6
|
+
// that import context.js will still get the real implementation with their
|
|
7
|
+
// own dependency mocks.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
let mockPlatformBaseUrl = "";
|
|
10
|
+
let mockAssistantApiKey = "";
|
|
11
|
+
|
|
12
|
+
const actualEnv = await import("../config/env.js");
|
|
13
|
+
mock.module("../config/env.js", () => ({
|
|
14
|
+
...actualEnv,
|
|
15
|
+
getPlatformBaseUrl: () => mockPlatformBaseUrl,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const actualSecureKeys = await import("../security/secure-keys.js");
|
|
19
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
20
|
+
...actualSecureKeys,
|
|
21
|
+
getSecureKey: (key: string) => {
|
|
22
|
+
if (key === "credential:vellum:assistant_api_key") {
|
|
23
|
+
return mockAssistantApiKey || null;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
2
28
|
|
|
3
29
|
import {
|
|
4
30
|
getFailoverProvider,
|
|
5
31
|
initializeProviders,
|
|
32
|
+
listProviders,
|
|
6
33
|
resolveProviderSelection,
|
|
7
34
|
} from "../providers/registry.js";
|
|
8
35
|
|
|
@@ -125,3 +152,109 @@ describe("getFailoverProvider (fail-open)", () => {
|
|
|
125
152
|
expect(provider.name).not.toBe("failover");
|
|
126
153
|
});
|
|
127
154
|
});
|
|
155
|
+
|
|
156
|
+
// -------------------------------------------------------------------------
|
|
157
|
+
// Managed proxy fallback
|
|
158
|
+
// -------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
describe("managed proxy fallback", () => {
|
|
161
|
+
function enableManagedProxy() {
|
|
162
|
+
mockPlatformBaseUrl = "https://platform.example.com";
|
|
163
|
+
mockAssistantApiKey = "ast-key-123";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function disableManagedProxy() {
|
|
167
|
+
mockPlatformBaseUrl = "";
|
|
168
|
+
mockAssistantApiKey = "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
test("openai registered via managed fallback when no user key but proxy context is valid", () => {
|
|
172
|
+
enableManagedProxy();
|
|
173
|
+
try {
|
|
174
|
+
initializeProviders({
|
|
175
|
+
apiKeys: { anthropic: "test-key" },
|
|
176
|
+
provider: "anthropic",
|
|
177
|
+
model: "test-model",
|
|
178
|
+
});
|
|
179
|
+
const registered = listProviders();
|
|
180
|
+
expect(registered).toContain("openai");
|
|
181
|
+
expect(registered).toContain("fireworks");
|
|
182
|
+
expect(registered).toContain("openrouter");
|
|
183
|
+
} finally {
|
|
184
|
+
disableManagedProxy();
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("user key takes precedence over managed fallback", () => {
|
|
189
|
+
enableManagedProxy();
|
|
190
|
+
try {
|
|
191
|
+
initializeProviders({
|
|
192
|
+
apiKeys: { anthropic: "test-key", openai: "user-openai-key" },
|
|
193
|
+
provider: "anthropic",
|
|
194
|
+
model: "test-model",
|
|
195
|
+
});
|
|
196
|
+
// openai should be registered (via user key, not managed)
|
|
197
|
+
const registered = listProviders();
|
|
198
|
+
expect(registered).toContain("openai");
|
|
199
|
+
// fireworks/openrouter should also be registered via managed fallback
|
|
200
|
+
expect(registered).toContain("fireworks");
|
|
201
|
+
expect(registered).toContain("openrouter");
|
|
202
|
+
} finally {
|
|
203
|
+
disableManagedProxy();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("managed fallback not activated when proxy context is disabled", () => {
|
|
208
|
+
disableManagedProxy();
|
|
209
|
+
initializeProviders({
|
|
210
|
+
apiKeys: { anthropic: "test-key" },
|
|
211
|
+
provider: "anthropic",
|
|
212
|
+
model: "test-model",
|
|
213
|
+
});
|
|
214
|
+
const registered = listProviders();
|
|
215
|
+
expect(registered).not.toContain("openai");
|
|
216
|
+
expect(registered).not.toContain("fireworks");
|
|
217
|
+
expect(registered).not.toContain("openrouter");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("managed providers participate in failover selection", () => {
|
|
221
|
+
enableManagedProxy();
|
|
222
|
+
try {
|
|
223
|
+
initializeProviders({
|
|
224
|
+
apiKeys: { anthropic: "test-key" },
|
|
225
|
+
provider: "anthropic",
|
|
226
|
+
model: "test-model",
|
|
227
|
+
});
|
|
228
|
+
const selection = resolveProviderSelection("anthropic", [
|
|
229
|
+
"openai",
|
|
230
|
+
"fireworks",
|
|
231
|
+
]);
|
|
232
|
+
expect(selection.availableProviders).toEqual([
|
|
233
|
+
"anthropic",
|
|
234
|
+
"openai",
|
|
235
|
+
"fireworks",
|
|
236
|
+
]);
|
|
237
|
+
expect(selection.selectedPrimary).toBe("anthropic");
|
|
238
|
+
expect(selection.usedFallbackPrimary).toBe(false);
|
|
239
|
+
} finally {
|
|
240
|
+
disableManagedProxy();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("managed provider selected as primary when configured primary unavailable", () => {
|
|
245
|
+
enableManagedProxy();
|
|
246
|
+
try {
|
|
247
|
+
// No anthropic key, no gemini key — only managed providers available
|
|
248
|
+
initializeProviders({
|
|
249
|
+
apiKeys: {},
|
|
250
|
+
provider: "openai",
|
|
251
|
+
model: "test-model",
|
|
252
|
+
});
|
|
253
|
+
const selection = resolveProviderSelection("openai", ["fireworks"]);
|
|
254
|
+
expect(selection.selectedPrimary).toBe("openai");
|
|
255
|
+
expect(selection.usedFallbackPrimary).toBe(false);
|
|
256
|
+
} finally {
|
|
257
|
+
disableManagedProxy();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { MANAGED_PROVIDER_META } from "../providers/managed-proxy/constants.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Mock the underlying dependencies that the real context module relies on.
|
|
7
|
+
// This avoids mocking the context module directly and prevents mock conflicts
|
|
8
|
+
// with context.test.ts (which also mocks these same underlying deps).
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
let mockPlatformBaseUrl = "";
|
|
11
|
+
let mockAssistantApiKey: string | null = null;
|
|
12
|
+
|
|
13
|
+
mock.module("../config/env.js", () => ({
|
|
14
|
+
getPlatformBaseUrl: () => mockPlatformBaseUrl,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
18
|
+
getSecureKey: (key: string) => {
|
|
19
|
+
if (key === "credential:vellum:assistant_api_key") {
|
|
20
|
+
return mockAssistantApiKey;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
getProviderRoutingSource,
|
|
28
|
+
initializeProviders,
|
|
29
|
+
listProviders,
|
|
30
|
+
} from "../providers/registry.js";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helpers
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const PLATFORM_BASE = "https://platform.example.com";
|
|
37
|
+
const MANAGED_API_KEY = "ast-managed-key-123";
|
|
38
|
+
|
|
39
|
+
const MANAGED_PROVIDERS: string[] = [
|
|
40
|
+
"openai",
|
|
41
|
+
"anthropic",
|
|
42
|
+
"gemini",
|
|
43
|
+
"fireworks",
|
|
44
|
+
"openrouter",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function enableManagedProxy() {
|
|
48
|
+
mockPlatformBaseUrl = PLATFORM_BASE;
|
|
49
|
+
mockAssistantApiKey = MANAGED_API_KEY;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function disableManagedProxy() {
|
|
53
|
+
mockPlatformBaseUrl = "";
|
|
54
|
+
mockAssistantApiKey = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build an apiKeys record with a user key for every provider in `names`.
|
|
59
|
+
*/
|
|
60
|
+
function userKeysFor(...names: string[]): Record<string, string> {
|
|
61
|
+
const keys: Record<string, string> = {};
|
|
62
|
+
for (const n of names) {
|
|
63
|
+
keys[n] = `user-key-${n}`;
|
|
64
|
+
}
|
|
65
|
+
return keys;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Tests
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
disableManagedProxy();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("managed proxy integration — credential precedence", () => {
|
|
77
|
+
describe("user keys present → providers use direct connections (not proxy)", () => {
|
|
78
|
+
test.each(MANAGED_PROVIDERS)(
|
|
79
|
+
"%s routes via user-key when user key is provided regardless of managed context",
|
|
80
|
+
(provider: string) => {
|
|
81
|
+
enableManagedProxy();
|
|
82
|
+
initializeProviders({
|
|
83
|
+
apiKeys: userKeysFor(provider),
|
|
84
|
+
provider,
|
|
85
|
+
model: "test-model",
|
|
86
|
+
});
|
|
87
|
+
expect(listProviders()).toContain(provider);
|
|
88
|
+
expect(getProviderRoutingSource(provider)).toBe("user-key");
|
|
89
|
+
},
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
test("all five managed providers route via user-key with user keys", () => {
|
|
93
|
+
enableManagedProxy();
|
|
94
|
+
initializeProviders({
|
|
95
|
+
apiKeys: userKeysFor(...MANAGED_PROVIDERS),
|
|
96
|
+
provider: "anthropic",
|
|
97
|
+
model: "test-model",
|
|
98
|
+
});
|
|
99
|
+
const registered = listProviders();
|
|
100
|
+
for (const p of MANAGED_PROVIDERS) {
|
|
101
|
+
expect(registered).toContain(p);
|
|
102
|
+
expect(getProviderRoutingSource(p)).toBe("user-key");
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("user keys still route via user-key when managed context is disabled", () => {
|
|
107
|
+
disableManagedProxy();
|
|
108
|
+
initializeProviders({
|
|
109
|
+
apiKeys: userKeysFor(...MANAGED_PROVIDERS),
|
|
110
|
+
provider: "anthropic",
|
|
111
|
+
model: "test-model",
|
|
112
|
+
});
|
|
113
|
+
const registered = listProviders();
|
|
114
|
+
for (const p of MANAGED_PROVIDERS) {
|
|
115
|
+
expect(registered).toContain(p);
|
|
116
|
+
expect(getProviderRoutingSource(p)).toBe("user-key");
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("user keys absent + managed context available → providers use managed proxy", () => {
|
|
122
|
+
test.each(MANAGED_PROVIDERS)(
|
|
123
|
+
"%s routes via managed-proxy when no user key",
|
|
124
|
+
(provider: string) => {
|
|
125
|
+
enableManagedProxy();
|
|
126
|
+
initializeProviders({
|
|
127
|
+
apiKeys: {},
|
|
128
|
+
// For ollama, provider selection does not trigger managed proxy
|
|
129
|
+
provider: provider === "openai" ? "openai" : "anthropic",
|
|
130
|
+
model: "test-model",
|
|
131
|
+
});
|
|
132
|
+
expect(listProviders()).toContain(provider);
|
|
133
|
+
expect(getProviderRoutingSource(provider)).toBe("managed-proxy");
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
test("all five managed providers route via managed-proxy simultaneously", () => {
|
|
138
|
+
enableManagedProxy();
|
|
139
|
+
initializeProviders({
|
|
140
|
+
apiKeys: {},
|
|
141
|
+
provider: "anthropic",
|
|
142
|
+
model: "test-model",
|
|
143
|
+
});
|
|
144
|
+
const registered = listProviders();
|
|
145
|
+
for (const p of MANAGED_PROVIDERS) {
|
|
146
|
+
expect(registered).toContain(p);
|
|
147
|
+
expect(getProviderRoutingSource(p)).toBe("managed-proxy");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("neither user keys nor managed context → providers not initialized", () => {
|
|
153
|
+
test.each(MANAGED_PROVIDERS)(
|
|
154
|
+
"%s is NOT registered when no user key and no managed context",
|
|
155
|
+
(provider: string) => {
|
|
156
|
+
disableManagedProxy();
|
|
157
|
+
initializeProviders({
|
|
158
|
+
apiKeys: {},
|
|
159
|
+
provider: "anthropic",
|
|
160
|
+
model: "test-model",
|
|
161
|
+
});
|
|
162
|
+
expect(listProviders()).not.toContain(provider);
|
|
163
|
+
expect(getProviderRoutingSource(provider)).toBeUndefined();
|
|
164
|
+
},
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
test("registry is empty when no keys and no managed context (non-ollama primary)", () => {
|
|
168
|
+
disableManagedProxy();
|
|
169
|
+
initializeProviders({
|
|
170
|
+
apiKeys: {},
|
|
171
|
+
provider: "anthropic",
|
|
172
|
+
model: "test-model",
|
|
173
|
+
});
|
|
174
|
+
expect(listProviders()).toEqual([]);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("mixed: some user keys + managed fallback fills gaps", () => {
|
|
179
|
+
test("user key for anthropic routes direct, managed fallback fills remaining four via proxy", () => {
|
|
180
|
+
enableManagedProxy();
|
|
181
|
+
initializeProviders({
|
|
182
|
+
apiKeys: userKeysFor("anthropic"),
|
|
183
|
+
provider: "anthropic",
|
|
184
|
+
model: "test-model",
|
|
185
|
+
});
|
|
186
|
+
const registered = listProviders();
|
|
187
|
+
expect(registered).toContain("anthropic");
|
|
188
|
+
expect(getProviderRoutingSource("anthropic")).toBe("user-key");
|
|
189
|
+
for (const p of ["openai", "gemini", "fireworks", "openrouter"]) {
|
|
190
|
+
expect(registered).toContain(p);
|
|
191
|
+
expect(getProviderRoutingSource(p)).toBe("managed-proxy");
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("user key for openai routes direct, managed fallback fills remaining four via proxy", () => {
|
|
196
|
+
enableManagedProxy();
|
|
197
|
+
initializeProviders({
|
|
198
|
+
apiKeys: userKeysFor("openai"),
|
|
199
|
+
provider: "openai",
|
|
200
|
+
model: "test-model",
|
|
201
|
+
});
|
|
202
|
+
const registered = listProviders();
|
|
203
|
+
expect(registered).toContain("openai");
|
|
204
|
+
expect(getProviderRoutingSource("openai")).toBe("user-key");
|
|
205
|
+
for (const p of ["anthropic", "gemini", "fireworks", "openrouter"]) {
|
|
206
|
+
expect(registered).toContain(p);
|
|
207
|
+
expect(getProviderRoutingSource(p)).toBe("managed-proxy");
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("managed proxy integration — ollama exclusion", () => {
|
|
214
|
+
test("ollama is never registered via managed proxy fallback", () => {
|
|
215
|
+
enableManagedProxy();
|
|
216
|
+
initializeProviders({
|
|
217
|
+
apiKeys: {},
|
|
218
|
+
provider: "anthropic",
|
|
219
|
+
model: "test-model",
|
|
220
|
+
});
|
|
221
|
+
expect(listProviders()).not.toContain("ollama");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("ollama registers only when explicitly configured as provider", () => {
|
|
225
|
+
enableManagedProxy();
|
|
226
|
+
initializeProviders({
|
|
227
|
+
apiKeys: {},
|
|
228
|
+
provider: "ollama",
|
|
229
|
+
model: "test-model",
|
|
230
|
+
});
|
|
231
|
+
expect(listProviders()).toContain("ollama");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("ollama registers with explicit API key", () => {
|
|
235
|
+
enableManagedProxy();
|
|
236
|
+
initializeProviders({
|
|
237
|
+
apiKeys: { ollama: "ollama-key" },
|
|
238
|
+
provider: "anthropic",
|
|
239
|
+
model: "test-model",
|
|
240
|
+
});
|
|
241
|
+
expect(listProviders()).toContain("ollama");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("ollama metadata is marked as non-managed", () => {
|
|
245
|
+
const meta = MANAGED_PROVIDER_META.ollama;
|
|
246
|
+
expect(meta).toBeDefined();
|
|
247
|
+
expect(meta.managed).toBe(false);
|
|
248
|
+
expect(meta.proxyPath).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("managed proxy integration — constants integrity", () => {
|
|
253
|
+
test("all five managed providers have metadata with managed=true and a proxyPath", () => {
|
|
254
|
+
for (const provider of MANAGED_PROVIDERS) {
|
|
255
|
+
const meta = MANAGED_PROVIDER_META[provider];
|
|
256
|
+
expect(meta).toBeDefined();
|
|
257
|
+
expect(meta.managed).toBe(true);
|
|
258
|
+
expect(meta.proxyPath).toBeTruthy();
|
|
259
|
+
expect(meta.proxyPath).toMatch(/^\/v1\/runtime-proxy\//);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("managed proxy paths are unique across providers", () => {
|
|
264
|
+
const paths = Object.values(MANAGED_PROVIDER_META)
|
|
265
|
+
.filter((m) => m.managed && m.proxyPath)
|
|
266
|
+
.map((m) => m.proxyPath);
|
|
267
|
+
expect(new Set(paths).size).toBe(paths.length);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -66,21 +66,6 @@ describe("normalizeScheduleSyntax", () => {
|
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
test("falls back to legacyCronExpression", () => {
|
|
70
|
-
const result = normalizeScheduleSyntax({
|
|
71
|
-
legacyCronExpression: "0 9 * * *",
|
|
72
|
-
});
|
|
73
|
-
expect(result).toEqual({ syntax: "cron", expression: "0 9 * * *" });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("honors explicit syntax hint in legacyCronExpression fallback", () => {
|
|
77
|
-
const result = normalizeScheduleSyntax({
|
|
78
|
-
syntax: "rrule",
|
|
79
|
-
legacyCronExpression: "0 9 * * *",
|
|
80
|
-
});
|
|
81
|
-
expect(result).toEqual({ syntax: "rrule", expression: "0 9 * * *" });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
69
|
test("returns null when nothing is provided", () => {
|
|
85
70
|
expect(normalizeScheduleSyntax({})).toBeNull();
|
|
86
71
|
});
|
|
@@ -42,7 +42,6 @@ mock.module("../util/platform.js", () => ({
|
|
|
42
42
|
getDbPath: () => join(testDir, "test.db"),
|
|
43
43
|
getLogPath: () => join(testDir, "test.log"),
|
|
44
44
|
ensureDataDir: () => {},
|
|
45
|
-
readHttpToken: () => null,
|
|
46
45
|
}));
|
|
47
46
|
|
|
48
47
|
mock.module("../util/logger.js", () => ({
|
|
@@ -61,9 +60,20 @@ mock.module("../daemon/identity-helpers.js", () => ({
|
|
|
61
60
|
|
|
62
61
|
// ── User-reference mock (isolate from real USER.md) ──────────────────
|
|
63
62
|
|
|
63
|
+
let mockUserReference = "my human";
|
|
64
64
|
mock.module("../config/user-reference.js", () => ({
|
|
65
|
-
resolveUserReference: () =>
|
|
65
|
+
resolveUserReference: () => mockUserReference,
|
|
66
66
|
resolveUserPronouns: () => null,
|
|
67
|
+
DEFAULT_USER_REFERENCE: "my human",
|
|
68
|
+
resolveGuardianName: (guardianDisplayName?: string | null) => {
|
|
69
|
+
if (mockUserReference !== "my human") {
|
|
70
|
+
return mockUserReference;
|
|
71
|
+
}
|
|
72
|
+
if (guardianDisplayName && guardianDisplayName.trim().length > 0) {
|
|
73
|
+
return guardianDisplayName.trim();
|
|
74
|
+
}
|
|
75
|
+
return "my human";
|
|
76
|
+
},
|
|
67
77
|
}));
|
|
68
78
|
|
|
69
79
|
// ── Config mock ─────────────────────────────────────────────────────
|
|
@@ -345,6 +355,7 @@ describe("relay-server", () => {
|
|
|
345
355
|
beforeEach(() => {
|
|
346
356
|
resetTables();
|
|
347
357
|
activeRelayConnections.clear();
|
|
358
|
+
mockUserReference = "my human";
|
|
348
359
|
mockSendMessage.mockImplementation(createMockProviderResponse(["Hello"]));
|
|
349
360
|
mockConfig.calls.verification.enabled = false;
|
|
350
361
|
mockConfig.calls.verification.maxAttempts = 3;
|
|
@@ -4129,4 +4140,136 @@ describe("relay-server", () => {
|
|
|
4129
4140
|
|
|
4130
4141
|
relay.destroy();
|
|
4131
4142
|
});
|
|
4143
|
+
|
|
4144
|
+
// ── resolveGuardianLabel resolution priority ─────────────────────────
|
|
4145
|
+
|
|
4146
|
+
test("guardian label: USER.md name takes precedence over Contact.displayName", async () => {
|
|
4147
|
+
mockUserReference = "Alice";
|
|
4148
|
+
|
|
4149
|
+
// Create a guardian binding with a different displayName
|
|
4150
|
+
createGuardianBinding({
|
|
4151
|
+
assistantId: "self",
|
|
4152
|
+
channel: "voice",
|
|
4153
|
+
guardianExternalUserId: "+15559990001",
|
|
4154
|
+
guardianDeliveryChatId: "+15559990001",
|
|
4155
|
+
guardianPrincipalId: "+15559990001",
|
|
4156
|
+
verifiedVia: "test",
|
|
4157
|
+
metadataJson: JSON.stringify({ displayName: "Bob" }),
|
|
4158
|
+
});
|
|
4159
|
+
|
|
4160
|
+
ensureConversation("conv-label-user-md");
|
|
4161
|
+
const session = createCallSession({
|
|
4162
|
+
conversationId: "conv-label-user-md",
|
|
4163
|
+
provider: "twilio",
|
|
4164
|
+
fromNumber: "+15559990099",
|
|
4165
|
+
toNumber: "+15551111111",
|
|
4166
|
+
assistantId: "self",
|
|
4167
|
+
});
|
|
4168
|
+
|
|
4169
|
+
const { ws, relay } = createMockWs(session.id);
|
|
4170
|
+
|
|
4171
|
+
await relay.handleMessage(
|
|
4172
|
+
JSON.stringify({
|
|
4173
|
+
type: "setup",
|
|
4174
|
+
callSid: "CA_label_user_md",
|
|
4175
|
+
from: "+15559990099",
|
|
4176
|
+
to: "+15551111111",
|
|
4177
|
+
}),
|
|
4178
|
+
);
|
|
4179
|
+
|
|
4180
|
+
expect(relay.getConnectionState()).toBe("awaiting_name");
|
|
4181
|
+
|
|
4182
|
+
// The greeting should use the USER.md name ("Alice"), not Contact.displayName ("Bob")
|
|
4183
|
+
const textMessages = ws.sentMessages
|
|
4184
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
4185
|
+
.filter((m) => m.type === "text");
|
|
4186
|
+
const promptText = textMessages.map((m) => m.token ?? "").join("");
|
|
4187
|
+
expect(promptText).toContain("Alice");
|
|
4188
|
+
expect(promptText).not.toContain("Bob");
|
|
4189
|
+
|
|
4190
|
+
relay.destroy();
|
|
4191
|
+
});
|
|
4192
|
+
|
|
4193
|
+
test("guardian label: Contact.displayName used when USER.md is empty", async () => {
|
|
4194
|
+
mockUserReference = "my human";
|
|
4195
|
+
|
|
4196
|
+
// Create a guardian binding with a displayName
|
|
4197
|
+
createGuardianBinding({
|
|
4198
|
+
assistantId: "self",
|
|
4199
|
+
channel: "voice",
|
|
4200
|
+
guardianExternalUserId: "+15559990002",
|
|
4201
|
+
guardianDeliveryChatId: "+15559990002",
|
|
4202
|
+
guardianPrincipalId: "+15559990002",
|
|
4203
|
+
verifiedVia: "test",
|
|
4204
|
+
metadataJson: JSON.stringify({ displayName: "Charlie" }),
|
|
4205
|
+
});
|
|
4206
|
+
|
|
4207
|
+
ensureConversation("conv-label-contact");
|
|
4208
|
+
const session = createCallSession({
|
|
4209
|
+
conversationId: "conv-label-contact",
|
|
4210
|
+
provider: "twilio",
|
|
4211
|
+
fromNumber: "+15559990098",
|
|
4212
|
+
toNumber: "+15551111111",
|
|
4213
|
+
assistantId: "self",
|
|
4214
|
+
});
|
|
4215
|
+
|
|
4216
|
+
const { ws, relay } = createMockWs(session.id);
|
|
4217
|
+
|
|
4218
|
+
await relay.handleMessage(
|
|
4219
|
+
JSON.stringify({
|
|
4220
|
+
type: "setup",
|
|
4221
|
+
callSid: "CA_label_contact",
|
|
4222
|
+
from: "+15559990098",
|
|
4223
|
+
to: "+15551111111",
|
|
4224
|
+
}),
|
|
4225
|
+
);
|
|
4226
|
+
|
|
4227
|
+
expect(relay.getConnectionState()).toBe("awaiting_name");
|
|
4228
|
+
|
|
4229
|
+
// The greeting should use Contact.displayName ("Charlie")
|
|
4230
|
+
const textMessages = ws.sentMessages
|
|
4231
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
4232
|
+
.filter((m) => m.type === "text");
|
|
4233
|
+
const promptText = textMessages.map((m) => m.token ?? "").join("");
|
|
4234
|
+
expect(promptText).toContain("Charlie");
|
|
4235
|
+
|
|
4236
|
+
relay.destroy();
|
|
4237
|
+
});
|
|
4238
|
+
|
|
4239
|
+
test("guardian label: DEFAULT_USER_REFERENCE used when both USER.md and Contact.displayName are empty", async () => {
|
|
4240
|
+
mockUserReference = "my human";
|
|
4241
|
+
|
|
4242
|
+
// No guardian binding — no Contact.displayName available
|
|
4243
|
+
|
|
4244
|
+
ensureConversation("conv-label-default");
|
|
4245
|
+
const session = createCallSession({
|
|
4246
|
+
conversationId: "conv-label-default",
|
|
4247
|
+
provider: "twilio",
|
|
4248
|
+
fromNumber: "+15559990097",
|
|
4249
|
+
toNumber: "+15551111111",
|
|
4250
|
+
assistantId: "self",
|
|
4251
|
+
});
|
|
4252
|
+
|
|
4253
|
+
const { ws, relay } = createMockWs(session.id);
|
|
4254
|
+
|
|
4255
|
+
await relay.handleMessage(
|
|
4256
|
+
JSON.stringify({
|
|
4257
|
+
type: "setup",
|
|
4258
|
+
callSid: "CA_label_default",
|
|
4259
|
+
from: "+15559990097",
|
|
4260
|
+
to: "+15551111111",
|
|
4261
|
+
}),
|
|
4262
|
+
);
|
|
4263
|
+
|
|
4264
|
+
expect(relay.getConnectionState()).toBe("awaiting_name");
|
|
4265
|
+
|
|
4266
|
+
// The greeting should use the default "my human"
|
|
4267
|
+
const textMessages = ws.sentMessages
|
|
4268
|
+
.map((raw) => JSON.parse(raw) as { type: string; token?: string })
|
|
4269
|
+
.filter((m) => m.type === "text");
|
|
4270
|
+
const promptText = textMessages.map((m) => m.token ?? "").join("");
|
|
4271
|
+
expect(promptText).toContain("my human");
|
|
4272
|
+
|
|
4273
|
+
relay.destroy();
|
|
4274
|
+
});
|
|
4132
4275
|
});
|
|
@@ -679,11 +679,14 @@ describe("Terminal output format: formatShellOutput shared by sandbox and host",
|
|
|
679
679
|
expect(result.isError).toBe(false);
|
|
680
680
|
});
|
|
681
681
|
|
|
682
|
-
test("non-zero exit code with empty output produces <command_exit /> tag", () => {
|
|
682
|
+
test("non-zero exit code with empty output produces <command_exit /> tag and descriptive message", () => {
|
|
683
683
|
const result = formatShellOutput("", "", 42, false, 120);
|
|
684
684
|
|
|
685
|
-
expect(result.content).
|
|
685
|
+
expect(result.content).toContain('<command_exit code="42" />');
|
|
686
|
+
expect(result.content).toContain("Command failed with exit code 42");
|
|
687
|
+
expect(result.content).toContain("No stdout or stderr output was produced");
|
|
686
688
|
expect(result.isError).toBe(true);
|
|
689
|
+
expect(result.status).toContain('<command_exit code="42" />');
|
|
687
690
|
});
|
|
688
691
|
|
|
689
692
|
test("stderr is appended to stdout with a newline separator", () => {
|