@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,31 +1,33 @@
|
|
|
1
|
-
import { mkdtempSync, rmSync } from
|
|
2
|
-
import { tmpdir } from
|
|
3
|
-
import { join } from
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
4
|
|
|
5
|
-
import { afterAll, beforeEach, describe, expect, mock, test } from
|
|
5
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
6
|
+
|
|
7
|
+
mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
|
|
6
8
|
|
|
7
9
|
// ---------------------------------------------------------------------------
|
|
8
10
|
// Test isolation: in-memory SQLite via temp directory
|
|
9
11
|
// ---------------------------------------------------------------------------
|
|
10
12
|
|
|
11
|
-
const testDir = mkdtempSync(join(tmpdir(),
|
|
13
|
+
const testDir = mkdtempSync(join(tmpdir(), "conv-attn-telegram-test-"));
|
|
12
14
|
|
|
13
|
-
mock.module(
|
|
15
|
+
mock.module("../util/platform.js", () => ({
|
|
14
16
|
getRootDir: () => testDir,
|
|
15
17
|
getDataDir: () => testDir,
|
|
16
|
-
isMacOS: () => process.platform ===
|
|
17
|
-
isLinux: () => process.platform ===
|
|
18
|
-
isWindows: () => process.platform ===
|
|
19
|
-
getSocketPath: () => join(testDir,
|
|
20
|
-
getPidPath: () => join(testDir,
|
|
21
|
-
getDbPath: () => join(testDir,
|
|
22
|
-
getLogPath: () => join(testDir,
|
|
18
|
+
isMacOS: () => process.platform === "darwin",
|
|
19
|
+
isLinux: () => process.platform === "linux",
|
|
20
|
+
isWindows: () => process.platform === "win32",
|
|
21
|
+
getSocketPath: () => join(testDir, "test.sock"),
|
|
22
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
23
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
24
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
23
25
|
ensureDataDir: () => {},
|
|
24
26
|
migrateToDataLayout: () => {},
|
|
25
27
|
migrateToWorkspaceLayout: () => {},
|
|
26
28
|
}));
|
|
27
29
|
|
|
28
|
-
mock.module(
|
|
30
|
+
mock.module("../util/logger.js", () => ({
|
|
29
31
|
getLogger: () =>
|
|
30
32
|
new Proxy({} as Record<string, unknown>, {
|
|
31
33
|
get: () => () => {},
|
|
@@ -35,29 +37,29 @@ mock.module('../util/logger.js', () => ({
|
|
|
35
37
|
}));
|
|
36
38
|
|
|
37
39
|
// Mock security check to always pass
|
|
38
|
-
mock.module(
|
|
40
|
+
mock.module("../security/secret-ingress.js", () => ({
|
|
39
41
|
checkIngressForSecrets: () => ({ blocked: false }),
|
|
40
42
|
}));
|
|
41
43
|
|
|
42
44
|
// Mock render to return the raw content as text
|
|
43
|
-
mock.module(
|
|
45
|
+
mock.module("../daemon/handlers.js", () => ({
|
|
44
46
|
renderHistoryContent: (content: unknown) => ({
|
|
45
|
-
text: typeof content ===
|
|
47
|
+
text: typeof content === "string" ? content : JSON.stringify(content),
|
|
46
48
|
}),
|
|
47
49
|
}));
|
|
48
50
|
|
|
49
51
|
// Mock ingress member store to return an active member for all lookups
|
|
50
|
-
mock.module(
|
|
52
|
+
mock.module("../memory/ingress-member-store.js", () => ({
|
|
51
53
|
findMember: () => ({
|
|
52
|
-
id:
|
|
53
|
-
assistantId:
|
|
54
|
-
sourceChannel:
|
|
55
|
-
externalUserId:
|
|
54
|
+
id: "member-test-default",
|
|
55
|
+
assistantId: "self",
|
|
56
|
+
sourceChannel: "telegram",
|
|
57
|
+
externalUserId: "telegram-user-default",
|
|
56
58
|
externalChatId: null,
|
|
57
59
|
displayName: null,
|
|
58
60
|
username: null,
|
|
59
|
-
status:
|
|
60
|
-
policy:
|
|
61
|
+
status: "active",
|
|
62
|
+
policy: "allow",
|
|
61
63
|
inviteId: null,
|
|
62
64
|
createdBySessionId: null,
|
|
63
65
|
revokedReason: null,
|
|
@@ -70,17 +72,17 @@ mock.module('../memory/ingress-member-store.js', () => ({
|
|
|
70
72
|
upsertMember: () => {},
|
|
71
73
|
}));
|
|
72
74
|
|
|
73
|
-
import { eq } from
|
|
75
|
+
import { eq } from "drizzle-orm";
|
|
74
76
|
|
|
75
|
-
import * as channelDeliveryStore from
|
|
76
|
-
import { getDb, initializeDb, resetDb } from
|
|
77
|
+
import * as channelDeliveryStore from "../memory/channel-delivery-store.js";
|
|
78
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
77
79
|
import {
|
|
78
80
|
attachments,
|
|
79
81
|
conversationAssistantAttentionState,
|
|
80
82
|
conversationAttentionEvents,
|
|
81
|
-
} from
|
|
82
|
-
import * as pendingInteractions from
|
|
83
|
-
import { handleChannelInbound } from
|
|
83
|
+
} from "../memory/schema.js";
|
|
84
|
+
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
85
|
+
import { handleChannelInbound } from "../runtime/routes/channel-routes.js";
|
|
84
86
|
|
|
85
87
|
initializeDb();
|
|
86
88
|
|
|
@@ -101,42 +103,42 @@ function resetTables(): void {
|
|
|
101
103
|
const db = getDb();
|
|
102
104
|
db.delete(conversationAttentionEvents).run();
|
|
103
105
|
db.delete(conversationAssistantAttentionState).run();
|
|
104
|
-
db.run(
|
|
105
|
-
db.run(
|
|
106
|
-
db.run(
|
|
107
|
-
db.run(
|
|
108
|
-
db.run(
|
|
109
|
-
db.run(
|
|
110
|
-
db.run(
|
|
111
|
-
db.run(
|
|
106
|
+
db.run("DELETE FROM channel_guardian_approval_requests");
|
|
107
|
+
db.run("DELETE FROM channel_guardian_verification_challenges");
|
|
108
|
+
db.run("DELETE FROM channel_guardian_bindings");
|
|
109
|
+
db.run("DELETE FROM conversation_keys");
|
|
110
|
+
db.run("DELETE FROM message_runs");
|
|
111
|
+
db.run("DELETE FROM channel_inbound_events");
|
|
112
|
+
db.run("DELETE FROM messages");
|
|
113
|
+
db.run("DELETE FROM conversations");
|
|
112
114
|
channelDeliveryStore.resetAllRunDeliveryClaims();
|
|
113
115
|
pendingInteractions.clear();
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
const TEST_BEARER_TOKEN =
|
|
118
|
+
const TEST_BEARER_TOKEN = "token";
|
|
117
119
|
|
|
118
120
|
function makeInboundRequest(overrides: Record<string, unknown> = {}): Request {
|
|
119
121
|
const body = {
|
|
120
|
-
sourceChannel:
|
|
121
|
-
interface:
|
|
122
|
-
conversationExternalId:
|
|
123
|
-
actorExternalId:
|
|
122
|
+
sourceChannel: "telegram",
|
|
123
|
+
interface: "telegram",
|
|
124
|
+
conversationExternalId: "chat-123",
|
|
125
|
+
actorExternalId: "telegram-user-default",
|
|
124
126
|
externalMessageId: `msg-${Date.now()}-${Math.random()}`,
|
|
125
|
-
content:
|
|
126
|
-
replyCallbackUrl:
|
|
127
|
+
content: "hello",
|
|
128
|
+
replyCallbackUrl: "https://gateway.test/deliver",
|
|
127
129
|
...overrides,
|
|
128
130
|
};
|
|
129
|
-
return new Request(
|
|
130
|
-
method:
|
|
131
|
+
return new Request("http://localhost/channels/inbound", {
|
|
132
|
+
method: "POST",
|
|
131
133
|
headers: {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
134
136
|
},
|
|
135
137
|
body: JSON.stringify(body),
|
|
136
138
|
});
|
|
137
139
|
}
|
|
138
140
|
|
|
139
|
-
const noopProcessMessage = mock(async () => ({ messageId:
|
|
141
|
+
const noopProcessMessage = mock(async () => ({ messageId: "msg-1" }));
|
|
140
142
|
|
|
141
143
|
function getAttentionEvents(conversationId: string) {
|
|
142
144
|
const db = getDb();
|
|
@@ -156,11 +158,15 @@ beforeEach(() => {
|
|
|
156
158
|
// Telegram inbound messages record inferred seen signals
|
|
157
159
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
158
160
|
|
|
159
|
-
describe(
|
|
160
|
-
test(
|
|
161
|
-
const req = makeInboundRequest({ content:
|
|
161
|
+
describe("Telegram inbound message seen signals", () => {
|
|
162
|
+
test("records inferred seen signal for non-duplicate text message", async () => {
|
|
163
|
+
const req = makeInboundRequest({ content: "Hello there!" });
|
|
162
164
|
|
|
163
|
-
const res = await handleChannelInbound(
|
|
165
|
+
const res = await handleChannelInbound(
|
|
166
|
+
req,
|
|
167
|
+
noopProcessMessage,
|
|
168
|
+
TEST_BEARER_TOKEN,
|
|
169
|
+
);
|
|
164
170
|
const body = (await res.json()) as Record<string, unknown>;
|
|
165
171
|
|
|
166
172
|
expect(body.accepted).toBe(true);
|
|
@@ -169,7 +175,7 @@ describe('Telegram inbound message seen signals', () => {
|
|
|
169
175
|
// Find the conversation ID from inbound events
|
|
170
176
|
const db = getDb();
|
|
171
177
|
const inboundEvents = db.$client
|
|
172
|
-
.prepare(
|
|
178
|
+
.prepare("SELECT conversation_id FROM channel_inbound_events")
|
|
173
179
|
.all() as Array<{ conversation_id: string }>;
|
|
174
180
|
expect(inboundEvents.length).toBeGreaterThan(0);
|
|
175
181
|
|
|
@@ -177,56 +183,64 @@ describe('Telegram inbound message seen signals', () => {
|
|
|
177
183
|
const events = getAttentionEvents(conversationId);
|
|
178
184
|
|
|
179
185
|
expect(events.length).toBe(1);
|
|
180
|
-
expect(events[0].signalType).toBe(
|
|
181
|
-
expect(events[0].confidence).toBe(
|
|
182
|
-
expect(events[0].sourceChannel).toBe(
|
|
183
|
-
expect(events[0].source).toBe(
|
|
186
|
+
expect(events[0].signalType).toBe("telegram_inbound_message");
|
|
187
|
+
expect(events[0].confidence).toBe("inferred");
|
|
188
|
+
expect(events[0].sourceChannel).toBe("telegram");
|
|
189
|
+
expect(events[0].source).toBe("inbound-message-handler");
|
|
184
190
|
expect(events[0].evidenceText).toBe("User sent message: 'Hello there!'");
|
|
185
191
|
});
|
|
186
192
|
|
|
187
|
-
test(
|
|
193
|
+
test("records inferred seen signal for media attachment without text", async () => {
|
|
188
194
|
// Insert a fake attachment directly so the handler's validation passes
|
|
189
195
|
const db = getDb();
|
|
190
196
|
const attachmentId = `att-${Date.now()}`;
|
|
191
197
|
db.insert(attachments)
|
|
192
198
|
.values({
|
|
193
199
|
id: attachmentId,
|
|
194
|
-
originalFilename:
|
|
195
|
-
mimeType:
|
|
200
|
+
originalFilename: "photo.jpg",
|
|
201
|
+
mimeType: "image/jpeg",
|
|
196
202
|
sizeBytes: 1024,
|
|
197
|
-
kind:
|
|
198
|
-
dataBase64:
|
|
203
|
+
kind: "base64",
|
|
204
|
+
dataBase64: "dGVzdA==",
|
|
199
205
|
createdAt: Date.now(),
|
|
200
206
|
})
|
|
201
207
|
.run();
|
|
202
208
|
|
|
203
209
|
const req = makeInboundRequest({
|
|
204
|
-
content:
|
|
210
|
+
content: "",
|
|
205
211
|
attachmentIds: [attachmentId],
|
|
206
212
|
});
|
|
207
213
|
|
|
208
|
-
const res = await handleChannelInbound(
|
|
214
|
+
const res = await handleChannelInbound(
|
|
215
|
+
req,
|
|
216
|
+
noopProcessMessage,
|
|
217
|
+
TEST_BEARER_TOKEN,
|
|
218
|
+
);
|
|
209
219
|
const body = (await res.json()) as Record<string, unknown>;
|
|
210
220
|
|
|
211
221
|
expect(body.accepted).toBe(true);
|
|
212
222
|
expect(body.duplicate).toBe(false);
|
|
213
223
|
|
|
214
224
|
const inboundEvents2 = db.$client
|
|
215
|
-
.prepare(
|
|
225
|
+
.prepare("SELECT conversation_id FROM channel_inbound_events")
|
|
216
226
|
.all() as Array<{ conversation_id: string }>;
|
|
217
227
|
const conversationId = inboundEvents2[0].conversation_id;
|
|
218
228
|
const events = getAttentionEvents(conversationId);
|
|
219
229
|
|
|
220
230
|
expect(events.length).toBe(1);
|
|
221
|
-
expect(events[0].signalType).toBe(
|
|
222
|
-
expect(events[0].evidenceText).toBe(
|
|
231
|
+
expect(events[0].signalType).toBe("telegram_inbound_message");
|
|
232
|
+
expect(events[0].evidenceText).toBe("User sent media attachment");
|
|
223
233
|
});
|
|
224
234
|
|
|
225
|
-
test(
|
|
226
|
-
const longMessage =
|
|
235
|
+
test("evidence text is correctly truncated for long messages", async () => {
|
|
236
|
+
const longMessage = "A".repeat(120);
|
|
227
237
|
const req = makeInboundRequest({ content: longMessage });
|
|
228
238
|
|
|
229
|
-
const res = await handleChannelInbound(
|
|
239
|
+
const res = await handleChannelInbound(
|
|
240
|
+
req,
|
|
241
|
+
noopProcessMessage,
|
|
242
|
+
TEST_BEARER_TOKEN,
|
|
243
|
+
);
|
|
230
244
|
const body = (await res.json()) as Record<string, unknown>;
|
|
231
245
|
|
|
232
246
|
expect(body.accepted).toBe(true);
|
|
@@ -234,15 +248,17 @@ describe('Telegram inbound message seen signals', () => {
|
|
|
234
248
|
|
|
235
249
|
const db = getDb();
|
|
236
250
|
const inboundEvents = db.$client
|
|
237
|
-
.prepare(
|
|
251
|
+
.prepare("SELECT conversation_id FROM channel_inbound_events")
|
|
238
252
|
.all() as Array<{ conversation_id: string }>;
|
|
239
253
|
const conversationId = inboundEvents[0].conversation_id;
|
|
240
254
|
const events = getAttentionEvents(conversationId);
|
|
241
255
|
|
|
242
256
|
expect(events.length).toBe(1);
|
|
243
257
|
// 80 chars of 'A' + '...'
|
|
244
|
-
const expectedPreview =
|
|
245
|
-
expect(events[0].evidenceText).toBe(
|
|
258
|
+
const expectedPreview = "A".repeat(80) + "...";
|
|
259
|
+
expect(events[0].evidenceText).toBe(
|
|
260
|
+
`User sent message: '${expectedPreview}'`,
|
|
261
|
+
);
|
|
246
262
|
});
|
|
247
263
|
});
|
|
248
264
|
|
|
@@ -250,42 +266,51 @@ describe('Telegram inbound message seen signals', () => {
|
|
|
250
266
|
// Telegram callbacks record inferred seen signals
|
|
251
267
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
252
268
|
|
|
253
|
-
describe(
|
|
254
|
-
test(
|
|
269
|
+
describe("Telegram callback seen signals", () => {
|
|
270
|
+
test("records inferred seen signal for handled callback", async () => {
|
|
255
271
|
// First, send a regular message to establish the conversation
|
|
256
|
-
const initReq = makeInboundRequest({ content:
|
|
272
|
+
const initReq = makeInboundRequest({ content: "init" });
|
|
257
273
|
await handleChannelInbound(initReq, noopProcessMessage, TEST_BEARER_TOKEN);
|
|
258
274
|
|
|
259
275
|
const db = getDb();
|
|
260
276
|
const inboundEvents = db.$client
|
|
261
|
-
.prepare(
|
|
277
|
+
.prepare("SELECT conversation_id FROM channel_inbound_events")
|
|
262
278
|
.all() as Array<{ conversation_id: string }>;
|
|
263
279
|
const conversationId = inboundEvents[0].conversation_id;
|
|
264
280
|
|
|
265
281
|
// Register a pending interaction so the approval interception handles it
|
|
266
282
|
const handleConfirmationResponse = mock(() => {});
|
|
267
|
-
const mockSession = {
|
|
268
|
-
|
|
283
|
+
const mockSession = {
|
|
284
|
+
handleConfirmationResponse,
|
|
285
|
+
} as unknown as import("../daemon/session.js").Session;
|
|
286
|
+
pendingInteractions.register("req-cb-test", {
|
|
269
287
|
session: mockSession,
|
|
270
288
|
conversationId,
|
|
271
|
-
kind:
|
|
289
|
+
kind: "confirmation",
|
|
272
290
|
confirmationDetails: {
|
|
273
|
-
toolName:
|
|
274
|
-
input: { command:
|
|
275
|
-
riskLevel:
|
|
276
|
-
allowlistOptions: [
|
|
277
|
-
|
|
291
|
+
toolName: "shell",
|
|
292
|
+
input: { command: "echo hello" },
|
|
293
|
+
riskLevel: "high",
|
|
294
|
+
allowlistOptions: [
|
|
295
|
+
{
|
|
296
|
+
label: "echo hello",
|
|
297
|
+
description: "echo hello",
|
|
298
|
+
pattern: "echo hello",
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
scopeOptions: [{ label: "everywhere", scope: "everywhere" }],
|
|
278
302
|
},
|
|
279
303
|
});
|
|
280
304
|
|
|
281
305
|
// Create a guardian binding so approval can be handled
|
|
282
|
-
const { createBinding } =
|
|
306
|
+
const { createBinding } =
|
|
307
|
+
await import("../memory/channel-guardian-store.js");
|
|
283
308
|
createBinding({
|
|
284
|
-
assistantId:
|
|
285
|
-
channel:
|
|
286
|
-
guardianExternalUserId:
|
|
287
|
-
guardianDeliveryChatId:
|
|
288
|
-
guardianPrincipalId:
|
|
309
|
+
assistantId: "self",
|
|
310
|
+
channel: "telegram",
|
|
311
|
+
guardianExternalUserId: "telegram-user-default",
|
|
312
|
+
guardianDeliveryChatId: "chat-123",
|
|
313
|
+
guardianPrincipalId: "telegram-user-default",
|
|
289
314
|
});
|
|
290
315
|
|
|
291
316
|
// Clear attention events from the init message
|
|
@@ -293,11 +318,15 @@ describe('Telegram callback seen signals', () => {
|
|
|
293
318
|
|
|
294
319
|
// Send callback data that matches the pending approval
|
|
295
320
|
const cbReq = makeInboundRequest({
|
|
296
|
-
content:
|
|
297
|
-
callbackData:
|
|
321
|
+
content: "approve",
|
|
322
|
+
callbackData: "apr:req-cb-test:approve_once",
|
|
298
323
|
});
|
|
299
324
|
|
|
300
|
-
const res = await handleChannelInbound(
|
|
325
|
+
const res = await handleChannelInbound(
|
|
326
|
+
cbReq,
|
|
327
|
+
noopProcessMessage,
|
|
328
|
+
TEST_BEARER_TOKEN,
|
|
329
|
+
);
|
|
301
330
|
const body = (await res.json()) as Record<string, unknown>;
|
|
302
331
|
|
|
303
332
|
expect(body.accepted).toBe(true);
|
|
@@ -305,11 +334,13 @@ describe('Telegram callback seen signals', () => {
|
|
|
305
334
|
|
|
306
335
|
const events = getAttentionEvents(conversationId);
|
|
307
336
|
expect(events.length).toBe(1);
|
|
308
|
-
expect(events[0].signalType).toBe(
|
|
309
|
-
expect(events[0].confidence).toBe(
|
|
310
|
-
expect(events[0].sourceChannel).toBe(
|
|
311
|
-
expect(events[0].source).toBe(
|
|
312
|
-
expect(events[0].evidenceText).toContain(
|
|
337
|
+
expect(events[0].signalType).toBe("telegram_callback");
|
|
338
|
+
expect(events[0].confidence).toBe("inferred");
|
|
339
|
+
expect(events[0].sourceChannel).toBe("telegram");
|
|
340
|
+
expect(events[0].source).toBe("inbound-message-handler");
|
|
341
|
+
expect(events[0].evidenceText).toContain(
|
|
342
|
+
"User tapped callback: 'apr:req-cb-test:approve_once'",
|
|
343
|
+
);
|
|
313
344
|
});
|
|
314
345
|
});
|
|
315
346
|
|
|
@@ -317,32 +348,40 @@ describe('Telegram callback seen signals', () => {
|
|
|
317
348
|
// Duplicate events do NOT produce duplicate seen signals
|
|
318
349
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
319
350
|
|
|
320
|
-
describe(
|
|
321
|
-
test(
|
|
351
|
+
describe("duplicate event deduplication", () => {
|
|
352
|
+
test("duplicate Telegram message does not record a second seen signal", async () => {
|
|
322
353
|
const fixedMessageId = `msg-dedup-${Date.now()}`;
|
|
323
354
|
|
|
324
355
|
// First (non-duplicate) message
|
|
325
356
|
const req1 = makeInboundRequest({
|
|
326
|
-
content:
|
|
357
|
+
content: "first message",
|
|
327
358
|
externalMessageId: fixedMessageId,
|
|
328
359
|
});
|
|
329
|
-
const res1 = await handleChannelInbound(
|
|
360
|
+
const res1 = await handleChannelInbound(
|
|
361
|
+
req1,
|
|
362
|
+
noopProcessMessage,
|
|
363
|
+
TEST_BEARER_TOKEN,
|
|
364
|
+
);
|
|
330
365
|
const body1 = (await res1.json()) as Record<string, unknown>;
|
|
331
366
|
expect(body1.duplicate).toBe(false);
|
|
332
367
|
|
|
333
368
|
// Same externalMessageId => duplicate
|
|
334
369
|
const req2 = makeInboundRequest({
|
|
335
|
-
content:
|
|
370
|
+
content: "first message",
|
|
336
371
|
externalMessageId: fixedMessageId,
|
|
337
372
|
});
|
|
338
|
-
const res2 = await handleChannelInbound(
|
|
373
|
+
const res2 = await handleChannelInbound(
|
|
374
|
+
req2,
|
|
375
|
+
noopProcessMessage,
|
|
376
|
+
TEST_BEARER_TOKEN,
|
|
377
|
+
);
|
|
339
378
|
const body2 = (await res2.json()) as Record<string, unknown>;
|
|
340
379
|
expect(body2.duplicate).toBe(true);
|
|
341
380
|
|
|
342
381
|
// Only one attention event should exist
|
|
343
382
|
const db = getDb();
|
|
344
383
|
const inboundEvents = db.$client
|
|
345
|
-
.prepare(
|
|
384
|
+
.prepare("SELECT conversation_id FROM channel_inbound_events")
|
|
346
385
|
.all() as Array<{ conversation_id: string }>;
|
|
347
386
|
const conversationId = inboundEvents[0].conversation_id;
|
|
348
387
|
const events = getAttentionEvents(conversationId);
|
|
@@ -355,16 +394,20 @@ describe('duplicate event deduplication', () => {
|
|
|
355
394
|
// Non-Telegram channels do NOT record Telegram seen signals
|
|
356
395
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
357
396
|
|
|
358
|
-
describe(
|
|
359
|
-
test(
|
|
397
|
+
describe("non-Telegram channel filtering", () => {
|
|
398
|
+
test("SMS inbound message does not record a Telegram seen signal", async () => {
|
|
360
399
|
// Override ingress member store for SMS channel
|
|
361
400
|
const req = makeInboundRequest({
|
|
362
|
-
sourceChannel:
|
|
363
|
-
interface:
|
|
364
|
-
content:
|
|
401
|
+
sourceChannel: "sms",
|
|
402
|
+
interface: "sms",
|
|
403
|
+
content: "sms message",
|
|
365
404
|
});
|
|
366
405
|
|
|
367
|
-
const res = await handleChannelInbound(
|
|
406
|
+
const res = await handleChannelInbound(
|
|
407
|
+
req,
|
|
408
|
+
noopProcessMessage,
|
|
409
|
+
TEST_BEARER_TOKEN,
|
|
410
|
+
);
|
|
368
411
|
const body = (await res.json()) as Record<string, unknown>;
|
|
369
412
|
|
|
370
413
|
expect(body.accepted).toBe(true);
|