Skip to content

Commit 47759c3

Browse files
committed
fix(qa): accept rich Telegram canary presence
(cherry picked from commit e86eb75)
1 parent 3429e33 commit 47759c3

2 files changed

Lines changed: 185 additions & 21 deletions

File tree

extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,58 @@ describe("telegram live qa runtime", () => {
321321
},
322322
})?.text,
323323
).toBe("<b>hello</b> rich");
324+
325+
expect(
326+
testing.normalizeTelegramObservedMessage({
327+
update_id: 10,
328+
message: {
329+
message_id: 12,
330+
date: 1_700_000_003,
331+
rich_message: {
332+
blocks: [
333+
{
334+
type: "paragraph",
335+
text: ["hello ", { type: "bold", text: "rich" }],
336+
},
337+
{
338+
type: "footer",
339+
text: { type: "italic", text: "footer" },
340+
},
341+
],
342+
},
343+
chat: { id: -100123 },
344+
from: {
345+
id: 88,
346+
is_bot: true,
347+
username: "sut_bot",
348+
},
349+
},
350+
})?.text,
351+
).toBe("hello rich\nfooter");
352+
353+
expect(
354+
testing.normalizeTelegramObservedMessage({
355+
update_id: 11,
356+
message: {
357+
message_id: 13,
358+
date: 1_700_000_004,
359+
rich_message: {
360+
blocks: [
361+
{
362+
type: "table",
363+
cells: [[{ text: "Open" }, { text: "Claw" }], [{ text: "release" }]],
364+
},
365+
],
366+
},
367+
chat: { id: -100123 },
368+
from: {
369+
id: 88,
370+
is_bot: true,
371+
username: "sut_bot",
372+
},
373+
},
374+
})?.text,
375+
).toBe("Open\tClaw\nrelease");
324376
});
325377

