@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.
- package/Dockerfile +6 -6
- package/README.md +1 -2
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +383 -254
- package/src/__tests__/call-controller.test.ts +1074 -751
- package/src/__tests__/call-routes-http.test.ts +329 -279
- package/src/__tests__/channel-approval-routes.test.ts +2 -13
- package/src/__tests__/channel-approvals.test.ts +227 -182
- package/src/__tests__/channel-guardian.test.ts +1 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +157 -114
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +164 -104
- package/src/__tests__/conversation-routes.test.ts +71 -41
- package/src/__tests__/daemon-server-session-init.test.ts +258 -191
- package/src/__tests__/deterministic-verification-control-plane.test.ts +183 -134
- package/src/__tests__/extract-email.test.ts +42 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +467 -368
- package/src/__tests__/gateway-only-guard.test.ts +54 -55
- package/src/__tests__/gmail-integration.test.ts +48 -46
- package/src/__tests__/guardian-action-followup-executor.test.ts +215 -150
- package/src/__tests__/guardian-outbound-http.test.ts +334 -208
- package/src/__tests__/guardian-routing-invariants.test.ts +680 -613
- package/src/__tests__/guardian-routing-state.test.ts +257 -209
- package/src/__tests__/guardian-verification-voice-binding.test.ts +47 -40
- package/src/__tests__/handle-user-message-secret-resume.test.ts +44 -21
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +269 -195
- package/src/__tests__/inbound-invite-redemption.test.ts +194 -151
- package/src/__tests__/ingress-reconcile.test.ts +184 -142
- package/src/__tests__/non-member-access-request.test.ts +291 -247
- package/src/__tests__/notification-telegram-adapter.test.ts +60 -46
- package/src/__tests__/pairing-concurrent.test.ts +78 -0
- package/src/__tests__/recording-intent-handler.test.ts +422 -291
- package/src/__tests__/runtime-attachment-metadata.test.ts +107 -69
- package/src/__tests__/runtime-events-sse.test.ts +67 -50
- package/src/__tests__/send-endpoint-busy.test.ts +314 -232
- package/src/__tests__/session-approval-overrides.test.ts +93 -91
- package/src/__tests__/sms-messaging-provider.test.ts +74 -47
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +339 -274
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +484 -372
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +261 -239
- package/src/__tests__/trusted-contact-multichannel.test.ts +179 -140
- package/src/__tests__/twilio-config.test.ts +49 -41
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +189 -162
- package/src/__tests__/twilio-routes.test.ts +389 -280
- package/src/calls/call-controller.ts +1 -1
- package/src/calls/guardian-action-sweep.ts +6 -6
- package/src/calls/twilio-routes.ts +2 -4
- package/src/config/bundled-skills/chatgpt-import/tools/chatgpt-import.ts +29 -4
- package/src/config/bundled-skills/messaging/SKILL.md +5 -4
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +69 -4
- package/src/config/env.ts +39 -29
- package/src/daemon/handlers/config-inbox.ts +5 -5
- package/src/daemon/handlers/skills.ts +18 -10
- package/src/daemon/ipc-contract/messages.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +7 -1
- package/src/daemon/pairing-store.ts +15 -2
- package/src/daemon/session-agent-loop-handlers.ts +5 -0
- package/src/daemon/session-agent-loop.ts +1 -1
- package/src/daemon/session-process.ts +1 -1
- package/src/daemon/session-slash.ts +4 -4
- package/src/daemon/session-surfaces.ts +42 -2
- package/src/runtime/auth/token-service.ts +95 -45
- package/src/runtime/channel-retry-sweep.ts +2 -2
- package/src/runtime/http-server.ts +8 -7
- package/src/runtime/http-types.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +1 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +3 -2
- package/src/runtime/routes/guardian-expiry-sweep.ts +5 -5
- package/src/runtime/routes/pairing-routes.ts +4 -1
- package/src/sequence/reply-matcher.ts +14 -4
- package/src/skills/frontmatter.ts +9 -6
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/util/platform.ts +0 -12
- package/docs/architecture/http-token-refresh.md +0 -274
|
@@ -1,38 +1,56 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from
|
|
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:
|
|
8
|
+
type: "not_consumed" as const,
|
|
7
9
|
})) as any;
|
|
8
|
-
const listPendingByDestinationMock = mock(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
21
|
-
getOrCreateConversation: () => ({ conversationId:
|
|
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(
|
|
40
|
+
mock.module("../memory/attachments-store.js", () => ({
|
|
26
41
|
getAttachmentsByIds: () => [],
|
|
27
42
|
}));
|
|
28
43
|
|
|
29
|
-
mock.module(
|
|
44
|
+
mock.module("../runtime/guardian-reply-router.js", () => ({
|
|
30
45
|
routeGuardianReply: routeGuardianReplyMock,
|
|
31
46
|
}));
|
|
32
47
|
|
|
33
|
-
mock.module(
|
|
34
|
-
createCanonicalGuardianRequest: () => ({
|
|
35
|
-
|
|
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(
|
|
62
|
+
mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
|
|
45
63
|
bridgeConfirmationRequestToGuardian: async () => undefined,
|
|
46
64
|
}));
|
|
47
65
|
|
|
48
|
-
mock.module(
|
|
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(
|
|
58
|
-
resolveLocalIpcGuardianContext: () => ({
|
|
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
|
|
62
|
-
import { handleSendMessage } from
|
|
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:
|
|
66
|
-
principalType:
|
|
67
|
-
assistantId:
|
|
68
|
-
actorPrincipalId:
|
|
69
|
-
scopeProfile:
|
|
70
|
-
scopes: new Set([
|
|
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(
|
|
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(
|
|
83
|
-
listPendingByDestinationMock.mockReturnValue([{ id:
|
|
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:
|
|
89
|
-
requestId:
|
|
90
|
-
replyText:
|
|
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 () =>
|
|
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:
|
|
139
|
+
enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
|
|
106
140
|
persistUserMessage,
|
|
107
141
|
runAgentLoop,
|
|
108
142
|
getMessages: () => [] as unknown[],
|
|
109
|
-
assistantId:
|
|
143
|
+
assistantId: "self",
|
|
110
144
|
guardianContext: undefined,
|
|
111
145
|
hasPendingConfirmation: () => false,
|
|
112
|
-
} as unknown as import(
|
|
146
|
+
} as unknown as import("../daemon/session.js").Session;
|
|
113
147
|
|
|
114
|
-
const req = new Request(
|
|
115
|
-
method:
|
|
116
|
-
headers: {
|
|
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:
|
|
119
|
-
content:
|
|
120
|
-
sourceChannel:
|
|
121
|
-
interface:
|
|
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(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
168
|
+
testAuthContext,
|
|
169
|
+
);
|
|
132
170
|
|
|
133
171
|
expect(res.status).toBe(202);
|
|
134
|
-
const body = await res.json() as {
|
|
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(
|
|
177
|
+
expect(body.messageId).toBe("persisted-user-id");
|
|
137
178
|
|
|
138
179
|
expect(routeGuardianReplyMock).toHaveBeenCalledTimes(1);
|
|
139
|
-
const routerCall = (routeGuardianReplyMock as any).mock
|
|
140
|
-
|
|
141
|
-
expect(routerCall.
|
|
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(
|
|
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:
|
|
195
|
+
type: "not_consumed",
|
|
154
196
|
});
|
|
155
197
|
|
|
156
|
-
const persistUserMessage = mock(async () =>
|
|
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:
|
|
210
|
+
enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
|
|
169
211
|
persistUserMessage,
|
|
170
212
|
runAgentLoop,
|
|
171
213
|
getMessages: () => [] as unknown[],
|
|
172
|
-
assistantId:
|
|
214
|
+
assistantId: "self",
|
|
173
215
|
guardianContext: undefined,
|
|
174
216
|
hasPendingConfirmation: () => false,
|
|
175
|
-
} as unknown as import(
|
|
217
|
+
} as unknown as import("../daemon/session.js").Session;
|
|
176
218
|
|
|
177
|
-
const req = new Request(
|
|
178
|
-
method:
|
|
179
|
-
headers: {
|
|
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:
|
|
182
|
-
content:
|
|
183
|
-
sourceChannel:
|
|
184
|
-
interface:
|
|
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(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
251
|
+
test("excludes stale tool_approval hints without a live pending confirmation", async () => {
|
|
205
252
|
listPendingByDestinationMock.mockReturnValue([
|
|
206
|
-
{ id:
|
|
207
|
-
{ id:
|
|
208
|
-
{ id:
|
|
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:
|
|
261
|
+
type: "not_consumed",
|
|
215
262
|
});
|
|
216
263
|
|
|
217
|
-
const persistUserMessage = mock(async () =>
|
|
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:
|
|
276
|
+
enqueueMessage: () => ({ queued: true, requestId: "queued-id" }),
|
|
230
277
|
persistUserMessage,
|
|
231
278
|
runAgentLoop,
|
|
232
279
|
getMessages: () => [] as unknown[],
|
|
233
|
-
assistantId:
|
|
280
|
+
assistantId: "self",
|
|
234
281
|
guardianContext: undefined,
|
|
235
|
-
hasPendingConfirmation: (requestId: string) =>
|
|
236
|
-
|
|
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(
|
|
239
|
-
method:
|
|
240
|
-
headers: {
|
|
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:
|
|
243
|
-
content:
|
|
244
|
-
sourceChannel:
|
|
245
|
-
interface:
|
|
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(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
|
260
|
-
|
|
261
|
-
expect(
|
|
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
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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(
|
|
12
|
-
getOrCreateConversation: () => ({ conversationId:
|
|
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(
|
|
19
|
+
mock.module("../memory/attachments-store.js", () => ({
|
|
17
20
|
getAttachmentsByIds: () => [],
|
|
18
21
|
}));
|
|
19
22
|
|
|
20
|
-
mock.module(
|
|
21
|
-
|
|
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
|
|
25
|
-
import { handleSendMessage } from
|
|
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:
|
|
30
|
-
principalType:
|
|
31
|
-
assistantId:
|
|
32
|
-
actorPrincipalId:
|
|
33
|
-
scopeProfile:
|
|
34
|
-
scopes: new Set([
|
|
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(
|
|
39
|
-
test(
|
|
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(
|
|
44
|
-
method:
|
|
45
|
-
headers: {
|
|
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:
|
|
48
|
-
content:
|
|
49
|
-
sourceChannel:
|
|
50
|
-
interface:
|
|
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(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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(
|
|
66
|
-
expect(capturedSourceChannel).toBe(
|
|
67
|
-
expect(capturedOptions?.guardianContext).toEqual(
|
|
68
|
-
|
|
69
|
-
|
|
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
|
});
|