@@ -206,6 +206,7 @@ type TelegramReplyMarkup = {
206206type TelegramRichMessage = {
207207 markdown ?: string ;
208208 html ?: string ;
209+ blocks ?: unknown [ ] ;
209210} ;
210211
211212type 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+
706797function 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+
13331434function 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