package main import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "path/filepath" "slices" "strings" "sync" "testing" "time" ) // bossBrain is a scripted brain that records how often it was called and hands // back one canned reply per call. type bossBrain struct { mu sync.Mutex calls int resp []string } func (b *bossBrain) Name() string { return "boss-script" } func (b *bossBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) { b.mu.Lock() defer b.mu.Unlock() r := "no reply" if b.calls < len(b.resp) { r = b.resp[b.calls] } b.calls++ return r, 10, nil } func (b *bossBrain) count() int { b.mu.Lock() defer b.mu.Unlock() return b.calls } // newBossServer builds a whole server by hand (like newDispatchServer) so the // Boss tests can attach a CrowStore at a temp path. func newBossServer(t *testing.T, replies ...string) (*Server, *bossBrain) { t.Helper() dir := t.TempDir() ledger, err := LoadLedger(filepath.Join(dir, "ledger.json")) if err != nil { t.Fatal(err) } flog, err := OpenFlightLog(filepath.Join(dir, "flight.jsonl")) if err != nil { t.Fatal(err) } t.Cleanup(func() { flog.Close() }) nests, err := LoadNests(filepath.Join(dir, "nests.json")) if err != nil { t.Fatal(err) } msgs, err := LoadMessageStore(filepath.Join(dir, "messages")) if err != nil { t.Fatal(err) } crows, err := LoadCrowStore(filepath.Join(dir, "crows.json")) if err != nil { t.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) g := DefaultGuardrails() bb := &bossBrain{resp: replies} appr := NewWebApprover(time.Minute) p := &Pipeline{Flock: NewFlock(bb, g, flog), Ledger: ledger, Log: flog, G: g, Approver: appr} s := NewServer(ctx, p, appr, nests).WithMessages(msgs).WithCrows(crows) return s, bb } func askBoss(t *testing.T, ts *httptest.Server, nestID, text string) (int, map[string]any) { t.Helper() code, out := post(t, ts.URL+"/api/nests/"+nestID+"/messages", `{"author":"owner","kind":"request","to":"Boss","text":`+jsonString(text)+`}`) if code != http.StatusCreated { t.Fatalf("owner POST to Boss: want 201, got %d %v", code, out) } return code, out } func jsonString(s string) string { b, _ := json.Marshal(s) return string(b) } func registryCrows(t *testing.T, ts *httptest.Server) []map[string]string { t.Helper() resp, err := http.Get(ts.URL + "/api/crows") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("GET /api/crows: want 200, got %d", resp.StatusCode) } var out struct { Crows []map[string]string `json:"crows"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { t.Fatal(err) } return out.Crows } // bossSay waits for the nest to hold minTotal messages and returns the latest // one from Boss. func bossSay(t *testing.T, ts *httptest.Server, nestID string, minTotal int) map[string]any { t.Helper() var last map[string]any for _, m := range awaitMessages(t, ts, nestID, minTotal) { if m["author"] == "Boss" { last = m } } if last == nil { t.Fatalf("Boss never answered: %v", nestMessages(t, ts, nestID)) } return last } func spunByName(crows []map[string]string, name string) map[string]string { for _, c := range crows { if c["name"] == name { return c } } return nil } func TestBossSpinUpCreatesCrow(t *testing.T) { s, bb := newBossServer(t, `SPIN {"name":"Ash","persona":"Soot","trust":"stranger"}`) ts := serve(s) defer ts.Close() id := newNest(t, ts, "Lab", `"Hugin"`) askBoss(t, ts, id, "spin up a crow named Ash") say := bossSay(t, ts, id, 2) if say["kind"] != "say" || say["to"] != "owner" { t.Fatalf("Boss must post a say back to the owner, got %v", say) } if !strings.Contains(say["text"].(string), "Ash") { t.Fatalf("Boss say must name the new crow, got %v", say["text"]) } got := spunByName(registryCrows(t, ts), "Ash") if got == nil { t.Fatalf("registry must contain Ash, got %v", registryCrows(t, ts)) } if got["persona"] != "Soot" || got["trust"] != "stranger" { t.Fatalf("want Ash on Soot at stranger, got %v", got) } rec := s.p.Ledger.Crows["Ash|other"] if rec == nil || rec.Level != Stranger { t.Fatalf("ledger must carry Ash|other at Stranger, got %+v", rec) } if bb.count() != 1 { t.Fatalf("one spin-up is exactly one model call, got %d", bb.count()) } } func TestSpinUpCapRefused(t *testing.T) { s, _ := newBossServer(t, `SPIN {"name":"Ash","persona":"Soot","trust":"stranger"}`, `SPIN {"name":"Beck","persona":"Jet","trust":"stranger"}`) s.p.G.MaxSpunCrows = 1 ts := serve(s) defer ts.Close() id := newNest(t, ts, "Lab", `"Hugin"`) askBoss(t, ts, id, "spin up a crow named Ash") bossSay(t, ts, id, 2) if n := len(registryCrows(t, ts)); n != 1 { t.Fatalf("first spin-up must land, registry has %d", n) } askBoss(t, ts, id, "spin up a crow named Beck") say := bossSay(t, ts, id, 4) text, _ := say["text"].(string) if !strings.Contains(text, "cap") { t.Fatalf("the refusal must name the cap, got %q", text) } if n := len(registryCrows(t, ts)); n != 1 { t.Fatalf("a refused spin-up must not touch the registry, has %d", n) } if _, ok := s.p.Ledger.Crows["Beck|other"]; ok { t.Fatal("a refused spin-up must not touch the ledger") } } func TestSpinUpDuplicateNameRefused(t *testing.T) { s, _ := newBossServer(t, `SPIN {"name":"Hugin","persona":"Soot","trust":"stranger"}`) ts := serve(s) defer ts.Close() id := newNest(t, ts, "Lab", `"Hugin"`) askBoss(t, ts, id, "spin up a crow named Hugin") say := bossSay(t, ts, id, 2) text, _ := say["text"].(string) if !strings.Contains(text, "taken") { t.Fatalf("the refusal must name the taken name, got %q", text) } if n := len(registryCrows(t, ts)); n != 0 { t.Fatalf("registry must stay empty, has %d", n) } } func TestSpinUpTrustGated(t *testing.T) { s, _ := newBossServer(t, `SPIN {"name":"Ash","persona":"Soot","trust":"trusted"}`) ts := serve(s) defer ts.Close() id := newNest(t, ts, "Lab", `"Hugin"`) askBoss(t, ts, id, "spin up a crow named Ash, make it trusted") bossSay(t, ts, id, 2) got := spunByName(registryCrows(t, ts), "Ash") if got == nil { t.Fatal("Ash must be in the registry") } if got["trust"] != "regular" { t.Fatalf("trusted must clamp to regular, got %q", got["trust"]) } rec := s.p.Ledger.Crows["Ash|other"] if rec == nil || rec.Level > Regular { t.Fatalf("ledger level must be at most Regular, got %+v", rec) } } func TestSpinUpGoesThroughFlock(t *testing.T) { s, _ := newBossServer(t, `SPIN {"name":"Ash","persona":"Soot","trust":"stranger"}`) s.p.Flock.sem = make(chan struct{}, 0) // saturated flock: ask must wait, never act ts := serve(s) defer ts.Close() id := newNest(t, ts, "Lab", `"Hugin"`) askBoss(t, ts, id, "spin up a crow named Ash") time.Sleep(300 * time.Millisecond) for _, m := range nestMessages(t, ts, id) { if m["author"] == "Boss" { t.Fatalf("a saturated flock must not produce a Boss message, got %v", m) } } if n := len(registryCrows(t, ts)); n != 0 { t.Fatalf("a saturated flock must not write the registry, has %d", n) } } func TestBossPlainReply(t *testing.T) { s, _ := newBossServer(t, "The schedule stands. Nothing needs whip.") ts := serve(s) defer ts.Close() id := newNest(t, ts, "Lab", `"Hugin"`) askBoss(t, ts, id, "how goes the books") say := bossSay(t, ts, id, 2) if say["kind"] != "say" || say["text"] != "The schedule stands. Nothing needs whip." { t.Fatalf("Boss posts its plain reply as a say, got %v", say) } if n := len(registryCrows(t, ts)); n != 0 { t.Fatalf("a plain reply must not touch the registry, has %d", n) } } func TestSpinUpGarbledRefused(t *testing.T) { s, _ := newBossServer(t, `SPIN {"name": broken json`) ts := serve(s) defer ts.Close() id := newNest(t, ts, "Lab", `"Hugin"`) askBoss(t, ts, id, "spin up a crow") say := bossSay(t, ts, id, 2) text, _ := say["text"].(string) if text == "" { t.Fatalf("an unreadable order must still get a refusal, got %v", say) } if n := len(registryCrows(t, ts)); n != 0 { t.Fatalf("an unreadable order must not touch the registry, has %d", n) } } func TestPersonasEndpointIncludesBoss(t *testing.T) { ts, _ := newTestNestServer(t) defer ts.Close() resp, err := http.Get(ts.URL + "/api/personas") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("GET /api/personas: want 200, got %d", resp.StatusCode) } var out []map[string]string if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { t.Fatal(err) } names := map[string]bool{} for _, p := range out { names[p["name"]] = true } for _, want := range []string{"Boss", "Quill", "Hugin"} { if !names[want] { t.Fatalf("/api/personas must list every persona crow (missing %s): %v", want, names) } } } func TestCrowsRegistryEmptyWithoutStore(t *testing.T) { ts, _ := newTestNestServer(t) // no WithCrows: the registry reads as empty defer ts.Close() resp, err := http.Get(ts.URL + "/api/crows") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("want 200 with an empty list, got %d", resp.StatusCode) } var out struct { Crows []map[string]string `json:"crows"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { t.Fatal(err) } if len(out.Crows) != 0 { t.Fatalf("want no crows, got %v", out.Crows) } } // --- #43: Boss creates nests and assigns members --- // listNests returns every nest from GET /api/nests. func listNests(t *testing.T, ts *httptest.Server) []map[string]any { t.Helper() resp, err := http.Get(ts.URL + "/api/nests") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("GET /api/nests: want 200, got %d", resp.StatusCode) } var out struct { Nests []map[string]any `json:"nests"` } if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { t.Fatal(err) } return out.Nests } func findNest(t *testing.T, ts *httptest.Server, name string) map[string]any { t.Helper() for _, n := range listNests(t, ts) { if n["name"] == name { return n } } return nil } func nestCrowNames(nest map[string]any) []string { raw, _ := nest["crows"].([]any) out := make([]string, 0, len(raw)) for _, c := range raw { out = append(out, c.(string)) } slices.Sort(out) return out } func TestBossCreatesNest(t *testing.T) { s, bb := newBossServer(t, `SPIN {"name":"Ash","persona":"Soot","trust":"stranger"}`, `NEST {"name":"Launch","crows":["Hugin","Ash"]}`, ) ts := serve(s) defer ts.Close() labID := newNest(t, ts, "Lab", `"Hugin"`) // Ash must exist as a registry crow: spin it through Boss first. askBoss(t, ts, labID, "spin up a crow named Ash") bossSay(t, ts, labID, 2) askBoss(t, ts, labID, "make a nest Launch with Hugin and Ash") var launch map[string]any deadline := time.Now().Add(2 * time.Second) for launch == nil && time.Now().Before(deadline) { launch = findNest(t, ts, "Launch") if launch == nil { time.Sleep(20 * time.Millisecond) } } if launch == nil { t.Fatalf("Boss must create the nest Launch, nests: %v", listNests(t, ts)) } if got := nestCrowNames(launch); !slices.Equal(got, []string{"Ash", "Hugin"}) { t.Fatalf("Launch crows must be exactly [Ash Hugin], got %v", got) } // Boss announces membership in the NEW nest: a say to the owner. announced := false for _, m := range awaitMessages(t, ts, launch["nest_id"].(string), 1) { if m["author"] == "Boss" && m["kind"] == "say" && m["to"] == "owner" { announced = true } } if !announced { t.Fatalf("Boss must post a say to the owner in Launch, got %v", nestMessages(t, ts, launch["nest_id"].(string))) } // And confirms in the requesting nest. confirmed := false for _, m := range awaitMessages(t, ts, labID, 4) { if m["author"] == "Boss" && m["kind"] == "say" && strings.Contains(m["text"].(string), "Launch") { confirmed = true } } if !confirmed { t.Fatalf("Boss must confirm the new nest in Lab, got %v", nestMessages(t, ts, labID)) } if bb.count() != 2 { t.Fatalf("two orders are two model calls, got %d", bb.count()) } } func TestBossNestUnknownMemberRefused(t *testing.T) { s, _ := newBossServer(t, `NEST {"name":"Nope","crows":["Gandalf"]}`) ts := serve(s) defer ts.Close() labID := newNest(t, ts, "Lab", `"Hugin"`) before := len(listNests(t, ts)) askBoss(t, ts, labID, "make a nest Nope with Gandalf") say := bossSay(t, ts, labID, 2) text, _ := say["text"].(string) if strings.HasPrefix(text, "NEST") { t.Fatalf("Boss must interpret the order, not echo it back: %q", text) } if !strings.Contains(text, "Gandalf") { t.Fatalf("the refusal must name the unknown member, got %q", text) } if after := len(listNests(t, ts)); after != before { t.Fatalf("an unknown member must refuse the whole create: %d before, %d after", before, after) } } func TestBossNestCapRespected(t *testing.T) { s, _ := newBossServer(t, `NEST {"name":"Overflow","crows":["Hugin"]}`) ts := serve(s) defer ts.Close() labID := newNest(t, ts, "Lab", `"Hugin"`) // Fill the store to its cap: todo + Lab + (maxNests-2) pads. for i := 0; 2+i < maxNests; i++ { if _, err := s.nests.Create(fmt.Sprintf("Pad %d", i), []string{"Hugin"}); err != nil { t.Fatalf("padding nests: %v", err) } } if n := len(listNests(t, ts)); n != maxNests { t.Fatalf("test setup: want %d nests, got %d", maxNests, n) } askBoss(t, ts, labID, "make a nest Overflow with Hugin") // 201: no 500 at the front door say := bossSay(t, ts, labID, 2) text, _ := say["text"].(string) if !strings.Contains(text, fmt.Sprintf("at most %d nests", maxNests)) { t.Fatalf("Boss must report the store's cap refusal, got %q", text) } if findNest(t, ts, "Overflow") != nil { t.Fatal("a capped create must not make a nest") } } func TestBossNestGoesThroughFlock(t *testing.T) { s, _ := newBossServer(t, `NEST {"name":"Launch","crows":["Hugin","Munin"]}`) s.p.Flock.sem = make(chan struct{}, 0) // saturated flock: ask must wait, never act ts := serve(s) defer ts.Close() labID := newNest(t, ts, "Lab", `"Hugin"`) before := len(listNests(t, ts)) askBoss(t, ts, labID, "make a nest Launch with Hugin and Munin") time.Sleep(300 * time.Millisecond) if n := len(listNests(t, ts)); n != before { t.Fatalf("a saturated flock must not create a nest, got %v", listNests(t, ts)) } for _, m := range nestMessages(t, ts, labID) { if m["author"] == "Boss" { t.Fatalf("a saturated flock must not produce a Boss message, got %v", m) } } }