@@ -6,23 +6,33 @@ import type { Writer } from "./writer.ts";
66const route : Route = { chatId : 1 , threadId : 0 } ;
77
88/** A Writer stub that records provisional messages, edits, and final output. */
9- function fakeWriter ( opts ?: { rejectHtml ?: boolean } ) {
9+ function fakeWriter ( opts ?: {
10+ rejectHtml ?: boolean ;
11+ rejectUnchanged ?: boolean ;
12+ } ) {
1013 const sent : { text : string ; html : boolean } [ ] = [ ] ;
1114 const edits : { id : number ; text : string ; html : boolean } [ ] = [ ] ;
1215 const deleted : number [ ] = [ ] ;
16+ const messages = new Map < number , string > ( ) ;
17+ let nextId = 1 ;
1318 const writer = {
1419 async persist ( text : string , extra ?: { parse_mode ?: string } ) {
1520 const html = extra ?. parse_mode === "HTML" ;
1621 if ( html && opts ?. rejectHtml )
1722 throw new Error ( "Bad Request: can't parse entities" ) ;
23+ const message_id = nextId ++ ;
1824 sent . push ( { text, html } ) ;
19- return { message_id : sent . length } ;
25+ messages . set ( message_id , text ) ;
26+ return { message_id } ;
2027 } ,
2128 async editText ( id : number , text : string , extra ?: { parse_mode ?: string } ) {
2229 const html = extra ?. parse_mode === "HTML" ;
2330 if ( html && opts ?. rejectHtml )
2431 throw new Error ( "Bad Request: can't parse entities" ) ;
32+ if ( opts ?. rejectUnchanged && messages . get ( id ) === text )
33+ throw new Error ( "Bad Request: message is not modified" ) ;
2534 edits . push ( { id, text, html } ) ;
35+ messages . set ( id , text ) ;
2636 return { message_id : id } ;
2737 } ,
2838 async deleteMessage ( id : number ) {
@@ -52,6 +62,39 @@ test("finalizes as Telegram HTML", async () => {
5262 expect ( edits [ 0 ] ! . text ) . toContain ( "<b>world</b>" ) ;
5363} ) ;
5464
65+ test ( "preserves a provisional message across continuation starts" , async ( ) => {
66+ const { sent, edits, writer } = fakeWriter ( ) ;
67+ const r = new Renderer ( writer , route ) ;
68+ r . onAgentStart ( ) ;
69+ r . onThinking ( "first attempt" ) ;
70+ await flush ( ) ;
71+ r . onAgentStart ( ) ; // Pi continuation/retry, not a new user turn
72+ r . onThinking ( " continues" ) ;
73+ r . onSettled ( "final answer" ) ;
74+ await flush ( ) ;
75+
76+ expect ( sent ) . toHaveLength ( 1 ) ;
77+ expect ( edits ) . toHaveLength ( 1 ) ;
78+ expect ( edits [ 0 ] ) . toMatchObject ( { id : 1 , text : "final answer" , html : true } ) ;
79+ } ) ;
80+
81+ test ( "does not duplicate a final answer when the preview is already identical" , async ( ) => {
82+ const { sent, edits, writer } = fakeWriter ( { rejectUnchanged : true } ) ;
83+ const finalized : { id : number ; text : string } [ ] = [ ] ;
84+ const r = new Renderer ( writer , route , ( id , text ) =>
85+ finalized . push ( { id, text } ) ,
86+ ) ;
87+ r . onAgentStart ( ) ;
88+ r . onText ( "same answer" ) ;
89+ await flush ( ) ;
90+ r . onSettled ( "same answer" ) ;
91+ await flush ( ) ;
92+
93+ expect ( sent ) . toEqual ( [ { text : "same answer" , html : false } ] ) ;
94+ expect ( edits ) . toHaveLength ( 0 ) ;
95+ expect ( finalized ) . toEqual ( [ { id : 1 , text : "same answer" } ] ) ;
96+ } ) ;
97+
5598test ( "falls back to plain text when Telegram rejects the HTML" , async ( ) => {
5699 const { sent, writer } = fakeWriter ( { rejectHtml : true } ) ;
57100 const r = new Renderer ( writer , route ) ;
0 commit comments