package main import ( "context" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" ) type gateBrain struct { mu sync.Mutex calls []string } func (b *gateBrain) Name() string { return "gate" } func (b *gateBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) { b.mu.Lock() b.calls = append(b.calls, system+"\n"+prompt) b.mu.Unlock() return "KIND: claim\nOn it.", 10, nil } func newGateServer(t *testing.T) (*httptest.Server, *Server, *gateBrain) { t.Helper() ts, s := newTestNestServer(t) gb := &gateBrain{} s.p.Flock.brain = gb return ts, s, gb } func TestStrangerOnlyWhenAddressed(t *testing.T) { ts, s, gb := newGateServer(t) id := newNest(t, ts, "Lab", `"Hugin","Munin","Soot"`) promote(s, "Munin", "Soot") // Hugin stays a stranger code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","kind":"request","mode":"conspiracy","text":"is this risky"}`) if code != http.StatusCreated { t.Fatalf("post: %d", code) } awaitMessages(t, ts, id, 3) // request + 2 claims (Munin, Soot) gb.mu.Lock() defer gb.mu.Unlock() for _, call := range gb.calls { if strings.Contains(call, "You are Hugin,") { t.Fatal("a stranger must not be invited for open work") } } names := 0 for _, n := range []string{"You are Munin,", "You are Soot,"} { for _, call := range gb.calls { if strings.Contains(call, n) { names++ break } } } if names != 2 { t.Fatalf("both regular crows should be invited, calls: %v", gb.calls) } } func TestRegularMayClaimOpen(t *testing.T) { ts, s, gb := newGateServer(t) id := newNest(t, ts, "Lab", `"Hugin","Munin"`) promote(s, "Hugin") // only Hugin is a regular post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","kind":"request","text":"tidy the branch notes"}`) // no to, no mode header awaitMessages(t, ts, id, 2) gb.mu.Lock() defer gb.mu.Unlock() if len(gb.calls) != 1 { t.Fatalf("an open request invites exactly one eligible crow, calls: %v", gb.calls) } if !strings.Contains(gb.calls[0], "You are Hugin,") { t.Fatalf("the eligible crow should be invited, got %q", gb.calls[0]) } } func TestAllStrangersStayOpen(t *testing.T) { ts, _, gb := newGateServer(t) id := newNest(t, ts, "Lab", `"Hugin","Munin"`) code, out := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","kind":"request","text":"do the thing"}`) if code != http.StatusCreated { t.Fatalf("post: %d %v", code, out) } time.Sleep(300 * time.Millisecond) gb.mu.Lock() defer gb.mu.Unlock() if len(gb.calls) != 0 { t.Fatalf("a nest of only strangers must not be summoned, calls: %v", gb.calls) } if len(nestMessages(t, ts, id)) != 1 { t.Fatal("the request must stay open until addressed") } } func TestClaimAuthorHolds(t *testing.T) { ts, s := newScriptedServer(t, "hmm idle chatter", "still not an answer") // addressee stays silent id := newNest(t, ts, "Lab", `"Hugin","Munin"`) code, req := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","kind":"request","to":"Hugin","text":"count the eggs"}`) if code != http.StatusCreated { t.Fatalf("request: %d", code) } // Munin cannot claim what owner asked Hugin. c2, out := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"Munin","kind":"claim","refers_to":"`+req["msg_id"].(string)+`","text":"mine"}`) // give the addressee's must-respond (and its flag) time to settle first awaitMessages(t, ts, id, 2) if c2 != http.StatusConflict { t.Fatalf("directed assignment must refuse a non-addressee claim, got %d %v", c2, out) } if msg, _ := out["message"].(string); !strings.Contains(msg, "Hugin") { t.Fatalf("refusal must name the asked crow, got %q", msg) } _ = s }