@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.
Files changed (193) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/docs/architecture/memory.md +1 -1
  3. package/package.json +1 -1
  4. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
  5. package/src/__tests__/access-request-decision.test.ts +83 -1
  6. package/src/__tests__/actor-token-service.test.ts +0 -1
  7. package/src/__tests__/anthropic-provider.test.ts +86 -1
  8. package/src/__tests__/approval-routes-http.test.ts +0 -1
  9. package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
  10. package/src/__tests__/call-controller.test.ts +0 -1
  11. package/src/__tests__/call-routes-http.test.ts +0 -1
  12. package/src/__tests__/channel-guardian.test.ts +0 -1
  13. package/src/__tests__/channel-invite-transport.test.ts +52 -40
  14. package/src/__tests__/checker.test.ts +37 -98
  15. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -23
  16. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -1
  17. package/src/__tests__/config-schema.test.ts +6 -5
  18. package/src/__tests__/credential-security-invariants.test.ts +2 -0
  19. package/src/__tests__/daemon-server-session-init.test.ts +1 -19
  20. package/src/__tests__/deterministic-verification-control-plane.test.ts +0 -1
  21. package/src/__tests__/followup-tools.test.ts +0 -30
  22. package/src/__tests__/gemini-provider.test.ts +79 -1
  23. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -1
  24. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  25. package/src/__tests__/guardian-outbound-http.test.ts +0 -1
  26. package/src/__tests__/handlers-telegram-config.test.ts +0 -1
  27. package/src/__tests__/inbound-invite-redemption.test.ts +1 -4
  28. package/src/__tests__/ingress-reconcile.test.ts +3 -36
  29. package/src/__tests__/ipc-snapshot.test.ts +0 -4
  30. package/src/__tests__/managed-proxy-context.test.ts +163 -0
  31. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  32. package/src/__tests__/memory-regressions.test.ts +6 -6
  33. package/src/__tests__/migration-cross-version-compatibility.test.ts +0 -1
  34. package/src/__tests__/migration-export-http.test.ts +0 -1
  35. package/src/__tests__/migration-import-commit-http.test.ts +0 -1
  36. package/src/__tests__/migration-import-preflight-http.test.ts +0 -1
  37. package/src/__tests__/migration-validate-http.test.ts +0 -1
  38. package/src/__tests__/non-member-access-request.test.ts +0 -1
  39. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  40. package/src/__tests__/notification-telegram-adapter.test.ts +0 -4
  41. package/src/__tests__/openai-provider.test.ts +82 -0
  42. package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
  43. package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
  44. package/src/__tests__/recurrence-types.test.ts +0 -15
  45. package/src/__tests__/relay-server.test.ts +145 -2
  46. package/src/__tests__/sandbox-host-parity.test.ts +5 -2
  47. package/src/__tests__/schedule-tools.test.ts +28 -44
  48. package/src/__tests__/session-init.benchmark.test.ts +0 -2
  49. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  50. package/src/__tests__/slack-channel-config.test.ts +0 -1
  51. package/src/__tests__/slack-inbound-verification.test.ts +0 -1
  52. package/src/__tests__/sms-messaging-provider.test.ts +0 -4
  53. package/src/__tests__/task-management-tools.test.ts +111 -0
  54. package/src/__tests__/terminal-tools.test.ts +5 -2
  55. package/src/__tests__/trusted-contact-approval-notifier.test.ts +66 -74
  56. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  57. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -1
  58. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  59. package/src/__tests__/trusted-contact-verification.test.ts +0 -1
  60. package/src/__tests__/twilio-config.test.ts +0 -3
  61. package/src/__tests__/twilio-routes.test.ts +0 -1
  62. package/src/__tests__/update-bulletin.test.ts +0 -2
  63. package/src/__tests__/user-reference.test.ts +47 -1
  64. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  65. package/src/__tests__/workspace-git-service.test.ts +2 -2
  66. package/src/amazon/session.ts +30 -91
  67. package/src/calls/call-controller.ts +423 -571
  68. package/src/calls/finalize-call.ts +20 -0
  69. package/src/calls/relay-access-wait.ts +340 -0
  70. package/src/calls/relay-server.ts +271 -956
  71. package/src/calls/relay-setup-router.ts +307 -0
  72. package/src/calls/relay-verification.ts +280 -0
  73. package/src/calls/twilio-config.ts +1 -8
  74. package/src/calls/voice-control-protocol.ts +184 -0
  75. package/src/calls/voice-session-bridge.ts +1 -8
  76. package/src/channels/config.ts +41 -2
  77. package/src/config/agent-schema.ts +1 -1
  78. package/src/config/bundled-skills/followups/TOOLS.json +0 -4
  79. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  80. package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
  81. package/src/config/bundled-skills/slack/SKILL.md +2 -0
  82. package/src/config/bundled-skills/slack-digest-setup/SKILL.md +164 -0
  83. package/src/config/core-schema.ts +1 -1
  84. package/src/config/env.ts +0 -14
  85. package/src/config/feature-flag-registry.json +5 -5
  86. package/src/config/loader.ts +19 -0
  87. package/src/config/schema.ts +2 -2
  88. package/src/config/user-reference.ts +47 -9
  89. package/src/daemon/handlers/config-channels.ts +11 -10
  90. package/src/daemon/handlers/contacts.ts +5 -1
  91. package/src/daemon/handlers/session-history.ts +398 -0
  92. package/src/daemon/handlers/session-user-message.ts +982 -0
  93. package/src/daemon/handlers/sessions.ts +9 -1338
  94. package/src/daemon/ipc-contract/sessions.ts +0 -6
  95. package/src/daemon/ipc-contract-inventory.json +0 -1
  96. package/src/daemon/lifecycle.ts +18 -55
  97. package/src/home-base/app-link-store.ts +0 -7
  98. package/src/memory/channel-delivery-store.ts +1 -0
  99. package/src/memory/conversation-attention-store.ts +1 -1
  100. package/src/memory/conversation-store.ts +0 -51
  101. package/src/memory/db-init.ts +9 -1
  102. package/src/memory/delivery-crud.ts +13 -0
  103. package/src/memory/invite-store.ts +71 -1
  104. package/src/memory/job-handlers/conflict.ts +24 -0
  105. package/src/memory/migrations/040-invite-code-hash-column.ts +16 -0
  106. package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
  107. package/src/memory/migrations/134-contacts-notes-column.ts +50 -33
  108. package/src/memory/migrations/index.ts +1 -0
  109. package/src/memory/migrations/registry.ts +6 -0
  110. package/src/memory/recall-cache.ts +0 -5
  111. package/src/memory/schema/calls.ts +274 -0
  112. package/src/memory/schema/contacts.ts +127 -0
  113. package/src/memory/schema/conversations.ts +129 -0
  114. package/src/memory/schema/guardian.ts +172 -0
  115. package/src/memory/schema/index.ts +8 -0
  116. package/src/memory/schema/infrastructure.ts +205 -0
  117. package/src/memory/schema/memory-core.ts +196 -0
  118. package/src/memory/schema/notifications.ts +191 -0
  119. package/src/memory/schema/tasks.ts +78 -0
  120. package/src/memory/schema.ts +1 -1385
  121. package/src/memory/slack-thread-store.ts +0 -69
  122. package/src/notifications/decisions-store.ts +2 -105
  123. package/src/notifications/deliveries-store.ts +0 -11
  124. package/src/notifications/preferences-store.ts +1 -58
  125. package/src/permissions/checker.ts +6 -17
  126. package/src/providers/anthropic/client.ts +6 -2
  127. package/src/providers/gemini/client.ts +13 -2
  128. package/src/providers/managed-proxy/constants.ts +55 -0
  129. package/src/providers/managed-proxy/context.ts +77 -0
  130. package/src/providers/registry.ts +112 -0
  131. package/src/runtime/auth/__tests__/guard-tests.test.ts +52 -26
  132. package/src/runtime/auth/token-service.ts +50 -0
  133. package/src/runtime/channel-guardian-service.ts +1 -3
  134. package/src/runtime/channel-invite-transport.ts +121 -34
  135. package/src/runtime/channel-invite-transports/email.ts +50 -0
  136. package/src/runtime/channel-invite-transports/slack.ts +81 -0
  137. package/src/runtime/channel-invite-transports/sms.ts +70 -0
  138. package/src/runtime/channel-invite-transports/telegram.ts +29 -11
  139. package/src/runtime/channel-invite-transports/voice.ts +12 -12
  140. package/src/runtime/http-server.ts +83 -722
  141. package/src/runtime/http-types.ts +0 -16
  142. package/src/runtime/invite-redemption-service.ts +193 -0
  143. package/src/runtime/invite-redemption-templates.ts +6 -6
  144. package/src/runtime/invite-service.ts +81 -11
  145. package/src/runtime/middleware/auth.ts +0 -12
  146. package/src/runtime/routes/access-request-decision.ts +52 -6
  147. package/src/runtime/routes/app-routes.ts +33 -0
  148. package/src/runtime/routes/approval-routes.ts +32 -0
  149. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -0
  150. package/src/runtime/routes/attachment-routes.ts +32 -0
  151. package/src/runtime/routes/brain-graph-routes.ts +27 -0
  152. package/src/runtime/routes/call-routes.ts +41 -0
  153. package/src/runtime/routes/channel-readiness-routes.ts +20 -0
  154. package/src/runtime/routes/channel-routes.ts +70 -0
  155. package/src/runtime/routes/contact-routes.ts +96 -6
  156. package/src/runtime/routes/conversation-attention-routes.ts +15 -0
  157. package/src/runtime/routes/conversation-routes.ts +190 -193
  158. package/src/runtime/routes/debug-routes.ts +15 -0
  159. package/src/runtime/routes/events-routes.ts +16 -0
  160. package/src/runtime/routes/global-search-routes.ts +15 -0
  161. package/src/runtime/routes/guardian-action-routes.ts +22 -0
  162. package/src/runtime/routes/guardian-bootstrap-routes.ts +21 -6
  163. package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
  164. package/src/runtime/routes/identity-routes.ts +20 -0
  165. package/src/runtime/routes/inbound-message-handler.ts +9 -3
  166. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +295 -10
  167. package/src/runtime/routes/inbound-stages/background-dispatch.ts +9 -42
  168. package/src/runtime/routes/inbound-stages/edit-intercept.ts +10 -0
  169. package/src/runtime/routes/integration-routes.ts +83 -0
  170. package/src/runtime/routes/invite-routes.ts +32 -0
  171. package/src/runtime/routes/migration-routes.ts +30 -0
  172. package/src/runtime/routes/pairing-routes.ts +18 -0
  173. package/src/runtime/routes/secret-routes.ts +20 -0
  174. package/src/runtime/routes/surface-action-routes.ts +26 -0
  175. package/src/runtime/routes/trust-rules-routes.ts +31 -0
  176. package/src/runtime/routes/twilio-routes.ts +79 -0
  177. package/src/schedule/recurrence-types.ts +1 -11
  178. package/src/tools/browser/browser-manager.ts +10 -1
  179. package/src/tools/browser/runtime-check.ts +3 -1
  180. package/src/tools/followups/followup_create.ts +9 -3
  181. package/src/tools/mcp/mcp-tool-factory.ts +0 -17
  182. package/src/tools/memory/definitions.ts +0 -6
  183. package/src/tools/network/script-proxy/session-manager.ts +38 -3
  184. package/src/tools/schedule/create.ts +1 -3
  185. package/src/tools/schedule/update.ts +9 -6
  186. package/src/tools/shared/shell-output.ts +7 -2
  187. package/src/twitter/session.ts +29 -77
  188. package/src/util/cookie-session.ts +114 -0
  189. package/src/util/platform.ts +0 -4
  190. package/src/workspace/git-service.ts +10 -4
  191. package/src/__tests__/conversation-routes.test.ts +0 -99
  192. package/src/__tests__/task-tools.test.ts +0 -685
  193. 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: () => "my human",
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).toBe('<command_exit code="42" />');
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", () => {