test(boss): red for #42 — Boss spin_up needs a crow registry, /api/crows and MaxSpunCrows

This commit is contained in:
Smokey committed 2026-10-28 09:00:54 -06:00
1 parent 0963210ffd
commit ccb0e503c5
2 files changed
+341 -2

No files matched your search

+330
View File
@@ -0,0 +1,330 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"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
}
func bossSay(t *testing.T, ts *httptest.Server, nestID string) map[string]any {
t.Helper()
for _, m := range awaitMessages(t, ts, nestID, 2) {
if m["author"] == "Boss" {
return m
}
}
t.Fatalf("Boss never answered: %v", nestMessages(t, ts, nestID))
return nil
}
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)
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)
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)
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)
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)
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)
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)
text, _ := say["text"].(string)
if text == "" {
t.Fatal("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)
}
}