diff --git a/quill_test.go b/quill_test.go new file mode 100644 index 0000000..cbbb9a2 --- /dev/null +++ b/quill_test.go @@ -0,0 +1,97 @@ +package main + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "sync" + "testing" +) + +type quillBrain struct { + mu sync.Mutex + pairs [][2]string + reply string +} + +func (b *quillBrain) Name() string { return "quill-rec" } +func (b *quillBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) { + b.mu.Lock() + b.pairs = append(b.pairs, [2]string{system, prompt}) + b.mu.Unlock() + return b.reply, 10, nil +} + +func newQuillServer(t *testing.T, reply string) (*httptest.Server, *Server, *quillBrain) { + t.Helper() + ts, s := newTestNestServer(t) + qb := &quillBrain{reply: reply} + s.p.Flock.brain = qb + return ts, s, qb +} + +func TestQuillIsServiceRole(t *testing.T) { + ts, _, _ := newQuillServer(t, "hello back") + id := newNest(t, ts, "Lab", `"Hugin","Munin"`) + code, msg := post(t, ts.URL+"/api/nests/"+id+"/messages", + `{"author":"owner","to":"Quill","kind":"say","text":"hello"}`) + if code != http.StatusCreated { + t.Fatalf("Quill must be addressable like Elder: got %d %v", code, msg) + } +} + +func TestQuillAnswersInCharacter(t *testing.T) { + ts, _, qb := newQuillServer(t, "Already on it, boss.") + id := newNest(t, ts, "Lab", `"Hugin","Munin"`) + code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages", + `{"author":"owner","to":"Quill","kind":"request","text":"quiet the room"}`) + if code != http.StatusCreated { + t.Fatalf("post: %d", code) + } + msgs := awaitMessages(t, ts, id, 2) + var reply map[string]any + for _, m := range msgs { + if m["author"] == "Quill" { + reply = m + } + } + if reply == nil { + t.Fatalf("Quill never answered: %v", msgs) + } + if reply["kind"] != "deliver" || reply["to"] != "owner" { + t.Fatalf("want deliver to owner, got %v", reply) + } + if reply["text"] != "Already on it, boss." { + t.Fatalf("reply text: %v", reply["text"]) + } + qb.mu.Lock() + defer qb.mu.Unlock() + if len(qb.pairs) != 1 { + t.Fatalf("want exactly one model call, got %d", len(qb.pairs)) + } + if !strings.Contains(qb.pairs[0][0], "Quill") || !strings.Contains(qb.pairs[0][0], "The Hand") { + t.Fatalf("system prompt must carry the Quill persona: %q", qb.pairs[0][0]) + } + if !strings.Contains(qb.pairs[0][1], "quiet the room") { + t.Fatalf("prompt must carry the request text: %q", qb.pairs[0][1]) + } +} + +func TestQuillNotInNestCrows(t *testing.T) { + ts, s, _ := newQuillServer(t, "noted") + id := newNest(t, ts, "Lab", `"Hugin"`) + post(t, ts.URL+"/api/nests/"+id+"/messages", + `{"author":"owner","to":"Quill","kind":"request","text":"status?"}`) + awaitMessages(t, ts, id, 2) + for _, c := range nestOf(s, id).Crows { + if c == "Quill" { + t.Fatal("Quill must never become a nest member") + } + } + // No trust promotion happened for Quill either: service roles act on + // address, not on trust (the test server ledger was never touched). + if _, ok := s.p.Ledger.Crows["Quill|other"]; ok { + t.Fatal("Quill must not need or earn a worker trust record") + } +}