Per-crow banter voice system: three-layer prompt (memory, voice, line)
This commit is contained in:
1 parent
017d11cd99
commit
fbf9fe67a6
6 files changed
+235
-3
No files matched your search
@@ -0,0 +1,93 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestVoicedPromptHasThreeLayers(t *testing.T) {
|
||||
p, _ := personaOf("Hugin")
|
||||
s := voicedBanterSystem(p, voices["Hugin"])
|
||||
for _, want := range []string{
|
||||
markerBanter,
|
||||
"You are Hugin, Thought",
|
||||
"MEMORY:", "LINE:", // layer 1
|
||||
"Favor:", "Rhythm:", "Never: Hedging", "Never: System words", // layer 2
|
||||
"word for word", "Hand the turn back: end with a dare", // layer 3
|
||||
} {
|
||||
if !strings.Contains(s, want) {
|
||||
t.Errorf("voiced prompt missing %q", want)
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(s, markerBanter) {
|
||||
t.Error("voiced prompt must start with the banter marker")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVoicedDropsMemoryAndKeepsLine(t *testing.T) {
|
||||
got, ok := parseVoiced("MEMORY: I flew that road once.\nLINE: \"Slow as ever, Munin. Race me?\"", "Hugin")
|
||||
if !ok || got != "Slow as ever, Munin. Race me?" {
|
||||
t.Fatalf("got %q %v", got, ok)
|
||||
}
|
||||
if got, ok := parseVoiced("MEMORY: x\nLINE:\nRace me, Munin.", "Hugin"); !ok || got != "Race me, Munin." {
|
||||
t.Fatalf("line on next row: got %q %v", got, ok)
|
||||
}
|
||||
// A memory with no line must never reach the chat.
|
||||
if got, ok := parseVoiced("MEMORY: I flew that road once.", "Hugin"); ok {
|
||||
t.Fatalf("leaked memory: %q", got)
|
||||
}
|
||||
// A reply that ignored the format (or came from an unrelated model) is dropped.
|
||||
for _, bad := range []string{"Race me, Munin.", "User Safety: safe"} {
|
||||
if got, ok := parseVoiced(bad, "Hugin"); ok {
|
||||
t.Fatalf("accepted unlabeled reply %q", got)
|
||||
}
|
||||
}
|
||||
// The line is still cleaned and validated.
|
||||
if _, ok := parseVoiced("MEMORY: x\nLINE: see https://example.com", "Hugin"); ok {
|
||||
t.Fatal("accepted a link")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVoicedCrowSpeaksThroughMemoryAndLine(t *testing.T) {
|
||||
b := &promptBrain{reply: "MEMORY: Odin feared for him less. Still stings.\nLINE: Back by breakfast, Munin. Try to keep up."}
|
||||
em := &recordEmitter{}
|
||||
c := newChatter(b, em, &flockMemory{})
|
||||
c.moment(context.Background(), "L-1", "Munin", "Hugin", "The human approved Munin's answer.")
|
||||
c.wait(5 * time.Second)
|
||||
if len(em.events) != 1 {
|
||||
t.Fatalf("got %d lines", len(em.events))
|
||||
}
|
||||
if got := em.events[0]["text"]; got != "Back by breakfast, Munin. Try to keep up." {
|
||||
t.Fatalf("memory leaked or line wrong: %q", got)
|
||||
}
|
||||
if !strings.Contains(b.prompts[0], "MEMORY:") || !strings.Contains(b.prompts[0], "Odin's two ravens") {
|
||||
t.Fatalf("voiced prompt or retrieved lore missing:\n%s", b.prompts[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnvoicedCrowKeepsPlainBanter(t *testing.T) {
|
||||
b := &promptBrain{reply: "Nice try."}
|
||||
em := &recordEmitter{}
|
||||
c := newChatter(b, em, &flockMemory{})
|
||||
c.moment(context.Background(), "L-1", "Hugin", "Soot", "The human rejected Hugin's answer.")
|
||||
c.wait(5 * time.Second)
|
||||
if len(em.events) != 1 || em.events[0]["text"] != "Nice try." {
|
||||
t.Fatalf("got %v", em.events)
|
||||
}
|
||||
if strings.Contains(b.prompts[0], "MEMORY:") {
|
||||
t.Error("an unvoiced crow must not get the two-step prompt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEveryVoiceBelongsToAPersonaAndIsComplete(t *testing.T) {
|
||||
for name, v := range voices {
|
||||
if _, ok := personaOf(name); !ok {
|
||||
t.Errorf("voice %q has no persona", name)
|
||||
}
|
||||
if len(v.Diction) == 0 || len(v.Rhythm) == 0 || len(v.Never) == 0 || v.Hook == "" {
|
||||
t.Errorf("voice %q is incomplete", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user