326378
it("ignores unrelated sut replies when matching the canary response", () => {
@@ -389,6 +441,27 @@ describe("telegram live qa runtime", () => {
389441
).toBe("match");
390442
});
391443

444+
it("keeps scenario text checks strict while allowing canary presence replies", () => {
445+
const message = {
446+
updateId: 3,
447+
messageId: 11,
448+
chatId: -100123,
449+
senderId: 88,
450+
senderIsBot: true,
451+
senderUsername: "sut_bot",
452+
text: "",
453+
replyToMessageId: 55,
454+
timestamp: 1_700_000_002_000,
455+
inlineButtons: [],
456+
mediaKinds: [],
457+
};
458+
459+
expect(() => testing.assertTelegramScenarioReply({ message })).toThrow(
460+
"reply message 11 was empty",
461+
);
462+
expect(() => testing.assertTelegramCanaryPresenceReply(message)).not.toThrow();
463+
});
464+
392465
it("fails when any requested Telegram scenario id is unknown", () => {
393466
expect(() => testing.findScenario(["telegram-help-command", "typo-scenario"])).toThrow(
394467
"unknown Telegram QA scenario id(s): typo-scenario",

extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts

Lines changed: 112 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ type TelegramReplyMarkup = {
206206
type TelegramRichMessage = {
207207
markdown?: string;
208208
html?: string;
209+
blocks?: unknown[];
209210
};
210211

211212
type TelegramMessage = {
@@ -703,13 +704,99 @@ function detectMediaKinds(message: TelegramMessage) {
703704
return kinds;
704705
}
705706

707+
function flattenTelegramRichText(value: unknown): string {
708+
if (typeof value === "string") {
709+
return value;
710+
}
711+
if (Array.isArray(value)) {
712+
return value.map((part) => flattenTelegramRichText(part)).join("");
713+
}
714+
if (!isRecord(value)) {
715+
return "";
716+
}
717+
if ("text" in value) {
718+
return flattenTelegramRichText(value.text);
719+
}
720+
if (typeof value.alternative_text === "string") {
721+
return value.alternative_text;
722+
}
723+
if (typeof value.expression === "string") {
724+
return value.expression;
725+
}
726+
return "";
727+
}
728+
729+
function flattenTelegramRichBlock(value: unknown): string {
730+
if (typeof value === "string" || Array.isArray(value)) {
731+
return flattenTelegramRichText(value);
732+
}
733+
if (!isRecord(value)) {
734+
return "";
735+
}
736+
const parts: string[] = [];
737+
if ("text" in value) {
738+
parts.push(flattenTelegramRichText(value.text));
739+
}
740+
if ("summary" in value) {
741+
parts.push(flattenTelegramRichText(value.summary));
742+
}
743+
if (typeof value.label === "string") {
744+
parts.push(value.label);
745+
}
746+
if (typeof value.expression === "string") {
747+
parts.push(value.expression);
748+
}
749+
if ("blocks" in value) {
750+
parts.push(flattenTelegramRichBlocks(value.blocks));
751+
}
752+
if ("items" in value) {
753+
parts.push(flattenTelegramRichBlocks(value.items));
754+
}
755+
if ("cells" in value) {
756+
parts.push(flattenTelegramRichTableCells(value.cells));
757+
}
758+
if ("caption" in value) {
759+
parts.push(flattenTelegramRichBlock(value.caption));
760+
}
761+
if ("credit" in value) {
762+
parts.push(flattenTelegramRichText(value.credit));
763+
}
764+
return parts.filter((part) => part.trim()).join("\n");
765+
}
766+
767+
function flattenTelegramRichBlocks(value: unknown): string {
768+
const blocks = Array.isArray(value) ? value : [value];
769+
return blocks
770+
.map((block) => flattenTelegramRichBlock(block))
771+
.filter((part) => part.trim())
772+
.join("\n");
773+
}
774+
775+
function flattenTelegramRichTableCells(value: unknown): string {
776+
if (!Array.isArray(value)) {
777+
return flattenTelegramRichBlock(value);
778+
}
779+
return value
780+
.map((row) => {
781+
const cells = Array.isArray(row) ? row : [row];
782+
return cells
783+
.map((cell) => flattenTelegramRichBlock(cell))
784+
.filter((cell) => cell.trim())
785+
.join("\t");
786+
})
787+
.filter((row) => row.trim())
788+
.join("\n");
789+
}
790+
791+
function selectTelegramRichMessageText(richMessage: TelegramRichMessage | undefined) {
792+
return (
793+
richMessage?.markdown || richMessage?.html || flattenTelegramRichBlocks(richMessage?.blocks)
794+
);
795+
}
796+
706797
function selectTelegramObservedText(message: TelegramMessage) {
707798
return (
708-
message.text ||
709-
message.caption ||
710-
message.rich_message?.markdown ||
711-
message.rich_message?.html ||
712-
""
799+
message.text || message.caption || selectTelegramRichMessageText(message.rich_message) || ""
713800
);
714801
}
715802

@@ -911,6 +998,7 @@ async function waitForObservedMessage(params: {
911998
observationScenarioId: string;
912999
observationScenarioTitle: string;
9131000
expectedTextIncludes?: string[];
1001+
validateMatchedMessage?: (message: TelegramObservedMessage) => void;
9141002
}) {
9151003
const startedAt = Date.now();
9161004
let offset = params.initialOffset;
@@ -963,10 +1051,14 @@ async function waitForObservedMessage(params: {
9631051
params.observedMessages.push(observedMessage);
9641052
if (matchedScenario) {
9651053
try {
966-
assertTelegramScenarioReply({
967-
expectedTextIncludes: params.expectedTextIncludes,
968-
message: observedMessage,
969-
});
1054+
if (params.validateMatchedMessage) {
1055+
params.validateMatchedMessage(observedMessage);
1056+
} else {
1057+
assertTelegramScenarioReply({
1058+
expectedTextIncludes: params.expectedTextIncludes,
1059+
message: observedMessage,
1060+
});
1061+
}
9701062
} catch (error) {
9711063
lastExpectedMismatch =
9721064
error instanceof Error ? error : new Error(formatErrorMessage(error));
@@ -1330,6 +1422,15 @@ function assertTelegramScenarioReply(params: {
13301422
}
13311423
}
13321424

1425+
function assertTelegramCanaryPresenceReply(message: TelegramObservedMessage) {
1426+
if (!message.senderIsBot) {
1427+
throw new Error(`canary reply message ${message.messageId} was not sent by a bot`);
1428+
}
1429+
// Telegram rich-message updates can arrive to the driver bot with no text
1430+
// body. The release canary proves command delivery plus threaded SUT output;
1431+
// text assertions stay on explicit command/scenario checks.
1432+
}
1433+
13331434
function isTelegramObservedMessageTimeoutError(error: unknown, timeoutMs: number) {
13341435
return formatErrorMessage(error).startsWith(
13351436
`timed out after ${timeoutMs}ms waiting for Telegram message`,
@@ -1451,6 +1552,7 @@ async function runCanary(params: {
14511552
observedMessages: params.observedMessages,
14521553
observationScenarioId: "telegram-canary",
14531554
observationScenarioTitle: "Telegram canary",
1555+
validateMatchedMessage: assertTelegramCanaryPresenceReply,
14541556
predicate: (message) => {
14551557
const classification = classifyCanaryReply({
14561558
message,
@@ -1497,18 +1599,6 @@ async function runCanary(params: {
14971599
},
14981600
);
14991601
}
1500-
if (!sutObserved.message.text.trim()) {
1501-
throw new TelegramQaCanaryError(
1502-
"sut_reply_empty",
1503-
"SUT bot replied to the canary message but the reply text was empty.",
1504-
{
1505-
groupId: params.groupId,
1506-
sutBotId: params.sutBotId,
1507-
driverMessageId: driverMessage.message_id,
1508-
sutMessageId: sutObserved.message.messageId,
1509-
},
1510-
);
1511-
}
15121602
return {
15131603
requestStartedAt,
15141604
responseObservedAt: new Date(sutObserved.observedAtMs).toISOString(),
@@ -2096,6 +2186,7 @@ export const testing = {
20962186
buildObservedMessagesArtifact,
20972187
canaryFailureMessage,
20982188
callTelegramApi,
2189+
assertTelegramCanaryPresenceReply,
20992190
assertTelegramScenarioMessageSet,
21002191
isRecoverableTelegramQaPollError,
21012192
assertTelegramScenarioReply,

0 commit comments

Comments
 (0)