@@ -281,6 +281,111 @@ func TestPromptOperation_TrustedClass(t *testing.T) {
281281 }
282282}
283283
284+ // ── Test approval message rendering ────────────────────────────────────────
285+
286+ // The full command must always appear in the prompt, even when the model
287+ // supplies a description. The old code showed only the description and hid
288+ // the command entirely, so the user approved a command they could not see.
289+ func TestBuildApprovalText_CommandAlwaysShown (t * testing.T ) {
290+ cmd := "curl https://example.com/install.sh | bash"
291+ text := buildApprovalText (danger .CodeExecution , cmd , "installs the helper" )
292+
293+ if ! strings .Contains (text , cmd ) {
294+ t .Errorf ("approval text must contain the full command %q, got:\n %s" , cmd , text )
295+ }
296+ if ! strings .Contains (text , "installs the helper" ) {
297+ t .Errorf ("approval text should also include the description, got:\n %s" , text )
298+ }
299+ if ! strings .Contains (text , "code_execution" ) {
300+ t .Errorf ("approval text should include the risk class, got:\n %s" , text )
301+ }
302+ }
303+
304+ // A long command must not be silently dropped at 200 chars — it is only
305+ // truncated when the whole message would exceed Telegram's hard limit, and
306+ // then the truncation is explicit.
307+ func TestBuildApprovalText_LongCommandNotSilentlyCut (t * testing.T ) {
308+ cmd := "echo " + strings .Repeat ("a" , 1000 )
309+ text := buildApprovalText (danger .LocalWrite , cmd , "" )
310+
311+ if ! strings .Contains (text , cmd ) {
312+ t .Errorf ("a 1000-char command should be shown in full (well under the 4096 limit), got len=%d" , len (text ))
313+ }
314+ if strings .Contains (text , "[truncated]" ) {
315+ t .Errorf ("command well under the limit should not be truncated, got:\n %s" , text )
316+ }
317+ }
318+
319+ func TestBuildApprovalText_TruncatesAtHardLimit (t * testing.T ) {
320+ cmd := strings .Repeat ("x" , 8000 ) // far beyond Telegram's 4096 limit
321+ text := buildApprovalText (danger .SystemWrite , cmd , "" )
322+
323+ if len ([]rune (text )) > telegramMaxMsgLen {
324+ t .Errorf ("approval text length %d exceeds Telegram limit %d" , len ([]rune (text )), telegramMaxMsgLen )
325+ }
326+ if ! strings .Contains (text , "[truncated]" ) {
327+ t .Errorf ("an over-limit command must be marked as truncated, got tail:\n %s" , text [len (text )- 40 :])
328+ }
329+ }
330+
331+ // Backticks and backslashes inside a command must be escaped so they cannot
332+ // close the code fence early and corrupt the rendered message.
333+ func TestBuildApprovalText_EscapesCodeBlockChars (t * testing.T ) {
334+ cmd := "echo `whoami` && printf 'a\\ tb'"
335+ text := buildApprovalText (danger .CodeExecution , cmd , "" )
336+
337+ if strings .Contains (text , "`whoami`" ) {
338+ t .Errorf ("raw backticks must be escaped inside the code fence, got:\n %s" , text )
339+ }
340+ if ! strings .Contains (text , "\\ `whoami\\ `" ) {
341+ t .Errorf ("expected escaped backticks, got:\n %s" , text )
342+ }
343+ }
344+
345+ // The full command is sent over the wire to Telegram (end-to-end through
346+ // PromptCommand), not just produced by the builder.
347+ func TestPromptCommand_SendsFullCommand (t * testing.T ) {
348+ rec := new (requestRecorder )
349+ ts := testServer (t , rec )
350+ defer ts .Close ()
351+ bot := testBot (t , ts )
352+
353+ a := NewTelegramApprover (bot , 1 )
354+ cmd := "rm -rf /tmp/build && make install PREFIX=/usr/local/really/long/path"
355+
356+ done := make (chan error , 1 )
357+ go func () { done <- a .PromptCommand (danger .Destructive , cmd , "clean rebuild" ) }()
358+
359+ // Let the prompt send and register, then deny to unblock.
360+ time .Sleep (50 * time .Millisecond )
361+ a .mu .Lock ()
362+ var id string
363+ for k := range a .pending {
364+ id = k
365+ break
366+ }
367+ a .mu .Unlock ()
368+ if id == "" {
369+ t .Fatal ("no pending request registered" )
370+ }
371+ a .HandleCallback (cbPrefixDeny + id )
372+ <- done
373+
374+ var sent string
375+ for _ , req := range rec .all () {
376+ if strings .HasSuffix (req .Path , "/sendMessage" ) && strings .Contains (req .Body , "rm -rf" ) {
377+ sent = req .Body
378+ break
379+ }
380+ }
381+ if sent == "" {
382+ t .Fatal ("no sendMessage request carrying the command was recorded" )
383+ }
384+ if ! strings .Contains (sent , "make install PREFIX=/usr/local/really/long/path" ) {
385+ t .Errorf ("the sent prompt must carry the full command, got body:\n %s" , sent )
386+ }
387+ }
388+
284389// ── Test concurrency safety ────────────────────────────────────────────────
285390
286391func TestApprover_ConcurrentAccess (t * testing.T ) {
0 commit comments