@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
|
@@ -9,33 +9,34 @@
|
|
|
9
9
|
* 5. Delivery failures allow retry on next poll
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { mkdtempSync, rmSync } from
|
|
13
|
-
import { tmpdir } from
|
|
14
|
-
import { join } from
|
|
12
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
15
|
|
|
16
|
-
import { afterAll, beforeEach, describe, expect, mock, test } from
|
|
16
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
17
17
|
|
|
18
|
-
const testDir = mkdtempSync(join(tmpdir(),
|
|
18
|
+
const testDir = mkdtempSync(join(tmpdir(), "tc-approval-notifier-test-"));
|
|
19
19
|
|
|
20
20
|
// ── Platform mock ──
|
|
21
|
-
mock.module(
|
|
21
|
+
mock.module("../util/platform.js", () => ({
|
|
22
22
|
getDataDir: () => testDir,
|
|
23
|
-
isMacOS: () => process.platform ===
|
|
24
|
-
isLinux: () => process.platform ===
|
|
25
|
-
isWindows: () => process.platform ===
|
|
26
|
-
getSocketPath: () => join(testDir,
|
|
27
|
-
getPidPath: () => join(testDir,
|
|
28
|
-
getDbPath: () => join(testDir,
|
|
29
|
-
getLogPath: () => join(testDir,
|
|
30
|
-
readHttpToken: () =>
|
|
23
|
+
isMacOS: () => process.platform === "darwin",
|
|
24
|
+
isLinux: () => process.platform === "linux",
|
|
25
|
+
isWindows: () => process.platform === "win32",
|
|
26
|
+
getSocketPath: () => join(testDir, "test.sock"),
|
|
27
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
28
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
29
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
30
|
+
readHttpToken: () => "test-token",
|
|
31
31
|
ensureDataDir: () => {},
|
|
32
32
|
migrateToDataLayout: () => {},
|
|
33
33
|
migrateToWorkspaceLayout: () => {},
|
|
34
|
-
normalizeAssistantId: (id: string) =>
|
|
34
|
+
normalizeAssistantId: (id: string) =>
|
|
35
|
+
id === "self" || id === "" ? "self" : id,
|
|
35
36
|
}));
|
|
36
37
|
|
|
37
38
|
// ── Logger mock ──
|
|
38
|
-
mock.module(
|
|
39
|
+
mock.module("../util/logger.js", () => ({
|
|
39
40
|
getLogger: () =>
|
|
40
41
|
new Proxy({} as Record<string, unknown>, {
|
|
41
42
|
get: () => () => {},
|
|
@@ -45,12 +46,12 @@ mock.module('../util/logger.js', () => ({
|
|
|
45
46
|
}));
|
|
46
47
|
|
|
47
48
|
// ── Notification signal mock ──
|
|
48
|
-
mock.module(
|
|
49
|
+
mock.module("../notifications/emit-signal.js", () => ({
|
|
49
50
|
emitNotificationSignal: async () => ({
|
|
50
|
-
signalId:
|
|
51
|
+
signalId: "test-signal",
|
|
51
52
|
deduplicated: false,
|
|
52
53
|
dispatched: true,
|
|
53
|
-
reason:
|
|
54
|
+
reason: "ok",
|
|
54
55
|
deliveryResults: [],
|
|
55
56
|
}),
|
|
56
57
|
registerBroadcastFn: () => {},
|
|
@@ -65,14 +66,14 @@ const deliveredReplies: Array<{
|
|
|
65
66
|
}> = [];
|
|
66
67
|
let deliverShouldFail = false;
|
|
67
68
|
|
|
68
|
-
mock.module(
|
|
69
|
+
mock.module("../runtime/gateway-client.js", () => ({
|
|
69
70
|
deliverChannelReply: async (
|
|
70
71
|
url: string,
|
|
71
72
|
payload: Record<string, unknown>,
|
|
72
73
|
bearerToken?: string,
|
|
73
74
|
) => {
|
|
74
75
|
if (deliverShouldFail) {
|
|
75
|
-
throw new Error(
|
|
76
|
+
throw new Error("Delivery failed");
|
|
76
77
|
}
|
|
77
78
|
deliveredReplies.push({ url, payload, bearerToken });
|
|
78
79
|
return { ok: true };
|
|
@@ -82,7 +83,7 @@ mock.module('../runtime/gateway-client.js', () => ({
|
|
|
82
83
|
// ── Guardian binding mock ──
|
|
83
84
|
let mockGuardianBinding: Record<string, unknown> | null = null;
|
|
84
85
|
|
|
85
|
-
mock.module(
|
|
86
|
+
mock.module("../runtime/channel-guardian-service.js", () => ({
|
|
86
87
|
getGuardianBinding: () => mockGuardianBinding,
|
|
87
88
|
// Re-export stubs for other functions to prevent import errors
|
|
88
89
|
bindSessionIdentity: () => {},
|
|
@@ -94,7 +95,10 @@ mock.module('../runtime/channel-guardian-service.js', () => ({
|
|
|
94
95
|
resolveBootstrapToken: () => null,
|
|
95
96
|
updateSessionDelivery: () => {},
|
|
96
97
|
updateSessionStatus: () => {},
|
|
97
|
-
validateAndConsumeChallenge: () => ({
|
|
98
|
+
validateAndConsumeChallenge: () => ({
|
|
99
|
+
success: false,
|
|
100
|
+
reason: "no_challenge",
|
|
101
|
+
}),
|
|
98
102
|
}));
|
|
99
103
|
|
|
100
104
|
// ── Pending interactions mock ──
|
|
@@ -105,20 +109,21 @@ let mockPendingApprovals: Array<{
|
|
|
105
109
|
riskLevel: string;
|
|
106
110
|
}> = [];
|
|
107
111
|
|
|
108
|
-
mock.module(
|
|
112
|
+
mock.module("../runtime/channel-approvals.js", () => ({
|
|
109
113
|
getApprovalInfoByConversation: () => mockPendingApprovals,
|
|
110
114
|
getChannelApprovalPrompt: () => null,
|
|
111
115
|
buildApprovalUIMetadata: () => ({}),
|
|
112
116
|
}));
|
|
113
117
|
|
|
114
118
|
// ── Config env mock ──
|
|
115
|
-
mock.module(
|
|
116
|
-
|
|
119
|
+
mock.module("../config/env.js", () => ({
|
|
120
|
+
isHttpAuthDisabled: () => true,
|
|
121
|
+
getGatewayInternalBaseUrl: () => "http://localhost:3000",
|
|
117
122
|
}));
|
|
118
123
|
|
|
119
124
|
// Import module under test AFTER mocks are set up
|
|
120
|
-
import type { ChannelId } from
|
|
121
|
-
import type { GuardianContext } from
|
|
125
|
+
import type { ChannelId } from "../channels/types.js";
|
|
126
|
+
import type { GuardianContext } from "../runtime/guardian-context-resolver.js";
|
|
122
127
|
|
|
123
128
|
// We need to test the private functions by importing the module.
|
|
124
129
|
// Since startTrustedContactApprovalNotifier is not exported, we test it
|
|
@@ -146,7 +151,7 @@ async function simulateNotifierPoll(params: {
|
|
|
146
151
|
conversationId: string;
|
|
147
152
|
sourceChannel: ChannelId;
|
|
148
153
|
externalChatId: string;
|
|
149
|
-
guardianTrustClass: GuardianContext[
|
|
154
|
+
guardianTrustClass: GuardianContext["trustClass"];
|
|
150
155
|
guardianExternalUserId?: string;
|
|
151
156
|
replyCallbackUrl: string;
|
|
152
157
|
bearerToken?: string;
|
|
@@ -161,19 +166,21 @@ async function simulateNotifierPoll(params: {
|
|
|
161
166
|
} = params;
|
|
162
167
|
|
|
163
168
|
// Gate check: only trusted contacts with guardian route
|
|
164
|
-
if (guardianTrustClass !==
|
|
169
|
+
if (guardianTrustClass !== "trusted_contact" || !guardianExternalUserId) {
|
|
165
170
|
return false;
|
|
166
171
|
}
|
|
167
172
|
|
|
168
|
-
const { getApprovalInfoByConversation } =
|
|
169
|
-
|
|
170
|
-
const {
|
|
173
|
+
const { getApprovalInfoByConversation } =
|
|
174
|
+
await import("../runtime/channel-approvals.js");
|
|
175
|
+
const { deliverChannelReply } = await import("../runtime/gateway-client.js");
|
|
176
|
+
const { getGuardianBinding } =
|
|
177
|
+
await import("../runtime/channel-guardian-service.js");
|
|
171
178
|
|
|
172
179
|
const pending = getApprovalInfoByConversation(params.conversationId);
|
|
173
180
|
const info = pending[0];
|
|
174
181
|
|
|
175
182
|
// Clean up resolved requests — only for THIS conversation's entries.
|
|
176
|
-
const currentPendingIds = new Set(pending.map(p => p.requestId));
|
|
183
|
+
const currentPendingIds = new Set(pending.map((p) => p.requestId));
|
|
177
184
|
for (const [rid, cid] of notifiedRequestIds) {
|
|
178
185
|
if (cid === conversationId && !currentPendingIds.has(rid)) {
|
|
179
186
|
notifiedRequestIds.delete(rid);
|
|
@@ -188,13 +195,25 @@ async function simulateNotifierPoll(params: {
|
|
|
188
195
|
|
|
189
196
|
// Resolve guardian name
|
|
190
197
|
let guardianName: string | undefined;
|
|
191
|
-
const binding = getGuardianBinding(
|
|
198
|
+
const binding = getGuardianBinding(
|
|
199
|
+
params.assistantId ?? "self",
|
|
200
|
+
params.sourceChannel,
|
|
201
|
+
);
|
|
192
202
|
if (binding?.metadataJson) {
|
|
193
203
|
try {
|
|
194
|
-
const parsed = JSON.parse(binding.metadataJson as string) as Record<
|
|
195
|
-
|
|
204
|
+
const parsed = JSON.parse(binding.metadataJson as string) as Record<
|
|
205
|
+
string,
|
|
206
|
+
unknown
|
|
207
|
+
>;
|
|
208
|
+
if (
|
|
209
|
+
typeof parsed.displayName === "string" &&
|
|
210
|
+
parsed.displayName.trim().length > 0
|
|
211
|
+
) {
|
|
196
212
|
guardianName = parsed.displayName.trim();
|
|
197
|
-
} else if (
|
|
213
|
+
} else if (
|
|
214
|
+
typeof parsed.username === "string" &&
|
|
215
|
+
parsed.username.trim().length > 0
|
|
216
|
+
) {
|
|
198
217
|
guardianName = `@${parsed.username.trim()}`;
|
|
199
218
|
}
|
|
200
219
|
} catch {
|
|
@@ -204,14 +223,18 @@ async function simulateNotifierPoll(params: {
|
|
|
204
223
|
|
|
205
224
|
const waitingText = guardianName
|
|
206
225
|
? `Waiting for ${guardianName}'s approval...`
|
|
207
|
-
:
|
|
226
|
+
: "Waiting for your guardian's approval...";
|
|
208
227
|
|
|
209
228
|
try {
|
|
210
|
-
await deliverChannelReply(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
229
|
+
await deliverChannelReply(
|
|
230
|
+
params.replyCallbackUrl,
|
|
231
|
+
{
|
|
232
|
+
chatId: params.externalChatId,
|
|
233
|
+
text: waitingText,
|
|
234
|
+
assistantId: params.assistantId ?? "self",
|
|
235
|
+
},
|
|
236
|
+
params.bearerToken,
|
|
237
|
+
);
|
|
215
238
|
return true;
|
|
216
239
|
} catch {
|
|
217
240
|
notifiedRequestIds.delete(info.requestId);
|
|
@@ -223,7 +246,7 @@ async function simulateNotifierPoll(params: {
|
|
|
223
246
|
// TESTS
|
|
224
247
|
// ===========================================================================
|
|
225
248
|
|
|
226
|
-
describe(
|
|
249
|
+
describe("trusted-contact pending-approval notifier", () => {
|
|
227
250
|
beforeEach(() => {
|
|
228
251
|
deliveredReplies.length = 0;
|
|
229
252
|
deliverShouldFail = false;
|
|
@@ -239,142 +262,160 @@ describe('trusted-contact pending-approval notifier', () => {
|
|
|
239
262
|
}
|
|
240
263
|
});
|
|
241
264
|
|
|
242
|
-
test(
|
|
243
|
-
mockPendingApprovals = [
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
265
|
+
test("sends waiting message to trusted contact when pending approval exists", async () => {
|
|
266
|
+
mockPendingApprovals = [
|
|
267
|
+
{
|
|
268
|
+
requestId: "req-1",
|
|
269
|
+
toolName: "bash",
|
|
270
|
+
input: { command: "ls" },
|
|
271
|
+
riskLevel: "medium",
|
|
272
|
+
},
|
|
273
|
+
];
|
|
249
274
|
|
|
250
275
|
mockGuardianBinding = {
|
|
251
|
-
id:
|
|
252
|
-
metadataJson: JSON.stringify({ displayName:
|
|
276
|
+
id: "binding-1",
|
|
277
|
+
metadataJson: JSON.stringify({ displayName: "Mom" }),
|
|
253
278
|
};
|
|
254
279
|
|
|
255
280
|
const notified = new Map<string, string>();
|
|
256
281
|
const sent = await simulateNotifierPoll({
|
|
257
|
-
conversationId:
|
|
258
|
-
sourceChannel:
|
|
259
|
-
externalChatId:
|
|
260
|
-
guardianTrustClass:
|
|
261
|
-
guardianExternalUserId:
|
|
262
|
-
replyCallbackUrl:
|
|
263
|
-
bearerToken:
|
|
264
|
-
assistantId:
|
|
282
|
+
conversationId: "conv-1",
|
|
283
|
+
sourceChannel: "telegram",
|
|
284
|
+
externalChatId: "chat-123",
|
|
285
|
+
guardianTrustClass: "trusted_contact",
|
|
286
|
+
guardianExternalUserId: "guardian-1",
|
|
287
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
288
|
+
bearerToken: "test-token",
|
|
289
|
+
assistantId: "self",
|
|
265
290
|
notifiedRequestIds: notified,
|
|
266
291
|
});
|
|
267
292
|
|
|
268
293
|
expect(sent).toBe(true);
|
|
269
294
|
expect(deliveredReplies).toHaveLength(1);
|
|
270
|
-
expect(deliveredReplies[0].payload.text).toBe(
|
|
271
|
-
|
|
272
|
-
|
|
295
|
+
expect(deliveredReplies[0].payload.text).toBe(
|
|
296
|
+
"Waiting for Mom's approval...",
|
|
297
|
+
);
|
|
298
|
+
expect(deliveredReplies[0].payload.chatId).toBe("chat-123");
|
|
299
|
+
expect(notified.has("req-1")).toBe(true);
|
|
273
300
|
});
|
|
274
301
|
|
|
275
|
-
test(
|
|
276
|
-
mockPendingApprovals = [
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
302
|
+
test("uses username with @ prefix when display name is not available", async () => {
|
|
303
|
+
mockPendingApprovals = [
|
|
304
|
+
{
|
|
305
|
+
requestId: "req-2",
|
|
306
|
+
toolName: "bash",
|
|
307
|
+
input: {},
|
|
308
|
+
riskLevel: "medium",
|
|
309
|
+
},
|
|
310
|
+
];
|
|
282
311
|
|
|
283
312
|
mockGuardianBinding = {
|
|
284
|
-
id:
|
|
285
|
-
metadataJson: JSON.stringify({ username:
|
|
313
|
+
id: "binding-1",
|
|
314
|
+
metadataJson: JSON.stringify({ username: "guardian_user" }),
|
|
286
315
|
};
|
|
287
316
|
|
|
288
317
|
const notified = new Map<string, string>();
|
|
289
318
|
await simulateNotifierPoll({
|
|
290
|
-
conversationId:
|
|
291
|
-
sourceChannel:
|
|
292
|
-
externalChatId:
|
|
293
|
-
guardianTrustClass:
|
|
294
|
-
guardianExternalUserId:
|
|
295
|
-
replyCallbackUrl:
|
|
319
|
+
conversationId: "conv-1",
|
|
320
|
+
sourceChannel: "telegram",
|
|
321
|
+
externalChatId: "chat-123",
|
|
322
|
+
guardianTrustClass: "trusted_contact",
|
|
323
|
+
guardianExternalUserId: "guardian-1",
|
|
324
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
296
325
|
notifiedRequestIds: notified,
|
|
297
326
|
});
|
|
298
327
|
|
|
299
328
|
expect(deliveredReplies).toHaveLength(1);
|
|
300
|
-
expect(deliveredReplies[0].payload.text).toBe(
|
|
329
|
+
expect(deliveredReplies[0].payload.text).toBe(
|
|
330
|
+
"Waiting for @guardian_user's approval...",
|
|
331
|
+
);
|
|
301
332
|
});
|
|
302
333
|
|
|
303
|
-
test(
|
|
304
|
-
mockPendingApprovals = [
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
334
|
+
test("uses generic phrasing when no guardian name is available", async () => {
|
|
335
|
+
mockPendingApprovals = [
|
|
336
|
+
{
|
|
337
|
+
requestId: "req-3",
|
|
338
|
+
toolName: "bash",
|
|
339
|
+
input: {},
|
|
340
|
+
riskLevel: "medium",
|
|
341
|
+
},
|
|
342
|
+
];
|
|
310
343
|
|
|
311
344
|
// No binding metadata
|
|
312
345
|
mockGuardianBinding = {
|
|
313
|
-
id:
|
|
346
|
+
id: "binding-1",
|
|
314
347
|
metadataJson: null,
|
|
315
348
|
};
|
|
316
349
|
|
|
317
350
|
const notified = new Map<string, string>();
|
|
318
351
|
await simulateNotifierPoll({
|
|
319
|
-
conversationId:
|
|
320
|
-
sourceChannel:
|
|
321
|
-
externalChatId:
|
|
322
|
-
guardianTrustClass:
|
|
323
|
-
guardianExternalUserId:
|
|
324
|
-
replyCallbackUrl:
|
|
352
|
+
conversationId: "conv-1",
|
|
353
|
+
sourceChannel: "telegram",
|
|
354
|
+
externalChatId: "chat-123",
|
|
355
|
+
guardianTrustClass: "trusted_contact",
|
|
356
|
+
guardianExternalUserId: "guardian-1",
|
|
357
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
325
358
|
notifiedRequestIds: notified,
|
|
326
359
|
});
|
|
327
360
|
|
|
328
361
|
expect(deliveredReplies).toHaveLength(1);
|
|
329
|
-
expect(deliveredReplies[0].payload.text).toBe(
|
|
362
|
+
expect(deliveredReplies[0].payload.text).toBe(
|
|
363
|
+
"Waiting for your guardian's approval...",
|
|
364
|
+
);
|
|
330
365
|
});
|
|
331
366
|
|
|
332
|
-
test(
|
|
333
|
-
mockPendingApprovals = [
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
367
|
+
test("uses generic phrasing when no guardian binding exists", async () => {
|
|
368
|
+
mockPendingApprovals = [
|
|
369
|
+
{
|
|
370
|
+
requestId: "req-4",
|
|
371
|
+
toolName: "bash",
|
|
372
|
+
input: {},
|
|
373
|
+
riskLevel: "medium",
|
|
374
|
+
},
|
|
375
|
+
];
|
|
339
376
|
|
|
340
377
|
mockGuardianBinding = null;
|
|
341
378
|
|
|
342
379
|
const notified = new Map<string, string>();
|
|
343
380
|
await simulateNotifierPoll({
|
|
344
|
-
conversationId:
|
|
345
|
-
sourceChannel:
|
|
346
|
-
externalChatId:
|
|
347
|
-
guardianTrustClass:
|
|
348
|
-
guardianExternalUserId:
|
|
349
|
-
replyCallbackUrl:
|
|
381
|
+
conversationId: "conv-1",
|
|
382
|
+
sourceChannel: "telegram",
|
|
383
|
+
externalChatId: "chat-123",
|
|
384
|
+
guardianTrustClass: "trusted_contact",
|
|
385
|
+
guardianExternalUserId: "guardian-1",
|
|
386
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
350
387
|
notifiedRequestIds: notified,
|
|
351
388
|
});
|
|
352
389
|
|
|
353
390
|
expect(deliveredReplies).toHaveLength(1);
|
|
354
|
-
expect(deliveredReplies[0].payload.text).toBe(
|
|
391
|
+
expect(deliveredReplies[0].payload.text).toBe(
|
|
392
|
+
"Waiting for your guardian's approval...",
|
|
393
|
+
);
|
|
355
394
|
});
|
|
356
395
|
|
|
357
|
-
test(
|
|
358
|
-
mockPendingApprovals = [
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
396
|
+
test("deduplicates by requestId — does not send twice for same request", async () => {
|
|
397
|
+
mockPendingApprovals = [
|
|
398
|
+
{
|
|
399
|
+
requestId: "req-5",
|
|
400
|
+
toolName: "bash",
|
|
401
|
+
input: {},
|
|
402
|
+
riskLevel: "medium",
|
|
403
|
+
},
|
|
404
|
+
];
|
|
364
405
|
|
|
365
406
|
mockGuardianBinding = {
|
|
366
|
-
id:
|
|
367
|
-
metadataJson: JSON.stringify({ displayName:
|
|
407
|
+
id: "binding-1",
|
|
408
|
+
metadataJson: JSON.stringify({ displayName: "Guardian" }),
|
|
368
409
|
};
|
|
369
410
|
|
|
370
411
|
const notified = new Map<string, string>();
|
|
371
412
|
const baseParams = {
|
|
372
|
-
conversationId:
|
|
373
|
-
sourceChannel:
|
|
374
|
-
externalChatId:
|
|
375
|
-
guardianTrustClass:
|
|
376
|
-
guardianExternalUserId:
|
|
377
|
-
replyCallbackUrl:
|
|
413
|
+
conversationId: "conv-1",
|
|
414
|
+
sourceChannel: "telegram" as ChannelId,
|
|
415
|
+
externalChatId: "chat-123",
|
|
416
|
+
guardianTrustClass: "trusted_contact" as const,
|
|
417
|
+
guardianExternalUserId: "guardian-1",
|
|
418
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
378
419
|
notifiedRequestIds: notified,
|
|
379
420
|
};
|
|
380
421
|
|
|
@@ -389,67 +430,73 @@ describe('trusted-contact pending-approval notifier', () => {
|
|
|
389
430
|
expect(deliveredReplies).toHaveLength(1); // Still just 1
|
|
390
431
|
});
|
|
391
432
|
|
|
392
|
-
test(
|
|
433
|
+
test("sends separate messages for different requestIds", async () => {
|
|
393
434
|
mockGuardianBinding = {
|
|
394
|
-
id:
|
|
395
|
-
metadataJson: JSON.stringify({ displayName:
|
|
435
|
+
id: "binding-1",
|
|
436
|
+
metadataJson: JSON.stringify({ displayName: "Guardian" }),
|
|
396
437
|
};
|
|
397
438
|
|
|
398
439
|
const notified = new Map<string, string>();
|
|
399
440
|
const baseParams = {
|
|
400
|
-
conversationId:
|
|
401
|
-
sourceChannel:
|
|
402
|
-
externalChatId:
|
|
403
|
-
guardianTrustClass:
|
|
404
|
-
guardianExternalUserId:
|
|
405
|
-
replyCallbackUrl:
|
|
441
|
+
conversationId: "conv-1",
|
|
442
|
+
sourceChannel: "telegram" as ChannelId,
|
|
443
|
+
externalChatId: "chat-123",
|
|
444
|
+
guardianTrustClass: "trusted_contact" as const,
|
|
445
|
+
guardianExternalUserId: "guardian-1",
|
|
446
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
406
447
|
notifiedRequestIds: notified,
|
|
407
448
|
};
|
|
408
449
|
|
|
409
450
|
// First request
|
|
410
|
-
mockPendingApprovals = [
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
451
|
+
mockPendingApprovals = [
|
|
452
|
+
{
|
|
453
|
+
requestId: "req-A",
|
|
454
|
+
toolName: "bash",
|
|
455
|
+
input: {},
|
|
456
|
+
riskLevel: "medium",
|
|
457
|
+
},
|
|
458
|
+
];
|
|
416
459
|
await simulateNotifierPoll(baseParams);
|
|
417
460
|
expect(deliveredReplies).toHaveLength(1);
|
|
418
461
|
|
|
419
462
|
// Second request (different requestId)
|
|
420
|
-
mockPendingApprovals = [
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
463
|
+
mockPendingApprovals = [
|
|
464
|
+
{
|
|
465
|
+
requestId: "req-B",
|
|
466
|
+
toolName: "read_file",
|
|
467
|
+
input: {},
|
|
468
|
+
riskLevel: "low",
|
|
469
|
+
},
|
|
470
|
+
];
|
|
426
471
|
await simulateNotifierPoll(baseParams);
|
|
427
472
|
expect(deliveredReplies).toHaveLength(2);
|
|
428
473
|
});
|
|
429
474
|
|
|
430
|
-
test(
|
|
475
|
+
test("concurrent pollers for different conversations do not evict each other", async () => {
|
|
431
476
|
mockGuardianBinding = {
|
|
432
|
-
id:
|
|
433
|
-
metadataJson: JSON.stringify({ displayName:
|
|
477
|
+
id: "binding-1",
|
|
478
|
+
metadataJson: JSON.stringify({ displayName: "Guardian" }),
|
|
434
479
|
};
|
|
435
480
|
|
|
436
481
|
// Shared dedupe map simulating the module-level global
|
|
437
482
|
const notified = new Map<string, string>();
|
|
438
483
|
|
|
439
484
|
// Conversation A gets a pending approval and notifies
|
|
440
|
-
mockPendingApprovals = [
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
485
|
+
mockPendingApprovals = [
|
|
486
|
+
{
|
|
487
|
+
requestId: "req-convA",
|
|
488
|
+
toolName: "bash",
|
|
489
|
+
input: {},
|
|
490
|
+
riskLevel: "medium",
|
|
491
|
+
},
|
|
492
|
+
];
|
|
446
493
|
const sentA = await simulateNotifierPoll({
|
|
447
|
-
conversationId:
|
|
448
|
-
sourceChannel:
|
|
449
|
-
externalChatId:
|
|
450
|
-
guardianTrustClass:
|
|
451
|
-
guardianExternalUserId:
|
|
452
|
-
replyCallbackUrl:
|
|
494
|
+
conversationId: "conv-A",
|
|
495
|
+
sourceChannel: "telegram",
|
|
496
|
+
externalChatId: "chat-A",
|
|
497
|
+
guardianTrustClass: "trusted_contact",
|
|
498
|
+
guardianExternalUserId: "guardian-1",
|
|
499
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
453
500
|
notifiedRequestIds: notified,
|
|
454
501
|
});
|
|
455
502
|
expect(sentA).toBe(true);
|
|
@@ -459,55 +506,59 @@ describe('trusted-contact pending-approval notifier', () => {
|
|
|
459
506
|
// NOT evict conv-A's entry from the shared map.
|
|
460
507
|
mockPendingApprovals = [];
|
|
461
508
|
await simulateNotifierPoll({
|
|
462
|
-
conversationId:
|
|
463
|
-
sourceChannel:
|
|
464
|
-
externalChatId:
|
|
465
|
-
guardianTrustClass:
|
|
466
|
-
guardianExternalUserId:
|
|
467
|
-
replyCallbackUrl:
|
|
509
|
+
conversationId: "conv-B",
|
|
510
|
+
sourceChannel: "telegram",
|
|
511
|
+
externalChatId: "chat-B",
|
|
512
|
+
guardianTrustClass: "trusted_contact",
|
|
513
|
+
guardianExternalUserId: "guardian-1",
|
|
514
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
468
515
|
notifiedRequestIds: notified,
|
|
469
516
|
});
|
|
470
517
|
|
|
471
518
|
// req-convA should still be in the notified map (not evicted by conv-B)
|
|
472
|
-
expect(notified.has(
|
|
519
|
+
expect(notified.has("req-convA")).toBe(true);
|
|
473
520
|
|
|
474
521
|
// Re-poll conversation A with the same pending approval — should NOT
|
|
475
522
|
// re-send because the entry was preserved.
|
|
476
|
-
mockPendingApprovals = [
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
523
|
+
mockPendingApprovals = [
|
|
524
|
+
{
|
|
525
|
+
requestId: "req-convA",
|
|
526
|
+
toolName: "bash",
|
|
527
|
+
input: {},
|
|
528
|
+
riskLevel: "medium",
|
|
529
|
+
},
|
|
530
|
+
];
|
|
482
531
|
const sentA2 = await simulateNotifierPoll({
|
|
483
|
-
conversationId:
|
|
484
|
-
sourceChannel:
|
|
485
|
-
externalChatId:
|
|
486
|
-
guardianTrustClass:
|
|
487
|
-
guardianExternalUserId:
|
|
488
|
-
replyCallbackUrl:
|
|
532
|
+
conversationId: "conv-A",
|
|
533
|
+
sourceChannel: "telegram",
|
|
534
|
+
externalChatId: "chat-A",
|
|
535
|
+
guardianTrustClass: "trusted_contact",
|
|
536
|
+
guardianExternalUserId: "guardian-1",
|
|
537
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
489
538
|
notifiedRequestIds: notified,
|
|
490
539
|
});
|
|
491
540
|
expect(sentA2).toBe(false);
|
|
492
541
|
expect(deliveredReplies).toHaveLength(1); // Still just 1 — no duplicate
|
|
493
542
|
});
|
|
494
543
|
|
|
495
|
-
test(
|
|
496
|
-
mockPendingApprovals = [
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
544
|
+
test("does not activate for guardian actors", async () => {
|
|
545
|
+
mockPendingApprovals = [
|
|
546
|
+
{
|
|
547
|
+
requestId: "req-6",
|
|
548
|
+
toolName: "bash",
|
|
549
|
+
input: {},
|
|
550
|
+
riskLevel: "medium",
|
|
551
|
+
},
|
|
552
|
+
];
|
|
502
553
|
|
|
503
554
|
const notified = new Map<string, string>();
|
|
504
555
|
const sent = await simulateNotifierPoll({
|
|
505
|
-
conversationId:
|
|
506
|
-
sourceChannel:
|
|
507
|
-
externalChatId:
|
|
508
|
-
guardianTrustClass:
|
|
509
|
-
guardianExternalUserId:
|
|
510
|
-
replyCallbackUrl:
|
|
556
|
+
conversationId: "conv-1",
|
|
557
|
+
sourceChannel: "telegram",
|
|
558
|
+
externalChatId: "chat-123",
|
|
559
|
+
guardianTrustClass: "guardian",
|
|
560
|
+
guardianExternalUserId: "guardian-1",
|
|
561
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
511
562
|
notifiedRequestIds: notified,
|
|
512
563
|
});
|
|
513
564
|
|
|
@@ -515,21 +566,23 @@ describe('trusted-contact pending-approval notifier', () => {
|
|
|
515
566
|
expect(deliveredReplies).toHaveLength(0);
|
|
516
567
|
});
|
|
517
568
|
|
|
518
|
-
test(
|
|
519
|
-
mockPendingApprovals = [
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
569
|
+
test("does not activate for unknown actors", async () => {
|
|
570
|
+
mockPendingApprovals = [
|
|
571
|
+
{
|
|
572
|
+
requestId: "req-7",
|
|
573
|
+
toolName: "bash",
|
|
574
|
+
input: {},
|
|
575
|
+
riskLevel: "medium",
|
|
576
|
+
},
|
|
577
|
+
];
|
|
525
578
|
|
|
526
579
|
const notified = new Map<string, string>();
|
|
527
580
|
const sent = await simulateNotifierPoll({
|
|
528
|
-
conversationId:
|
|
529
|
-
sourceChannel:
|
|
530
|
-
externalChatId:
|
|
531
|
-
guardianTrustClass:
|
|
532
|
-
replyCallbackUrl:
|
|
581
|
+
conversationId: "conv-1",
|
|
582
|
+
sourceChannel: "telegram",
|
|
583
|
+
externalChatId: "chat-123",
|
|
584
|
+
guardianTrustClass: "unknown",
|
|
585
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
533
586
|
notifiedRequestIds: notified,
|
|
534
587
|
});
|
|
535
588
|
|
|
@@ -537,22 +590,24 @@ describe('trusted-contact pending-approval notifier', () => {
|
|
|
537
590
|
expect(deliveredReplies).toHaveLength(0);
|
|
538
591
|
});
|
|
539
592
|
|
|
540
|
-
test(
|
|
541
|
-
mockPendingApprovals = [
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
593
|
+
test("does not activate for trusted contact without guardian identity", async () => {
|
|
594
|
+
mockPendingApprovals = [
|
|
595
|
+
{
|
|
596
|
+
requestId: "req-8",
|
|
597
|
+
toolName: "bash",
|
|
598
|
+
input: {},
|
|
599
|
+
riskLevel: "medium",
|
|
600
|
+
},
|
|
601
|
+
];
|
|
547
602
|
|
|
548
603
|
const notified = new Map<string, string>();
|
|
549
604
|
const sent = await simulateNotifierPoll({
|
|
550
|
-
conversationId:
|
|
551
|
-
sourceChannel:
|
|
552
|
-
externalChatId:
|
|
553
|
-
guardianTrustClass:
|
|
605
|
+
conversationId: "conv-1",
|
|
606
|
+
sourceChannel: "telegram",
|
|
607
|
+
externalChatId: "chat-123",
|
|
608
|
+
guardianTrustClass: "trusted_contact",
|
|
554
609
|
guardianExternalUserId: undefined,
|
|
555
|
-
replyCallbackUrl:
|
|
610
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
556
611
|
notifiedRequestIds: notified,
|
|
557
612
|
});
|
|
558
613
|
|
|
@@ -560,27 +615,29 @@ describe('trusted-contact pending-approval notifier', () => {
|
|
|
560
615
|
expect(deliveredReplies).toHaveLength(0);
|
|
561
616
|
});
|
|
562
617
|
|
|
563
|
-
test(
|
|
564
|
-
mockPendingApprovals = [
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
618
|
+
test("retries delivery on failure — removes requestId from notified set", async () => {
|
|
619
|
+
mockPendingApprovals = [
|
|
620
|
+
{
|
|
621
|
+
requestId: "req-9",
|
|
622
|
+
toolName: "bash",
|
|
623
|
+
input: {},
|
|
624
|
+
riskLevel: "medium",
|
|
625
|
+
},
|
|
626
|
+
];
|
|
570
627
|
|
|
571
628
|
mockGuardianBinding = {
|
|
572
|
-
id:
|
|
573
|
-
metadataJson: JSON.stringify({ displayName:
|
|
629
|
+
id: "binding-1",
|
|
630
|
+
metadataJson: JSON.stringify({ displayName: "Guardian" }),
|
|
574
631
|
};
|
|
575
632
|
|
|
576
633
|
const notified = new Map<string, string>();
|
|
577
634
|
const baseParams = {
|
|
578
|
-
conversationId:
|
|
579
|
-
sourceChannel:
|
|
580
|
-
externalChatId:
|
|
581
|
-
guardianTrustClass:
|
|
582
|
-
guardianExternalUserId:
|
|
583
|
-
replyCallbackUrl:
|
|
635
|
+
conversationId: "conv-1",
|
|
636
|
+
sourceChannel: "telegram" as ChannelId,
|
|
637
|
+
externalChatId: "chat-123",
|
|
638
|
+
guardianTrustClass: "trusted_contact" as const,
|
|
639
|
+
guardianExternalUserId: "guardian-1",
|
|
640
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
584
641
|
notifiedRequestIds: notified,
|
|
585
642
|
};
|
|
586
643
|
|
|
@@ -588,27 +645,27 @@ describe('trusted-contact pending-approval notifier', () => {
|
|
|
588
645
|
deliverShouldFail = true;
|
|
589
646
|
const sent1 = await simulateNotifierPoll(baseParams);
|
|
590
647
|
expect(sent1).toBe(false);
|
|
591
|
-
expect(notified.has(
|
|
648
|
+
expect(notified.has("req-9")).toBe(false); // Removed for retry
|
|
592
649
|
|
|
593
650
|
// Second attempt: delivery succeeds
|
|
594
651
|
deliverShouldFail = false;
|
|
595
652
|
const sent2 = await simulateNotifierPoll(baseParams);
|
|
596
653
|
expect(sent2).toBe(true);
|
|
597
654
|
expect(deliveredReplies).toHaveLength(1);
|
|
598
|
-
expect(notified.has(
|
|
655
|
+
expect(notified.has("req-9")).toBe(true);
|
|
599
656
|
});
|
|
600
657
|
|
|
601
|
-
test(
|
|
658
|
+
test("does not send when no pending approvals exist", async () => {
|
|
602
659
|
mockPendingApprovals = [];
|
|
603
660
|
|
|
604
661
|
const notified = new Map<string, string>();
|
|
605
662
|
const sent = await simulateNotifierPoll({
|
|
606
|
-
conversationId:
|
|
607
|
-
sourceChannel:
|
|
608
|
-
externalChatId:
|
|
609
|
-
guardianTrustClass:
|
|
610
|
-
guardianExternalUserId:
|
|
611
|
-
replyCallbackUrl:
|
|
663
|
+
conversationId: "conv-1",
|
|
664
|
+
sourceChannel: "telegram",
|
|
665
|
+
externalChatId: "chat-123",
|
|
666
|
+
guardianTrustClass: "trusted_contact",
|
|
667
|
+
guardianExternalUserId: "guardian-1",
|
|
668
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
612
669
|
notifiedRequestIds: notified,
|
|
613
670
|
});
|
|
614
671
|
|
|
@@ -616,63 +673,71 @@ describe('trusted-contact pending-approval notifier', () => {
|
|
|
616
673
|
expect(deliveredReplies).toHaveLength(0);
|
|
617
674
|
});
|
|
618
675
|
|
|
619
|
-
test(
|
|
620
|
-
mockPendingApprovals = [
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
676
|
+
test("prefers displayName over username when both are present", async () => {
|
|
677
|
+
mockPendingApprovals = [
|
|
678
|
+
{
|
|
679
|
+
requestId: "req-10",
|
|
680
|
+
toolName: "bash",
|
|
681
|
+
input: {},
|
|
682
|
+
riskLevel: "medium",
|
|
683
|
+
},
|
|
684
|
+
];
|
|
626
685
|
|
|
627
686
|
mockGuardianBinding = {
|
|
628
|
-
id:
|
|
687
|
+
id: "binding-1",
|
|
629
688
|
metadataJson: JSON.stringify({
|
|
630
|
-
displayName:
|
|
631
|
-
username:
|
|
689
|
+
displayName: "Sarah",
|
|
690
|
+
username: "sarah_bot",
|
|
632
691
|
}),
|
|
633
692
|
};
|
|
634
693
|
|
|
635
694
|
const notified = new Map<string, string>();
|
|
636
695
|
await simulateNotifierPoll({
|
|
637
|
-
conversationId:
|
|
638
|
-
sourceChannel:
|
|
639
|
-
externalChatId:
|
|
640
|
-
guardianTrustClass:
|
|
641
|
-
guardianExternalUserId:
|
|
642
|
-
replyCallbackUrl:
|
|
696
|
+
conversationId: "conv-1",
|
|
697
|
+
sourceChannel: "telegram",
|
|
698
|
+
externalChatId: "chat-123",
|
|
699
|
+
guardianTrustClass: "trusted_contact",
|
|
700
|
+
guardianExternalUserId: "guardian-1",
|
|
701
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
643
702
|
notifiedRequestIds: notified,
|
|
644
703
|
});
|
|
645
704
|
|
|
646
705
|
expect(deliveredReplies).toHaveLength(1);
|
|
647
|
-
expect(deliveredReplies[0].payload.text).toBe(
|
|
706
|
+
expect(deliveredReplies[0].payload.text).toBe(
|
|
707
|
+
"Waiting for Sarah's approval...",
|
|
708
|
+
);
|
|
648
709
|
});
|
|
649
710
|
|
|
650
|
-
test(
|
|
651
|
-
mockPendingApprovals = [
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
711
|
+
test("handles malformed metadataJson gracefully", async () => {
|
|
712
|
+
mockPendingApprovals = [
|
|
713
|
+
{
|
|
714
|
+
requestId: "req-11",
|
|
715
|
+
toolName: "bash",
|
|
716
|
+
input: {},
|
|
717
|
+
riskLevel: "medium",
|
|
718
|
+
},
|
|
719
|
+
];
|
|
657
720
|
|
|
658
721
|
mockGuardianBinding = {
|
|
659
|
-
id:
|
|
660
|
-
metadataJson:
|
|
722
|
+
id: "binding-1",
|
|
723
|
+
metadataJson: "not-valid-json{{{",
|
|
661
724
|
};
|
|
662
725
|
|
|
663
726
|
const notified = new Map<string, string>();
|
|
664
727
|
await simulateNotifierPoll({
|
|
665
|
-
conversationId:
|
|
666
|
-
sourceChannel:
|
|
667
|
-
externalChatId:
|
|
668
|
-
guardianTrustClass:
|
|
669
|
-
guardianExternalUserId:
|
|
670
|
-
replyCallbackUrl:
|
|
728
|
+
conversationId: "conv-1",
|
|
729
|
+
sourceChannel: "telegram",
|
|
730
|
+
externalChatId: "chat-123",
|
|
731
|
+
guardianTrustClass: "trusted_contact",
|
|
732
|
+
guardianExternalUserId: "guardian-1",
|
|
733
|
+
replyCallbackUrl: "http://localhost:3000/deliver/telegram",
|
|
671
734
|
notifiedRequestIds: notified,
|
|
672
735
|
});
|
|
673
736
|
|
|
674
737
|
expect(deliveredReplies).toHaveLength(1);
|
|
675
738
|
// Falls back to generic phrasing
|
|
676
|
-
expect(deliveredReplies[0].payload.text).toBe(
|
|
739
|
+
expect(deliveredReplies[0].payload.text).toBe(
|
|
740
|
+
"Waiting for your guardian's approval...",
|
|
741
|
+
);
|
|
677
742
|
});
|
|
678
743
|
});
|