package main import ( "context" "fmt" "net/http" "net/http/httptest" "strings" "sync" "testing" ) type quillBrain struct { mu sync.Mutex pairs [][2]string reply string } func (b *quillBrain) Name() string { return "quill-rec" } func (b *quillBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) { b.mu.Lock() b.pairs = append(b.pairs, [2]string{system, prompt}) b.mu.Unlock() return b.reply, 10, nil } func newQuillServer(t *testing.T, reply string) (*httptest.Server, *Server, *quillBrain) { t.Helper() ts, s := newTestNestServer(t) qb := &quillBrain{reply: reply} s.p.Flock.brain = qb return ts, s, qb } func TestQuillIsServiceRole(t *testing.T) { ts, _, _ := newQuillServer(t, "hello back") id := newNest(t, ts, "Lab", `"Hugin","Munin"`) code, msg := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","to":"Quill","kind":"say","text":"hello"}`) if code != http.StatusCreated { t.Fatalf("Quill must be addressable like Elder: got %d %v", code, msg) } } func TestQuillAnswersInCharacter(t *testing.T) { ts, _, qb := newQuillServer(t, "Already on it, boss.") id := newNest(t, ts, "Lab", `"Hugin","Munin"`) code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","to":"Quill","kind":"request","text":"quiet the room"}`) if code != http.StatusCreated { t.Fatalf("post: %d", code) } msgs := awaitMessages(t, ts, id, 2) var reply map[string]any for _, m := range msgs { if m["author"] == "Quill" { reply = m } } if reply == nil { t.Fatalf("Quill never answered: %v", msgs) } if reply["kind"] != "deliver" || reply["to"] != "owner" { t.Fatalf("want deliver to owner, got %v", reply) } if reply["text"] != "Already on it, boss." { t.Fatalf("reply text: %v", reply["text"]) } qb.mu.Lock() defer qb.mu.Unlock() if len(qb.pairs) != 1 { t.Fatalf("want exactly one model call, got %d", len(qb.pairs)) } if !strings.Contains(qb.pairs[0][0], "Quill") || !strings.Contains(qb.pairs[0][0], "The Hand") { t.Fatalf("system prompt must carry the Quill persona: %q", qb.pairs[0][0]) } if !strings.Contains(qb.pairs[0][1], "quiet the room") { t.Fatalf("prompt must carry the request text: %q", qb.pairs[0][1]) } } func TestQuillNotInNestCrows(t *testing.T) { ts, s, _ := newQuillServer(t, "noted") id := newNest(t, ts, "Lab", `"Hugin"`) post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","to":"Quill","kind":"request","text":"status?"}`) awaitMessages(t, ts, id, 2) for _, c := range nestOf(s, id).Crows { if c == "Quill" { t.Fatal("Quill must never become a nest member") } } // No trust promotion happened for Quill either: service roles act on // address, not on trust (the test server ledger was never touched). if _, ok := s.p.Ledger.Crows["Quill|other"]; ok { t.Fatal("Quill must not need or earn a worker trust record") } } func TestQuillPromptContainsNestHistory(t *testing.T) { ts, _, qb := newQuillServer(t, "Hugin finished the build already.") id := newNest(t, ts, "Lab", `"Hugin","Munin"`) // 1. owner says something that sets up context code, out := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","to":"Hugin","kind":"say","text":"build the widget factory"}`) if code != http.StatusCreated { t.Fatalf("post say: %d", code) } sayMsgID := out["msg_id"].(string) // 2. Hugin delivers an answer code, _ = post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"Hugin","kind":"deliver","text":"the widget factory is live","refers_to":"`+sayMsgID+`"}`) if code != http.StatusCreated { t.Fatalf("post deliver: %d", code) } // 3. owner asks Quill for status code, _ = post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","to":"Quill","kind":"request","text":"where did that get to"}`) if code != http.StatusCreated { t.Fatalf("post request to Quill: %d", code) } // Wait for Quill's reply (3 prior + 1 Quill reply = 4) msgs := awaitMessages(t, ts, id, 4) var quillReply map[string]any for _, m := range msgs { if m["author"] == "Quill" { quillReply = m } } if quillReply == nil { t.Fatalf("Quill never answered: %v", msgs) } qb.mu.Lock() defer qb.mu.Unlock() if len(qb.pairs) != 1 { t.Fatalf("want exactly one model call, got %d", len(qb.pairs)) } prompt := qb.pairs[0][1] if !strings.Contains(prompt, "build the widget factory") { t.Fatalf("prompt must contain the owner's earlier message: %q", prompt) } if !strings.Contains(prompt, "the widget factory is live") { t.Fatalf("prompt must contain Hugin's deliver: %q", prompt) } } func TestQuillEmptyNestSaysQuiet(t *testing.T) { ts, _, qb := newQuillServer(t, "All quiet here.") id := newNest(t, ts, "Quiet", `"Hugin"`) // Only message is the request to Quill — history excluding it is empty code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","to":"Quill","kind":"request","text":"any updates?"}`) if code != http.StatusCreated { t.Fatalf("post: %d", code) } awaitMessages(t, ts, id, 2) qb.mu.Lock() defer qb.mu.Unlock() if len(qb.pairs) != 1 { t.Fatalf("want exactly one model call, got %d", len(qb.pairs)) } prompt := qb.pairs[0][1] if !strings.Contains(prompt, "no earlier messages") { t.Fatalf("prompt must state the nest is empty: %q", prompt) } } func TestQuillHistoryCap(t *testing.T) { ts, _, qb := newQuillServer(t, "got it") id := newNest(t, ts, "Busy", `"Hugin"`) // Post 25 filler messages from owner for i := 0; i < 25; i++ { code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages", fmt.Sprintf(`{"author":"owner","kind":"say","text":"filler line %d"}`, i)) if code != http.StatusCreated { t.Fatalf("post filler %d: %d", i, code) } } // Ask Quill code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","to":"Quill","kind":"request","text":"status"}`) if code != http.StatusCreated { t.Fatalf("post to Quill: %d", code) } awaitMessages(t, ts, id, 27) qb.mu.Lock() defer qb.mu.Unlock() if len(qb.pairs) != 1 { t.Fatalf("want exactly one model call, got %d", len(qb.pairs)) } prompt := qb.pairs[0][1] historyLines := 0 for _, line := range strings.Split(prompt, "\n") { if strings.HasPrefix(line, "[") { historyLines++ } } if historyLines > 20 { t.Fatalf("history lines must be capped at 20, got %d\nprompt:\n%s", historyLines, prompt) } if historyLines == 0 { t.Fatalf("expected some history lines, got none") } }