package main import ( "context" "strings" "sync" "testing" "time" ) type recordEmitter struct { mu sync.Mutex events []map[string]any } func (r *recordEmitter) Emit(event string, fields map[string]any) { if event != "crow.banter" { return } r.mu.Lock() defer r.mu.Unlock() r.events = append(r.events, fields) } // promptBrain records the banter prompts it receives. type promptBrain struct { mu sync.Mutex prompts []string reply string } func (b *promptBrain) Name() string { return "prompt" } func (b *promptBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) { b.mu.Lock() b.prompts = append(b.prompts, system+"\n"+prompt) b.mu.Unlock() return b.reply, 5, nil } func newChatter(b Brain, em Emitter, mem *flockMemory) *chatter { return &chatter{ f: NewFlock(b, DefaultGuardrails(), nil), mem: mem, em: em, budget: &Budget{name: "list", limit: 100_000}, roll: func() float64 { return 0 }, } } func TestRetrieveUsesBackstoryRelationshipAndMemory(t *testing.T) { mem := &flockMemory{} mem.add("Hugin's answer to \"ravens\" was rejected by the human") mem.add("Pepper answered \"names\"") lore := strings.Join(retrieve(mem, "Munin", "Hugin"), "\n") for _, want := range []string{"Hugin (Thought)", "Munin (Memory)", "Between them:", "Recently: Hugin's answer"} { if !strings.Contains(lore, want) { t.Errorf("lore missing %q:\n%s", want, lore) } } if strings.Contains(lore, "Pepper") { t.Errorf("retrieved an unrelated memory:\n%s", lore) } } func TestBanterIsGeneratedFromRetrievedLoreAndCapped(t *testing.T) { b := &promptBrain{reply: "MEMORY: Bold, that one.\nLINE: Munin: \"Bold as ever, Hugin.\""} em := &recordEmitter{} c := newChatter(b, em, &flockMemory{}) for i := 0; i < 5; i++ { c.moment(context.Background(), "L-1", "Hugin", "Munin", "The human rejected Hugin's answer.") } c.wait(5 * time.Second) if len(em.events) != MaxBanterPerList { t.Fatalf("got %d banter lines, want the cap of %d", len(em.events), MaxBanterPerList) } if got := em.events[0]["text"]; got != "Bold as ever, Hugin." { t.Fatalf("line not cleaned: %q", got) } if !strings.Contains(b.prompts[0], "Odin's two ravens") || !strings.Contains(b.prompts[0], markerBanter) { t.Fatalf("prompt missing retrieved lore or banter instructions:\n%s", b.prompts[0]) } } func TestBanterPicksARelatedSpeaker(t *testing.T) { b := &promptBrain{reply: "Caw."} em := &recordEmitter{} c := newChatter(b, em, &flockMemory{}) c.moment(context.Background(), "L-1", "Corvina", "", "Corvina leveled up.") c.wait(5 * time.Second) if len(em.events) != 1 || em.events[0]["crow"] != "Edgar" { t.Fatalf("want Edgar (Corvina's only relationship) to speak, got %v", em.events) } } func TestCleanBanterRejectsBadLines(t *testing.T) { for _, bad := range []string{"", strings.Repeat("caw ", 80), "see https://example.com", "visit www.crows.biz"} { if _, ok := cleanBanter(bad, "Soot"); ok { t.Errorf("accepted %q", bad) } } if got, ok := cleanBanter("Soot: “Nice try.”\nsecond line", "Soot"); !ok || got != "Nice try." { t.Errorf("got %q %v", got, ok) } } func TestWorkPromptsCarryPersonaAndPolicies(t *testing.T) { s := systemFor("Munin", crowSystem, true) for _, want := range []string{"You are Munin, Memory", "Mark anything uncertain", "Never invent sources"} { if !strings.Contains(s, want) { t.Errorf("Munin's prompt missing %q", want) } } if !strings.HasPrefix(systemFor("Sentry", sentrySystem, false), markerSentry) { t.Error("Sentry prompt must still start with its marker") } }