@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
|
@@ -7,53 +7,55 @@
|
|
|
7
7
|
* 3. Create a guardian approval request for the access request
|
|
8
8
|
* 4. Deduplicate: don't create duplicate requests for repeated messages
|
|
9
9
|
*/
|
|
10
|
-
import { mkdtempSync, rmSync } from
|
|
11
|
-
import { tmpdir } from
|
|
12
|
-
import { join } from
|
|
10
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
13
|
|
|
14
|
-
import { afterAll, beforeEach, describe, expect, mock, test } from
|
|
14
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
15
15
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Test isolation: in-memory SQLite via temp directory
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
|
-
const testDir = mkdtempSync(join(tmpdir(),
|
|
20
|
+
const testDir = mkdtempSync(join(tmpdir(), "non-member-access-request-test-"));
|
|
21
21
|
|
|
22
|
-
mock.module(
|
|
22
|
+
mock.module("../util/platform.js", () => ({
|
|
23
23
|
getRootDir: () => testDir,
|
|
24
24
|
getDataDir: () => testDir,
|
|
25
|
-
isMacOS: () => process.platform ===
|
|
26
|
-
isLinux: () => process.platform ===
|
|
27
|
-
isWindows: () => process.platform ===
|
|
28
|
-
getSocketPath: () => join(testDir,
|
|
29
|
-
getPidPath: () => join(testDir,
|
|
30
|
-
getDbPath: () => join(testDir,
|
|
31
|
-
getLogPath: () => join(testDir,
|
|
25
|
+
isMacOS: () => process.platform === "darwin",
|
|
26
|
+
isLinux: () => process.platform === "linux",
|
|
27
|
+
isWindows: () => process.platform === "win32",
|
|
28
|
+
getSocketPath: () => join(testDir, "test.sock"),
|
|
29
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
30
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
31
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
32
32
|
ensureDataDir: () => {},
|
|
33
|
-
readHttpToken: () =>
|
|
33
|
+
readHttpToken: () => "test-bearer-token",
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
|
-
mock.module(
|
|
37
|
-
getLogger: () =>
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
mock.module("../util/logger.js", () => ({
|
|
37
|
+
getLogger: () =>
|
|
38
|
+
new Proxy({} as Record<string, unknown>, {
|
|
39
|
+
get: () => () => {},
|
|
40
|
+
}),
|
|
40
41
|
}));
|
|
41
42
|
|
|
42
43
|
// Mock security check to always pass
|
|
43
|
-
mock.module(
|
|
44
|
+
mock.module("../security/secret-ingress.js", () => ({
|
|
44
45
|
checkIngressForSecrets: () => ({ blocked: false }),
|
|
45
46
|
}));
|
|
46
47
|
|
|
47
48
|
// Mock ingress member store: findMember always returns null (non-member),
|
|
48
49
|
// updateLastSeen is a no-op.
|
|
49
|
-
mock.module(
|
|
50
|
+
mock.module("../memory/ingress-member-store.js", () => ({
|
|
50
51
|
findMember: () => null,
|
|
51
52
|
updateLastSeen: () => {},
|
|
52
53
|
upsertMember: () => {},
|
|
53
54
|
}));
|
|
54
55
|
|
|
55
|
-
mock.module(
|
|
56
|
-
|
|
56
|
+
mock.module("../config/env.js", () => ({
|
|
57
|
+
isHttpAuthDisabled: () => true,
|
|
58
|
+
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
57
59
|
}));
|
|
58
60
|
|
|
59
61
|
// Track emitNotificationSignal calls
|
|
@@ -65,13 +67,13 @@ let mockEmitResult: {
|
|
|
65
67
|
reason: string;
|
|
66
68
|
deliveryResults: Array<Record<string, unknown>>;
|
|
67
69
|
} = {
|
|
68
|
-
signalId:
|
|
70
|
+
signalId: "mock-signal-id",
|
|
69
71
|
deduplicated: false,
|
|
70
72
|
dispatched: true,
|
|
71
|
-
reason:
|
|
73
|
+
reason: "mock",
|
|
72
74
|
deliveryResults: [],
|
|
73
75
|
};
|
|
74
|
-
mock.module(
|
|
76
|
+
mock.module("../notifications/emit-signal.js", () => ({
|
|
75
77
|
emitNotificationSignal: async (params: Record<string, unknown>) => {
|
|
76
78
|
emitSignalCalls.push(params);
|
|
77
79
|
return mockEmitResult;
|
|
@@ -79,9 +81,15 @@ mock.module('../notifications/emit-signal.js', () => ({
|
|
|
79
81
|
}));
|
|
80
82
|
|
|
81
83
|
// Track deliverChannelReply calls
|
|
82
|
-
const deliverReplyCalls: Array<{
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
const deliverReplyCalls: Array<{
|
|
85
|
+
url: string;
|
|
86
|
+
payload: Record<string, unknown>;
|
|
87
|
+
}> = [];
|
|
88
|
+
mock.module("../runtime/gateway-client.js", () => ({
|
|
89
|
+
deliverChannelReply: async (
|
|
90
|
+
url: string,
|
|
91
|
+
payload: Record<string, unknown>,
|
|
92
|
+
) => {
|
|
85
93
|
deliverReplyCalls.push({ url, payload });
|
|
86
94
|
},
|
|
87
95
|
}));
|
|
@@ -89,43 +97,45 @@ mock.module('../runtime/gateway-client.js', () => ({
|
|
|
89
97
|
import {
|
|
90
98
|
listCanonicalGuardianDeliveries,
|
|
91
99
|
listCanonicalGuardianRequests,
|
|
92
|
-
} from
|
|
93
|
-
import {
|
|
94
|
-
|
|
95
|
-
} from
|
|
96
|
-
import {
|
|
97
|
-
import { notifyGuardianOfAccessRequest } from '../runtime/access-request-helper.js';
|
|
98
|
-
import { handleChannelInbound } from '../runtime/routes/channel-routes.js';
|
|
100
|
+
} from "../memory/canonical-guardian-store.js";
|
|
101
|
+
import { createBinding } from "../memory/channel-guardian-store.js";
|
|
102
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
103
|
+
import { notifyGuardianOfAccessRequest } from "../runtime/access-request-helper.js";
|
|
104
|
+
import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
|
|
99
105
|
|
|
100
106
|
initializeDb();
|
|
101
107
|
|
|
102
108
|
afterAll(() => {
|
|
103
109
|
resetDb();
|
|
104
|
-
try {
|
|
110
|
+
try {
|
|
111
|
+
rmSync(testDir, { recursive: true });
|
|
112
|
+
} catch {
|
|
113
|
+
/* best effort */
|
|
114
|
+
}
|
|
105
115
|
});
|
|
106
116
|
|
|
107
117
|
// ---------------------------------------------------------------------------
|
|
108
118
|
// Helpers
|
|
109
119
|
// ---------------------------------------------------------------------------
|
|
110
120
|
|
|
111
|
-
const TEST_BEARER_TOKEN =
|
|
121
|
+
const TEST_BEARER_TOKEN = "test-token";
|
|
112
122
|
|
|
113
123
|
function resetState(): void {
|
|
114
124
|
const db = getDb();
|
|
115
|
-
db.run(
|
|
116
|
-
db.run(
|
|
117
|
-
db.run(
|
|
118
|
-
db.run(
|
|
119
|
-
db.run(
|
|
120
|
-
db.run(
|
|
121
|
-
db.run(
|
|
125
|
+
db.run("DELETE FROM channel_guardian_approval_requests");
|
|
126
|
+
db.run("DELETE FROM channel_guardian_bindings");
|
|
127
|
+
db.run("DELETE FROM channel_inbound_events");
|
|
128
|
+
db.run("DELETE FROM conversations");
|
|
129
|
+
db.run("DELETE FROM notification_events");
|
|
130
|
+
db.run("DELETE FROM canonical_guardian_requests");
|
|
131
|
+
db.run("DELETE FROM canonical_guardian_deliveries");
|
|
122
132
|
emitSignalCalls.length = 0;
|
|
123
133
|
deliverReplyCalls.length = 0;
|
|
124
134
|
mockEmitResult = {
|
|
125
|
-
signalId:
|
|
135
|
+
signalId: "mock-signal-id",
|
|
126
136
|
deduplicated: false,
|
|
127
137
|
dispatched: true,
|
|
128
|
-
reason:
|
|
138
|
+
reason: "mock",
|
|
129
139
|
deliveryResults: [],
|
|
130
140
|
};
|
|
131
141
|
}
|
|
@@ -136,23 +146,23 @@ async function flushAsyncAccessRequestBookkeeping(): Promise<void> {
|
|
|
136
146
|
|
|
137
147
|
function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
|
|
138
148
|
const body: Record<string, unknown> = {
|
|
139
|
-
sourceChannel:
|
|
140
|
-
interface:
|
|
141
|
-
conversationExternalId:
|
|
149
|
+
sourceChannel: "telegram",
|
|
150
|
+
interface: "telegram",
|
|
151
|
+
conversationExternalId: "chat-123",
|
|
142
152
|
externalMessageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
143
|
-
content:
|
|
144
|
-
actorExternalId:
|
|
145
|
-
actorDisplayName:
|
|
146
|
-
actorUsername:
|
|
147
|
-
replyCallbackUrl:
|
|
153
|
+
content: "Hello, can I use this assistant?",
|
|
154
|
+
actorExternalId: "user-unknown-456",
|
|
155
|
+
actorDisplayName: "Alice Unknown",
|
|
156
|
+
actorUsername: "alice_unknown",
|
|
157
|
+
replyCallbackUrl: "http://localhost:7830/deliver/telegram",
|
|
148
158
|
...overrides,
|
|
149
159
|
};
|
|
150
160
|
|
|
151
|
-
return new Request(
|
|
152
|
-
method:
|
|
161
|
+
return new Request("http://localhost:8080/channels/inbound", {
|
|
162
|
+
method: "POST",
|
|
153
163
|
headers: {
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
156
166
|
},
|
|
157
167
|
body: JSON.stringify(body),
|
|
158
168
|
});
|
|
@@ -162,75 +172,80 @@ function buildInboundRequest(overrides: Record<string, unknown> = {}): Request {
|
|
|
162
172
|
// Tests
|
|
163
173
|
// ---------------------------------------------------------------------------
|
|
164
174
|
|
|
165
|
-
describe(
|
|
175
|
+
describe("non-member access request notification", () => {
|
|
166
176
|
beforeEach(() => {
|
|
167
177
|
resetState();
|
|
168
178
|
});
|
|
169
179
|
|
|
170
|
-
test(
|
|
180
|
+
test("non-member message is denied with rejection reply", async () => {
|
|
171
181
|
const req = buildInboundRequest();
|
|
172
182
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
173
|
-
const json = await resp.json() as Record<string, unknown>;
|
|
183
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
174
184
|
|
|
175
185
|
expect(json.denied).toBe(true);
|
|
176
|
-
expect(json.reason).toBe(
|
|
186
|
+
expect(json.reason).toBe("not_a_member");
|
|
177
187
|
|
|
178
188
|
// Rejection reply was delivered — always-notify behavior means the reply
|
|
179
189
|
// indicates the guardian will be notified, even without a same-channel binding.
|
|
180
190
|
expect(deliverReplyCalls.length).toBe(1);
|
|
181
|
-
expect(
|
|
191
|
+
expect(
|
|
192
|
+
(deliverReplyCalls[0].payload as Record<string, unknown>).text,
|
|
193
|
+
).toContain("let them know");
|
|
182
194
|
});
|
|
183
195
|
|
|
184
|
-
test(
|
|
196
|
+
test("guardian is notified when a non-member messages and a guardian binding exists", async () => {
|
|
185
197
|
// Set up a guardian binding for this channel
|
|
186
198
|
createBinding({
|
|
187
|
-
assistantId:
|
|
188
|
-
channel:
|
|
189
|
-
guardianExternalUserId:
|
|
190
|
-
guardianDeliveryChatId:
|
|
191
|
-
guardianPrincipalId:
|
|
199
|
+
assistantId: "self",
|
|
200
|
+
channel: "telegram",
|
|
201
|
+
guardianExternalUserId: "guardian-user-789",
|
|
202
|
+
guardianDeliveryChatId: "guardian-chat-789",
|
|
203
|
+
guardianPrincipalId: "test-principal-id",
|
|
192
204
|
});
|
|
193
205
|
|
|
194
206
|
const req = buildInboundRequest();
|
|
195
207
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
196
|
-
const json = await resp.json() as Record<string, unknown>;
|
|
208
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
197
209
|
|
|
198
210
|
// Message is still denied
|
|
199
211
|
expect(json.denied).toBe(true);
|
|
200
|
-
expect(json.reason).toBe(
|
|
212
|
+
expect(json.reason).toBe("not_a_member");
|
|
201
213
|
|
|
202
214
|
// Rejection reply was delivered
|
|
203
215
|
expect(deliverReplyCalls.length).toBe(1);
|
|
204
216
|
|
|
205
217
|
// A notification signal was emitted
|
|
206
218
|
expect(emitSignalCalls.length).toBe(1);
|
|
207
|
-
expect(emitSignalCalls[0].sourceEventName).toBe(
|
|
208
|
-
expect(emitSignalCalls[0].sourceChannel).toBe(
|
|
209
|
-
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
expect(emitSignalCalls[0].sourceEventName).toBe("ingress.access_request");
|
|
220
|
+
expect(emitSignalCalls[0].sourceChannel).toBe("telegram");
|
|
221
|
+
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
222
|
+
string,
|
|
223
|
+
unknown
|
|
224
|
+
>;
|
|
225
|
+
expect(payload.actorExternalId).toBe("user-unknown-456");
|
|
226
|
+
expect(payload.actorDisplayName).toBe("Alice Unknown");
|
|
212
227
|
|
|
213
228
|
// A canonical access request was created
|
|
214
229
|
const pending = listCanonicalGuardianRequests({
|
|
215
|
-
status:
|
|
216
|
-
requesterExternalUserId:
|
|
217
|
-
sourceChannel:
|
|
218
|
-
kind:
|
|
230
|
+
status: "pending",
|
|
231
|
+
requesterExternalUserId: "user-unknown-456",
|
|
232
|
+
sourceChannel: "telegram",
|
|
233
|
+
kind: "access_request",
|
|
219
234
|
});
|
|
220
235
|
expect(pending.length).toBe(1);
|
|
221
|
-
expect(pending[0].status).toBe(
|
|
222
|
-
expect(pending[0].requesterExternalUserId).toBe(
|
|
223
|
-
expect(pending[0].guardianExternalUserId).toBe(
|
|
224
|
-
expect(pending[0].toolName).toBe(
|
|
236
|
+
expect(pending[0].status).toBe("pending");
|
|
237
|
+
expect(pending[0].requesterExternalUserId).toBe("user-unknown-456");
|
|
238
|
+
expect(pending[0].guardianExternalUserId).toBe("guardian-user-789");
|
|
239
|
+
expect(pending[0].toolName).toBe("ingress_access_request");
|
|
225
240
|
});
|
|
226
241
|
|
|
227
|
-
test(
|
|
242
|
+
test("no duplicate approval requests for repeated messages from same non-member", async () => {
|
|
228
243
|
createBinding({
|
|
229
|
-
assistantId:
|
|
230
|
-
channel:
|
|
231
|
-
guardianExternalUserId:
|
|
232
|
-
guardianDeliveryChatId:
|
|
233
|
-
guardianPrincipalId:
|
|
244
|
+
assistantId: "self",
|
|
245
|
+
channel: "telegram",
|
|
246
|
+
guardianExternalUserId: "guardian-user-789",
|
|
247
|
+
guardianDeliveryChatId: "guardian-chat-789",
|
|
248
|
+
guardianPrincipalId: "test-principal-id",
|
|
234
249
|
});
|
|
235
250
|
|
|
236
251
|
// First message
|
|
@@ -240,7 +255,7 @@ describe('non-member access request notification', () => {
|
|
|
240
255
|
// Second message from the same user
|
|
241
256
|
const req2 = buildInboundRequest({
|
|
242
257
|
externalMessageId: `msg-second-${Date.now()}`,
|
|
243
|
-
content:
|
|
258
|
+
content: "Please let me in!",
|
|
244
259
|
});
|
|
245
260
|
await handleChannelInbound(req2, undefined, TEST_BEARER_TOKEN);
|
|
246
261
|
|
|
@@ -252,38 +267,40 @@ describe('non-member access request notification', () => {
|
|
|
252
267
|
|
|
253
268
|
// Only one canonical request should exist
|
|
254
269
|
const pending = listCanonicalGuardianRequests({
|
|
255
|
-
status:
|
|
256
|
-
requesterExternalUserId:
|
|
257
|
-
sourceChannel:
|
|
258
|
-
kind:
|
|
270
|
+
status: "pending",
|
|
271
|
+
requesterExternalUserId: "user-unknown-456",
|
|
272
|
+
sourceChannel: "telegram",
|
|
273
|
+
kind: "access_request",
|
|
259
274
|
});
|
|
260
275
|
expect(pending.length).toBe(1);
|
|
261
276
|
});
|
|
262
277
|
|
|
263
|
-
test(
|
|
278
|
+
test("access request is created with self-healed principal even without same-channel guardian binding", async () => {
|
|
264
279
|
// No guardian binding on any channel — self-heal creates a vellum binding
|
|
265
280
|
// so the access_request (now decisionable) has a guardianPrincipalId.
|
|
266
281
|
const req = buildInboundRequest();
|
|
267
282
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
268
|
-
const json = await resp.json() as Record<string, unknown>;
|
|
283
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
269
284
|
|
|
270
285
|
expect(json.denied).toBe(true);
|
|
271
|
-
expect(json.reason).toBe(
|
|
286
|
+
expect(json.reason).toBe("not_a_member");
|
|
272
287
|
|
|
273
288
|
// Rejection reply indicates guardian was notified
|
|
274
289
|
expect(deliverReplyCalls.length).toBe(1);
|
|
275
|
-
expect(
|
|
290
|
+
expect(
|
|
291
|
+
(deliverReplyCalls[0].payload as Record<string, unknown>).text,
|
|
292
|
+
).toContain("let them know");
|
|
276
293
|
|
|
277
294
|
// Notification signal was emitted
|
|
278
295
|
expect(emitSignalCalls.length).toBe(1);
|
|
279
|
-
expect(emitSignalCalls[0].sourceEventName).toBe(
|
|
296
|
+
expect(emitSignalCalls[0].sourceEventName).toBe("ingress.access_request");
|
|
280
297
|
|
|
281
298
|
// Canonical request was created with a self-healed principal
|
|
282
299
|
const pending = listCanonicalGuardianRequests({
|
|
283
|
-
status:
|
|
284
|
-
requesterExternalUserId:
|
|
285
|
-
sourceChannel:
|
|
286
|
-
kind:
|
|
300
|
+
status: "pending",
|
|
301
|
+
requesterExternalUserId: "user-unknown-456",
|
|
302
|
+
sourceChannel: "telegram",
|
|
303
|
+
kind: "access_request",
|
|
287
304
|
});
|
|
288
305
|
expect(pending.length).toBe(1);
|
|
289
306
|
// Self-heal bootstraps a vellum binding — guardianExternalUserId is now set
|
|
@@ -291,46 +308,49 @@ describe('non-member access request notification', () => {
|
|
|
291
308
|
expect(pending[0].guardianPrincipalId).toBeDefined();
|
|
292
309
|
});
|
|
293
310
|
|
|
294
|
-
test(
|
|
311
|
+
test("cross-channel fallback: SMS guardian binding resolves for Telegram access request", async () => {
|
|
295
312
|
// Only an SMS guardian binding exists — no Telegram binding
|
|
296
313
|
createBinding({
|
|
297
|
-
assistantId:
|
|
298
|
-
channel:
|
|
299
|
-
guardianExternalUserId:
|
|
300
|
-
guardianDeliveryChatId:
|
|
301
|
-
guardianPrincipalId:
|
|
314
|
+
assistantId: "self",
|
|
315
|
+
channel: "sms",
|
|
316
|
+
guardianExternalUserId: "guardian-sms-user",
|
|
317
|
+
guardianDeliveryChatId: "guardian-sms-chat",
|
|
318
|
+
guardianPrincipalId: "test-principal-id",
|
|
302
319
|
});
|
|
303
320
|
|
|
304
321
|
const req = buildInboundRequest();
|
|
305
322
|
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
306
|
-
const json = await resp.json() as Record<string, unknown>;
|
|
323
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
307
324
|
|
|
308
325
|
expect(json.denied).toBe(true);
|
|
309
|
-
expect(json.reason).toBe(
|
|
326
|
+
expect(json.reason).toBe("not_a_member");
|
|
310
327
|
|
|
311
328
|
// Notification signal emitted
|
|
312
329
|
expect(emitSignalCalls.length).toBe(1);
|
|
313
|
-
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
314
|
-
|
|
330
|
+
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
331
|
+
string,
|
|
332
|
+
unknown
|
|
333
|
+
>;
|
|
334
|
+
expect(payload.guardianBindingChannel).toBe("sms");
|
|
315
335
|
|
|
316
336
|
// Canonical request has the SMS guardian's external user ID
|
|
317
337
|
const pending = listCanonicalGuardianRequests({
|
|
318
|
-
status:
|
|
319
|
-
requesterExternalUserId:
|
|
320
|
-
sourceChannel:
|
|
321
|
-
kind:
|
|
338
|
+
status: "pending",
|
|
339
|
+
requesterExternalUserId: "user-unknown-456",
|
|
340
|
+
sourceChannel: "telegram",
|
|
341
|
+
kind: "access_request",
|
|
322
342
|
});
|
|
323
343
|
expect(pending.length).toBe(1);
|
|
324
|
-
expect(pending[0].guardianExternalUserId).toBe(
|
|
344
|
+
expect(pending[0].guardianExternalUserId).toBe("guardian-sms-user");
|
|
325
345
|
});
|
|
326
346
|
|
|
327
|
-
test(
|
|
347
|
+
test("no notification when actorExternalId is absent", async () => {
|
|
328
348
|
createBinding({
|
|
329
|
-
assistantId:
|
|
330
|
-
channel:
|
|
331
|
-
guardianExternalUserId:
|
|
332
|
-
guardianDeliveryChatId:
|
|
333
|
-
guardianPrincipalId:
|
|
349
|
+
assistantId: "self",
|
|
350
|
+
channel: "telegram",
|
|
351
|
+
guardianExternalUserId: "guardian-user-789",
|
|
352
|
+
guardianDeliveryChatId: "guardian-chat-789",
|
|
353
|
+
guardianPrincipalId: "test-principal-id",
|
|
334
354
|
});
|
|
335
355
|
|
|
336
356
|
// Message without actorExternalId — the handler returns BAD_REQUEST.
|
|
@@ -345,36 +365,39 @@ describe('non-member access request notification', () => {
|
|
|
345
365
|
});
|
|
346
366
|
});
|
|
347
367
|
|
|
348
|
-
describe(
|
|
368
|
+
describe("access-request-helper unit tests", () => {
|
|
349
369
|
beforeEach(() => {
|
|
350
370
|
resetState();
|
|
351
371
|
});
|
|
352
372
|
|
|
353
|
-
test(
|
|
373
|
+
test("notifyGuardianOfAccessRequest returns no_sender_id when actorExternalId is absent", () => {
|
|
354
374
|
const result = notifyGuardianOfAccessRequest({
|
|
355
|
-
canonicalAssistantId:
|
|
356
|
-
sourceChannel:
|
|
357
|
-
conversationExternalId:
|
|
375
|
+
canonicalAssistantId: "self",
|
|
376
|
+
sourceChannel: "telegram",
|
|
377
|
+
conversationExternalId: "chat-123",
|
|
358
378
|
actorExternalId: undefined,
|
|
359
379
|
});
|
|
360
380
|
|
|
361
381
|
expect(result.notified).toBe(false);
|
|
362
382
|
if (!result.notified) {
|
|
363
|
-
expect(result.reason).toBe(
|
|
383
|
+
expect(result.reason).toBe("no_sender_id");
|
|
364
384
|
}
|
|
365
385
|
|
|
366
386
|
// No canonical request created
|
|
367
|
-
const pending = listCanonicalGuardianRequests({
|
|
387
|
+
const pending = listCanonicalGuardianRequests({
|
|
388
|
+
status: "pending",
|
|
389
|
+
kind: "access_request",
|
|
390
|
+
});
|
|
368
391
|
expect(pending.length).toBe(0);
|
|
369
392
|
});
|
|
370
393
|
|
|
371
|
-
test(
|
|
394
|
+
test("notifyGuardianOfAccessRequest creates request with self-healed principal when no binding exists", () => {
|
|
372
395
|
const result = notifyGuardianOfAccessRequest({
|
|
373
|
-
canonicalAssistantId:
|
|
374
|
-
sourceChannel:
|
|
375
|
-
conversationExternalId:
|
|
376
|
-
actorExternalId:
|
|
377
|
-
actorDisplayName:
|
|
396
|
+
canonicalAssistantId: "self",
|
|
397
|
+
sourceChannel: "telegram",
|
|
398
|
+
conversationExternalId: "chat-123",
|
|
399
|
+
actorExternalId: "unknown-user",
|
|
400
|
+
actorDisplayName: "Bob",
|
|
378
401
|
});
|
|
379
402
|
|
|
380
403
|
expect(result.notified).toBe(true);
|
|
@@ -383,9 +406,9 @@ describe('access-request-helper unit tests', () => {
|
|
|
383
406
|
}
|
|
384
407
|
|
|
385
408
|
const pending = listCanonicalGuardianRequests({
|
|
386
|
-
status:
|
|
387
|
-
requesterExternalUserId:
|
|
388
|
-
kind:
|
|
409
|
+
status: "pending",
|
|
410
|
+
requesterExternalUserId: "unknown-user",
|
|
411
|
+
kind: "access_request",
|
|
389
412
|
});
|
|
390
413
|
expect(pending.length).toBe(1);
|
|
391
414
|
// Self-heal bootstraps a vellum binding
|
|
@@ -396,167 +419,182 @@ describe('access-request-helper unit tests', () => {
|
|
|
396
419
|
expect(emitSignalCalls.length).toBe(1);
|
|
397
420
|
});
|
|
398
421
|
|
|
399
|
-
test(
|
|
422
|
+
test("notifyGuardianOfAccessRequest uses cross-channel binding when source-channel binding is missing", () => {
|
|
400
423
|
// Only SMS binding exists
|
|
401
424
|
createBinding({
|
|
402
|
-
assistantId:
|
|
403
|
-
channel:
|
|
404
|
-
guardianExternalUserId:
|
|
405
|
-
guardianDeliveryChatId:
|
|
406
|
-
guardianPrincipalId:
|
|
425
|
+
assistantId: "self",
|
|
426
|
+
channel: "sms",
|
|
427
|
+
guardianExternalUserId: "guardian-sms",
|
|
428
|
+
guardianDeliveryChatId: "sms-chat",
|
|
429
|
+
guardianPrincipalId: "test-principal-id",
|
|
407
430
|
});
|
|
408
431
|
|
|
409
432
|
const result = notifyGuardianOfAccessRequest({
|
|
410
|
-
canonicalAssistantId:
|
|
411
|
-
sourceChannel:
|
|
412
|
-
conversationExternalId:
|
|
413
|
-
actorExternalId:
|
|
433
|
+
canonicalAssistantId: "self",
|
|
434
|
+
sourceChannel: "telegram",
|
|
435
|
+
conversationExternalId: "tg-chat",
|
|
436
|
+
actorExternalId: "unknown-tg-user",
|
|
414
437
|
});
|
|
415
438
|
|
|
416
439
|
expect(result.notified).toBe(true);
|
|
417
440
|
|
|
418
441
|
const pending = listCanonicalGuardianRequests({
|
|
419
|
-
status:
|
|
420
|
-
requesterExternalUserId:
|
|
421
|
-
kind:
|
|
442
|
+
status: "pending",
|
|
443
|
+
requesterExternalUserId: "unknown-tg-user",
|
|
444
|
+
kind: "access_request",
|
|
422
445
|
});
|
|
423
446
|
expect(pending.length).toBe(1);
|
|
424
|
-
expect(pending[0].guardianExternalUserId).toBe(
|
|
447
|
+
expect(pending[0].guardianExternalUserId).toBe("guardian-sms");
|
|
425
448
|
|
|
426
449
|
// Signal payload includes fallback channel
|
|
427
|
-
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
428
|
-
|
|
450
|
+
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
451
|
+
string,
|
|
452
|
+
unknown
|
|
453
|
+
>;
|
|
454
|
+
expect(payload.guardianBindingChannel).toBe("sms");
|
|
429
455
|
});
|
|
430
456
|
|
|
431
|
-
test(
|
|
457
|
+
test("notifyGuardianOfAccessRequest prefers source-channel binding over cross-channel fallback", () => {
|
|
432
458
|
// Both Telegram and SMS bindings exist
|
|
433
459
|
createBinding({
|
|
434
|
-
assistantId:
|
|
435
|
-
channel:
|
|
436
|
-
guardianExternalUserId:
|
|
437
|
-
guardianDeliveryChatId:
|
|
438
|
-
guardianPrincipalId:
|
|
460
|
+
assistantId: "self",
|
|
461
|
+
channel: "telegram",
|
|
462
|
+
guardianExternalUserId: "guardian-tg",
|
|
463
|
+
guardianDeliveryChatId: "tg-chat",
|
|
464
|
+
guardianPrincipalId: "test-principal-tg",
|
|
439
465
|
});
|
|
440
466
|
createBinding({
|
|
441
|
-
assistantId:
|
|
442
|
-
channel:
|
|
443
|
-
guardianExternalUserId:
|
|
444
|
-
guardianDeliveryChatId:
|
|
445
|
-
guardianPrincipalId:
|
|
467
|
+
assistantId: "self",
|
|
468
|
+
channel: "sms",
|
|
469
|
+
guardianExternalUserId: "guardian-sms",
|
|
470
|
+
guardianDeliveryChatId: "sms-chat",
|
|
471
|
+
guardianPrincipalId: "test-principal-sms",
|
|
446
472
|
});
|
|
447
473
|
|
|
448
474
|
const result = notifyGuardianOfAccessRequest({
|
|
449
|
-
canonicalAssistantId:
|
|
450
|
-
sourceChannel:
|
|
451
|
-
conversationExternalId:
|
|
452
|
-
actorExternalId:
|
|
475
|
+
canonicalAssistantId: "self",
|
|
476
|
+
sourceChannel: "telegram",
|
|
477
|
+
conversationExternalId: "chat-123",
|
|
478
|
+
actorExternalId: "unknown-user",
|
|
453
479
|
});
|
|
454
480
|
|
|
455
481
|
expect(result.notified).toBe(true);
|
|
456
482
|
|
|
457
483
|
const pending = listCanonicalGuardianRequests({
|
|
458
|
-
status:
|
|
459
|
-
requesterExternalUserId:
|
|
460
|
-
kind:
|
|
484
|
+
status: "pending",
|
|
485
|
+
requesterExternalUserId: "unknown-user",
|
|
486
|
+
kind: "access_request",
|
|
461
487
|
});
|
|
462
488
|
expect(pending.length).toBe(1);
|
|
463
489
|
// Should use the Telegram binding, not SMS fallback
|
|
464
|
-
expect(pending[0].guardianExternalUserId).toBe(
|
|
490
|
+
expect(pending[0].guardianExternalUserId).toBe("guardian-tg");
|
|
465
491
|
|
|
466
|
-
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
467
|
-
|
|
492
|
+
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
493
|
+
string,
|
|
494
|
+
unknown
|
|
495
|
+
>;
|
|
496
|
+
expect(payload.guardianBindingChannel).toBe("telegram");
|
|
468
497
|
});
|
|
469
498
|
|
|
470
|
-
test(
|
|
499
|
+
test("notifyGuardianOfAccessRequest for voice channel includes actorDisplayName in contextPayload", () => {
|
|
471
500
|
const result = notifyGuardianOfAccessRequest({
|
|
472
|
-
canonicalAssistantId:
|
|
473
|
-
sourceChannel:
|
|
474
|
-
conversationExternalId:
|
|
475
|
-
actorExternalId:
|
|
476
|
-
actorDisplayName:
|
|
501
|
+
canonicalAssistantId: "self",
|
|
502
|
+
sourceChannel: "voice",
|
|
503
|
+
conversationExternalId: "+15559998888",
|
|
504
|
+
actorExternalId: "+15559998888",
|
|
505
|
+
actorDisplayName: "Alice Caller",
|
|
477
506
|
});
|
|
478
507
|
|
|
479
508
|
expect(result.notified).toBe(true);
|
|
480
509
|
expect(emitSignalCalls.length).toBe(1);
|
|
481
510
|
|
|
482
|
-
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
expect(payload.
|
|
511
|
+
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
512
|
+
string,
|
|
513
|
+
unknown
|
|
514
|
+
>;
|
|
515
|
+
expect(payload.sourceChannel).toBe("voice");
|
|
516
|
+
expect(payload.actorDisplayName).toBe("Alice Caller");
|
|
517
|
+
expect(payload.actorExternalId).toBe("+15559998888");
|
|
518
|
+
expect(payload.senderIdentifier).toBe("Alice Caller");
|
|
487
519
|
|
|
488
520
|
// Canonical request should exist
|
|
489
521
|
const pending = listCanonicalGuardianRequests({
|
|
490
|
-
status:
|
|
491
|
-
requesterExternalUserId:
|
|
492
|
-
sourceChannel:
|
|
493
|
-
kind:
|
|
522
|
+
status: "pending",
|
|
523
|
+
requesterExternalUserId: "+15559998888",
|
|
524
|
+
sourceChannel: "voice",
|
|
525
|
+
kind: "access_request",
|
|
494
526
|
});
|
|
495
527
|
expect(pending.length).toBe(1);
|
|
496
528
|
});
|
|
497
529
|
|
|
498
|
-
test(
|
|
530
|
+
test("notifyGuardianOfAccessRequest includes requestCode in contextPayload", () => {
|
|
499
531
|
const result = notifyGuardianOfAccessRequest({
|
|
500
|
-
canonicalAssistantId:
|
|
501
|
-
sourceChannel:
|
|
502
|
-
conversationExternalId:
|
|
503
|
-
actorExternalId:
|
|
504
|
-
actorDisplayName:
|
|
532
|
+
canonicalAssistantId: "self",
|
|
533
|
+
sourceChannel: "telegram",
|
|
534
|
+
conversationExternalId: "chat-123",
|
|
535
|
+
actorExternalId: "unknown-user",
|
|
536
|
+
actorDisplayName: "Test User",
|
|
505
537
|
});
|
|
506
538
|
|
|
507
539
|
expect(result.notified).toBe(true);
|
|
508
540
|
expect(emitSignalCalls.length).toBe(1);
|
|
509
541
|
|
|
510
|
-
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
542
|
+
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
543
|
+
string,
|
|
544
|
+
unknown
|
|
545
|
+
>;
|
|
511
546
|
expect(payload.requestCode).toBeDefined();
|
|
512
|
-
expect(typeof payload.requestCode).toBe(
|
|
547
|
+
expect(typeof payload.requestCode).toBe("string");
|
|
513
548
|
expect((payload.requestCode as string).length).toBe(6);
|
|
514
549
|
});
|
|
515
550
|
|
|
516
|
-
test(
|
|
551
|
+
test("notifyGuardianOfAccessRequest includes previousMemberStatus in contextPayload", () => {
|
|
517
552
|
const result = notifyGuardianOfAccessRequest({
|
|
518
|
-
canonicalAssistantId:
|
|
519
|
-
sourceChannel:
|
|
520
|
-
conversationExternalId:
|
|
521
|
-
actorExternalId:
|
|
522
|
-
actorDisplayName:
|
|
523
|
-
previousMemberStatus:
|
|
553
|
+
canonicalAssistantId: "self",
|
|
554
|
+
sourceChannel: "telegram",
|
|
555
|
+
conversationExternalId: "chat-123",
|
|
556
|
+
actorExternalId: "revoked-user",
|
|
557
|
+
actorDisplayName: "Revoked User",
|
|
558
|
+
previousMemberStatus: "revoked",
|
|
524
559
|
});
|
|
525
560
|
|
|
526
561
|
expect(result.notified).toBe(true);
|
|
527
562
|
expect(emitSignalCalls.length).toBe(1);
|
|
528
563
|
|
|
529
|
-
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
530
|
-
|
|
564
|
+
const payload = emitSignalCalls[0].contextPayload as Record<
|
|
565
|
+
string,
|
|
566
|
+
unknown
|
|
567
|
+
>;
|
|
568
|
+
expect(payload.previousMemberStatus).toBe("revoked");
|
|
531
569
|
});
|
|
532
570
|
|
|
533
|
-
test(
|
|
571
|
+
test("notifyGuardianOfAccessRequest persists canonical delivery rows from notification results", async () => {
|
|
534
572
|
mockEmitResult = {
|
|
535
|
-
signalId:
|
|
573
|
+
signalId: "sig-deliveries",
|
|
536
574
|
deduplicated: false,
|
|
537
575
|
dispatched: true,
|
|
538
|
-
reason:
|
|
576
|
+
reason: "ok",
|
|
539
577
|
deliveryResults: [
|
|
540
578
|
{
|
|
541
|
-
channel:
|
|
542
|
-
destination:
|
|
543
|
-
status:
|
|
544
|
-
conversationId:
|
|
579
|
+
channel: "vellum",
|
|
580
|
+
destination: "vellum",
|
|
581
|
+
status: "sent",
|
|
582
|
+
conversationId: "conv-guardian-access-request",
|
|
545
583
|
},
|
|
546
584
|
{
|
|
547
|
-
channel:
|
|
548
|
-
destination:
|
|
549
|
-
status:
|
|
585
|
+
channel: "telegram",
|
|
586
|
+
destination: "guardian-chat-123",
|
|
587
|
+
status: "sent",
|
|
550
588
|
},
|
|
551
589
|
],
|
|
552
590
|
};
|
|
553
591
|
|
|
554
592
|
const result = notifyGuardianOfAccessRequest({
|
|
555
|
-
canonicalAssistantId:
|
|
556
|
-
sourceChannel:
|
|
557
|
-
conversationExternalId:
|
|
558
|
-
actorExternalId:
|
|
559
|
-
actorDisplayName:
|
|
593
|
+
canonicalAssistantId: "self",
|
|
594
|
+
sourceChannel: "voice",
|
|
595
|
+
conversationExternalId: "+15556667777",
|
|
596
|
+
actorExternalId: "+15556667777",
|
|
597
|
+
actorDisplayName: "Noah",
|
|
560
598
|
});
|
|
561
599
|
|
|
562
600
|
expect(result.notified).toBe(true);
|
|
@@ -565,38 +603,42 @@ describe('access-request-helper unit tests', () => {
|
|
|
565
603
|
await flushAsyncAccessRequestBookkeeping();
|
|
566
604
|
|
|
567
605
|
const deliveries = listCanonicalGuardianDeliveries(result.requestId);
|
|
568
|
-
const vellum = deliveries.find((d) => d.destinationChannel ===
|
|
569
|
-
const telegram = deliveries.find(
|
|
606
|
+
const vellum = deliveries.find((d) => d.destinationChannel === "vellum");
|
|
607
|
+
const telegram = deliveries.find(
|
|
608
|
+
(d) => d.destinationChannel === "telegram",
|
|
609
|
+
);
|
|
570
610
|
|
|
571
611
|
expect(vellum).toBeDefined();
|
|
572
|
-
expect(vellum!.destinationConversationId).toBe(
|
|
573
|
-
|
|
612
|
+
expect(vellum!.destinationConversationId).toBe(
|
|
613
|
+
"conv-guardian-access-request",
|
|
614
|
+
);
|
|
615
|
+
expect(vellum!.status).toBe("sent");
|
|
574
616
|
expect(telegram).toBeDefined();
|
|
575
|
-
expect(telegram!.destinationChatId).toBe(
|
|
576
|
-
expect(telegram!.status).toBe(
|
|
617
|
+
expect(telegram!.destinationChatId).toBe("guardian-chat-123");
|
|
618
|
+
expect(telegram!.status).toBe("sent");
|
|
577
619
|
});
|
|
578
620
|
|
|
579
|
-
test(
|
|
621
|
+
test("notifyGuardianOfAccessRequest records failed vellum fallback when pipeline has no vellum delivery", async () => {
|
|
580
622
|
mockEmitResult = {
|
|
581
|
-
signalId:
|
|
623
|
+
signalId: "sig-no-vellum",
|
|
582
624
|
deduplicated: false,
|
|
583
625
|
dispatched: true,
|
|
584
|
-
reason:
|
|
626
|
+
reason: "telegram-only",
|
|
585
627
|
deliveryResults: [
|
|
586
628
|
{
|
|
587
|
-
channel:
|
|
588
|
-
destination:
|
|
589
|
-
status:
|
|
629
|
+
channel: "telegram",
|
|
630
|
+
destination: "guardian-chat-456",
|
|
631
|
+
status: "sent",
|
|
590
632
|
},
|
|
591
633
|
],
|
|
592
634
|
};
|
|
593
635
|
|
|
594
636
|
const result = notifyGuardianOfAccessRequest({
|
|
595
|
-
canonicalAssistantId:
|
|
596
|
-
sourceChannel:
|
|
597
|
-
conversationExternalId:
|
|
598
|
-
actorExternalId:
|
|
599
|
-
actorDisplayName:
|
|
637
|
+
canonicalAssistantId: "self",
|
|
638
|
+
sourceChannel: "telegram",
|
|
639
|
+
conversationExternalId: "chat-123",
|
|
640
|
+
actorExternalId: "unknown-user",
|
|
641
|
+
actorDisplayName: "Alice",
|
|
600
642
|
});
|
|
601
643
|
|
|
602
644
|
expect(result.notified).toBe(true);
|
|
@@ -605,13 +647,15 @@ describe('access-request-helper unit tests', () => {
|
|
|
605
647
|
await flushAsyncAccessRequestBookkeeping();
|
|
606
648
|
|
|
607
649
|
const deliveries = listCanonicalGuardianDeliveries(result.requestId);
|
|
608
|
-
const vellum = deliveries.find((d) => d.destinationChannel ===
|
|
609
|
-
const telegram = deliveries.find(
|
|
650
|
+
const vellum = deliveries.find((d) => d.destinationChannel === "vellum");
|
|
651
|
+
const telegram = deliveries.find(
|
|
652
|
+
(d) => d.destinationChannel === "telegram",
|
|
653
|
+
);
|
|
610
654
|
|
|
611
655
|
expect(vellum).toBeDefined();
|
|
612
|
-
expect(vellum!.status).toBe(
|
|
656
|
+
expect(vellum!.status).toBe("failed");
|
|
613
657
|
expect(telegram).toBeDefined();
|
|
614
|
-
expect(telegram!.destinationChatId).toBe(
|
|
615
|
-
expect(telegram!.status).toBe(
|
|
658
|
+
expect(telegram!.destinationChatId).toBe("guardian-chat-456");
|
|
659
|
+
expect(telegram!.status).toBe("sent");
|
|
616
660
|
});
|
|
617
661
|
});
|