@@ -336,4 +336,142 @@ describe("bot/commands/tasklist", () => {
336336 expect ( text ) . toContain ( "..." ) ;
337337 expect ( Buffer . byteLength ( text , "utf-8" ) ) . toBeLessThanOrEqual ( 4096 ) ;
338338 } ) ;
339+
340+ it ( "shows the 'Show full prompt' button when the prompt is truncated" , async ( ) => {
341+ interactionManager . start ( {
342+ kind : "custom" ,
343+ expectedInput : "callback" ,
344+ metadata : {
345+ flow : "tasklist" ,
346+ stage : "list" ,
347+ messageId : 1000 ,
348+ } ,
349+ } ) ;
350+
351+ mocked . getScheduledTaskMock . mockReturnValue (
352+ createTask ( "task-long" , { prompt : "B" . repeat ( 4000 ) } ) ,
353+ ) ;
354+
355+ const ctx = createCallbackContext ( "tasklist:open:task-long" , 1000 ) ;
356+ await handleTaskListCallback ( ctx ) ;
357+
358+ const [ , options ] = ( ctx . editMessageText as ReturnType < typeof vi . fn > ) . mock . calls [ 0 ] as [
359+ string ,
360+ { reply_markup : { inline_keyboard : Array < Array < { text : string ; callback_data ?: string } > > } } ,
361+ ] ;
362+
363+ const buttons = options . reply_markup . inline_keyboard . flat ( ) ;
364+ const showButton = buttons . find ( ( button ) => button . callback_data === "tasklist:prompt:task-long" ) ;
365+ expect ( showButton ) . toBeTruthy ( ) ;
366+ expect ( showButton ?. text ) . toBe ( t ( "tasklist.button.show_prompt" ) ) ;
367+ } ) ;
368+
369+ it ( "hides the 'Show full prompt' button when the prompt fits the detail budget" , async ( ) => {
370+ interactionManager . start ( {
371+ kind : "custom" ,
372+ expectedInput : "callback" ,
373+ metadata : {
374+ flow : "tasklist" ,
375+ stage : "list" ,
376+ messageId : 1100 ,
377+ } ,
378+ } ) ;
379+
380+ mocked . getScheduledTaskMock . mockReturnValue (
381+ createTask ( "task-short" , { prompt : "Check weather" } ) ,
382+ ) ;
383+
384+ const ctx = createCallbackContext ( "tasklist:open:task-short" , 1100 ) ;
385+ await handleTaskListCallback ( ctx ) ;
386+
387+ const [ , options ] = ( ctx . editMessageText as ReturnType < typeof vi . fn > ) . mock . calls [ 0 ] as [
388+ string ,
389+ { reply_markup : { inline_keyboard : Array < Array < { text : string ; callback_data ?: string } > > } } ,
390+ ] ;
391+
392+ const buttons = options . reply_markup . inline_keyboard . flat ( ) ;
393+ expect ( buttons . some ( ( button ) => button . callback_data ?. startsWith ( "tasklist:prompt:" ) ) ) . toBe ( false ) ;
394+ } ) ;
395+
396+ it ( "replies with the full prompt when 'Show full prompt' is tapped" , async ( ) => {
397+ interactionManager . start ( {
398+ kind : "custom" ,
399+ expectedInput : "callback" ,
400+ metadata : {
401+ flow : "tasklist" ,
402+ stage : "detail" ,
403+ messageId : 1200 ,
404+ taskId : "task-full" ,
405+ } ,
406+ } ) ;
407+
408+ const longPrompt = "C" . repeat ( 4000 ) ;
409+ mocked . getScheduledTaskMock . mockReturnValue (
410+ createTask ( "task-full" , { prompt : longPrompt } ) ,
411+ ) ;
412+
413+ const ctx = createCallbackContext ( "tasklist:prompt:task-full" , 1200 ) ;
414+ const handled = await handleTaskListCallback ( ctx ) ;
415+
416+ expect ( handled ) . toBe ( true ) ;
417+ expect ( ctx . reply ) . toHaveBeenCalledTimes ( 1 ) ;
418+
419+ const [ text ] = ( ctx . reply as ReturnType < typeof vi . fn > ) . mock . calls [ 0 ] as [ string ] ;
420+ expect ( text ) . toContain ( t ( "tasklist.full_prompt_header" ) ) ;
421+ expect ( text ) . toContain ( longPrompt ) ;
422+ } ) ;
423+
424+ it ( "splits the full prompt across messages when it exceeds the Telegram limit" , async ( ) => {
425+ interactionManager . start ( {
426+ kind : "custom" ,
427+ expectedInput : "callback" ,
428+ metadata : {
429+ flow : "tasklist" ,
430+ stage : "detail" ,
431+ messageId : 1300 ,
432+ taskId : "task-huge" ,
433+ } ,
434+ } ) ;
435+
436+ const hugePrompt = "D" . repeat ( 12000 ) ;
437+ mocked . getScheduledTaskMock . mockReturnValue (
438+ createTask ( "task-huge" , { prompt : hugePrompt } ) ,
439+ ) ;
440+
441+ const ctx = createCallbackContext ( "tasklist:prompt:task-huge" , 1300 ) ;
442+ await handleTaskListCallback ( ctx ) ;
443+
444+ const calls = ( ctx . reply as ReturnType < typeof vi . fn > ) . mock . calls as Array < [ string ] > ;
445+ expect ( calls . length ) . toBeGreaterThan ( 1 ) ;
446+
447+ const combined = calls . map ( ( [ text ] ) => text ) . join ( "\n\n" ) ;
448+ expect ( combined ) . toContain ( hugePrompt . slice ( 0 , 1000 ) ) ;
449+ expect ( combined ) . toContain ( hugePrompt . slice ( - 1000 ) ) ;
450+
451+ for ( const [ text ] of calls ) {
452+ expect ( Buffer . byteLength ( text , "utf-8" ) ) . toBeLessThanOrEqual ( 4096 ) ;
453+ }
454+ } ) ;
455+
456+ it ( "rejects 'Show full prompt' taps outside the active detail view" , async ( ) => {
457+ interactionManager . start ( {
458+ kind : "custom" ,
459+ expectedInput : "callback" ,
460+ metadata : {
461+ flow : "tasklist" ,
462+ stage : "list" ,
463+ messageId : 1400 ,
464+ } ,
465+ } ) ;
466+
467+ const ctx = createCallbackContext ( "tasklist:prompt:task-full" , 1400 ) ;
468+ const handled = await handleTaskListCallback ( ctx ) ;
469+
470+ expect ( handled ) . toBe ( true ) ;
471+ expect ( ctx . answerCallbackQuery ) . toHaveBeenCalledWith ( {
472+ text : t ( "tasklist.inactive_callback" ) ,
473+ show_alert : true ,
474+ } ) ;
475+ expect ( ctx . reply ) . not . toHaveBeenCalled ( ) ;
476+ } ) ;
339477} ) ;
0 commit comments