140 lines
5.2 KiB
Go
140 lines
5.2 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|