package main import ( "context" "net/http" "net/http/httptest" "sync" "testing" "time" ) // relayBrain records every (system, prompt) pair and answers with scripted // replies in call order, falling back to plain chatter once they run out. type relayBrain struct { mu sync.Mutex pairs [][2]string replies []string calls int } func (b *relayBrain) Name() string { return "relay-rec" } func (b *relayBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) { b.mu.Lock() defer b.mu.Unlock() b.pairs = append(b.pairs, [2]string{system, prompt}) reply := "just chatter" if b.calls < len(b.replies) { reply = b.replies[b.calls] } b.calls++ return reply, 10, nil } func newRelayServer(t *testing.T, replies ...string) (*httptest.Server, *Server, *relayBrain) { t.Helper() ts, s := newTestNestServer(t) rb := &relayBrain{replies: replies} s.p.Flock.brain = rb return ts, s, rb } // askQuill posts the owner's request addressed to Quill and returns the // stored record of that message. func askQuill(t *testing.T, ts *httptest.Server, id, text string) map[string]any { t.Helper() code, req := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","to":"Quill","kind":"request","text":"`+text+`"}`) if code != http.StatusCreated { t.Fatalf("owner request: %d %v", code, req) } return req } func countBy(t *testing.T, ts *httptest.Server, id, author string, kinds ...string) (total int, matched int) { t.Helper() for _, m := range nestMessages(t, ts, id) { if m["author"] != author { continue } total++ for _, k := range kinds { if m["kind"] == k { matched++ } } } return } func TestQuillRelayPostsRequest(t *testing.T) { ts, _, _ := newRelayServer(t, `RELAY {"crow":"Hugin","task":"draft the tagline"}`, "KIND: deliver\nSo many crows, one banner.") id := newNest(t, ts, "Lab", `"Hugin","Munin"`) req := askQuill(t, ts, id, "get Hugin to draft the tagline") msgs := awaitMessages(t, ts, id, 3) var relay map[string]any for _, m := range msgs { if m["author"] == "Quill" && m["kind"] == "request" { relay = m } } if relay == nil { t.Fatalf("Quill never posted the relay request: %v", msgs) } if relay["to"] != "Hugin" { t.Fatalf("relay must address Hugin, got %v", relay["to"]) } if relay["refers_to"] != req["msg_id"] { t.Fatalf("relay must refer to the owner message %v, got %v", req["msg_id"], relay["refers_to"]) } if relay["text"] != "draft the tagline" { t.Fatalf("relay text must carry the task, got %v", relay["text"]) } hugin := false for _, m := range msgs { if m["author"] == "Hugin" { hugin = true } } if !hugin { t.Fatalf("the addressed crow must answer the relay through must-respond: %v", msgs) } } func TestQuillRelayUnknownCrowStalls(t *testing.T) { ts, _, _ := newRelayServer(t, `RELAY {"crow":"Soot","task":"blacken the roof"}`) id := newNest(t, ts, "Lab", `"Hugin","Munin"`) askQuill(t, ts, id, "get Soot to blacken the roof") awaitMessages(t, ts, id, 2) time.Sleep(200 * time.Millisecond) msgs := nestMessages(t, ts, id) total, requests := countBy(t, ts, id, "Quill", "request") if total != 1 || requests != 0 { t.Fatalf("want exactly one Quill message and zero Quill requests, got %v in %v", msgs, total) } var stall map[string]any for _, m := range msgs { if m["author"] == "Quill" { stall = m } } if stall == nil || stall["kind"] != "say" { t.Fatalf("the unknown-crow reply must be a Quill say, got %v", stall) } if stall["to"] != "owner" { t.Fatalf("the say must go back to the requester, got %v", stall["to"]) } } func TestQuillRelayNotAnsweredTwice(t *testing.T) { ts, _, _ := newRelayServer(t, `RELAY {"crow":"Hugin","task":"draft the tagline"}`, "KIND: deliver\nSo many crows, one banner.") id := newNest(t, ts, "Lab", `"Hugin","Munin"`) askQuill(t, ts, id, "get Hugin to draft the tagline") awaitMessages(t, ts, id, 3) time.Sleep(200 * time.Millisecond) total, delivers := countBy(t, ts, id, "Quill", "deliver") if total != 1 { t.Fatalf("a successful relay must leave Quill with exactly one message, got %d", total) } if delivers != 0 { t.Fatalf("a relay must not also deliver, got %d delivers", delivers) } } func TestQuillRelayGoesThroughFlock(t *testing.T) { ts, s := newTestNestServer(t) id := newNest(t, ts, "Lab", `"Hugin","Munin"`) s.p.Flock.brain = &relayBrain{replies: []string{`RELAY {"crow":"Hugin","task":"draft the tagline"}`}} s.p.Flock.sem = make(chan struct{}, 0) askQuill(t, ts, id, "get Hugin to draft the tagline") time.Sleep(300 * time.Millisecond) if got := nestMessages(t, ts, id); len(got) != 1 { t.Fatalf("a saturated flock means ask fails and Quill posts nothing: %v", got) } }