feat(persona): 人格系统(YAML loader / 角色人格注册+记忆隔离 / 情绪状态机+好感度)
This commit is contained in:
1 parent
653c2489bf
commit
70fd915242
11 files changed
+1568
No files matched your search
@@ -0,0 +1,209 @@
|
||||
package registry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/persona/emotion"
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/persona/loader"
|
||||
)
|
||||
|
||||
func mkPersona(id, ref, ns string) *loader.Persona {
|
||||
return &loader.Persona{
|
||||
ID: id,
|
||||
Name: id,
|
||||
CharacterRef: ref,
|
||||
Identity: loader.Identity{Role: "role", SystemPrompt: "system prompt"},
|
||||
Emotion: loader.EmotionParams{InitialState: emotion.Calm, MemoryNamespace: ns},
|
||||
}
|
||||
}
|
||||
|
||||
func mkChar(id, personaID string, def bool) *Character {
|
||||
return &Character{ID: id, Name: id, PersonaID: personaID, Default: def, Modes: []string{"chat", "home"}}
|
||||
}
|
||||
|
||||
func TestRegistry_RegisterAndGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := New()
|
||||
if err := r.RegisterPersona(mkPersona("xilian", "c", "")); err != nil {
|
||||
t.Fatalf("RegisterPersona() error: %v", err)
|
||||
}
|
||||
if err := r.RegisterCharacter(mkChar("xilian-char", "xilian", false)); err != nil {
|
||||
t.Fatalf("RegisterCharacter() error: %v", err)
|
||||
}
|
||||
|
||||
p, ok := r.Persona("xilian")
|
||||
if !ok || p.Name != "xilian" {
|
||||
t.Fatalf("Persona(xilian) = %+v, ok=%v", p, ok)
|
||||
}
|
||||
c, ok := r.Character("xilian-char")
|
||||
if !ok || c.PersonaID != "xilian" {
|
||||
t.Fatalf("Character(xilian-char) = %+v, ok=%v", c, ok)
|
||||
}
|
||||
if r.CountPersonas() != 1 || r.CountCharacters() != 1 {
|
||||
t.Fatalf("counts = %d/%d, want 1/1", r.CountPersonas(), r.CountCharacters())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_DefaultCharacter(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := New()
|
||||
_ = r.RegisterPersona(mkPersona("pa", "c", ""))
|
||||
_ = r.RegisterPersona(mkPersona("pb", "c", ""))
|
||||
if err := r.RegisterCharacter(mkChar("first", "pa", true)); err != nil {
|
||||
t.Fatalf("RegisterCharacter(first) error: %v", err)
|
||||
}
|
||||
_ = r.RegisterCharacter(&Character{ID: "second", Name: "second", PersonaID: "pb"})
|
||||
|
||||
def, ok := r.DefaultCharacter()
|
||||
if !ok || def.ID != "first" {
|
||||
t.Fatalf("DefaultCharacter() = %+v, ok=%v; want first", def, ok)
|
||||
}
|
||||
|
||||
if err := r.SetDefaultCharacter("second"); err != nil {
|
||||
t.Fatalf("SetDefaultCharacter(second) error: %v", err)
|
||||
}
|
||||
def, ok = r.DefaultCharacter()
|
||||
if !ok || def.ID != "second" {
|
||||
t.Fatalf("DefaultCharacter() after set = %+v, ok=%v; want second", def, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_SetDefaultUnknown(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := New()
|
||||
if err := r.SetDefaultCharacter("ghost"); err == nil {
|
||||
t.Fatal("SetDefaultCharacter(ghost) want error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_RegisterCharacterUnknownPersona(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := New()
|
||||
if err := r.RegisterCharacter(mkChar("c", "missing-persona", false)); err == nil {
|
||||
t.Fatal("RegisterCharacter with unknown persona want error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_DuplicateRegistration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := New()
|
||||
if err := r.RegisterPersona(mkPersona("a", "c", "")); err != nil {
|
||||
t.Fatalf("first RegisterPersona error: %v", err)
|
||||
}
|
||||
if err := r.RegisterPersona(mkPersona("a", "c", "")); err == nil {
|
||||
t.Fatal("duplicate persona want error, got nil")
|
||||
}
|
||||
|
||||
_ = r.RegisterCharacter(mkChar("c1", "a", false))
|
||||
if err := r.RegisterCharacter(mkChar("c1", "a", false)); err == nil {
|
||||
t.Fatal("duplicate character want error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_RegisterCharacterValidation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := New()
|
||||
if err := r.RegisterCharacter(&Character{ID: "c", Name: "", PersonaID: "a"}); err == nil {
|
||||
t.Fatal("character with empty name want error, got nil")
|
||||
}
|
||||
if err := r.RegisterCharacter(&Character{ID: "c", Name: "c", PersonaID: ""}); err == nil {
|
||||
t.Fatal("character with empty personaID want error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_MemoryNamespaceIsolation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := New()
|
||||
_ = r.RegisterPersona(mkPersona("explicit", "c", "memory.special"))
|
||||
_ = r.RegisterPersona(mkPersona("derived", "c", ""))
|
||||
|
||||
if ns, ok := r.MemoryNamespace("explicit"); !ok || ns != "memory.special" {
|
||||
t.Fatalf("MemoryNamespace(explicit) = %q, ok=%v; want memory.special", ns, ok)
|
||||
}
|
||||
if ns, ok := r.MemoryNamespace("derived"); !ok || ns != "persona.derived" {
|
||||
t.Fatalf("MemoryNamespace(derived) = %q, ok=%v; want persona.derived", ns, ok)
|
||||
}
|
||||
nsExplicit, _ := r.MemoryNamespace("explicit")
|
||||
nsDerived, _ := r.MemoryNamespace("derived")
|
||||
if nsExplicit == nsDerived {
|
||||
t.Fatalf("namespaces must be isolated: %q == %q", nsExplicit, nsDerived)
|
||||
}
|
||||
if _, ok := r.MemoryNamespace("ghost"); ok {
|
||||
t.Fatal("MemoryNamespace(ghost) ok=true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_EmotionsFollowPersona(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := New()
|
||||
_ = r.RegisterPersona(mkPersona("xilian", "c", ""))
|
||||
|
||||
m, aff, ok := r.Emotions().Get("xilian")
|
||||
if !ok {
|
||||
t.Fatal("registered persona missing from emotion manager")
|
||||
}
|
||||
if m.Current() != emotion.Calm {
|
||||
t.Fatalf("emotion Current() = %q, want calm", m.Current())
|
||||
}
|
||||
if aff.Value() != 50 {
|
||||
t.Fatalf("affection value = %d, want 50 (default)", aff.Value())
|
||||
}
|
||||
|
||||
if err := r.Emotions().Transition("xilian", emotion.Happy); err != nil {
|
||||
t.Fatalf("Transition(xilian, happy) error: %v", err)
|
||||
}
|
||||
if m.Current() != emotion.Happy {
|
||||
t.Fatalf("emotion Current() = %q, want happy", m.Current())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistry_ConcurrentLifecycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
r := New()
|
||||
const n = 16
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < n; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
defer wg.Done()
|
||||
id := fmt.Sprintf("p%d", i)
|
||||
if err := r.RegisterPersona(mkPersona(id, "c", "")); err != nil {
|
||||
t.Errorf("RegisterPersona(%s): %v", id, err)
|
||||
return
|
||||
}
|
||||
if _, ok := r.Persona(id); !ok {
|
||||
t.Errorf("Persona(%s) missing after register", id)
|
||||
}
|
||||
ch := &Character{ID: "c" + id, Name: id, PersonaID: id, Modes: []string{"chat"}}
|
||||
if err := r.RegisterCharacter(ch); err != nil {
|
||||
t.Errorf("RegisterCharacter(%s): %v", id, err)
|
||||
return
|
||||
}
|
||||
if _, ok := r.Character(ch.ID); !ok {
|
||||
t.Errorf("Character(%s) missing after register", ch.ID)
|
||||
}
|
||||
if _, ok := r.MemoryNamespace(id); !ok {
|
||||
t.Errorf("MemoryNamespace(%s) missing", id)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if got := r.CountPersonas(); got != n {
|
||||
t.Fatalf("CountPersonas() = %d, want %d", got, n)
|
||||
}
|
||||
if got := r.CountCharacters(); got != n {
|
||||
t.Fatalf("CountCharacters() = %d, want %d", got, n)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user