@vellumai/assistant 0.4.15 → 0.4.17

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 (73) hide show
  1. package/Dockerfile +6 -6
  2. package/README.md +1 -2
  3. package/package.json +1 -1
  4. package/src/__tests__/approval-routes-http.test.ts +383 -254
  5. package/src/__tests__/call-controller.test.ts +1074 -751
  6. package/src/__tests__/call-routes-http.test.ts +329 -279
  7. package/src/__tests__/channel-approval-routes.test.ts +2 -13
  8. package/src/__tests__/channel-approvals.test.ts +227 -182
  9. package/src/__tests__/channel-guardian.test.ts +1 -0
  10. package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
  11. package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
  12. package/src/__tests__/conversation-routes.test.ts +71 -41
  13. package/src/__tests__/daemon-server-session-init.test.ts +258 -191
  14. package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
  15. package/src/__tests__/extract-email.test.ts +42 -0
  16. package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
  17. package/src/__tests__/gateway-only-guard.test.ts +54 -55
  18. package/src/__tests__/gmail-integration.test.ts +48 -46
  19. package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
  20. package/src/__tests__/guardian-outbound-http.test.ts +334 -208
  21. package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
  22. package/src/__tests__/guardian-routing-state.test.ts +257 -209
  23. package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
  24. package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
  25. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
  26. package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
  27. package/src/__tests__/ingress-reconcile.test.ts +184 -142
  28. package/src/__tests__/non-member-access-request.test.ts +291 -247
  29. package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
  30. package/src/__tests__/pairing-concurrent.test.ts +78 -0
  31. package/src/__tests__/recording-intent-handler.test.ts +422 -291
  32. package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
  33. package/src/__tests__/runtime-events-sse.test.ts +67 -50
  34. package/src/__tests__/send-endpoint-busy.test.ts +314 -232
  35. package/src/__tests__/session-approval-overrides.test.ts +93 -91
  36. package/src/__tests__/sms-messaging-provider.test.ts +74 -47
  37. package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
  38. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
  39. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
  40. package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
  41. package/src/__tests__/twilio-config.test.ts +49 -41
  42. package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
  43. package/src/__tests__/twilio-routes.test.ts +389 -280
  44. package/src/calls/call-controller.ts +1 -1
  45. package/src/calls/guardian-action-sweep.ts +6 -6
  46. package/src/calls/twilio-routes.ts +2 -4
  47. package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
  48. package/src/config/bundled-skills/messaging/SKILL.md +5 -4
  49. package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +69 -4
  50. package/src/config/env.ts +39 -29
  51. package/src/daemon/handlers/config-inbox.ts +5 -5
  52. package/src/daemon/handlers/skills.ts +18 -10
  53. package/src/daemon/ipc-contract/messages.ts +1 -0
  54. package/src/daemon/ipc-contract/surfaces.ts +7 -1
  55. package/src/daemon/pairing-store.ts +15 -2
  56. package/src/daemon/session-agent-loop-handlers.ts +5 -0
  57. package/src/daemon/session-agent-loop.ts +1 -1
  58. package/src/daemon/session-process.ts +1 -1
  59. package/src/daemon/session-slash.ts +4 -4
  60. package/src/daemon/session-surfaces.ts +42 -2
  61. package/src/runtime/auth/token-service.ts +95 -45
  62. package/src/runtime/channel-retry-sweep.ts +2 -2
  63. package/src/runtime/http-server.ts +8 -7
  64. package/src/runtime/http-types.ts +1 -1
  65. package/src/runtime/routes/conversation-routes.ts +1 -1
  66. package/src/runtime/routes/guardian-bootstrap-routes.ts +3 -2
  67. package/src/runtime/routes/guardian-expiry-sweep.ts +5 -5
  68. package/src/runtime/routes/pairing-routes.ts +4 -1
  69. package/src/sequence/reply-matcher.ts +14 -4
  70. package/src/skills/frontmatter.ts +9 -6
  71. package/src/tools/ui-surface/definitions.ts +3 -1
  72. package/src/util/platform.ts +0 -12
  73. package/docs/architecture/http-token-refresh.md +0 -274
@@ -1,38 +1,56 @@
1
- import { beforeEach, describe, expect, mock, test } from 'bun:test';
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
2
4
 
