package agents import ( "context" "encoding/json" "strings" "testing" "g.fc.run/misc/byre/internal/api" "g.fc.run/misc/byre/internal/store" ) func TestValidateAsk(t *testing.T) { m := &Manager{} l := &live{id: "s1"} yes := true cases := []struct { name string args askArgs want []string }{ {"valid", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "a", Blocking: &yes}, nil}, {"empty", askArgs{}, []string{"context is empty", "question is empty", "why_blocked is empty", "at least two", "recommended_option", "blocking is required"}}, {"one option", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a"}, RecommendedOption: "a", Blocking: &yes}, []string{"at least two"}}, {"bad recommendation", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "c", Blocking: &yes}, []string{"recommended_option"}}, {"bad attachment", askArgs{Context: "c", Question: "q", WhyBlocked: "w", Options: []string{"a", "b"}, RecommendedOption: "a", Blocking: &yes, Attachments: []api.Attachment{{Kind: "video"}}}, []string{`attachments[0].kind "video"`}}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { err := m.validateAsk(context.Background(), l, c.args) if len(c.want) == 0 { if err != nil { t.Fatalf("unexpected error %v", err) } return } if err == nil { t.Fatalf("want rejection mentioning %v", c.want) } if !strings.Contains(err.Error(), "nothing was filed") { t.Fatalf("rejection doesn't say nothing was filed: %v", err) } for _, w := range c.want { if !strings.Contains(err.Error(), w) { t.Fatalf("rejection %q doesn't mention %q", err, w) } } }) } } func TestAsksQuestion(t *testing.T) { cases := []struct { name string text string want bool }{ {"prose question (seq 61)", "Which of these do you want to start with?", true}, {"question mid-message", "I can do either.\n\nWhich do you want first? I'd start with the spinner.", true}, {"bold question", "**Should I deploy now?**", true}, {"question mark only inside quotes (seq 108)", "- **A:** block if the final message contains a \"?\".\n- **C:** block if the turn's report lists open questions.\n\nMy recommendation is A and C together.", false}, {"quoted question", "Without it, a question like \"Which of these do you want to start with?\" still reaches you as plain text.", false}, {"inline code", "The view calls `GET /api/inbox?task=x` on load.", false}, {"fenced code", "Run:\n```\nif ok? then\n```\nDone.", false}, {"url query", "See https://example.com/?a=1 for details.", false}, {"blockquote", "> did it work?\n\nIt did.", false}, {"no question", "I committed everything and the tests pass.", false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { if got := asksQuestion(c.text); got != c.want { t.Fatalf("asksQuestion(%q) = %v, want %v", c.text, got, c.want) } }) } } func TestBounceProseQuestion(t *testing.T) { m := &Manager{} stop := func(l *live, text string) map[string]any { t.Helper() input, err := json.Marshal(map[string]any{"hook_event_name": "Stop", "last_assistant_message": text}) if err != nil { t.Fatal(err) } out, err := m.bounceProseQuestion(l)(context.Background(), input) if err != nil { t.Fatal(err) } return out } l := &live{} if out := stop(l, "Which of these do you want to start with?"); out["decision"] != "block" || out["reason"] != proseQuestionNudge { t.Fatalf("prose question not bounced: %v", out) } if out := stop(l, "Which of these do you want to start with?"); len(out) != 0 { t.Fatalf("bounced twice in one turn: %v", out) } asked := &live{asked: true} if out := stop(asked, "Should I go ahead?"); len(out) != 0 { t.Fatalf("bounced a turn that called ask_human: %v", out) } if out := stop(&live{}, "I committed everything and the tests pass."); len(out) != 0 { t.Fatalf("bounced a message with no question: %v", out) } } func TestDeliveryMessage(t *testing.T) { str := func(s string) *string { return &s } question := api.InboxItem{Id: "q1", ThreadId: "q1", Kind: api.Question, Payload: map[string]any{"question": "Which color?"}} failure := api.InboxItem{Id: "e1", ThreadId: "e1", Kind: api.SessionError, Payload: map[string]any{"error": "process died"}} msg := deliveryMessage([]store.Pending{ {Item: question, Response: api.InboxResponse{User: "alice", Type: api.Option, Option: str("blue")}}, {Item: question, Response: api.InboxResponse{User: "bob", Type: api.Other, Text: str("what is it for?"), Attachments: []api.Attachment{{Kind: api.AttachmentKindUrl, Url: str("https://example.com")}}}}, {Item: failure, Response: api.InboxResponse{User: "alice", Type: api.Other, Text: str("stop and write tests")}}, }) for _, want := range []string{ "Your question q1: Which color?", "alice chose: blue", "bob answered (none of your options): what is it for?", `calling ask_human with thread_id "q1"`, `"url":"https://example.com"`, "Your error e1: process died", "alice replied: stop and write tests", } { if !strings.Contains(msg, want) { t.Fatalf("message missing %q:\n%s", want, msg) } } }