Files
so-many-crows/banter_test.go
T

114 lines
3.5 KiB
Go

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")
}
}