3
5
  const routeGuardianReplyMock = mock(async () => ({
4
6
  consumed: false,
5
7
  decisionApplied: false,
6
- type: 'not_consumed' as const,
8
+ type: "not_consumed" as const,
7
9
  })) as any;
8
- const listPendingByDestinationMock = mock((_conversationId: string, _sourceChannel?: string) => [] as Array<{ id: string; kind?: string }>);
9
- const listCanonicalMock = mock((_filters?: Record<string, unknown>) => [] as Array<{ id: string }>);
10
- const addMessageMock = mock(async (_conversationId: string, role: string, _content?: string, _metadata?: Record<string, unknown>) => ({
11
- id: role === 'user' ? 'persisted-user-id' : 'persisted-assistant-id',
12
- }));
13
-
14
- mock.module('../util/logger.js', () => ({
15
- getLogger: () => new Proxy({} as Record<string, unknown>, {
16
- get: () => () => {},
10
+ const listPendingByDestinationMock = mock(
11
+ (_conversationId: string, _sourceChannel?: string) =>
12
+ [] as Array<{ id: string; kind?: string }>,
13
+ );
14
+ const listCanonicalMock = mock(
15
+ (_filters?: Record<string, unknown>) => [] as Array<{ id: string }>,
16
+ );
17
+ const addMessageMock = mock(
18
+ async (
19
+ _conversationId: string,
20
+ role: string,
21
+ _content?: string,
22
+ _metadata?: Record<string, unknown>,
23
+ ) => ({
24
+ id: role === "user" ? "persisted-user-id" : "persisted-assistant-id",
17
25
  }),
26
+ );
27
+
28
+ mock.module("../util/logger.js", () => ({
29
+ getLogger: () =>
30
+ new Proxy({} as Record<string, unknown>, {
31
+ get: () => () => {},
32
+ }),
18
33
  }));
19
34
 
20
- mock.module('../memory/conversation-key-store.js', () => ({
21
- getOrCreateConversation: () => ({ conversationId: 'conv-canonical-reply' }),
35
+ mock.module("../memory/conversation-key-store.js", () => ({
36
+ getOrCreateConversation: () => ({ conversationId: "conv-canonical-reply" }),
22
37
  getConversationByKey: () => null,
23
38
  }));
24
39
 
25
- mock.module('../memory/attachments-store.js', () => ({
40
+ mock.module("../memory/attachments-store.js", () => ({
26
41
  getAttachmentsByIds: () => [],
27
42
  }));
28
43
 
29
- mock.module('../runtime/guardian-reply-router.js', () => ({
44
+ mock.module("../runtime/guardian-reply-router.js", () => ({
30
45
  routeGuardianReply: routeGuardianReplyMock,
31
46
  }));
32
47
 
33
- mock.module('../memory/canonical-guardian-store.js', () => ({
34
- createCanonicalGuardianRequest: () => ({ id: 'canonical-id', requestCode: 'ABC123' }),
35
- generateCanonicalRequestCode: () => 'ABC123',
48
+ mock.module("../memory/canonical-guardian-store.js", () => ({
49
+ createCanonicalGuardianRequest: () => ({
50
+ id: "canonical-id",
51
+ requestCode: "ABC123",
52
+ }),
53
+ generateCanonicalRequestCode: () => "ABC123",
36
54
  listPendingCanonicalGuardianRequestsByDestinationConversation: (
37
55
  conversationId: string,
38
56
  sourceChannel?: string,
@@ -41,11 +59,11 @@ mock.module('../memory/canonical-guardian-store.js', () => ({
41
59
  listCanonicalMock(filters),
42
60
  }));
43
61
 
44
- mock.module('../runtime/confirmation-request-guardian-bridge.js', () => ({
62
+ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
45
63
  bridgeConfirmationRequestToGuardian: async () => undefined,
46
64
  }));
47
65
 
48
- mock.module('../memory/conversation-store.js', () => ({
66
+ mock.module("../memory/conversation-store.js", () => ({
49
67
  addMessage: (
50
68
  conversationId: string,
51
69
  role: string,
@@ -54,24 +72,40 @@ mock.module('../memory/conversation-store.js', () => ({
54
72
  ) => addMessageMock(conversationId, role, content, metadata),
55
73
  }));
56
74
 
57
- mock.module('../runtime/local-actor-identity.js', () => ({
58
- resolveLocalIpcGuardianContext: () => ({ trustClass: 'guardian', sourceChannel: 'vellum' }),
75
+ mock.module("../runtime/local-actor-identity.js", () => ({
76
+ resolveLocalIpcGuardianContext: () => ({
77
+ trustClass: "guardian",
78
+ sourceChannel: "vellum",
79
+ }),
59
80
  }));
60
81
 
61
- import type { AuthContext } from '../runtime/auth/types.js';
62
- import { handleSendMessage } from '../runtime/routes/conversation-routes.js';
82
+ import type { AuthContext } from "../runtime/auth/types.js";
83
+ import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
63
84
 
64
85
  const testAuthContext: AuthContext = {
65
- subject: 'actor:self:test-guardian',
66
- principalType: 'actor',
67
- assistantId: 'self',
68
- actorPrincipalId: 'test-guardian',
69
- scopeProfile: 'actor_client_v1',
70
- scopes: new Set(['chat.read', 'chat.write', 'approval.read', 'approval.write', 'settings.read', 'settings.write', 'attachments.read', 'attachments.write', 'calls.read', 'calls.write', 'feature_flags.read', 'feature_flags.write']),
86
+ subject: "actor:self:test-guardian",
87
+ principalType: "actor",
88
+ assistantId: "self",
89
+ actorPrincipalId: "test-guardian",
90
+ scopeProfile: "actor_client_v1",
91
+ scopes: new Set([
92
+ "chat.read",
93
+ "chat.write",
94
+ "approval.read",
95
+ "approval.write",
96
+ "settings.read",
97
+ "settings.write",
98
+ "attachments.read",
99
+ "attachments.write",
100
+ "calls.read",
101
+ "calls.write",
102
+ "feature_flags.read",
103
+ "feature_flags.write",
104
+ ]),
71
105
  policyEpoch: 1,
72
106
  };
73
107
 
74
- describe('handleSendMessage canonical guardian reply interception', () => {
108
+ describe("handleSendMessage canonical guardian reply interception", () => {
75
109
  beforeEach(() => {
76
110
  routeGuardianReplyMock.mockClear();
77
111
  listPendingByDestinationMock.mockClear();
@@ -79,18 +113,18 @@ describe('handleSendMessage canonical guardian reply interception', () => {
79
113
  addMessageMock.mockClear();
80
114
  });
81
115
 
82
- test('consumes access-request code replies on desktop HTTP path without pending confirmations', async () => {
83
- listPendingByDestinationMock.mockReturnValue([{ id: 'access-req-1' }]);
116
+ test("consumes access-request code replies on desktop HTTP path without pending confirmations", async () => {
117
+ listPendingByDestinationMock.mockReturnValue([{ id: "access-req-1" }]);
84
118
  listCanonicalMock.mockReturnValue([]);
85
119
  routeGuardianReplyMock.mockResolvedValue({
86
120
  consumed: true,
87
121
  decisionApplied: true,
88
- type: 'canonical_decision_applied',
89
- requestId: 'access-req-1',
90
- replyText: 'Access approved. Verification code: 123456.',
122
+ type: "canonical_decision_applied",
123
+ requestId: "access-req-1",
124
+ replyText: "Access approved. Verification code: 123456.",
91
125
  });
92
126
 
93
- const persistUserMessage = mock(async () => 'should-not-be-called');
127
+ const persistUserMessage = mock(async () => "should-not-be-called");
94
128
  const runAgentLoop = mock(async () => undefined);
95
129
  const session = {
96
130
  setGuardianContext: () => {},
@@ -102,58 +136,66 @@ describe('handleSendMessage canonical guardian reply interception', () => {
102
136
  isProcessing: () => false,
103
137
  hasAnyPendingConfirmation: () => false,
104
138
  denyAllPendingConfirmations: () => {},
105
- enqueueMessage: () => ({ queued: true, requestId: 'queued-id' }),
139
+ enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
106
140
  persistUserMessage,
107
141
  runAgentLoop,
108
142
  getMessages: () => [] as unknown[],
109
- assistantId: 'self',
143
+ assistantId: "self",
110
144
  guardianContext: undefined,
111
145
  hasPendingConfirmation: () => false,
112
- } as unknown as import('../daemon/session.js').Session;
146
+ } as unknown as import("../daemon/session.js").Session;
113
147
 
114
- const req = new Request('http://localhost/v1/messages', {
115
- method: 'POST',
116
- headers: { 'Content-Type': 'application/json' },
148
+ const req = new Request("http://localhost/v1/messages", {
149
+ method: "POST",
150
+ headers: { "Content-Type": "application/json" },
117
151
  body: JSON.stringify({
118
- conversationKey: 'guardian-thread-key',
119
- content: '05BECB approve',
120
- sourceChannel: 'vellum',
121
- interface: 'macos',
152
+ conversationKey: "guardian-thread-key",
153
+ content: "05BECB approve",
154
+ sourceChannel: "vellum",
155
+ interface: "macos",
122
156
  }),
123
157
  });
124
158
 
125
- const res = await handleSendMessage(req, {
126
- sendMessageDeps: {
127
- getOrCreateSession: async () => session,
128
- assistantEventHub: { publish: async () => {} } as any,
129
- resolveAttachments: () => [],
159
+ const res = await handleSendMessage(
160
+ req,
161
+ {
162
+ sendMessageDeps: {
163
+ getOrCreateSession: async () => session,
164
+ assistantEventHub: { publish: async () => {} } as any,
165
+ resolveAttachments: () => [],
166
+ },
130
167
  },
131
- }, testAuthContext);
168
+ testAuthContext,
169
+ );
132
170
 
133
171
  expect(res.status).toBe(202);
134
- const body = await res.json() as { accepted: boolean; messageId?: string };
172
+ const body = (await res.json()) as {
173
+ accepted: boolean;
174
+ messageId?: string;
175
+ };
135
176
  expect(body.accepted).toBe(true);
136
- expect(body.messageId).toBe('persisted-user-id');
177
+ expect(body.messageId).toBe("persisted-user-id");
137
178
 
138
179
  expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
139
- const routerCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
140
- expect(routerCall.messageText).toBe('05BECB approve');
141
- expect(routerCall.pendingRequestIds).toEqual(['access-req-1']);
180
+ const routerCall = (routeGuardianReplyMock as any).mock
181
+ .calls[0][0] as Record<string, unknown>;
182
+ expect(routerCall.messageText).toBe("05BECB approve");
183
+ expect(routerCall.pendingRequestIds).toEqual(["access-req-1"]);
142
184
  expect(addMessageMock).toHaveBeenCalledTimes(2);
143
185
  expect(persistUserMessage).toHaveBeenCalledTimes(0);
144
186
  expect(runAgentLoop).toHaveBeenCalledTimes(0);
145
187
  });
146
188
 
147
- test('passes undefined pendingRequestIds when no canonical hints are found', async () => {
189
+ test("passes undefined pendingRequestIds when no canonical hints are found", async () => {
148
190
  listPendingByDestinationMock.mockReturnValue([]);
149
191
  listCanonicalMock.mockReturnValue([]);
150
192
  routeGuardianReplyMock.mockResolvedValue({
151
193
  consumed: false,
152
194
  decisionApplied: false,
153
- type: 'not_consumed',
195
+ type: "not_consumed",
154
196
  });
155
197
 
156
- const persistUserMessage = mock(async () => 'persisted-user-id');
198
+ const persistUserMessage = mock(async () => "persisted-user-id");
157
199
  const runAgentLoop = mock(async () => undefined);
158
200
  const session = {
159
201
  setGuardianContext: () => {},
@@ -165,56 +207,61 @@ describe('handleSendMessage canonical guardian reply interception', () => {
165
207
  isProcessing: () => false,
166
208
  hasAnyPendingConfirmation: () => false,
167
209
  denyAllPendingConfirmations: () => {},
168
- enqueueMessage: () => ({ queued: true, requestId: 'queued-id' }),
210
+ enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
169
211
  persistUserMessage,
170
212
  runAgentLoop,
171
213
  getMessages: () => [] as unknown[],
172
- assistantId: 'self',
214
+ assistantId: "self",
173
215
  guardianContext: undefined,
174
216
  hasPendingConfirmation: () => false,
175
- } as unknown as import('../daemon/session.js').Session;
217
+ } as unknown as import("../daemon/session.js").Session;
176
218
 
177
- const req = new Request('http://localhost/v1/messages', {
178
- method: 'POST',
179
- headers: { 'Content-Type': 'application/json' },
219
+ const req = new Request("http://localhost/v1/messages", {
220
+ method: "POST",
221
+ headers: { "Content-Type": "application/json" },
180
222
  body: JSON.stringify({
181
- conversationKey: 'guardian-thread-key',
182
- content: 'hello there',
183
- sourceChannel: 'vellum',
184
- interface: 'macos',
223
+ conversationKey: "guardian-thread-key",
224
+ content: "hello there",
225
+ sourceChannel: "vellum",
226
+ interface: "macos",
185
227
  }),
186
228
  });
187
229
 
188
- const res = await handleSendMessage(req, {
189
- sendMessageDeps: {
190
- getOrCreateSession: async () => session,
191
- assistantEventHub: { publish: async () => {} } as any,
192
- resolveAttachments: () => [],
230
+ const res = await handleSendMessage(
231
+ req,
232
+ {
233
+ sendMessageDeps: {
234
+ getOrCreateSession: async () => session,
235
+ assistantEventHub: { publish: async () => {} } as any,
236
+ resolveAttachments: () => [],
237
+ },
193
238
  },
194
- }, testAuthContext);
239
+ testAuthContext,
240
+ );
195
241
 
196
242
  expect(res.status).toBe(202);
197
243
  expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
198
- const routerCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
244
+ const routerCall = (routeGuardianReplyMock as any).mock
245
+ .calls[0][0] as Record<string, unknown>;
199
246
  expect(routerCall.pendingRequestIds).toBeUndefined();
200
247
  expect(persistUserMessage).toHaveBeenCalledTimes(1);
201
248
  expect(runAgentLoop).toHaveBeenCalledTimes(1);
202
249
  });
203
250
 
204
- test('excludes stale tool_approval hints without a live pending confirmation', async () => {
251
+ test("excludes stale tool_approval hints without a live pending confirmation", async () => {
205
252
  listPendingByDestinationMock.mockReturnValue([
206
- { id: 'tool-approval-live', kind: 'tool_approval' },
207
- { id: 'tool-approval-stale', kind: 'tool_approval' },
208
- { id: 'access-req-1', kind: 'access_request' },
253
+ { id: "tool-approval-live", kind: "tool_approval" },
254
+ { id: "tool-approval-stale", kind: "tool_approval" },
255
+ { id: "access-req-1", kind: "access_request" },
209
256
  ]);
210
257
  listCanonicalMock.mockReturnValue([]);
211
258
  routeGuardianReplyMock.mockResolvedValue({
212
259
  consumed: false,
213
260
  decisionApplied: false,
214
- type: 'not_consumed',
261
+ type: "not_consumed",
215
262
  });
216
263
 
217
- const persistUserMessage = mock(async () => 'persisted-user-id');
264
+ const persistUserMessage = mock(async () => "persisted-user-id");
218
265
  const runAgentLoop = mock(async () => undefined);
219
266
  const session = {
220
267
  setGuardianContext: () => {},
@@ -226,38 +273,51 @@ describe('handleSendMessage canonical guardian reply interception', () => {
226
273
  isProcessing: () => false,
227
274
  hasAnyPendingConfirmation: () => true,
228
275
  denyAllPendingConfirmations: () => {},
229
- enqueueMessage: () => ({ queued: true, requestId: 'queued-id' }),
276
+ enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
230
277
  persistUserMessage,
231
278
  runAgentLoop,
232
279
  getMessages: () => [] as unknown[],
233
- assistantId: 'self',
280
+ assistantId: "self",
234
281
  guardianContext: undefined,
235
- hasPendingConfirmation: (requestId: string) => requestId === 'tool-approval-live',
236
- } as unknown as import('../daemon/session.js').Session;
282
+ hasPendingConfirmation: (requestId: string) =>
283
+ requestId === "tool-approval-live",
284
+ } as unknown as import("../daemon/session.js").Session;
237
285
 
238
- const req = new Request('http://localhost/v1/messages', {
239
- method: 'POST',
240
- headers: { 'Content-Type': 'application/json' },
286
+ const req = new Request("http://localhost/v1/messages", {
287
+ method: "POST",
288
+ headers: { "Content-Type": "application/json" },
241
289
  body: JSON.stringify({
242
- conversationKey: 'guardian-thread-key',
243
- content: 'approve',
244
- sourceChannel: 'vellum',
245
- interface: 'macos',
290
+ conversationKey: "guardian-thread-key",
291
+ content: "approve",
292
+ sourceChannel: "vellum",
293
+ interface: "macos",
246
294
  }),
247
295
  });
248
296
 
249
- const res = await handleSendMessage(req, {
250
- sendMessageDeps: {
251
- getOrCreateSession: async () => session,
252
- assistantEventHub: { publish: async () => {} } as any,
253
- resolveAttachments: () => [],
297
+ const res = await handleSendMessage(
298
+ req,
299
+ {
300
+ sendMessageDeps: {
301
+ getOrCreateSession: async () => session,
302
+ assistantEventHub: { publish: async () => {} } as any,
303
+ resolveAttachments: () => [],
304
+ },
254
305
  },
255
- }, testAuthContext);
306
+ testAuthContext,
307
+ );
256
308
 
257
309
  expect(res.status).toBe(202);
258
310
  expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
259
- const routerCall = (routeGuardianReplyMock as any).mock.calls[0][0] as Record<string, unknown>;
260
- expect(routerCall.pendingRequestIds).toEqual(['tool-approval-live', 'access-req-1']);
261
- expect((routerCall.pendingRequestIds as string[]).includes('tool-approval-stale')).toBe(false);
311
+ const routerCall = (routeGuardianReplyMock as any).mock
312
+ .calls[0][0] as Record<string, unknown>;
313
+ expect(routerCall.pendingRequestIds).toEqual([
314
+ "tool-approval-live",
315
+ "access-req-1",
316
+ ]);
317
+ expect(
318
+ (routerCall.pendingRequestIds as string[]).includes(
319
+ "tool-approval-stale",
320
+ ),
321
+ ).toBe(false);
262
322
  });
263
323
  });
@@ -1,72 +1,102 @@
1
- import { describe, expect, mock, test } from 'bun:test';
1
+ import { describe, expect, mock, test } from "bun:test";
2
2
 
3
- import type { RuntimeMessageSessionOptions } from '../runtime/http-types.js';
3
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
4
4
 
5
- mock.module('../util/logger.js', () => ({
6
- getLogger: () => new Proxy({} as Record<string, unknown>, {
7
- get: () => () => {},
8
- }),
5
+ import type { RuntimeMessageSessionOptions } from "../runtime/http-types.js";
6
+
7
+ mock.module("../util/logger.js", () => ({
8
+ getLogger: () =>
9
+ new Proxy({} as Record<string, unknown>, {
10
+ get: () => () => {},
11
+ }),
9
12
  }));
10
13
 
11
- mock.module('../memory/conversation-key-store.js', () => ({
12
- getOrCreateConversation: () => ({ conversationId: 'conv-legacy-test' }),
14
+ mock.module("../memory/conversation-key-store.js", () => ({
15
+ getOrCreateConversation: () => ({ conversationId: "conv-legacy-test" }),
13
16
  getConversationByKey: () => null,
14
17
  }));
15
18
 
16
- mock.module('../memory/attachments-store.js', () => ({
19
+ mock.module("../memory/attachments-store.js", () => ({
17
20
  getAttachmentsByIds: () => [],
18
21
  }));
19
22
 
20
- mock.module('../runtime/local-actor-identity.js', () => ({
21
- resolveLocalIpcGuardianContext: (sourceChannel: string) => ({ trustClass: 'guardian', sourceChannel }),
23
+ mock.module("../runtime/guardian-context-resolver.js", () => ({
24
+ resolveGuardianContext: (input: { sourceChannel?: string }) => ({
25
+ trustClass: "guardian",
26
+ sourceChannel: input.sourceChannel ?? "vellum",
27
+ }),
28
+ toGuardianRuntimeContext: (
29
+ sourceChannel: string,
30
+ ctx: Record<string, unknown>,
31
+ ) => ({
32
+ ...ctx,
33
+ sourceChannel,
34
+ }),
22
35
  }));
23
36
 
24
- import type { AuthContext } from '../runtime/auth/types.js';
25
- import { handleSendMessage } from '../runtime/routes/conversation-routes.js';
37
+ import type { AuthContext } from "../runtime/auth/types.js";
38
+ import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
26
39
 
27
40
  /** Synthetic AuthContext for tests — mimics a local actor with full scopes. */
28
41
  const mockAuthContext: AuthContext = {
29
- subject: 'actor:self:test-principal',
30
- principalType: 'actor',
31
- assistantId: 'self',
32
- actorPrincipalId: 'test-principal',
33
- scopeProfile: 'actor_client_v1',
34
- scopes: new Set(['chat.read', 'chat.write', 'approval.read', 'approval.write']),
42
+ subject: "actor:self:test-principal",
43
+ principalType: "actor",
44
+ assistantId: "self",
45
+ actorPrincipalId: "test-principal",
46
+ scopeProfile: "actor_client_v1",
47
+ scopes: new Set([
48
+ "chat.read",
49
+ "chat.write",
50
+ "approval.read",
51
+ "approval.write",
52
+ ]),
35
53
  policyEpoch: 1,
36
54
  };
37
55
 
38
- describe('handleSendMessage', () => {
39
- test('legacy fallback passes guardian context to processor', async () => {
56
+ describe("handleSendMessage", () => {
57
+ test("legacy fallback passes guardian context to processor", async () => {
40
58
  let capturedOptions: RuntimeMessageSessionOptions | undefined;
41
59
  let capturedSourceChannel: string | undefined;
42
60
 
43
- const req = new Request('http://localhost/v1/messages', {
44
- method: 'POST',
45
- headers: { 'Content-Type': 'application/json' },
61
+ const req = new Request("http://localhost/v1/messages", {
62
+ method: "POST",
63
+ headers: { "Content-Type": "application/json" },
46
64
  body: JSON.stringify({
47
- conversationKey: 'legacy-fallback-key',
48
- content: 'Hello from legacy fallback',
49
- sourceChannel: 'telegram',
50
- interface: 'telegram',
65
+ conversationKey: "legacy-fallback-key",
66
+ content: "Hello from legacy fallback",
67
+ sourceChannel: "telegram",
68
+ interface: "telegram",
51
69
  }),
52
70
  });
53
71
 
54
- const res = await handleSendMessage(req, {
55
- processMessage: async (_conversationId, _content, _attachmentIds, options, sourceChannel) => {
56
- capturedOptions = options;
57
- capturedSourceChannel = sourceChannel;
58
- return { messageId: 'msg-legacy-fallback' };
72
+ const res = await handleSendMessage(
73
+ req,
74
+ {
75
+ processMessage: async (
76
+ _conversationId,
77
+ _content,
78
+ _attachmentIds,
79
+ options,
80
+ sourceChannel,
81
+ ) => {
82
+ capturedOptions = options;
83
+ capturedSourceChannel = sourceChannel;
84
+ return { messageId: "msg-legacy-fallback" };
85
+ },
59
86
  },
60
- }, mockAuthContext);
87
+ mockAuthContext,
88
+ );
61
89
 
62
- const body = await res.json() as { accepted: boolean; messageId: string };
90
+ const body = (await res.json()) as { accepted: boolean; messageId: string };
63
91
  expect(res.status).toBe(202);
64
92
  expect(body.accepted).toBe(true);
65
- expect(body.messageId).toBe('msg-legacy-fallback');
66
- expect(capturedSourceChannel).toBe('telegram');
67
- expect(capturedOptions?.guardianContext).toEqual(expect.objectContaining({
68
- trustClass: 'guardian',
69
- sourceChannel: 'telegram',
70
- }));
93
+ expect(body.messageId).toBe("msg-legacy-fallback");
94
+ expect(capturedSourceChannel).toBe("telegram");
95
+ expect(capturedOptions?.guardianContext).toEqual(
96
+ expect.objectContaining({
97
+ trustClass: "guardian",
98
+ sourceChannel: "telegram",
99
+ }),
100
+ );
71
101
  });
72
102
  });