test(dispatch): red for #28 — saturated flock must honor context, all dispatch through Flock

This commit is contained in:
Smokey committed 2026-10-28 06:46:15 -06:00
1 parent 9f457eb512
commit c957bdd3f6
2 files changed
+175

No files matched your search

+169
View File
@@ -0,0 +1,169 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"sync"
"sync/atomic"
"testing"
"time"
)
// fakeDispatchJev answers kind/mode as choice and restates_earlier as noul.
func fakeDispatchJev(t *testing.T) *JevClient {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body struct {
Questions map[string]any `json:"questions"`
}
json.NewDecoder(r.Body).Decode(&body)
out := map[string]any{}
for q := range body.Questions {
switch q {
case "kind":
out[q] = map[string]any{"type": "choice", "choice": "say", "confidence": 0.5}
case "mode":
out[q] = map[string]any{"type": "choice", "choice": "conspiracy", "confidence": 0.9,
"probabilities": map[string]float64{"conspiracy": 0.9, "solo": 0.1}}
case "restates_earlier":
out[q] = map[string]any{"type": "noul", "noul": 0.85}
}
}
json.NewEncoder(w).Encode(map[string]any{"model": "jev-fake", "answers": out,
"usage": map[string]int{"input_tokens": 50, "output_tokens": 0}})
}))
t.Cleanup(srv.Close)
return &JevClient{url: srv.URL, key: "tk", model: "jev-fake", threshold: 0.7, client: srv.Client()}
}
// peakBrain tracks concurrent model calls.
type peakBrain struct {
mu sync.Mutex
peak, now int32
}
func (b *peakBrain) Name() string { return "peak" }
func (b *peakBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) {
c := atomic.AddInt32(&b.now, 1)
if c > atomic.LoadInt32(&b.peak) {
atomic.StoreInt32(&b.peak, c)
}
time.Sleep(30 * time.Millisecond)
atomic.AddInt32(&b.now, -1)
return "KIND: deliver\n[a] and [b] matter", 10, nil
}
func newDispatchServer(t *testing.T, g Guardrails) (*Server, string, *peakBrain) {
t.Helper()
dir := t.TempDir()
ledger, _ := LoadLedger(filepath.Join(dir, "ledger.json"))
flog, err := OpenFlightLog(filepath.Join(dir, "flight.jsonl"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { flog.Close() })
nests, _ := LoadNests(filepath.Join(dir, "nests.json"))
msgs, _ := LoadMessageStore(filepath.Join(dir, "messages"))
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
pb := &peakBrain{}
appr := NewWebApprover(time.Minute)
p := &Pipeline{Flock: NewFlock(pb, g, flog), Ledger: ledger, Log: flog, G: g, Approver: appr}
p.Flock.SetClassifier(fakeDispatchJev(t))
s := NewServer(ctx, p, appr, nests).WithMessages(msgs)
return s, filepath.Join(dir, "flight.jsonl"), pb
}
func TestDispatchCallsGoThroughFlock(t *testing.T) {
g := DefaultGuardrails()
flog := ""
dir := t.TempDir()
lg, err := OpenFlightLog(filepath.Join(dir, "flight.jsonl"))
if err != nil {
t.Fatal(err)
}
defer lg.Close()
flog = filepath.Join(dir, "flight.jsonl")
f := NewFlock(MockBrain{}, g, lg)
f.SetClassifier(fakeDispatchJev(t))
mode, ok := f.ClassifyMode(context.Background(), "is this risky")
if !ok || mode != "conspiracy" {
t.Fatalf("want conspiracy from the fake, got %q ok=%v", mode, ok)
}
evs, _ := ReadEvents(flog)
probLogged := false
for _, e := range evs {
if e.Kind == "mode" && e.Prob != nil {
probLogged = true
}
}
if !probLogged {
t.Fatal("ClassifyMode must write a flight-log event with probabilities")
}
// Saturate the flock semaphore: a wrapper call must wait for a slot,
// proving it goes through Flock (never a direct client call).
for i := 0; i < g.MaxCrowsInFlight; i++ {
f.sem <- struct{}{}
}
cctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
defer cancel()
_, blockedOk := f.ClassifyMode(cctx, "risky?")
if blockedOk {
t.Fatal("a saturated flock must not answer (semaphore not enforced)")
}
for i := 0; i < g.MaxCrowsInFlight; i++ {
<-f.sem
}
}
func TestConspiracyInviteCapInFlight(t *testing.T) {
g := DefaultGuardrails()
g.MaxCrowsInFlight = 1
s, _, _ := newDispatchServer(t, g)
id := newNest(t, serve(s), "Wide", `"Hugin","Munin","Soot"`)
promote(s, "Hugin", "Munin", "Soot")
// inviteCount caps at MaxCrowsInFlight=1 -> at most one invited
if n := s.inviteCount(mustNest(s, id), 4); n != 1 {
t.Fatalf("want invite capped to 1 by in-flight limit, got %d", n)
}
}
func TestDispatchGuardrailsEndToEnd(t *testing.T) {
g := DefaultGuardrails()
g.MaxCrowsInFlight = 2
s, logPath, pb := newDispatchServer(t, g)
ts := serve(s)
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
promote(s, "Hugin", "Munin")
code, req := post(t, ts.URL+"/api/nests/"+id+"/messages",
`{"author":"owner","kind":"request","text":"is this launch risky"}`)
if code != http.StatusCreated || req["mode"] != "conspiracy" || req["tagged_by"] != "header" {
t.Fatalf("post: %d %v", code, req)
}
awaitMessages(t, ts, id, 3) // request + 2 lens claims
evs, err := ReadEvents(logPath)
if err != nil {
t.Fatal(err)
}
kinds := map[string]int{}
for _, e := range evs {
kinds[e.Kind]++
}
if kinds["mode"] == 0 || kinds["tag"] == 0 || kinds["flight"] == 0 {
t.Fatalf("dispatch decisions must be logged (mode/tag/flight), got %v", kinds)
}
if pb.peak > 2 {
t.Fatalf("flock cap violated: peak concurrent calls %d > 2", pb.peak)
}
}
func mustNest(s *Server, id string) Nest {
n, _ := s.nests.Get(id)
return n
}
+6
View File
@@ -214,3 +214,9 @@ func promote(s *Server, crows ...string) {
s.p.Ledger.Crows[c+"|other"] = &Record{Approved: 10, Streak: 10, Level: Regular} s.p.Ledger.Crows[c+"|other"] = &Record{Approved: 10, Streak: 10, Level: Regular}
} }
} }
// serve mounts a server's handler on a test HTTP server.
func serve(s *Server) *httptest.Server {
ts := httptest.NewServer(s.Handler())
return ts
}