@vellumai/vellum-gateway 0.12.2-staging.7 → 0.12.2
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 +1 -3
- package/README.md +1 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +0 -21
- package/node_modules/@vellumai/environments/src/shell.ts +0 -24
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +2 -8
- package/package.json +1 -1
- package/src/__tests__/telegram-normalize.test.ts +111 -123
- package/src/__tests__/telegram-webhook-handler.test.ts +0 -160
- package/src/channels/inbound-event.ts +0 -9
- package/src/discord/admission-log.test.ts +12 -15
- package/src/discord/admission-log.ts +95 -11
- package/src/discord/admit.test.ts +9 -8
- package/src/discord/admit.ts +93 -48
- package/src/discord/gateway-socket.ts +2 -4
- package/src/discord/normalize.test.ts +1 -34
- package/src/discord/normalize.ts +3 -23
- package/src/handlers/handle-inbound.ts +0 -3
- package/src/http/routes/telegram-webhook.ts +9 -50
- package/src/risk/bash-risk-classifier.test.ts +0 -63
- package/src/risk/command-registry/commands/dig.ts +1 -2
- package/src/risk/command-registry/commands/host.ts +1 -2
- package/src/risk/command-registry/commands/mtr.ts +1 -2
- package/src/risk/command-registry/commands/nslookup.ts +1 -2
- package/src/risk/command-registry/commands/ping.ts +1 -2
- package/src/risk/command-registry/commands/tracepath.ts +1 -2
- package/src/risk/command-registry/commands/traceroute.ts +1 -2
- package/src/risk/risk-types.ts +0 -1
- package/src/risk/shell-parser.test.ts +1 -82
- package/src/risk/shell-parser.ts +2 -50
- package/src/slack/message-normalizer.ts +1 -14
- package/src/slack/source-metadata.test.ts +2 -5
- package/src/slack/source-metadata.ts +6 -2
- package/src/telegram/normalize.test.ts +134 -343
- package/src/telegram/normalize.ts +197 -197
- package/src/telegram/send.ts +1 -1
- package/src/__tests__/telegram-webhook-drop-visibility.test.ts +0 -229
- package/src/channels/admission-drop-log.test.ts +0 -47
- package/src/channels/admission-drop-log.ts +0 -94
- package/src/channels/room-admission.test.ts +0 -95
- package/src/channels/room-admission.ts +0 -149
- package/src/telegram/admit.test.ts +0 -155
- package/src/telegram/admit.ts +0 -149
- package/src/telegram/bot-identity.test.ts +0 -124
- package/src/telegram/bot-identity.ts +0 -127
- package/src/telegram/drop-log.ts +0 -41
- package/src/telegram/schemas.ts +0 -217
package/AGENTS.md
CHANGED
|
@@ -90,8 +90,6 @@ 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
|
-
|
|
95
93
|
Physical DB column names (`externalUserId`, `externalChatId`) are unchanged; the rename is at the API/type layer only.
|
|
96
94
|
|
|
97
95
|
**Provider words that are not our words.** A Discord **guild** is what Discord's
|
|
@@ -195,7 +193,7 @@ Both the flat (`/v1/channel-admission-policy/...`) and assistant-scoped (`/v1/as
|
|
|
195
193
|
|
|
196
194
|
- **Gateway kill switch** — `handle-inbound.ts` enforces the `no_one` floor before forwarding. Zero contact-table lookups, zero daemon I/O, true kill.
|
|
197
195
|
- **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.
|
|
198
|
-
- **Gateway vs runtime reciprocity
|
|
196
|
+
- **Gateway vs runtime reciprocity** — the gateway section in `gateway/CLAUDE.md` records _which channels the gateway enforces_; the assistant section records _how the runtime classifies_. Either side getting out of sync is a bug, not an over-defended boundary.
|
|
199
197
|
|
|
200
198
|
**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.
|
|
201
199
|
|
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:** Only DM-only (`private` chat type) callback queries are processed. Group and channel callbacks 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.
|
|
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,7 +2,6 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
buildShellInvocation,
|
|
5
|
-
buildShellSpawnFlags,
|
|
6
5
|
pathListDelimiter,
|
|
7
6
|
prependUniquePathEntries,
|
|
8
7
|
} from "./shell.js";
|
|
@@ -39,26 +38,6 @@ describe("buildShellInvocation", () => {
|
|
|
39
38
|
});
|
|
40
39
|
});
|
|
41
40
|
|
|
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
|
-
|
|
62
41
|
describe("path list handling", () => {
|
|
63
42
|
test("uses the platform delimiter", () => {
|
|
64
43
|
expect(pathListDelimiter("win32")).toBe(";");
|
|
@@ -5,11 +5,6 @@ export interface ShellInvocation {
|
|
|
5
5
|
args: string[];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export interface ShellSpawnFlags {
|
|
9
|
-
detached: boolean;
|
|
10
|
-
windowsHide: true;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
const WINDOWS_UTF8_PREAMBLE =
|
|
14
9
|
"try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}; " +
|
|
15
10
|
"$OutputEncoding = [System.Text.Encoding]::UTF8; " +
|
|
@@ -48,25 +43,6 @@ export function buildShellInvocation(
|
|
|
48
43
|
return { command: "bash", args: ["-c", "--", command] };
|
|
49
44
|
}
|
|
50
45
|
|
|
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
|
-
|
|
70
46
|
export function pathListDelimiter(
|
|
71
47
|
hostPlatform: NodeJS.Platform = process.platform,
|
|
72
48
|
): string {
|
|
@@ -83,14 +83,8 @@ 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
|
-
|
|
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(),
|
|
86
|
+
/** Slack-specific: whether the bot was @-mentioned. */
|
|
87
|
+
slackBotMentioned: z.boolean().optional(),
|
|
94
88
|
/**
|
|
95
89
|
* Slack-specific: the `edited.ts` float-string from a `message_changed`
|
|
96
90
|
* event. Used by the daemon to reject out-of-order edit deliveries whose
|
package/package.json
CHANGED
|
@@ -1,29 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
normalizeTelegramUpdate,
|
|
4
|
-
type TelegramDropReason,
|
|
5
|
-
type TelegramNormalization,
|
|
6
|
-
} from "../telegram/normalize.js";
|
|
2
|
+
import { normalizeTelegramUpdate } from "../telegram/normalize.js";
|
|
7
3
|
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
|
-
}
|
|
27
4
|
|
|
28
5
|
describe("normalizeTelegramUpdate", () => {
|
|
29
6
|
const validPayload = {
|
|
@@ -44,36 +21,36 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
44
21
|
};
|
|
45
22
|
|
|
46
23
|
test("normalizes a valid private text message", () => {
|
|
47
|
-
const result =
|
|
48
|
-
expect(result
|
|
49
|
-
expect(result
|
|
50
|
-
expect(result
|
|
51
|
-
expect(result
|
|
52
|
-
expect(result
|
|
53
|
-
expect(result.
|
|
54
|
-
expect(result
|
|
55
|
-
expect(result
|
|
56
|
-
expect(result
|
|
57
|
-
expect(result
|
|
58
|
-
expect(result
|
|
59
|
-
expect(result
|
|
60
|
-
expect(result.
|
|
61
|
-
expect(result
|
|
62
|
-
expect(result
|
|
63
|
-
expect(result.
|
|
24
|
+
const result = normalizeTelegramUpdate(validPayload);
|
|
25
|
+
expect(result).not.toBeNull();
|
|
26
|
+
expect(result!.version).toBe("v1");
|
|
27
|
+
expect(result!.sourceChannel).toBe("telegram");
|
|
28
|
+
expect(result!.message.content).toBe("Hello bot");
|
|
29
|
+
expect(result!.message.conversationExternalId).toBe("99001");
|
|
30
|
+
expect(result!.message.externalMessageId).toBe("123456");
|
|
31
|
+
expect(result!.actor.actorExternalId).toBe("55001");
|
|
32
|
+
expect(result!.actor.username).toBe("testuser");
|
|
33
|
+
expect(result!.actor.displayName).toBe("Test User");
|
|
34
|
+
expect(result!.actor.firstName).toBe("Test");
|
|
35
|
+
expect(result!.actor.lastName).toBe("User");
|
|
36
|
+
expect(result!.actor.languageCode).toBe("en");
|
|
37
|
+
expect(result!.actor.isBot).toBe(false);
|
|
38
|
+
expect(result!.source.updateId).toBe("123456");
|
|
39
|
+
expect(result!.source.messageId).toBe("42");
|
|
40
|
+
expect(result!.source.chatType).toBe("private");
|
|
41
|
+
expect(result!.raw).toEqual(validPayload);
|
|
64
42
|
});
|
|
65
43
|
|
|
66
|
-
test("
|
|
44
|
+
test("returns null for unsupported message types (e.g. sticker-only)", () => {
|
|
67
45
|
const payload = {
|
|
68
46
|
update_id: 1,
|
|
69
47
|
message: {
|
|
70
48
|
message_id: 1,
|
|
71
49
|
chat: { id: 1, type: "private" },
|
|
72
|
-
from: { id: 7 },
|
|
73
50
|
sticker: { file_id: "abc" },
|
|
74
51
|
},
|
|
75
52
|
};
|
|
76
|
-
|
|
53
|
+
expect(normalizeTelegramUpdate(payload)).toBeNull();
|
|
77
54
|
});
|
|
78
55
|
|
|
79
56
|
test("normalizes a photo message", () => {
|
|
@@ -106,10 +83,11 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
106
83
|
caption: "Check this out",
|
|
107
84
|
},
|
|
108
85
|
};
|
|
109
|
-
const result =
|
|
110
|
-
expect(result.
|
|
111
|
-
expect(result
|
|
112
|
-
expect(result
|
|
86
|
+
const result = normalizeTelegramUpdate(payload);
|
|
87
|
+
expect(result).not.toBeNull();
|
|
88
|
+
expect(result!.message.content).toBe("Check this out");
|
|
89
|
+
expect(result!.message.attachments).toHaveLength(1);
|
|
90
|
+
expect(result!.message.attachments![0]).toEqual({
|
|
113
91
|
type: "photo",
|
|
114
92
|
fileId: "large_id",
|
|
115
93
|
fileSize: undefined,
|
|
@@ -128,10 +106,11 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
128
106
|
],
|
|
129
107
|
},
|
|
130
108
|
};
|
|
131
|
-
const result =
|
|
132
|
-
expect(result.
|
|
133
|
-
expect(result
|
|
134
|
-
expect(result
|
|
109
|
+
const result = normalizeTelegramUpdate(payload);
|
|
110
|
+
expect(result).not.toBeNull();
|
|
111
|
+
expect(result!.message.content).toBe("");
|
|
112
|
+
expect(result!.message.attachments).toHaveLength(1);
|
|
113
|
+
expect(result!.message.attachments![0].fileId).toBe("only_id");
|
|
135
114
|
});
|
|
136
115
|
|
|
137
116
|
test("normalizes a document message", () => {
|
|
@@ -151,10 +130,11 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
151
130
|
caption: "Here is the report",
|
|
152
131
|
},
|
|
153
132
|
};
|
|
154
|
-
const result =
|
|
155
|
-
expect(result.
|
|
156
|
-
expect(result
|
|
157
|
-
expect(result
|
|
133
|
+
const result = normalizeTelegramUpdate(payload);
|
|
134
|
+
expect(result).not.toBeNull();
|
|
135
|
+
expect(result!.message.content).toBe("Here is the report");
|
|
136
|
+
expect(result!.message.attachments).toHaveLength(1);
|
|
137
|
+
expect(result!.message.attachments![0]).toEqual({
|
|
158
138
|
type: "document",
|
|
159
139
|
fileId: "doc_file_id",
|
|
160
140
|
fileName: "report.pdf",
|
|
@@ -178,38 +158,40 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
178
158
|
},
|
|
179
159
|
},
|
|
180
160
|
};
|
|
181
|
-
const result =
|
|
182
|
-
expect(result.
|
|
183
|
-
expect(result
|
|
161
|
+
const result = normalizeTelegramUpdate(payload);
|
|
162
|
+
expect(result).not.toBeNull();
|
|
163
|
+
expect(result!.message.content).toBe("");
|
|
164
|
+
expect(result!.message.attachments).toHaveLength(1);
|
|
184
165
|
});
|
|
185
166
|
|
|
186
167
|
test("text-only messages have no attachments field", () => {
|
|
187
|
-
const result =
|
|
188
|
-
expect(result.
|
|
168
|
+
const result = normalizeTelegramUpdate(validPayload);
|
|
169
|
+
expect(result).not.toBeNull();
|
|
170
|
+
expect(result!.message.attachments).toBeUndefined();
|
|
189
171
|
});
|
|
190
172
|
|
|
191
|
-
test("
|
|
173
|
+
test("returns null for group messages", () => {
|
|
192
174
|
const payload = {
|
|
193
175
|
...validPayload,
|
|
194
176
|
message: { ...validPayload.message, chat: { id: 99001, type: "group" } },
|
|
195
177
|
};
|
|
196
|
-
|
|
178
|
+
expect(normalizeTelegramUpdate(payload)).toBeNull();
|
|
197
179
|
});
|
|
198
180
|
|
|
199
|
-
test("
|
|
181
|
+
test("returns null for payloads without update_id", () => {
|
|
200
182
|
const { update_id: _, ...rest } = validPayload;
|
|
201
|
-
|
|
183
|
+
expect(normalizeTelegramUpdate(rest)).toBeNull();
|
|
202
184
|
});
|
|
203
185
|
|
|
204
|
-
test("
|
|
186
|
+
test("returns null for payloads without chat id", () => {
|
|
205
187
|
const payload = {
|
|
206
188
|
update_id: 1,
|
|
207
189
|
message: { message_id: 1, text: "hello", chat: {} },
|
|
208
190
|
};
|
|
209
|
-
|
|
191
|
+
expect(normalizeTelegramUpdate(payload)).toBeNull();
|
|
210
192
|
});
|
|
211
193
|
|
|
212
|
-
test("
|
|
194
|
+
test("returns null when from.id is missing", () => {
|
|
213
195
|
const payload = {
|
|
214
196
|
update_id: 1,
|
|
215
197
|
message: {
|
|
@@ -219,15 +201,15 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
219
201
|
},
|
|
220
202
|
};
|
|
221
203
|
const result = normalizeTelegramUpdate(payload);
|
|
222
|
-
|
|
204
|
+
expect(result).toBeNull();
|
|
223
205
|
});
|
|
224
206
|
|
|
225
|
-
test("
|
|
207
|
+
test("returns null for callback_query without message context", () => {
|
|
226
208
|
const payload = {
|
|
227
209
|
update_id: 1,
|
|
228
210
|
callback_query: { id: "abc", from: { id: 123 }, data: "some_data" },
|
|
229
211
|
};
|
|
230
|
-
|
|
212
|
+
expect(normalizeTelegramUpdate(payload)).toBeNull();
|
|
231
213
|
});
|
|
232
214
|
|
|
233
215
|
test("normalizes an edited_message update as an edit", () => {
|
|
@@ -245,14 +227,15 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
245
227
|
},
|
|
246
228
|
},
|
|
247
229
|
};
|
|
248
|
-
const result =
|
|
249
|
-
expect(result.
|
|
250
|
-
expect(result
|
|
251
|
-
expect(result
|
|
252
|
-
expect(result
|
|
253
|
-
expect(result.
|
|
254
|
-
expect(result
|
|
255
|
-
expect(result.
|
|
230
|
+
const result = normalizeTelegramUpdate(payload);
|
|
231
|
+
expect(result).not.toBeNull();
|
|
232
|
+
expect(result!.message.eventKind).toBe("edit");
|
|
233
|
+
expect(result!.message.content).toBe("Hello bot (edited)");
|
|
234
|
+
expect(result!.message.conversationExternalId).toBe("99001");
|
|
235
|
+
expect(result!.message.externalMessageId).toBe("200");
|
|
236
|
+
expect(result!.source.updateId).toBe("200");
|
|
237
|
+
expect(result!.source.messageId).toBe("42");
|
|
238
|
+
expect(result!.actor.actorExternalId).toBe("55001");
|
|
256
239
|
});
|
|
257
240
|
|
|
258
241
|
test("prefers message over edited_message when both are present", () => {
|
|
@@ -271,9 +254,10 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
271
254
|
from: { id: 55001, is_bot: false },
|
|
272
255
|
},
|
|
273
256
|
};
|
|
274
|
-
const result =
|
|
275
|
-
expect(result.
|
|
276
|
-
expect(result
|
|
257
|
+
const result = normalizeTelegramUpdate(payload);
|
|
258
|
+
expect(result).not.toBeNull();
|
|
259
|
+
expect(result!.message.isEdit).toBeUndefined();
|
|
260
|
+
expect(result!.message.content).toBe("Original");
|
|
277
261
|
});
|
|
278
262
|
|
|
279
263
|
test("classifies an edited_message with photo as an edit", () => {
|
|
@@ -294,13 +278,14 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
294
278
|
caption: "Updated caption",
|
|
295
279
|
},
|
|
296
280
|
};
|
|
297
|
-
const result =
|
|
298
|
-
expect(result.
|
|
299
|
-
expect(result
|
|
300
|
-
expect(result
|
|
281
|
+
const result = normalizeTelegramUpdate(payload);
|
|
282
|
+
expect(result).not.toBeNull();
|
|
283
|
+
expect(result!.message.eventKind).toBe("edit");
|
|
284
|
+
expect(result!.message.content).toBe("Updated caption");
|
|
285
|
+
expect(result!.message.attachments).toHaveLength(1);
|
|
301
286
|
});
|
|
302
287
|
|
|
303
|
-
test("
|
|
288
|
+
test("returns null for edited_message in group chat", () => {
|
|
304
289
|
const payload = {
|
|
305
290
|
update_id: 500,
|
|
306
291
|
edited_message: {
|
|
@@ -310,7 +295,7 @@ describe("normalizeTelegramUpdate", () => {
|
|
|
310
295
|
from: { id: 55001, is_bot: false },
|
|
311
296
|
},
|
|
312
297
|
};
|
|
313
|
-
|
|
298
|
+
expect(normalizeTelegramUpdate(payload)).toBeNull();
|
|
314
299
|
});
|
|
315
300
|
});
|
|
316
301
|
|
|
@@ -337,29 +322,30 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
337
322
|
},
|
|
338
323
|
};
|
|
339
324
|
|
|
340
|
-
const result =
|
|
341
|
-
|
|
342
|
-
expect(result
|
|
343
|
-
expect(result
|
|
344
|
-
expect(result
|
|
345
|
-
expect(result
|
|
346
|
-
expect(result
|
|
347
|
-
expect(result
|
|
348
|
-
expect(result
|
|
349
|
-
expect(result
|
|
350
|
-
expect(result.
|
|
351
|
-
expect(result
|
|
352
|
-
expect(result
|
|
353
|
-
expect(result
|
|
354
|
-
expect(result
|
|
355
|
-
expect(result
|
|
356
|
-
expect(result
|
|
357
|
-
expect(result.
|
|
358
|
-
expect(result
|
|
359
|
-
expect(result
|
|
325
|
+
const result = normalizeTelegramUpdate(payload);
|
|
326
|
+
|
|
327
|
+
expect(result).not.toBeNull();
|
|
328
|
+
expect(result!.version).toBe("v1");
|
|
329
|
+
expect(result!.sourceChannel).toBe("telegram");
|
|
330
|
+
expect(result!.message.content).toBe("apr:run-abc:approve");
|
|
331
|
+
expect(result!.message.conversationExternalId).toBe("12345");
|
|
332
|
+
expect(result!.message.externalMessageId).toBe("5001");
|
|
333
|
+
expect(result!.message.callbackQueryId).toBe("cbq-123");
|
|
334
|
+
expect(result!.message.callbackData).toBe("apr:run-abc:approve");
|
|
335
|
+
expect(result!.message.attachments).toBeUndefined();
|
|
336
|
+
expect(result!.actor.actorExternalId).toBe("67890");
|
|
337
|
+
expect(result!.actor.username).toBe("testuser");
|
|
338
|
+
expect(result!.actor.displayName).toBe("Test User");
|
|
339
|
+
expect(result!.actor.firstName).toBe("Test");
|
|
340
|
+
expect(result!.actor.lastName).toBe("User");
|
|
341
|
+
expect(result!.actor.languageCode).toBe("en");
|
|
342
|
+
expect(result!.actor.isBot).toBe(false);
|
|
343
|
+
expect(result!.source.updateId).toBe("5001");
|
|
344
|
+
expect(result!.source.messageId).toBe("42");
|
|
345
|
+
expect(result!.source.chatType).toBe("private");
|
|
360
346
|
});
|
|
361
347
|
|
|
362
|
-
test("
|
|
348
|
+
test("returns null when callback_query has no message (inline mode edge case)", () => {
|
|
363
349
|
const payload = {
|
|
364
350
|
update_id: 5002,
|
|
365
351
|
callback_query: {
|
|
@@ -375,10 +361,10 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
375
361
|
};
|
|
376
362
|
|
|
377
363
|
const result = normalizeTelegramUpdate(payload);
|
|
378
|
-
|
|
364
|
+
expect(result).toBeNull();
|
|
379
365
|
});
|
|
380
366
|
|
|
381
|
-
test("
|
|
367
|
+
test("returns null when callback_query has no data", () => {
|
|
382
368
|
const payload = {
|
|
383
369
|
update_id: 5003,
|
|
384
370
|
callback_query: {
|
|
@@ -398,10 +384,10 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
398
384
|
};
|
|
399
385
|
|
|
400
386
|
const result = normalizeTelegramUpdate(payload);
|
|
401
|
-
|
|
387
|
+
expect(result).toBeNull();
|
|
402
388
|
});
|
|
403
389
|
|
|
404
|
-
test("
|
|
390
|
+
test("returns null when callback_query message has no chat id", () => {
|
|
405
391
|
const payload = {
|
|
406
392
|
update_id: 5004,
|
|
407
393
|
callback_query: {
|
|
@@ -422,10 +408,10 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
422
408
|
};
|
|
423
409
|
|
|
424
410
|
const result = normalizeTelegramUpdate(payload);
|
|
425
|
-
|
|
411
|
+
expect(result).toBeNull();
|
|
426
412
|
});
|
|
427
413
|
|
|
428
|
-
test("
|
|
414
|
+
test("returns null when callback_query from.id is missing", () => {
|
|
429
415
|
const payload = {
|
|
430
416
|
update_id: 5005,
|
|
431
417
|
callback_query: {
|
|
@@ -441,7 +427,7 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
441
427
|
};
|
|
442
428
|
|
|
443
429
|
const result = normalizeTelegramUpdate(payload);
|
|
444
|
-
|
|
430
|
+
expect(result).toBeNull();
|
|
445
431
|
});
|
|
446
432
|
|
|
447
433
|
test("callback_query does not set isEdit or attachments", () => {
|
|
@@ -464,9 +450,10 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
464
450
|
},
|
|
465
451
|
};
|
|
466
452
|
|
|
467
|
-
const result =
|
|
468
|
-
expect(result.
|
|
469
|
-
expect(result
|
|
453
|
+
const result = normalizeTelegramUpdate(payload);
|
|
454
|
+
expect(result).not.toBeNull();
|
|
455
|
+
expect(result!.message.isEdit).toBeUndefined();
|
|
456
|
+
expect(result!.message.attachments).toBeUndefined();
|
|
470
457
|
});
|
|
471
458
|
|
|
472
459
|
test("regular text messages are unaffected by callback_query support", () => {
|
|
@@ -485,10 +472,11 @@ describe("normalizeTelegramUpdate: callback_query", () => {
|
|
|
485
472
|
},
|
|
486
473
|
};
|
|
487
474
|
|
|
488
|
-
const result =
|
|
489
|
-
expect(result.
|
|
490
|
-
expect(result
|
|
491
|
-
expect(result
|
|
475
|
+
const result = normalizeTelegramUpdate(payload);
|
|
476
|
+
expect(result).not.toBeNull();
|
|
477
|
+
expect(result!.message.content).toBe("Hello world");
|
|
478
|
+
expect(result!.message.callbackQueryId).toBeUndefined();
|
|
479
|
+
expect(result!.message.callbackData).toBeUndefined();
|
|
492
480
|
});
|
|
493
481
|
});
|
|
494
482
|
|