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_List(t *testing.T) { t.Parallel() r := New() // An empty registry lists nothing but stays non-nil. if got := r.List(); got == nil || len(got) != 0 { t.Fatalf("List() on empty registry = %#v, want empty non-nil", got) } if err := r.RegisterPersona(mkPersona("pb", "c", "")); err != nil { t.Fatalf("RegisterPersona(pb): %v", err) } if err := r.RegisterPersona(mkPersona("pa", "c", "")); err != nil { t.Fatalf("RegisterPersona(pa): %v", err) } if err := r.RegisterCharacter(mkChar("cb", "pb", false)); err != nil { t.Fatalf("RegisterCharacter(cb): %v", err) } if err := r.RegisterCharacter(mkChar("ca", "pa", true)); err != nil { t.Fatalf("RegisterCharacter(ca): %v", err) } got := r.List() if len(got) != 2 { t.Fatalf("List() = %d characters, want 2", len(got)) } // Sorted by ID: ca before cb. if got[0].ID != "ca" || got[1].ID != "cb" { t.Fatalf("List() order = %q,%q, want ca,cb", got[0].ID, got[1].ID) } if got[0].PersonaID != "pa" || !got[0].Default || got[1].Default { t.Fatalf("default/persona refs wrong: %+v", got) } if len(got[0].Modes) != 2 || got[0].Modes[0] != "chat" { t.Fatalf("Modes = %v, want [chat home]", got[0].Modes) } // The returned slice is a copy: mutating it must not touch the registry. got[0].Modes[0] = "hacked" got[0].Name = "hacked" // Re-read and assert the registry is unchanged. ch, ok := r.Character("ca") if !ok || ch.Name != "ca" || ch.Modes[0] != "chat" { t.Fatalf("registry mutated through List copy: %+v", ch) } } 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) } }