@vellumai/vellum-gateway 0.12.2-staging.6 → 0.12.2-staging.7
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/AGENTS.md +3 -1
- package/README.md +1 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/package.json +1 -1
- package/src/__tests__/telegram-normalize.test.ts +123 -111
- package/src/__tests__/telegram-webhook-drop-visibility.test.ts +229 -0
- package/src/__tests__/telegram-webhook-handler.test.ts +160 -0
- package/src/channels/admission-drop-log.test.ts +47 -0
- package/src/channels/admission-drop-log.ts +94 -0
- package/src/channels/inbound-event.ts +9 -0
- package/src/channels/room-admission.test.ts +95 -0
- package/src/channels/room-admission.ts +149 -0
- package/src/discord/admission-log.test.ts +15 -12
- package/src/discord/admission-log.ts +11 -95
- package/src/discord/admit.test.ts +8 -9
- package/src/discord/admit.ts +48 -93
- package/src/discord/gateway-socket.ts +4 -2
- package/src/discord/normalize.test.ts +34 -1
- package/src/discord/normalize.ts +23 -3
- package/src/handlers/handle-inbound.ts +3 -0
- package/src/http/routes/telegram-webhook.ts +50 -9
- package/src/risk/bash-risk-classifier.test.ts +63 -0
- package/src/risk/command-registry/commands/dig.ts +2 -1
- package/src/risk/command-registry/commands/host.ts +2 -1
- package/src/risk/command-registry/commands/mtr.ts +2 -1
- package/src/risk/command-registry/commands/nslookup.ts +2 -1
- package/src/risk/command-registry/commands/ping.ts +2 -1
- package/src/risk/command-registry/commands/tracepath.ts +2 -1
- package/src/risk/command-registry/commands/traceroute.ts +2 -1
- package/src/risk/risk-types.ts +1 -0
- package/src/risk/shell-parser.test.ts +82 -1
- package/src/risk/shell-parser.ts +50 -2
- package/src/slack/message-normalizer.ts +14 -1
- package/src/slack/source-metadata.test.ts +5 -2
- package/src/slack/source-metadata.ts +2 -6
- package/src/telegram/admit.test.ts +155 -0
- package/src/telegram/admit.ts +149 -0
- package/src/telegram/bot-identity.test.ts +124 -0
- package/src/telegram/bot-identity.ts +127 -0
- package/src/telegram/drop-log.ts +41 -0
- package/src/telegram/normalize.test.ts +343 -134
- package/src/telegram/normalize.ts +197 -197
- package/src/telegram/schemas.ts +217 -0
- package/src/telegram/send.ts +1 -1
package/AGENTS.md
CHANGED
|
@@ -90,6 +90,8 @@ Gateway inbound events use a channel-discriminated union model (`GatewayInboundE
|
|
|
90
90
|
|
|
91
91
|
Trust/guardian decisions must be keyed on `actorExternalId` only — never fall back to `conversationExternalId` for actor identity.
|
|
92
92
|
|
|
93
|
+
**Room admission is one verdict, two adapter halves.** Whether the gateway acts on an inbound message at all is decided by `channels/room-admission.ts` over a neutral candidate: self and bot authors drop first, a direct chat is admitted on its own lane without a mention, a room only when the message addresses the bot, a chat kind the product does not serve is refused, and an unknown bot identity drops rather than admits. Each channel builds the candidate from its own facts (`discord/admit.ts`, `telegram/admit.ts`): how the platform proves a chat is direct (guild absence, `chat.type`) and how it proves a message is addressed (a mentions array, text entities, a reply to the bot's post). Extend the verdict, never a copy of it; a platform-only rule rides a neutral fact (Discord's legacy allow-list is `roomAllowed`). A drop is never silent: every channel logs it through `channels/admission-drop-log.ts`, one promoted line per reason and conversation, with a drop that names no conversation promoted every time. Slack's socket filter predates this and still carries the same rules inline; converging it is queued with the Slack envelope work.
|
|
94
|
+
|
|
93
95
|
Physical DB column names (`externalUserId`, `externalChatId`) are unchanged; the rename is at the API/type layer only.
|
|
94
96
|
|
|
95
97
|
**Provider words that are not our words.** A Discord **guild** is what Discord's
|
|
@@ -193,7 +195,7 @@ Both the flat (`/v1/channel-admission-policy/...`) and assistant-scoped (`/v1/as
|
|
|
193
195
|
|
|
194
196
|
- **Gateway kill switch** — `handle-inbound.ts` enforces the `no_one` floor before forwarding. Zero contact-table lookups, zero daemon I/O, true kill.
|
|
195
197
|
- **Runtime floor** — every other policy flows through the gateway unchanged; the runtime evaluates rank-vs-floor inside `admission-policy.ts`. This keeps the canonical gateway classifier (`gateway/src/risk/trust-verdict-resolver.ts`) as the single source of `TrustClass` truth (no fork): the runtime consumes the stamped verdict; the daemon's `actor-trust-resolver.ts` is only a residual sync guardian-or-unknown view for the vellum reset-drift path.
|
|
196
|
-
- **Gateway vs runtime reciprocity
|
|
198
|
+
- **Gateway vs runtime reciprocity**: this section records _which channels the gateway enforces_; the runtime's evaluation lives in `assistant/src/runtime/routes/inbound-stages/admission-policy.ts`, which reads `TRUST_CLASS_RANK` and `ADMISSION_FLOOR` from the shared contract. Either side getting out of sync is a bug, not an over-defended boundary.
|
|
197
199
|
|
|
198
200
|
**Adding a new policy**: extend the `AdmissionPolicy` union in `packages/gateway-client/src/admission-policy-contract.ts`, add its floor in `ADMISSION_FLOOR`, update the openapi schema, and update `gateway/src/__tests__/channel-admission-policy-routes.test.ts` + `assistant/src/runtime/routes/inbound-stages/admission-policy.test.ts`. Do not add a 6th floor without also bumping the `TRUST_CLASS_RANK` ceiling to match.
|
|
199
201
|
|
package/README.md
CHANGED
|
@@ -106,7 +106,7 @@ The gateway normalizes Telegram `callback_query` updates (inline button clicks)
|
|
|
106
106
|
|
|
107
107
|
These fields are forwarded to the runtime in the `/channels/inbound` payload alongside the standard `conversationExternalId`, `externalMessageId`, and actor metadata. The runtime uses `callbackData` to route the click to the appropriate approval handler.
|
|
108
108
|
|
|
109
|
-
**Normalization constraints:**
|
|
109
|
+
**Normalization constraints:** Callback queries from private chats, groups, and supergroups are processed; a tap on a keyboard the bot posted is addressed to the bot by construction, and who may press it is the runtime's decision. Callbacks from broadcast channels are dropped and acknowledged with `answerCallbackQuery` so the Telegram button spinner clears. Callback queries with no `data` field or no associated `message` are also dropped. Every drop is logged with its reason before the update is acknowledged (`telegram/drop-log.ts`).
|
|
110
110
|
|
|
111
111
|
**Stale callback blocking:** When the runtime receives `callbackData` that does not match any pending approval (e.g., a button from an old prompt), it returns `stale_ignored` and does not process the payload as a regular message. This is enforced regardless of whether the callback has non-empty content. The gateway sends a best-effort `answerCallbackQuery` acknowledgment for normalized callback updates (including stale, rejected, and forward-failure paths) so the button spinner clears promptly. Transient forwarding failures may still return `500` so Telegram retries update delivery.
|
|
112
112
|
|
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
buildShellInvocation,
|
|
5
|
+
buildShellSpawnFlags,
|
|
5
6
|
pathListDelimiter,
|
|
6
7
|
prependUniquePathEntries,
|
|
7
8
|
} from "./shell.js";
|
|
@@ -38,6 +39,26 @@ describe("buildShellInvocation", () => {
|
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
|
|
42
|
+
describe("buildShellSpawnFlags", () => {
|
|
43
|
+
test("creates a POSIX process group and hides Windows consoles", () => {
|
|
44
|
+
expect(buildShellSpawnFlags("linux")).toEqual({
|
|
45
|
+
detached: true,
|
|
46
|
+
windowsHide: true,
|
|
47
|
+
});
|
|
48
|
+
expect(buildShellSpawnFlags("darwin")).toEqual({
|
|
49
|
+
detached: true,
|
|
50
|
+
windowsHide: true,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("does not detach Windows children that use piped stdio", () => {
|
|
55
|
+
expect(buildShellSpawnFlags("win32")).toEqual({
|
|
56
|
+
detached: false,
|
|
57
|
+
windowsHide: true,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
41
62
|
describe("path list handling", () => {
|
|
42
63
|
test("uses the platform delimiter", () => {
|
|
43
64
|
expect(pathListDelimiter("win32")).toBe(";");
|
|
@@ -5,6 +5,11 @@ export interface ShellInvocation {
|
|
|
5
5
|
args: string[];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface ShellSpawnFlags {
|
|
9
|
+
detached: boolean;
|
|
10
|
+
windowsHide: true;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
const WINDOWS_UTF8_PREAMBLE =
|
|
9
14
|
"try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}; " +
|
|
10
15
|
"$OutputEncoding = [System.Text.Encoding]::UTF8; " +
|
|
@@ -43,6 +48,25 @@ export function buildShellInvocation(
|
|
|
43
48
|
return { command: "bash", args: ["-c", "--", command] };
|
|
44
49
|
}
|
|
45
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Spawn flags for assistant-owned shell children (sandbox bash, local
|
|
53
|
+
* host_bash fallback, sanitized CLI bash, skill runners, scheduled scripts).
|
|
54
|
+
*
|
|
55
|
+
* POSIX uses a new process group so timeout/abort can SIGKILL the tree via
|
|
56
|
+
* `-pid`. Windows process trees are torn down with `taskkill /T`, which does
|
|
57
|
+
* not need a detached process. Combining `DETACHED_PROCESS`,
|
|
58
|
+
* `CREATE_NO_WINDOW`, and piped stdio on Windows can emit `close` with exit
|
|
59
|
+
* 0 and empty pipes without running the encoded command.
|
|
60
|
+
*/
|
|
61
|
+
export function buildShellSpawnFlags(
|
|
62
|
+
hostPlatform: NodeJS.Platform = process.platform,
|
|
63
|
+
): ShellSpawnFlags {
|
|
64
|
+
return {
|
|
65
|
+
detached: hostPlatform !== "win32",
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
46
70
|
export function pathListDelimiter(
|
|
47
71
|
hostPlatform: NodeJS.Platform = process.platform,
|
|
48
72
|
): string {
|
|
@@ -83,8 +83,14 @@ export const SourceMetadataSchema = z
|
|
|
83
83
|
clientTimezone: z.string().optional(),
|
|
84
84
|
/** Channel command intent (e.g. Telegram /start). */
|
|
85
85
|
commandIntent: CommandIntentSchema.optional(),
|
|
86
|
-
/**
|
|
87
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Whether the message addresses the assistant by name, an @-mention on
|
|
88
|
+
* the platforms that have one. Stated by the channel's normalizer only
|
|
89
|
+
* where it proved the answer; absent means "not established". Read to
|
|
90
|
+
* decide whether a reply is expected before any text exists, never to
|
|
91
|
+
* admit or deny: admission is the gate's job, upstream of this.
|
|
92
|
+
*/
|
|
93
|
+
botMentioned: z.boolean().optional(),
|
|
88
94
|
/**
|
|
89
95
|
* Slack-specific: the `edited.ts` float-string from a `message_changed`
|
|
90
96
|
* event. Used by the daemon to reject out-of-order edit deliveries whose
|
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
normalizeTelegramUpdate,
|
|
4
|
+
type TelegramDropReason,
|
|
5
|
+
type TelegramNormalization,
|
|
6
|
+
} from "../telegram/normalize.js";
|
|
3
7
|
import { verifyWebhookSecret } from "../telegram/verify.js";
|
|
8
|
+
import type { GatewayInboundEvent } from "../types.js";
|
|
9
|
+
|
|
10
|
+
/** The event a normalization produced, failing the test if it dropped. */
|
|
11
|
+
function eventOf(result: TelegramNormalization): GatewayInboundEvent {
|
|
12
|
+
if (result.dropped) {
|
|
13
|
+
throw new Error(`expected an event, got drop: ${result.reason}`);
|
|
14
|
+
}
|
|
15
|
+
return result.event;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function expectDrop(
|
|
19
|
+
result: TelegramNormalization,
|
|
20
|
+
reason: TelegramDropReason,
|
|
21
|
+
): void {
|
|
22
|
+
expect(result.dropped).toBe(true);
|
|
23
|
+
if (result.dropped) {
|
|
24
|
+
expect(result.reason).toBe(reason);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
4
27
|
|
|
5
28
|
describe("normalizeTelegramUpdate", () => {
|
|
6
29
|
const validPayload = {
|
|
@@ -21,36 +44,36 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
21
44
|
};
|
|
22
45
|
|
|
23
46
|
test("normalizes a valid private text message", () => {
|
|
24
|
-
const result = normalizeTelegramUpdate(validPayload);
|
|
25
|
-
expect(result).
|
|
26
|
-
expect(result
|
|
27
|
-
expect(result
|
|
28
|
-
expect(result
|
|
29
|
-
expect(result
|
|
30
|
-
expect(result
|
|
31
|
-
expect(result
|
|
32
|
-
expect(result
|
|
33
|
-
expect(result
|
|
34
|
-
expect(result
|
|
35
|
-
expect(result
|
|
36
|
-
expect(result
|
|
37
|
-
expect(result
|
|
38
|
-
expect(result
|
|
39
|
-
expect(result
|
|
40
|
-
expect(result
|
|
41
|
-
expect(result!.raw).toEqual(validPayload);
|
|
47
|
+
const result = eventOf(normalizeTelegramUpdate(validPayload));
|
|
48
|
+
expect(result.version).toBe("v1");
|
|
49
|
+
expect(result.sourceChannel).toBe("telegram");
|
|
50
|
+
expect(result.message.content).toBe("Hello bot");
|
|
51
|
+
expect(result.message.conversationExternalId).toBe("99001");
|
|
52
|
+
expect(result.message.externalMessageId).toBe("123456");
|
|
53
|
+
expect(result.actor.actorExternalId).toBe("55001");
|
|
54
|
+
expect(result.actor.username).toBe("testuser");
|
|
55
|
+
expect(result.actor.displayName).toBe("Test User");
|
|
56
|
+
expect(result.actor.firstName).toBe("Test");
|
|
57
|
+
expect(result.actor.lastName).toBe("User");
|
|
58
|
+
expect(result.actor.languageCode).toBe("en");
|
|
59
|
+
expect(result.actor.isBot).toBe(false);
|
|
60
|
+
expect(result.source.updateId).toBe("123456");
|
|
61
|
+
expect(result.source.messageId).toBe("42");
|
|
62
|
+
expect(result.source.chatType).toBe("private");
|
|
63
|
+
expect(result.raw).toEqual(validPayload);
|
|
42
64
|
});
|
|
43
65
|
|
|
44
|
-
test("
|
|
66
|
+
test("drops unsupported message types (e.g. sticker-only)", () => {
|
|
45
67
|
const payload = {
|
|
46
68
|
update_id: 1,
|
|
47
69
|
message: {
|
|
48
70
|
message_id: 1,
|
|
49
71
|
chat: { id: 1, type: "private" },
|
|
72
|
+
from: { id: 7 },
|
|
50
73
|
sticker: { file_id: "abc" },
|
|
51
74
|
},
|
|
52
75
|
};
|
|
53
|
-
|
|
76
|
+
expectDrop(normalizeTelegramUpdate(payload), "no_supported_content");
|
|
54
77
|
});
|
|
55
78
|
|
|
56
79
|
test("normalizes a photo message", () => {
|
|
@@ -83,11 +106,10 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
83
106
|
caption: "Check this out",
|
|
84
107
|
},
|
|
85
108
|
};
|
|
86
|
-
const result = normalizeTelegramUpdate(payload);
|
|
87
|
-
expect(result).
|
|
88
|
-
expect(result
|
|
89
|
-
expect(result
|
|
90
|
-
expect(result!.message.attachments![0]).toEqual({
|
|
109
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
110
|
+
expect(result.message.content).toBe("Check this out");
|
|
111
|
+
expect(result.message.attachments).toHaveLength(1);
|
|
112
|
+
expect(result.message.attachments![0]).toEqual({
|
|
91
113
|
type: "photo",
|
|
92
114
|
fileId: "large_id",
|
|
93
115
|
fileSize: undefined,
|
|
@@ -106,11 +128,10 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
106
128
|
],
|
|
107
129
|
},
|
|
108
130
|
};
|
|
109
|
-
const result = normalizeTelegramUpdate(payload);
|
|
110
|
-
expect(result).
|
|
111
|
-
expect(result
|
|
112
|
-
expect(result
|
|
113
|
-
expect(result!.message.attachments![0].fileId).toBe("only_id");
|
|
131
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
132
|
+
expect(result.message.content).toBe("");
|
|
133
|
+
expect(result.message.attachments).toHaveLength(1);
|
|
134
|
+
expect(result.message.attachments![0].fileId).toBe("only_id");
|
|
114
135
|
});
|
|
115
136
|
|
|
116
137
|
test("normalizes a document message", () => {
|
|
@@ -130,11 +151,10 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
130
151
|
caption: "Here is the report",
|
|
131
152
|
},
|
|
132
153
|
};
|
|
133
|
-
const result = normalizeTelegramUpdate(payload);
|
|
134
|
-
expect(result).
|
|
135
|
-
expect(result
|
|
136
|
-
expect(result
|
|
137
|
-
expect(result!.message.attachments![0]).toEqual({
|
|
154
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
155
|
+
expect(result.message.content).toBe("Here is the report");
|
|
156
|
+
expect(result.message.attachments).toHaveLength(1);
|
|
157
|
+
expect(result.message.attachments![0]).toEqual({
|
|
138
158
|
type: "document",
|
|
139
159
|
fileId: "doc_file_id",
|
|
140
160
|
fileName: "report.pdf",
|
|
@@ -158,40 +178,38 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
158
178
|
},
|
|
159
179
|
},
|
|
160
180
|
};
|
|
161
|
-
const result = normalizeTelegramUpdate(payload);
|
|
162
|
-
expect(result).
|
|
163
|
-
expect(result
|
|
164
|
-
expect(result!.message.attachments).toHaveLength(1);
|
|
181
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
182
|
+
expect(result.message.content).toBe("");
|
|
183
|
+
expect(result.message.attachments).toHaveLength(1);
|
|
165
184
|
});
|
|
166
185
|
|
|
167
186
|
test("text-only messages have no attachments field", () => {
|
|
168
|
-
const result = normalizeTelegramUpdate(validPayload);
|
|
169
|
-
expect(result).
|
|
170
|
-
expect(result!.message.attachments).toBeUndefined();
|
|
187
|
+
const result = eventOf(normalizeTelegramUpdate(validPayload));
|
|
188
|
+
expect(result.message.attachments).toBeUndefined();
|
|
171
189
|
});
|
|
172
190
|
|
|
173
|
-
test("
|
|
191
|
+
test("drops group messages when the bot's identity is unknown", () => {
|
|
174
192
|
const payload = {
|
|
175
193
|
...validPayload,
|
|
176
194
|
message: { ...validPayload.message, chat: { id: 99001, type: "group" } },
|
|
177
195
|
};
|
|
178
|
-
|
|
196
|
+
expectDrop(normalizeTelegramUpdate(payload), "bot_identity_unknown");
|
|
179
197
|
});
|
|
180
198
|
|
|
181
|
-
test("
|
|
199
|
+
test("drops payloads without update_id", () => {
|
|
182
200
|
const { update_id: _, ...rest } = validPayload;
|
|
183
|
-
|
|
201
|
+
expectDrop(normalizeTelegramUpdate(rest), "missing_update_id");
|
|
184
202
|
});
|
|
185
203
|
|
|
186
|
-
test("
|
|
204
|
+
test("drops payloads without chat id", () => {
|
|
187
205
|
const payload = {
|
|
188
206
|
update_id: 1,
|
|
189
207
|
message: { message_id: 1, text: "hello", chat: {} },
|
|
190
208
|
};
|
|
191
|
-
|
|
209
|
+
expectDrop(normalizeTelegramUpdate(payload), "missing_chat");
|
|
192
210
|
});
|
|
193
211
|
|
|
194
|
-
test("
|
|
212
|
+
test("drops a message when from.id is missing", () => {
|
|
195
213
|
const payload = {
|
|
196
214
|
update_id: 1,
|
|
197
215
|
message: {
|
|
@@ -201,15 +219,15 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
201
219
|
},
|
|
202
220
|
};
|
|
203
221
|
const result = normalizeTelegramUpdate(payload);
|
|
204
|
-
|
|
222
|
+
expectDrop(result, "missing_sender");
|
|
205
223
|
});
|
|
206
224
|
|
|
207
|
-
test("
|
|
225
|
+
test("drops callback_query without message context", () => {
|
|
208
226
|
const payload = {
|
|
209
227
|
update_id: 1,
|
|
210
228
|
callback_query: { id: "abc", from: { id: 123 }, data: "some_data" },
|
|
211
229
|
};
|
|
212
|
-
|
|
230
|
+
expectDrop(normalizeTelegramUpdate(payload), "callback_without_message");
|
|
213
231
|
});
|
|
214
232
|
|
|
215
233
|
test("normalizes an edited_message update as an edit", () => {
|
|
@@ -227,15 +245,14 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
227
245
|
},
|
|
228
246
|
},
|
|
229
247
|
};
|
|
230
|
-
const result = normalizeTelegramUpdate(payload);
|
|
231
|
-
expect(result).
|
|
232
|
-
expect(result
|
|
233
|
-
expect(result
|
|
234
|
-
expect(result
|
|
235
|
-
expect(result
|
|
236
|
-
expect(result
|
|
237
|
-
expect(result
|
|
238
|
-
expect(result!.actor.actorExternalId).toBe("55001");
|
|
248
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
249
|
+
expect(result.message.eventKind).toBe("edit");
|
|
250
|
+
expect(result.message.content).toBe("Hello bot (edited)");
|
|
251
|
+
expect(result.message.conversationExternalId).toBe("99001");
|
|
252
|
+
expect(result.message.externalMessageId).toBe("200");
|
|
253
|
+
expect(result.source.updateId).toBe("200");
|
|
254
|
+
expect(result.source.messageId).toBe("42");
|
|
255
|
+
expect(result.actor.actorExternalId).toBe("55001");
|
|
239
256
|
});
|
|
240
257
|
|
|
241
258
|
test("prefers message over edited_message when both are present", () => {
|
|
@@ -254,10 +271,9 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
254
271
|
from: { id: 55001, is_bot: false },
|
|
255
272
|
},
|
|
256
273
|
};
|
|
257
|
-
const result = normalizeTelegramUpdate(payload);
|
|
258
|
-
expect(result).
|
|
259
|
-
expect(result
|
|
260
|
-
expect(result!.message.content).toBe("Original");
|
|
274
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
275
|
+
expect(result.message.isEdit).toBeUndefined();
|
|
276
|
+
expect(result.message.content).toBe("Original");
|
|
261
277
|
});
|
|
262
278
|
|
|
263
279
|
test("classifies an edited_message with photo as an edit", () => {
|
|
@@ -278,14 +294,13 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
278
294
|
caption: "Updated caption",
|
|
279
295
|
},
|
|
280
296
|
};
|
|
281
|
-
const result = normalizeTelegramUpdate(payload);
|
|
282
|
-
expect(result).
|
|
283
|
-
expect(result
|
|
284
|
-
expect(result
|
|
285
|
-
expect(result!.message.attachments).toHaveLength(1);
|
|
297
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
298
|
+
expect(result.message.eventKind).toBe("edit");
|
|
299
|
+
expect(result.message.content).toBe("Updated caption");
|
|
300
|
+
expect(result.message.attachments).toHaveLength(1);
|
|
286
301
|
});
|
|
287
302
|
|
|
288
|
-
test("
|
|
303
|
+
test("drops edited_message in a group when the bot's identity is unknown", () => {
|
|
289
304
|
const payload = {
|
|
290
305
|
update_id: 500,
|
|
291
306
|
edited_message: {
|
|
@@ -295,7 +310,7 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
295
310
|
from: { id: 55001, is_bot: false },
|
|
296
311
|
},
|
|
297
312
|
};
|
|
298
|
-
|
|
313
|
+
expectDrop(normalizeTelegramUpdate(payload), "bot_identity_unknown");
|
|
299
314
|
});
|
|
300
315
|
});
|
|
301
316
|
|
|
@@ -322,30 +337,29 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
322
337
|
},
|
|
323
338
|
};
|
|
324
339
|
|
|
325
|
-
const result = normalizeTelegramUpdate(payload);
|
|
326
|
-
|
|
327
|
-
expect(result).
|
|
328
|
-
expect(result
|
|
329
|
-
expect(result
|
|
330
|
-
expect(result
|
|
331
|
-
expect(result
|
|
332
|
-
expect(result
|
|
333
|
-
expect(result
|
|
334
|
-
expect(result
|
|
335
|
-
expect(result
|
|
336
|
-
expect(result
|
|
337
|
-
expect(result
|
|
338
|
-
expect(result
|
|
339
|
-
expect(result
|
|
340
|
-
expect(result
|
|
341
|
-
expect(result
|
|
342
|
-
expect(result
|
|
343
|
-
expect(result
|
|
344
|
-
expect(result
|
|
345
|
-
expect(result!.source.chatType).toBe("private");
|
|
340
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
341
|
+
|
|
342
|
+
expect(result.version).toBe("v1");
|
|
343
|
+
expect(result.sourceChannel).toBe("telegram");
|
|
344
|
+
expect(result.message.content).toBe("apr:run-abc:approve");
|
|
345
|
+
expect(result.message.conversationExternalId).toBe("12345");
|
|
346
|
+
expect(result.message.externalMessageId).toBe("5001");
|
|
347
|
+
expect(result.message.callbackQueryId).toBe("cbq-123");
|
|
348
|
+
expect(result.message.callbackData).toBe("apr:run-abc:approve");
|
|
349
|
+
expect(result.message.attachments).toBeUndefined();
|
|
350
|
+
expect(result.actor.actorExternalId).toBe("67890");
|
|
351
|
+
expect(result.actor.username).toBe("testuser");
|
|
352
|
+
expect(result.actor.displayName).toBe("Test User");
|
|
353
|
+
expect(result.actor.firstName).toBe("Test");
|
|
354
|
+
expect(result.actor.lastName).toBe("User");
|
|
355
|
+
expect(result.actor.languageCode).toBe("en");
|
|
356
|
+
expect(result.actor.isBot).toBe(false);
|
|
357
|
+
expect(result.source.updateId).toBe("5001");
|
|
358
|
+
expect(result.source.messageId).toBe("42");
|
|
359
|
+
expect(result.source.chatType).toBe("private");
|
|
346
360
|
});
|
|
347
361
|
|
|
348
|
-
test("
|
|
362
|
+
test("drops a callback_query with no message (inline mode edge case)", () => {
|
|
349
363
|
const payload = {
|
|
350
364
|
update_id: 5002,
|
|
351
365
|
callback_query: {
|
|
@@ -361,10 +375,10 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
361
375
|
};
|
|
362
376
|
|
|
363
377
|
const result = normalizeTelegramUpdate(payload);
|
|
364
|
-
|
|
378
|
+
expectDrop(result, "callback_without_message");
|
|
365
379
|
});
|
|
366
380
|
|
|
367
|
-
test("
|
|
381
|
+
test("drops a callback_query with no data", () => {
|
|
368
382
|
const payload = {
|
|
369
383
|
update_id: 5003,
|
|
370
384
|
callback_query: {
|
|
@@ -384,10 +398,10 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
384
398
|
};
|
|
385
399
|
|
|
386
400
|
const result = normalizeTelegramUpdate(payload);
|
|
387
|
-
|
|
401
|
+
expectDrop(result, "callback_without_data");
|
|
388
402
|
});
|
|
389
403
|
|
|
390
|
-
test("
|
|
404
|
+
test("drops a callback_query whose message has no chat id", () => {
|
|
391
405
|
const payload = {
|
|
392
406
|
update_id: 5004,
|
|
393
407
|
callback_query: {
|
|
@@ -408,10 +422,10 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
408
422
|
};
|
|
409
423
|
|
|
410
424
|
const result = normalizeTelegramUpdate(payload);
|
|
411
|
-
|
|
425
|
+
expectDrop(result, "callback_without_message");
|
|
412
426
|
});
|
|
413
427
|
|
|
414
|
-
test("
|
|
428
|
+
test("drops a callback_query when from.id is missing", () => {
|
|
415
429
|
const payload = {
|
|
416
430
|
update_id: 5005,
|
|
417
431
|
callback_query: {
|
|
@@ -427,7 +441,7 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
427
441
|
};
|
|
428
442
|
|
|
429
443
|
const result = normalizeTelegramUpdate(payload);
|
|
430
|
-
|
|
444
|
+
expectDrop(result, "missing_sender");
|
|
431
445
|
});
|
|
432
446
|
|
|
433
447
|
test("callback_query does not set isEdit or attachments", () => {
|
|
@@ -450,10 +464,9 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
450
464
|
},
|
|
451
465
|
};
|
|
452
466
|
|
|
453
|
-
const result = normalizeTelegramUpdate(payload);
|
|
454
|
-
expect(result).
|
|
455
|
-
expect(result
|
|
456
|
-
expect(result!.message.attachments).toBeUndefined();
|
|
467
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
468
|
+
expect(result.message.isEdit).toBeUndefined();
|
|
469
|
+
expect(result.message.attachments).toBeUndefined();
|
|
457
470
|
});
|
|
458
471
|
|
|
459
472
|
test("regular text messages are unaffected by callback_query support", () => {
|
|
@@ -472,11 +485,10 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
472
485
|
},
|
|
473
486
|
};
|
|
474
487
|
|
|
475
|
-
const result = normalizeTelegramUpdate(payload);
|
|
476
|
-
expect(result).
|
|
477
|
-
expect(result
|
|
478
|
-
expect(result
|
|
479
|
-
expect(result!.message.callbackData).toBeUndefined();
|
|
488
|
+
const result = eventOf(normalizeTelegramUpdate(payload));
|
|
489
|
+
expect(result.message.content).toBe("Hello world");
|
|
490
|
+
expect(result.message.callbackQueryId).toBeUndefined();
|
|
491
|
+
expect(result.message.callbackData).toBeUndefined();
|
|
480
492
|
});
|
|
481
493
|
});
|
|
482
494
|
|