Support Codex 0.134 approvals
Use available approval decisions from the app-server schema, preserve structured policy decisions in callbacks, and keep approval rendering aligned with normal tool-call output. Also simplify thread commands, clear stale active turns more carefully, and update command/help docs.
This commit is contained in:
@@ -53,6 +53,72 @@ func TestApprovalCallbackData(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApprovalCallbackDataForStructuredDecision(t *testing.T) {
|
||||
data := ApprovalCallbackData(12, "acceptWithExecpolicyAmendment")
|
||||
id, decision, ok := ParseApprovalCallbackData(data)
|
||||
if !ok || id != 12 || decision != "acceptWithExecpolicyAmendment" {
|
||||
t.Fatalf("unexpected structured callback parse: id=%d decision=%s ok=%v", id, decision, ok)
|
||||
}
|
||||
data = ApprovalCallbackData(12, "networkPolicy0")
|
||||
id, decision, ok = ParseApprovalCallbackData(data)
|
||||
if !ok || id != 12 || decision != "networkPolicy0" {
|
||||
t.Fatalf("unexpected network callback parse: id=%d decision=%s ok=%v", id, decision, ok)
|
||||
}
|
||||
if _, _, ok := ParseApprovalCallbackData("approval:12:networkPolicyx"); ok {
|
||||
t.Fatal("invalid network policy callback should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApprovalMarkupHonorsAvailableDecisions(t *testing.T) {
|
||||
raw := json.RawMessage(`{"availableDecisions":["accept",{"acceptWithExecpolicyAmendment":{"execpolicy_amendment":["git","push"]}},"decline"]}`)
|
||||
markup := approvalMarkupForPayload(42, raw)
|
||||
var labels []string
|
||||
for _, row := range markup.InlineKeyboard {
|
||||
for _, button := range row {
|
||||
labels = append(labels, button.Text)
|
||||
}
|
||||
}
|
||||
joined := strings.Join(labels, "|")
|
||||
for _, want := range []string{"Approve", "Approve rule", "Deny", "Details"} {
|
||||
if !strings.Contains(joined, want) {
|
||||
t.Fatalf("approval markup missing %q in %#v", want, markup.InlineKeyboard)
|
||||
}
|
||||
}
|
||||
if strings.Contains(joined, "Cancel") {
|
||||
t.Fatalf("cancel should not be shown when Codex does not advertise it: %#v", markup.InlineKeyboard)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApprovalResponseForCommandStructuredDecision(t *testing.T) {
|
||||
approval := store.PendingApproval{
|
||||
Kind: "item/commandExecution/requestApproval",
|
||||
PayloadJSON: `{"availableDecisions":[{"acceptWithExecpolicyAmendment":{"execpolicy_amendment":["git","push"]}},{"applyNetworkPolicyAmendment":{"network_policy_amendment":{"action":"allow","host":"example.com"}}}]}`,
|
||||
}
|
||||
response, ok := approvalResponse(approval, "acceptWithExecpolicyAmendment").(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("structured command response should be a map")
|
||||
}
|
||||
decision, ok := response["decision"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("decision should be structured: %#v", response["decision"])
|
||||
}
|
||||
if _, ok := decision["acceptWithExecpolicyAmendment"]; !ok {
|
||||
t.Fatalf("missing execpolicy decision: %#v", decision)
|
||||
}
|
||||
|
||||
response, ok = approvalResponse(approval, "networkPolicy0").(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("network command response should be a map")
|
||||
}
|
||||
decision, ok = response["decision"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("network decision should be structured: %#v", response["decision"])
|
||||
}
|
||||
if _, ok := decision["applyNetworkPolicyAmendment"]; !ok {
|
||||
t.Fatalf("missing network policy decision: %#v", decision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApprovalResponseForPermissions(t *testing.T) {
|
||||
approval := store.PendingApproval{
|
||||
Kind: "item/permissions/requestApproval",
|
||||
@@ -131,9 +197,25 @@ func TestEditReplyMarkupClearsInlineKeyboard(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBotCommandsUseSingleThreadCommand(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 _, removed := range []string{"threads", "resume"} {
|
||||
if seen[removed] {
|
||||
t.Fatalf("bot command list should not include /%s", removed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCommand(t *testing.T) {
|
||||
name, args, ok := parseCommand("/resume@my_bot 123")
|
||||
if !ok || name != "resume" || len(args) != 1 || args[0] != "123" {
|
||||
name, args, ok := parseCommand("/thread@my_bot 123")
|
||||
if !ok || name != "thread" || len(args) != 1 || args[0] != "123" {
|
||||
t.Fatalf("unexpected command parse: %q %#v %v", name, args, ok)
|
||||
}
|
||||
}
|
||||
@@ -204,15 +286,43 @@ func TestRenderCodexCommandExecutionItem(t *testing.T) {
|
||||
ExitCode: &exitCode,
|
||||
}
|
||||
text := renderCodexItemCompleted(item)
|
||||
for _, want := range []string{"Tool call: command finished", "<b>Command</b>", "<pre><code class=\"language-bash\">go test ./...</code></pre>", "Exit code: 0", "<pre><code class=\"language-text\">line 1\nline 2</code></pre>"} {
|
||||
for _, want := range []string{"Tool call: command finished", "<b>CWD:</b> /workspace/project", "<b>Command:</b> <pre><code class=\"language-bash\">go test ./...</code></pre>", "<b>Output:</b> <pre><code class=\"language-text\">line 1\nline 2</code></pre>", "<b>Exit code:</b> 0"} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("rendered command item missing %q in %q", want, text)
|
||||
}
|
||||
}
|
||||
commandAt := strings.Index(text, "<b>Command</b>")
|
||||
summary, _, _ := strings.Cut(text, "<blockquote expandable>")
|
||||
if strings.Contains(summary, "Exit code") {
|
||||
t.Fatalf("exit code should not be in summary: %q", summary)
|
||||
}
|
||||
cwdAt := strings.Index(text, "<b>CWD:</b>")
|
||||
if cwdAt >= 0 && commandAt > cwdAt {
|
||||
t.Fatalf("command label should render before CWD to avoid Telegram attaching it to the CWD line: %q", text)
|
||||
commandAt := strings.Index(text, "<b>Command:</b>")
|
||||
outputAt := strings.Index(text, "<b>Output:</b>")
|
||||
exitAt := strings.Index(text, "<b>Exit code:</b>")
|
||||
if !(cwdAt >= 0 && commandAt > cwdAt && outputAt > commandAt && exitAt > outputAt) {
|
||||
t.Fatalf("command details order should be CWD, Command, Output, fields: %q", text)
|
||||
}
|
||||
if got := strings.Count(text, "<blockquote expandable>"); got != 4 {
|
||||
t.Fatalf("command details should use four quoted sections, got %d in %q", got, text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFitHTMLMessageKeepsCodeBlockTagsBalanced(t *testing.T) {
|
||||
longOutput := strings.Repeat("0123456789abcdef\n", 600)
|
||||
text := SummaryRawHTMLSectionsLimited("Tool call: command finished", []string{"<b>Output:</b> " + CodeBlockHTML("text", longOutput)}, 900)
|
||||
if len([]rune(text)) > 900 {
|
||||
t.Fatalf("fitted message exceeds limit: %d", len([]rune(text)))
|
||||
}
|
||||
for _, tag := range []string{"<blockquote expandable>", "</blockquote>", "<pre>", "</pre>", "<code class=\"language-text\">", "</code>"} {
|
||||
if !strings.Contains(text, tag) {
|
||||
t.Fatalf("fitted message missing %q in %q", tag, text)
|
||||
}
|
||||
}
|
||||
if strings.Count(text, "<pre>") != strings.Count(text, "</pre>") || strings.Count(text, "<code") != strings.Count(text, "</code>") || strings.Count(text, "<blockquote expandable>") != strings.Count(text, "</blockquote>") {
|
||||
t.Fatalf("fitted message has unbalanced HTML tags: %q", text)
|
||||
}
|
||||
if strings.Contains(text, "<b>Output:</b>\n") {
|
||||
t.Fatalf("label should not be separated from code block by an immediate newline: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +342,7 @@ func TestRenderDynamicToolDetailsSelectsUsefulArguments(t *testing.T) {
|
||||
Arguments: json.RawMessage(`{"cmd":"go test ./...","irrelevant":{"large":"object"}}`),
|
||||
}
|
||||
text := renderCodexItemCompleted(item)
|
||||
for _, want := range []string{"Tool: functions.exec_command", "<b>cmd</b>", "language-bash", "go test ./..."} {
|
||||
for _, want := range []string{"Tool: functions.exec_command", "<b>cmd:</b>", "language-bash", "go test ./..."} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("rendered tool details missing %q in %q", want, text)
|
||||
}
|
||||
@@ -277,22 +387,28 @@ func TestApprovalOnlyToolMessageCanReceiveCompletionDetails(t *testing.T) {
|
||||
AggregatedOutput: &output,
|
||||
})
|
||||
text := tool.html()
|
||||
for _, want := range []string{"Tool call: command finished", "Exit code: 0", "Duration ms", "1234", "Codex requests command approval"} {
|
||||
for _, want := range []string{"Tool call: command finished", "<b>Exit code:</b> 0", "<b>Duration ms:</b>", "1234", "Codex requests command approval"} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("combined approval tool message missing %q in %q", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolMessageAddsEditedAtBeforeDetails(t *testing.T) {
|
||||
func TestToolMessageAddsEditedAtInsideDetails(t *testing.T) {
|
||||
tool := toolMessageState{
|
||||
toolHTML: SummaryDetailsHTML("Tool call: command finished\nCommand: go test ./...", "full output"),
|
||||
toolHTML: SummaryDetailsHTML("Tool call: command finished", "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<blockquote expandable>"
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("edited timestamp not placed before details: %q", text)
|
||||
summary, details, ok := strings.Cut(text, "<blockquote expandable>")
|
||||
if !ok {
|
||||
t.Fatalf("tool message should contain details quote: %q", text)
|
||||
}
|
||||
if strings.Contains(summary, "Edited at") {
|
||||
t.Fatalf("edited timestamp should not be in summary: %q", summary)
|
||||
}
|
||||
if !strings.Contains(details, "<b>Edited at:</b> 2026-05-21 12:34:56 UTC") {
|
||||
t.Fatalf("edited timestamp not placed inside details: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +424,7 @@ func TestToolMessageFitsCombinedApprovalDetails(t *testing.T) {
|
||||
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]"} {
|
||||
for _, want := range []string{"Tool call: command finished", "Codex requests command approval", "<b>Edited at:</b> 2026-05-21 12:34:56 UTC", "...[truncated]"} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("fitted tool message missing %q in %q", want, text)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user