Files
synapbus/internal/mcp/injection_core_test.go
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

159 lines
5.0 KiB
Go

// Integration test for US2 → US1 wiring: a real CoreMemoryStore, wired
// through messaging.NewCoreProvider, surfaces a seeded blob in the
// wrapped tool response's `relevant_context.core_memory` field; an agent
// with no row gets no relevant_context.
package mcp
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"testing"
mcplib "github.com/mark3labs/mcp-go/mcp"
_ "modernc.org/sqlite"
"github.com/synapbus/synapbus/internal/agents"
"github.com/synapbus/synapbus/internal/messaging"
"github.com/synapbus/synapbus/internal/storage"
)
func newInjectionTestDB(t *testing.T) *sql.DB {
t.Helper()
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
db, err := sql.Open("sqlite", dsn)
if err != nil {
t.Fatalf("open db: %v", err)
}
t.Cleanup(func() { db.Close() })
if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil {
t.Fatalf("foreign keys: %v", err)
}
if err := storage.RunMigrations(context.Background(), db); err != nil {
t.Fatalf("migrations: %v", err)
}
return db
}
// TestInjection_CoreMemoryWiring proves a wrapped session-start handler
// surfaces a blob seeded via CoreMemoryStore as
// `relevant_context.core_memory`. Mirrors the contract example in
// `specs/020-proactive-memory-dream-worker/contracts/mcp-injection.md`.
func TestInjection_CoreMemoryWiring(t *testing.T) {
db := newInjectionTestDB(t)
ctx := context.Background()
const owner = "42"
const agent = "a1"
const blob = "I am a1. Currently focused on memory tests."
coreStore := messaging.NewCoreMemoryStore(db, 2048)
if err := coreStore.Set(ctx, owner, agent, blob, "human"); err != nil {
t.Fatalf("seed core memory: %v", err)
}
cfg := WrapConfig{
Cfg: messaging.MemoryConfig{
InjectionEnabled: true,
InjectionBudgetTokens: 500,
InjectionMaxItems: 5,
InjectionMinScore: 0.25,
},
SearchSvc: nil, // query="" forces no retrieval — only core matters.
IncludeCore: true,
CoreProvider: messaging.NewCoreProvider(coreStore),
QuerySource: func(_ context.Context, _ string, _ map[string]any, _ map[string]any) string { return "" },
}
inner := stubHandler(map[string]any{"agent": agent})
wrapped := WrapInjection(inner, "my_status", cfg)
// Owner 42 ↔ caller a1.
callerCtx := agents.ContextWithAgent(ctx, &agents.Agent{Name: agent, OwnerID: 42})
res, err := wrapped(callerCtx, mcplib.CallToolRequest{})
if err != nil {
t.Fatalf("wrapped my_status: %v", err)
}
body := extractJSON(t, res)
rc, ok := body["relevant_context"].(map[string]any)
if !ok {
t.Fatalf("relevant_context missing: %+v", body)
}
if got := rc["core_memory"]; got != blob {
t.Errorf("core_memory: got %v want %q", got, blob)
}
}
// TestInjection_NoCoreMemoryYieldsNoPacket verifies that an agent
// without a memory_core row gets the original handler response back,
// without a `relevant_context` field appended.
func TestInjection_NoCoreMemoryYieldsNoPacket(t *testing.T) {
db := newInjectionTestDB(t)
coreStore := messaging.NewCoreMemoryStore(db, 2048)
// Intentionally NO Set — the agent has no row.
cfg := WrapConfig{
Cfg: messaging.MemoryConfig{
InjectionEnabled: true,
InjectionBudgetTokens: 500,
InjectionMaxItems: 5,
InjectionMinScore: 0.25,
},
SearchSvc: nil,
IncludeCore: true,
CoreProvider: messaging.NewCoreProvider(coreStore),
QuerySource: func(_ context.Context, _ string, _ map[string]any, _ map[string]any) string { return "" },
}
inner := stubHandler(map[string]any{"agent": "a2"})
wrapped := WrapInjection(inner, "my_status", cfg)
ctx := agents.ContextWithAgent(context.Background(), &agents.Agent{Name: "a2", OwnerID: 42})
res, err := wrapped(ctx, mcplib.CallToolRequest{})
if err != nil {
t.Fatalf("wrapped: %v", err)
}
body := extractJSON(t, res)
if _, has := body["relevant_context"]; has {
t.Errorf("relevant_context attached for agent with no core row: %v", body["relevant_context"])
}
if body["agent"] != "a2" {
t.Errorf("inner body lost: %+v", body)
}
}
// Verify ContextPacket round-trips its core_memory through json. This is
// the contract field consumed by clients.
func TestInjection_CoreMemoryJSONShape(t *testing.T) {
db := newInjectionTestDB(t)
ctx := context.Background()
coreStore := messaging.NewCoreMemoryStore(db, 2048)
if err := coreStore.Set(ctx, "1", "a1", "hello", "human"); err != nil {
t.Fatalf("seed: %v", err)
}
provider := messaging.NewCoreProvider(coreStore)
got, err := provider.Get(ctx, "1", "a1")
if err != nil {
t.Fatalf("provider.Get: %v", err)
}
if got != "hello" {
t.Errorf("provider.Get: got %q want hello", got)
}
// Empty case (no row) yields "" without error.
got, err = provider.Get(ctx, "1", "nobody")
if err != nil || got != "" {
t.Errorf("provider.Get on missing: got %q err=%v", got, err)
}
// Sanity: ensure the adapter is reachable through json marshaling of a packet.
type fakePacket struct {
Core string `json:"core_memory,omitempty"`
}
b, _ := json.Marshal(fakePacket{Core: "hello"})
if string(b) != `{"core_memory":"hello"}` {
t.Errorf("json marshaling: got %s", b)
}
}