Files
synapbus/internal/messaging/memory_core_test.go
T
Algis DumbrisandClaude Opus 4.7 a52d68ed88 feat(020): US2 — per-agent core memory blob
Letta-style identity blob, one per (owner, agent), always included in
session-start (my_status) responses. Replaces wholesale on rewrite; size
capped at SYNAPBUS_CORE_MEMORY_MAX_BYTES (default 2048); owner-scoped.

Components:
- internal/messaging/memory_core.go (+ test): CoreMemoryStore with
  Get/Set/Delete/List, ErrCoreMemoryTooLarge, NewCoreProvider adapter
  for search.CoreMemoryProvider.
- internal/mcp/server.go SetInjection: wires the core provider into
  the my_status handler wrap.
- internal/mcp/injection_core_test.go: seed → wrapped my_status →
  relevant_context.core_memory matches; missing row → no field.
- internal/api/memory_core.go + router: GET/PUT/DELETE
  /api/owner/{ownerID}/agents/{agentName}/core-memory, session-auth,
  413 on oversize.
- internal/admin/socket.go: memory.core.{get,set,delete} dispatch
  handlers with username→user.id resolution.
- cmd/synapbus/admin.go: `synapbus memory core {get,set,delete}` cobra
  subtree.
- cmd/synapbus/main.go: wires ParseMemoryConfig, CoreMemoryStore,
  MemoryInjections; calls mcpSrv.SetInjection on startup.

Pin overlay still TODO (US3-T029).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:18:06 +03:00

180 lines
4.8 KiB
Go

package messaging
import (
"context"
"errors"
"strings"
"testing"
)
func TestCoreMemoryStore_GetSetRoundTrip(t *testing.T) {
db := newTestDB(t)
store := NewCoreMemoryStore(db, 2048)
ctx := context.Background()
const owner = "1"
const agent = "research-mcpproxy"
const blob = "You are research-mcpproxy. Focus on benchmarking."
if err := store.Set(ctx, owner, agent, blob, "human"); err != nil {
t.Fatalf("Set: %v", err)
}
got, updatedAt, ok, err := store.Get(ctx, owner, agent)
if err != nil {
t.Fatalf("Get: %v", err)
}
if !ok {
t.Fatal("Get: expected ok=true, got false")
}
if got != blob {
t.Errorf("Get blob mismatch: got %q want %q", got, blob)
}
if updatedAt.IsZero() {
t.Error("Get: expected non-zero updated_at")
}
}
func TestCoreMemoryStore_SetReplacesWholesale(t *testing.T) {
db := newTestDB(t)
store := NewCoreMemoryStore(db, 2048)
ctx := context.Background()
const owner = "1"
const agent = "alpha"
if err := store.Set(ctx, owner, agent, "first version", "human"); err != nil {
t.Fatalf("first Set: %v", err)
}
if err := store.Set(ctx, owner, agent, "second version", "dream:1"); err != nil {
t.Fatalf("second Set: %v", err)
}
got, _, ok, err := store.Get(ctx, owner, agent)
if err != nil {
t.Fatalf("Get: %v", err)
}
if !ok || got != "second version" {
t.Errorf("expected wholesale replace, got ok=%v blob=%q", ok, got)
}
if strings.Contains(got, "first") {
t.Errorf("expected merge to NOT happen; got %q", got)
}
}
func TestCoreMemoryStore_SetRejectsOverSize(t *testing.T) {
db := newTestDB(t)
store := NewCoreMemoryStore(db, 16)
ctx := context.Background()
tooBig := strings.Repeat("x", 17)
err := store.Set(ctx, "1", "alpha", tooBig, "human")
if err == nil {
t.Fatal("Set: expected ErrCoreMemoryTooLarge, got nil")
}
if !errors.Is(err, ErrCoreMemoryTooLarge) {
t.Errorf("Set: expected ErrCoreMemoryTooLarge, got %v", err)
}
// Exactly at-cap is fine.
atCap := strings.Repeat("x", 16)
if err := store.Set(ctx, "1", "alpha", atCap, "human"); err != nil {
t.Errorf("Set at cap: unexpected error %v", err)
}
}
func TestCoreMemoryStore_Delete(t *testing.T) {
db := newTestDB(t)
store := NewCoreMemoryStore(db, 2048)
ctx := context.Background()
if err := store.Set(ctx, "1", "alpha", "blob", "human"); err != nil {
t.Fatalf("Set: %v", err)
}
if err := store.Delete(ctx, "1", "alpha"); err != nil {
t.Fatalf("Delete: %v", err)
}
_, _, ok, err := store.Get(ctx, "1", "alpha")
if err != nil {
t.Fatalf("Get after Delete: %v", err)
}
if ok {
t.Error("Get after Delete: expected ok=false")
}
// Delete on missing row is idempotent.
if err := store.Delete(ctx, "1", "alpha"); err != nil {
t.Errorf("Delete on missing: unexpected error %v", err)
}
}
func TestCoreMemoryStore_OwnerScoping(t *testing.T) {
db := newTestDB(t)
store := NewCoreMemoryStore(db, 2048)
ctx := context.Background()
if err := store.Set(ctx, "1", "shared-name", "H1 blob", "h1"); err != nil {
t.Fatalf("Set H1: %v", err)
}
if err := store.Set(ctx, "2", "shared-name", "H2 blob", "h2"); err != nil {
t.Fatalf("Set H2: %v", err)
}
got1, _, ok1, err := store.Get(ctx, "1", "shared-name")
if err != nil || !ok1 || got1 != "H1 blob" {
t.Errorf("H1 Get: ok=%v got=%q err=%v", ok1, got1, err)
}
got2, _, ok2, err := store.Get(ctx, "2", "shared-name")
if err != nil || !ok2 || got2 != "H2 blob" {
t.Errorf("H2 Get: ok=%v got=%q err=%v", ok2, got2, err)
}
// H1 cannot see H2's blob and vice versa — distinct PKs.
if got1 == got2 {
t.Error("owner scoping broken: H1 and H2 see the same blob")
}
// List is scoped to a single owner.
listH1, err := store.List(ctx, "1")
if err != nil {
t.Fatalf("List H1: %v", err)
}
if len(listH1) != 1 || listH1[0].OwnerID != "1" {
t.Errorf("List H1: expected 1 row owned by 1, got %#v", listH1)
}
}
func TestCoreMemoryStore_GetForInjection(t *testing.T) {
db := newTestDB(t)
store := NewCoreMemoryStore(db, 2048)
ctx := context.Background()
// Missing row → "" + no error (NOT sql.ErrNoRows).
blob, err := store.GetForInjection(ctx, "1", "missing-agent")
if err != nil {
t.Fatalf("GetForInjection on missing: unexpected error %v", err)
}
if blob != "" {
t.Errorf("GetForInjection on missing: expected \"\" got %q", blob)
}
// Present row → blob.
if err := store.Set(ctx, "1", "alpha", "core blob", "human"); err != nil {
t.Fatalf("Set: %v", err)
}
blob, err = store.GetForInjection(ctx, "1", "alpha")
if err != nil {
t.Fatalf("GetForInjection: %v", err)
}
if blob != "core blob" {
t.Errorf("GetForInjection: got %q want %q", blob, "core blob")
}
// Adapter satisfies search.CoreMemoryProvider implicitly.
provider := NewCoreProvider(store)
got, err := provider.Get(ctx, "1", "alpha")
if err != nil || got != "core blob" {
t.Errorf("CoreProvider.Get: got %q err=%v", got, err)
}
}