package telegram import ( "strings" "testing" "codex-telegram-bot/internal/store" ) func TestEscapeHTML(t *testing.T) { got := EscapeHTML(``) want := "<run & "test">" if got != want { t.Fatalf("EscapeHTML() = %q, want %q", got, want) } } func TestChunkText(t *testing.T) { text := strings.Repeat("a", 25) chunks := ChunkText(text, 10) if len(chunks) != 3 { t.Fatalf("got %d chunks", len(chunks)) } for _, chunk := range chunks { if len([]rune(chunk)) > 10 { t.Fatalf("chunk too long: %q", chunk) } } } func TestApprovalCallbackData(t *testing.T) { data := ApprovalCallbackData(12, "accept") id, decision, ok := ParseApprovalCallbackData(data) if !ok || id != 12 || decision != "accept" { t.Fatalf("unexpected callback parse: id=%d decision=%s ok=%v", id, decision, ok) } if _, _, ok := ParseApprovalCallbackData("approval:12:unknown"); ok { t.Fatal("unknown decisions should be rejected") } } func TestApprovalResponseForPermissions(t *testing.T) { approval := store.PendingApproval{ Kind: "item/permissions/requestApproval", PayloadJSON: `{"permissions":{"network":{"enabled":true}}}`, } response, ok := approvalResponse(approval, "accept").(map[string]any) if !ok { t.Fatal("approval response should be a map") } if response["scope"] != "turn" { t.Fatalf("scope = %v, want turn", response["scope"]) } permissions, ok := response["permissions"].(map[string]any) if !ok { t.Fatal("permissions should be a map") } network, ok := permissions["network"].(map[string]any) if !ok || network["enabled"] != true { t.Fatalf("unexpected permissions: %#v", permissions) } denied, ok := approvalResponse(approval, "decline").(map[string]any) if !ok { t.Fatal("denied response should be a map") } deniedPermissions, ok := denied["permissions"].(map[string]any) if !ok || len(deniedPermissions) != 0 { t.Fatalf("denied permissions = %#v, want empty map", denied["permissions"]) } } func TestParseCommand(t *testing.T) { name, args, ok := parseCommand("/resume@my_bot 123") if !ok || name != "resume" || len(args) != 1 || args[0] != "123" { t.Fatalf("unexpected command parse: %q %#v %v", name, args, ok) } } func TestRenderCodexCommandExecutionItem(t *testing.T) { output := "line 1\nline 2" exitCode := 0 item := codexThreadItemView{ Type: "commandExecution", Command: "go test ./...", AggregatedOutput: &output, ExitCode: &exitCode, } text := renderCodexItemCompleted(item) for _, want := range []string{"Tool call: command finished", "Command: go test ./...", "Exit code: 0", "line 1"} { if !strings.Contains(text, want) { t.Fatalf("rendered command item missing %q in %q", want, text) } } } func TestRenderCodexStartedItems(t *testing.T) { text := renderCodexItemStarted(codexThreadItemView{Type: "webSearch", Query: "telegram bot api"}) if !strings.Contains(text, "web search started") || !strings.Contains(text, "telegram bot api") { t.Fatalf("unexpected web search render: %q", text) } } func TestToolMessageAddsEditedAtBeforeDetails(t *testing.T) { tool := toolMessageState{ toolHTML: SummaryDetailsHTML("Tool call: command finished\nCommand: go test ./...", "full output"), editedAt: "2026-05-21 12:34:56 UTC", } text := tool.html() want := "Command: go test ./...\nEdited at: 2026-05-21 12:34:56 UTC\n
" if !strings.Contains(text, want) { t.Fatalf("edited timestamp not placed before details: %q", text) } } func TestToolMessageFitsCombinedApprovalDetails(t *testing.T) { largeToolDetails := strings.Repeat("tool output line\n", 600) largeApprovalDetails := strings.Repeat("approval payload line\n", 600) tool := toolMessageState{ toolHTML: SummaryDetailsHTML("Tool call: command finished", largeToolDetails), approvalHTML: SummaryDetailsHTML("Codex requests command approval", largeApprovalDetails), editedAt: "2026-05-21 12:34:56 UTC", } text := tool.html() if len([]rune(text)) > TelegramHTMLMessageLimit { t.Fatalf("tool message exceeds Telegram limit: %d", len([]rune(text))) } for _, want := range []string{"Tool call: command finished", "Codex requests command approval", "Edited at: 2026-05-21 12:34:56 UTC", "...[truncated]"} { if !strings.Contains(text, want) { t.Fatalf("fitted tool message missing %q in %q", want, text) } } } func TestResumeCallbackData(t *testing.T) { threadID, ok := ParseResumeThreadCallbackData(ResumeThreadCallbackData(123)) if !ok || threadID != 123 { t.Fatalf("unexpected resume thread callback: id=%d ok=%v", threadID, ok) } page, ok := ParseResumePageCallbackData(ResumePageCallbackData(2)) if !ok || page != 2 { t.Fatalf("unexpected resume page callback: page=%d ok=%v", page, ok) } } func TestResumeThreadListText(t *testing.T) { threads := []store.Thread{{ID: 42, Title: "do xyz"}, {ID: 43, Title: "executed xxx command"}} text := resumeThreadListText(threads, 0) for _, want := range []string{"Thread ID 42: do xyz", "Thread ID 43: executed xxx command"} { if !strings.Contains(text, want) { t.Fatalf("resume list missing %q in %q", want, text) } } markup := resumeThreadMarkup(threads, 0, true) if len(markup.InlineKeyboard) != 2 || markup.InlineKeyboard[0][0].Text != "ID 42" || markup.InlineKeyboard[0][1].Text != "ID 43" { t.Fatalf("unexpected resume buttons: %#v", markup.InlineKeyboard) } firstID, ok := ParseResumeThreadCallbackData(markup.InlineKeyboard[0][0].CallbackData) if !ok || firstID != 42 { t.Fatalf("first resume button targets id=%d ok=%v", firstID, ok) } secondID, ok := ParseResumeThreadCallbackData(markup.InlineKeyboard[0][1].CallbackData) if !ok || secondID != 43 { t.Fatalf("second resume button targets id=%d ok=%v", secondID, ok) } } func TestModelAndEffortCallbackData(t *testing.T) { modelID := strings.Join([]string{"server", "model", "id"}, "-") data, ok := ModelCallbackData(modelID) if !ok { t.Fatal("model callback should fit") } model, ok := ParseModelCallbackData(data) if !ok || model != modelID { t.Fatalf("unexpected model callback parse: model=%q ok=%v", model, ok) } effortName := strings.Join([]string{"server", "effort"}, "-") effort, ok := ParseEffortCallbackData(EffortCallbackData(effortName)) if !ok || effort != effortName { t.Fatalf("unexpected effort callback parse: effort=%q ok=%v", effort, ok) } }