215 lines
6.3 KiB
Go
215 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"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")
|
|
}
|
|
}
|
|
|
|
func TestQuillPromptContainsNestHistory(t *testing.T) {
|
|
ts, _, qb := newQuillServer(t, "Hugin finished the build already.")
|
|
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
|
|
|
|
// 1. owner says something that sets up context
|
|
code, out := post(t, ts.URL+"/api/nests/"+id+"/messages",
|
|
`{"author":"owner","to":"Hugin","kind":"say","text":"build the widget factory"}`)
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("post say: %d", code)
|
|
}
|
|
sayMsgID := out["msg_id"].(string)
|
|
|
|
// 2. Hugin delivers an answer
|
|
code, _ = post(t, ts.URL+"/api/nests/"+id+"/messages",
|
|
`{"author":"Hugin","kind":"deliver","text":"the widget factory is live","refers_to":"`+sayMsgID+`"}`)
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("post deliver: %d", code)
|
|
}
|
|
|
|
// 3. owner asks Quill for status
|
|
code, _ = post(t, ts.URL+"/api/nests/"+id+"/messages",
|
|
`{"author":"owner","to":"Quill","kind":"request","text":"where did that get to"}`)
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("post request to Quill: %d", code)
|
|
}
|
|
|
|
// Wait for Quill's reply (3 prior + 1 Quill reply = 4)
|
|
msgs := awaitMessages(t, ts, id, 4)
|
|
var quillReply map[string]any
|
|
for _, m := range msgs {
|
|
if m["author"] == "Quill" {
|
|
quillReply = m
|
|
}
|
|
}
|
|
if quillReply == nil {
|
|
t.Fatalf("Quill never answered: %v", msgs)
|
|
}
|
|
|
|
qb.mu.Lock()
|
|
defer qb.mu.Unlock()
|
|
if len(qb.pairs) != 1 {
|
|
t.Fatalf("want exactly one model call, got %d", len(qb.pairs))
|
|
}
|
|
prompt := qb.pairs[0][1]
|
|
if !strings.Contains(prompt, "build the widget factory") {
|
|
t.Fatalf("prompt must contain the owner's earlier message: %q", prompt)
|
|
}
|
|
if !strings.Contains(prompt, "the widget factory is live") {
|
|
t.Fatalf("prompt must contain Hugin's deliver: %q", prompt)
|
|
}
|
|
}
|
|
|
|
func TestQuillEmptyNestSaysQuiet(t *testing.T) {
|
|
ts, _, qb := newQuillServer(t, "All quiet here.")
|
|
id := newNest(t, ts, "Quiet", `"Hugin"`)
|
|
|
|
// Only message is the request to Quill — history excluding it is empty
|
|
code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages",
|
|
`{"author":"owner","to":"Quill","kind":"request","text":"any updates?"}`)
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("post: %d", code)
|
|
}
|
|
awaitMessages(t, ts, id, 2)
|
|
|
|
qb.mu.Lock()
|
|
defer qb.mu.Unlock()
|
|
if len(qb.pairs) != 1 {
|
|
t.Fatalf("want exactly one model call, got %d", len(qb.pairs))
|
|
}
|
|
prompt := qb.pairs[0][1]
|
|
if !strings.Contains(prompt, "no earlier messages") {
|
|
t.Fatalf("prompt must state the nest is empty: %q", prompt)
|
|
}
|
|
}
|
|
|
|
func TestQuillHistoryCap(t *testing.T) {
|
|
ts, _, qb := newQuillServer(t, "got it")
|
|
id := newNest(t, ts, "Busy", `"Hugin"`)
|
|
|
|
// Post 25 filler messages from owner
|
|
for i := 0; i < 25; i++ {
|
|
code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages",
|
|
fmt.Sprintf(`{"author":"owner","kind":"say","text":"filler line %d"}`, i))
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("post filler %d: %d", i, code)
|
|
}
|
|
}
|
|
|
|
// Ask Quill
|
|
code, _ := post(t, ts.URL+"/api/nests/"+id+"/messages",
|
|
`{"author":"owner","to":"Quill","kind":"request","text":"status"}`)
|
|
if code != http.StatusCreated {
|
|
t.Fatalf("post to Quill: %d", code)
|
|
}
|
|
awaitMessages(t, ts, id, 27)
|
|
|
|
qb.mu.Lock()
|
|
defer qb.mu.Unlock()
|
|
if len(qb.pairs) != 1 {
|
|
t.Fatalf("want exactly one model call, got %d", len(qb.pairs))
|
|
}
|
|
prompt := qb.pairs[0][1]
|
|
historyLines := 0
|
|
for _, line := range strings.Split(prompt, "\n") {
|
|
if strings.HasPrefix(line, "[") {
|
|
historyLines++
|
|
}
|
|
}
|
|
if historyLines > 20 {
|
|
t.Fatalf("history lines must be capped at 20, got %d\nprompt:\n%s", historyLines, prompt)
|
|
}
|
|
if historyLines == 0 {
|
|
t.Fatalf("expected some history lines, got none")
|
|
}
|
|
}
|