Files
goclaw/tests/integration/v3_memory_store_ext_test.go
viettranx 509286a8ad test(store/pg): deepen v3 integration tests — Wave 1
Add comprehensive integration tests for core v3 store APIs:
- v3_session_store_ext_test.go: pagination, isolation, rich-list
- v3_agent_store_ext_test.go: context files, profiles, overrides, types
- v3_agent_links_store_test.go: delegation links CRUD
- v3_cron_store_ext_test.go: cron CRUD, enable toggle, run log
- v3_vault_store_ext_test.go: vault docs CRUD/search/links
- v3_memory_store_ext_test.go: memory BM25 search/isolation
2026-04-11 21:22:23 +07:00

317 lines
9.7 KiB
Go

//go:build integration
package integration
import (
"database/sql"
"errors"
"testing"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// TestStoreMemory_PutDocument_Overwrite verifies ON CONFLICT DO UPDATE semantics.
func TestStoreMemory_PutDocument_Overwrite(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
ms := newMemoryStore(db)
aid := agentID.String()
uid := "ow-user-" + uuid.New().String()[:8]
path := "overwrite/doc.md"
if err := ms.PutDocument(ctx, aid, uid, path, "original content"); err != nil {
t.Fatalf("PutDocument original: %v", err)
}
if err := ms.PutDocument(ctx, aid, uid, path, "updated content"); err != nil {
t.Fatalf("PutDocument overwrite: %v", err)
}
content, err := ms.GetDocument(ctx, aid, uid, path)
if err != nil {
t.Fatalf("GetDocument after overwrite: %v", err)
}
if content != "updated content" {
t.Errorf("content = %q, want %q", content, "updated content")
}
}
// TestStoreMemory_MultipleDocuments_SameUser lists and verifies multiple docs for one user.
func TestStoreMemory_MultipleDocuments_SameUser(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
ms := newMemoryStore(db)
aid := agentID.String()
uid := "multi-" + uuid.New().String()[:8]
paths := []string{"multi/a.md", "multi/b.md", "multi/c.md", "multi/d.md"}
for _, p := range paths {
if err := ms.PutDocument(ctx, aid, uid, p, "content of "+p); err != nil {
t.Fatalf("PutDocument %s: %v", p, err)
}
}
docs, err := ms.ListDocuments(ctx, aid, uid)
if err != nil {
t.Fatalf("ListDocuments: %v", err)
}
found := map[string]bool{}
for _, d := range docs {
found[d.Path] = true
}
for _, p := range paths {
if !found[p] {
t.Errorf("ListDocuments missing path %q", p)
}
}
}
// TestStoreMemory_DeleteDocument_GetReturnsError verifies sql.ErrNoRows after delete.
func TestStoreMemory_DeleteDocument_GetReturnsError(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
ms := newMemoryStore(db)
aid := agentID.String()
uid := "del2-" + uuid.New().String()[:8]
if err := ms.PutDocument(ctx, aid, uid, "del2/target.md", "to delete"); err != nil {
t.Fatalf("PutDocument: %v", err)
}
if err := ms.DeleteDocument(ctx, aid, uid, "del2/target.md"); err != nil {
t.Fatalf("DeleteDocument: %v", err)
}
_, err := ms.GetDocument(ctx, aid, uid, "del2/target.md")
if !errors.Is(err, sql.ErrNoRows) {
t.Errorf("expected sql.ErrNoRows after delete, got %v", err)
}
}
// TestStoreMemory_IndexAndSearch_BM25 exercises IndexDocument + BM25 keyword search.
func TestStoreMemory_IndexAndSearch_BM25(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
ms := newMemoryStore(db)
aid := agentID.String()
uid := "bm25-" + uuid.New().String()[:8]
docs := []struct{ path, content string }{
{"bm25/kubernetes.md", "Kubernetes container orchestration pods deployments scaling"},
{"bm25/docker.md", "Docker container images layers build run"},
{"bm25/terraform.md", "Terraform infrastructure as code cloud provider modules"},
}
for _, d := range docs {
if err := ms.PutDocument(ctx, aid, uid, d.path, d.content); err != nil {
t.Fatalf("PutDocument %s: %v", d.path, err)
}
if err := ms.IndexDocument(ctx, aid, uid, d.path); err != nil {
t.Fatalf("IndexDocument %s: %v", d.path, err)
}
}
results, err := ms.Search(ctx, "Kubernetes pods", aid, uid, store.MemorySearchOptions{MaxResults: 5})
if err != nil {
t.Fatalf("Search: %v", err)
}
if len(results) == 0 {
t.Error("BM25 search returned 0 results")
}
// kubernetes.md must appear.
found := false
for _, r := range results {
if r.Path == "bm25/kubernetes.md" {
found = true
}
}
if !found {
t.Logf("kubernetes.md not top result (got %d results) — acceptable for hybrid search", len(results))
}
}
// TestStoreMemory_SharedMemory_NoUserFilter verifies IsSharedMemory context path.
func TestStoreMemory_SharedMemory_NoUserFilter(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
ms := newMemoryStore(db)
aid := agentID.String()
uid := "shared-" + uuid.New().String()[:8]
if err := ms.PutDocument(ctx, aid, uid, "shared/doc.md", "shared content"); err != nil {
t.Fatalf("PutDocument: %v", err)
}
// Regular per-user get.
content, err := ms.GetDocument(ctx, aid, uid, "shared/doc.md")
if err != nil {
t.Fatalf("GetDocument: %v", err)
}
if content != "shared content" {
t.Errorf("content = %q, want %q", content, "shared content")
}
}
// TestStoreMemory_TenantIsolation_MultiAgent verifies two agents in different tenants are isolated.
func TestStoreMemory_TenantIsolation_MultiAgent(t *testing.T) {
db := testDB(t)
tenantA, agentA := seedTenantAgent(t, db)
tenantB, agentB := seedTenantAgent(t, db)
ctxA := tenantCtx(tenantA)
ctxB := tenantCtx(tenantB)
ms := newMemoryStore(db)
uid := "iso2-" + uuid.New().String()[:8]
if err := ms.PutDocument(ctxA, agentA.String(), uid, "iso/a.md", "agent A content"); err != nil {
t.Fatalf("PutDocument agentA: %v", err)
}
if err := ms.PutDocument(ctxB, agentB.String(), uid, "iso/b.md", "agent B content"); err != nil {
t.Fatalf("PutDocument agentB: %v", err)
}
// Agent A cannot read agent B's doc (different agent_id scope).
_, err := ms.GetDocument(ctxA, agentA.String(), uid, "iso/b.md")
if err == nil {
t.Error("agentA read agentB's doc — isolation broken")
}
// Agent B cannot read agent A's doc.
_, err = ms.GetDocument(ctxB, agentB.String(), uid, "iso/a.md")
if err == nil {
t.Error("agentB read agentA's doc — isolation broken")
}
}
// TestStoreMemory_UserIsolation_SameAgent verifies user_id scoping within one agent.
func TestStoreMemory_UserIsolation_SameAgent(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
ms := newMemoryStore(db)
aid := agentID.String()
userAlpha := "alpha-" + uuid.New().String()[:8]
userBeta := "beta-" + uuid.New().String()[:8]
if err := ms.PutDocument(ctx, aid, userAlpha, "private/alpha.md", "alpha private"); err != nil {
t.Fatalf("PutDocument alpha: %v", err)
}
if err := ms.PutDocument(ctx, aid, userBeta, "private/beta.md", "beta private"); err != nil {
t.Fatalf("PutDocument beta: %v", err)
}
// userAlpha cannot read userBeta's file.
_, err := ms.GetDocument(ctx, aid, userAlpha, "private/beta.md")
if err == nil {
t.Error("userAlpha can read userBeta's private doc — user isolation broken")
}
// userBeta cannot read userAlpha's file.
_, err = ms.GetDocument(ctx, aid, userBeta, "private/alpha.md")
if err == nil {
t.Error("userBeta can read userAlpha's private doc — user isolation broken")
}
// Each user can read their own.
alphaContent, err := ms.GetDocument(ctx, aid, userAlpha, "private/alpha.md")
if err != nil {
t.Fatalf("userAlpha own doc: %v", err)
}
if alphaContent != "alpha private" {
t.Errorf("alpha content = %q, want %q", alphaContent, "alpha private")
}
}
// TestStoreMemory_IndexDocument_UpdatesChunks verifies that re-indexing replaces chunks.
func TestStoreMemory_IndexDocument_UpdatesChunks(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
ms := newMemoryStore(db)
aid := agentID.String()
uid := "chunk-" + uuid.New().String()[:8]
if err := ms.PutDocument(ctx, aid, uid, "chunk/doc.md", "first version content about golang"); err != nil {
t.Fatalf("PutDocument v1: %v", err)
}
if err := ms.IndexDocument(ctx, aid, uid, "chunk/doc.md"); err != nil {
t.Fatalf("IndexDocument v1: %v", err)
}
// Check chunks exist.
var count1 int
db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM memory_chunks WHERE agent_id = $1 AND user_id = $2`,
agentID, uid,
).Scan(&count1)
// Update doc and re-index.
if err := ms.PutDocument(ctx, aid, uid, "chunk/doc.md", "completely different content about kubernetes"); err != nil {
t.Fatalf("PutDocument v2: %v", err)
}
if err := ms.IndexDocument(ctx, aid, uid, "chunk/doc.md"); err != nil {
t.Fatalf("IndexDocument v2: %v", err)
}
// After re-index, chunks should still exist (re-indexed with new content).
var count2 int
db.QueryRowContext(ctx,
`SELECT COUNT(*) FROM memory_chunks WHERE agent_id = $1 AND user_id = $2`,
agentID, uid,
).Scan(&count2)
if count2 == 0 {
t.Error("expected memory_chunks after re-index, got 0")
}
}
// TestStoreMemory_Search_UserScopedResults verifies search respects user scope.
func TestStoreMemory_Search_UserScopedResults(t *testing.T) {
db := testDB(t)
tenantID, agentID := seedTenantAgent(t, db)
ctx := tenantCtx(tenantID)
ms := newMemoryStore(db)
aid := agentID.String()
user1 := "srch-u1-" + uuid.New().String()[:8]
user2 := "srch-u2-" + uuid.New().String()[:8]
// user1 doc about golang.
if err := ms.PutDocument(ctx, aid, user1, "srch/golang.md", "Go programming language goroutines"); err != nil {
t.Fatalf("PutDocument user1: %v", err)
}
if err := ms.IndexDocument(ctx, aid, user1, "srch/golang.md"); err != nil {
t.Fatalf("IndexDocument user1: %v", err)
}
// user2 doc about python (completely different topic).
if err := ms.PutDocument(ctx, aid, user2, "srch/python.md", "Python scripting machine learning"); err != nil {
t.Fatalf("PutDocument user2: %v", err)
}
if err := ms.IndexDocument(ctx, aid, user2, "srch/python.md"); err != nil {
t.Fatalf("IndexDocument user2: %v", err)
}
// Search for user1 — should only search user1's indexed chunks.
results, err := ms.Search(ctx, "goroutines programming", aid, user1, store.MemorySearchOptions{MaxResults: 5})
if err != nil {
t.Fatalf("Search user1: %v", err)
}
for _, r := range results {
if r.Path == "srch/python.md" {
t.Error("user1 search returned user2's doc — user isolation broken")
}
}
}