Refine Telegram thread commands

This commit is contained in:
Codex
2026-06-23 11:19:48 +00:00
parent ac8d5c2803
commit 595e8aee0e
9 changed files with 628 additions and 102 deletions

View File

@@ -7,6 +7,7 @@ import (
"strings"
"testing"
"codex-telegram-bot/internal/codexapp"
"codex-telegram-bot/internal/store"
)
@@ -197,16 +198,18 @@ func TestEditReplyMarkupClearsInlineKeyboard(t *testing.T) {
}
}
func TestBotCommandsUseSingleThreadCommand(t *testing.T) {
func TestBotCommandsExposeCurrentPromptList(t *testing.T) {
commands := botCommands()
seen := map[string]bool{}
for _, command := range commands {
seen[command.Command] = true
}
if !seen["thread"] {
t.Fatal("bot command list should include /thread")
for _, command := range []string{"start", "new", "resume", "rename", "fork", "archive", "unarchive", "delete", "status", "cancel", "workspace", "model", "sandbox", "pic"} {
if !seen[command] {
t.Fatalf("bot command list should include /%s", command)
}
}
for _, removed := range []string{"threads", "resume"} {
for _, removed := range []string{"help", "thread", "threads", "workspaces", "diff"} {
if seen[removed] {
t.Fatalf("bot command list should not include /%s", removed)
}
@@ -214,8 +217,8 @@ func TestBotCommandsUseSingleThreadCommand(t *testing.T) {
}
func TestParseCommand(t *testing.T) {
name, args, ok := parseCommand("/thread@my_bot 123")
if !ok || name != "thread" || len(args) != 1 || args[0] != "123" {
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)
}
}
@@ -394,9 +397,9 @@ func TestRenderDynamicToolDetailsSelectsUsefulArguments(t *testing.T) {
}
func TestRenderApprovalDetailsAvoidsRawJSONDump(t *testing.T) {
raw := json.RawMessage(`{"command":"go test ./...","cwd":"/workspace/project","unused":{"nested":true}}`)
raw := json.RawMessage(`{"command":"go test ./...","cwd":"/workspace/project","environmentId":"env_123","unused":{"nested":true}}`)
text := renderApprovalHTML("item/commandExecution/requestApproval", raw, "")
for _, want := range []string{"Codex requests command approval", "language-bash", "go test ./...", "CWD"} {
for _, want := range []string{"Codex requests command approval", "language-bash", "go test ./...", "CWD", "Environment ID", "env_123"} {
if !strings.Contains(text, want) {
t.Fatalf("approval render missing %q in %q", want, text)
}
@@ -534,10 +537,39 @@ func TestResumeCallbackData(t *testing.T) {
}
}
func TestThreadActionCallbackData(t *testing.T) {
action, threadID, ok := ParseThreadActionCallbackData(ThreadActionCallbackData(threadActionDelete, 123))
if !ok || action != threadActionDelete || threadID != 123 {
t.Fatalf("unexpected thread action callback: action=%q id=%d ok=%v", action, threadID, ok)
}
action, page, ok := ParseThreadActionPageCallbackData(ThreadActionPageCallbackData(threadActionUnarchive, 2))
if !ok || action != threadActionUnarchive || page != 2 {
t.Fatalf("unexpected thread action page callback: action=%q page=%d ok=%v", action, page, ok)
}
if _, _, ok := ParseThreadActionCallbackData("thread:unknown:123"); ok {
t.Fatal("unknown thread action should not parse")
}
}
func TestIsMissingCodexThreadError(t *testing.T) {
for _, message := range []string{
"no rollout found for thread id 019ef2ea",
"thread not loaded: 019ef2ea",
} {
err := codexapp.RPCError{Code: -32600, Message: message}
if !isMissingCodexThreadError(err) {
t.Fatalf("expected stale thread error for %q", message)
}
}
if isMissingCodexThreadError(codexapp.RPCError{Code: -32600, Message: "permission denied"}) {
t.Fatal("unrelated -32600 error should not be treated as stale thread")
}
}
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"} {
for _, want := range []string{"Thread ID 42: do xyz", "Thread ID 43: executed xxx command", "/resume THREAD_ID"} {
if !strings.Contains(text, want) {
t.Fatalf("resume list missing %q in %q", want, text)
}
@@ -554,6 +586,36 @@ func TestResumeThreadListText(t *testing.T) {
if !ok || secondID != 43 {
t.Fatalf("second resume button targets id=%d ok=%v", secondID, ok)
}
deleteText := threadActionListText(threads, 0, threadActionDelete)
if !strings.Contains(deleteText, "Choose a thread to delete") || !strings.Contains(deleteText, "/delete THREAD_ID") {
t.Fatalf("delete list text missing action copy: %q", deleteText)
}
deleteMarkup := threadActionMarkup(threads, 0, true, threadActionDelete)
if deleteMarkup.InlineKeyboard[0][0].Text != "Delete 42" {
t.Fatalf("unexpected delete button label: %#v", deleteMarkup.InlineKeyboard)
}
action, deleteID, ok := ParseThreadActionCallbackData(deleteMarkup.InlineKeyboard[0][0].CallbackData)
if !ok || action != threadActionDelete || deleteID != 42 {
t.Fatalf("delete button targets action=%q id=%d ok=%v", action, deleteID, ok)
}
action, page, ok := ParseThreadActionPageCallbackData(deleteMarkup.InlineKeyboard[1][0].CallbackData)
if !ok || action != threadActionDelete || page != 1 {
t.Fatalf("delete next button targets action=%q page=%d ok=%v", action, page, ok)
}
unarchiveText := threadActionListText(threads, 0, threadActionUnarchive)
if !strings.Contains(unarchiveText, "Choose an archived thread to restore") || !strings.Contains(unarchiveText, "/unarchive THREAD_ID") {
t.Fatalf("unarchive list text missing action copy: %q", unarchiveText)
}
unarchiveMarkup := threadActionMarkup(threads, 0, false, threadActionUnarchive)
if unarchiveMarkup.InlineKeyboard[0][0].Text != "Restore 42" {
t.Fatalf("unexpected unarchive button label: %#v", unarchiveMarkup.InlineKeyboard)
}
action, unarchiveID, ok := ParseThreadActionCallbackData(unarchiveMarkup.InlineKeyboard[0][0].CallbackData)
if !ok || action != threadActionUnarchive || unarchiveID != 42 {
t.Fatalf("unarchive button targets action=%q id=%d ok=%v", action, unarchiveID, ok)
}
}
func TestModelEffortAndSandboxCallbackData(t *testing.T) {