mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-08-21 00:24:49 +00:00
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
268 lines
7.7 KiB
Go
268 lines
7.7 KiB
Go
//go:build integration
|
|
|
|
package integration
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/nextlevelbuilder/goclaw/internal/providers"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store/pg"
|
|
)
|
|
|
|
// TestStoreSession_ListPaged_Pagination verifies limit/offset pagination
|
|
// and cross-tenant isolation in ListPaged.
|
|
func TestStoreSession_ListPaged_Pagination(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, agentID := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
ss := pg.NewPGSessionStore(db)
|
|
|
|
// Create 5 sessions for this tenant.
|
|
var keys []string
|
|
for i := 0; i < 5; i++ {
|
|
k := "agent:" + agentID.String() + ":paged:" + uuid.New().String()[:8]
|
|
ss.GetOrCreate(ctx, k)
|
|
if err := ss.Save(ctx, k); err != nil {
|
|
t.Fatalf("Save [%d]: %v", i, err)
|
|
}
|
|
keys = append(keys, k)
|
|
}
|
|
t.Cleanup(func() {
|
|
for _, k := range keys {
|
|
db.Exec("DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", k, tenantID)
|
|
}
|
|
})
|
|
|
|
t.Run("limit=2 offset=0", func(t *testing.T) {
|
|
result := ss.ListPaged(ctx, store.SessionListOpts{Limit: 2, Offset: 0})
|
|
if result.Total < 5 {
|
|
t.Errorf("Total = %d, want >= 5", result.Total)
|
|
}
|
|
if len(result.Sessions) != 2 {
|
|
t.Errorf("Sessions len = %d, want 2", len(result.Sessions))
|
|
}
|
|
})
|
|
|
|
t.Run("limit=3 offset=2", func(t *testing.T) {
|
|
result := ss.ListPaged(ctx, store.SessionListOpts{Limit: 3, Offset: 2})
|
|
if result.Total < 5 {
|
|
t.Errorf("Total = %d, want >= 5", result.Total)
|
|
}
|
|
// At least 1 result — depends on how many total exist; if exactly 5, expect 3.
|
|
if len(result.Sessions) == 0 {
|
|
t.Error("expected at least 1 session on page 2")
|
|
}
|
|
})
|
|
|
|
t.Run("tenant_isolation", func(t *testing.T) {
|
|
tenantB, _ := seedTenantAgent(t, db)
|
|
ctxB := tenantCtx(tenantB)
|
|
// Create 1 session for tenant B.
|
|
kb := "agent:" + agentID.String() + ":iso:" + uuid.New().String()[:8]
|
|
ss.GetOrCreate(ctxB, kb)
|
|
_ = ss.Save(ctxB, kb)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", kb, tenantB)
|
|
})
|
|
|
|
// Tenant A querying with its own context must not see tenant B's session.
|
|
resultA := ss.ListPaged(ctx, store.SessionListOpts{Limit: 100, Offset: 0})
|
|
for _, s := range resultA.Sessions {
|
|
if s.Key == kb {
|
|
t.Error("tenant A sees tenant B session — isolation broken")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestStoreSession_ListPaged_Filter_UserID verifies user_id scoping.
|
|
func TestStoreSession_ListPaged_Filter_UserID(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, agentID := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
ss := pg.NewPGSessionStore(db)
|
|
|
|
prefix := "agent:" + agentID.String() + ":uid-"
|
|
suffix1 := uuid.New().String()[:8]
|
|
suffix2 := uuid.New().String()[:8]
|
|
k1 := prefix + suffix1
|
|
k2 := prefix + suffix2
|
|
|
|
// Create two sessions with distinct userIDs stored via metadata.
|
|
s1 := ss.GetOrCreate(ctx, k1)
|
|
s1.UserID = "filter-user-A"
|
|
_ = ss.Save(ctx, k1)
|
|
|
|
s2 := ss.GetOrCreate(ctx, k2)
|
|
s2.UserID = "filter-user-B"
|
|
_ = ss.Save(ctx, k2)
|
|
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", k1, tenantID)
|
|
db.Exec("DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", k2, tenantID)
|
|
})
|
|
|
|
result := ss.ListPaged(ctx, store.SessionListOpts{
|
|
UserID: "filter-user-A",
|
|
Limit: 50,
|
|
})
|
|
for _, s := range result.Sessions {
|
|
if s.Key == k2 {
|
|
t.Error("filter by user_id should not return session of a different user")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestStoreSession_List_Simple verifies the simple List() path.
|
|
func TestStoreSession_List_Simple(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, agentID := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
ss := pg.NewPGSessionStore(db)
|
|
pg.InitSqlx(db)
|
|
|
|
k := "agent:" + agentID.String() + ":listtest:" + uuid.New().String()[:8]
|
|
ss.GetOrCreate(ctx, k)
|
|
_ = ss.Save(ctx, k)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", k, tenantID)
|
|
})
|
|
|
|
// List with agentID filter should include the created session.
|
|
infos := ss.List(ctx, agentID.String())
|
|
found := false
|
|
for _, s := range infos {
|
|
if s.Key == k {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("List(agentID) did not return key %q", k)
|
|
}
|
|
}
|
|
|
|
// TestStoreSession_GetOrCreate_LoadsFromDB verifies that GetOrCreate
|
|
// returns DB data on a cache miss (new store instance).
|
|
func TestStoreSession_GetOrCreate_LoadsFromDB(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, _ := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
ss1 := pg.NewPGSessionStore(db)
|
|
|
|
k := "test-reload-" + uuid.New().String()[:8]
|
|
data := ss1.GetOrCreate(ctx, k)
|
|
data.Label = "reload-label"
|
|
_ = ss1.Save(ctx, k)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", k, tenantID)
|
|
})
|
|
|
|
// New store instance — no cache. GetOrCreate must reload from DB.
|
|
ss2 := pg.NewPGSessionStore(db)
|
|
got := ss2.GetOrCreate(ctx, k)
|
|
if got == nil {
|
|
t.Fatal("GetOrCreate on new store returned nil")
|
|
}
|
|
if got.Key != k {
|
|
t.Errorf("Key = %q, want %q", got.Key, k)
|
|
}
|
|
}
|
|
|
|
// TestStoreSession_SetGetLabel_Persisted verifies label persists through Save/load cycle.
|
|
func TestStoreSession_SetGetLabel_Persisted(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, _ := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
ss := pg.NewPGSessionStore(db)
|
|
|
|
k := "persist-label-" + uuid.New().String()[:8]
|
|
ss.GetOrCreate(ctx, k)
|
|
ss.SetLabel(ctx, k, "my-persisted-label")
|
|
_ = ss.Save(ctx, k)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", k, tenantID)
|
|
})
|
|
|
|
// Re-load from DB.
|
|
ss2 := pg.NewPGSessionStore(db)
|
|
got := ss2.GetOrCreate(ctx, k)
|
|
if got == nil {
|
|
t.Fatal("reload returned nil")
|
|
}
|
|
if got.Label != "my-persisted-label" {
|
|
t.Errorf("Label after reload = %q, want %q", got.Label, "my-persisted-label")
|
|
}
|
|
}
|
|
|
|
// TestStoreSession_AddMessage_Ordering verifies that messages are stored in insertion order.
|
|
func TestStoreSession_AddMessage_Ordering(t *testing.T) {
|
|
db := testDB(t)
|
|
tenantID, _ := seedTenantAgent(t, db)
|
|
ctx := tenantCtx(tenantID)
|
|
ss := pg.NewPGSessionStore(db)
|
|
|
|
k := "order-" + uuid.New().String()[:8]
|
|
ss.GetOrCreate(ctx, k)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", k, tenantID)
|
|
})
|
|
|
|
contents := []string{"first", "second", "third", "fourth", "fifth"}
|
|
for _, c := range contents {
|
|
ss.AddMessage(ctx, k, providers.Message{Role: "user", Content: c})
|
|
}
|
|
_ = ss.Save(ctx, k)
|
|
|
|
// Reload and verify order.
|
|
ss2 := pg.NewPGSessionStore(db)
|
|
ss2.GetOrCreate(ctx, k) // triggers DB load
|
|
hist := ss2.GetHistory(ctx, k)
|
|
if len(hist) != len(contents) {
|
|
t.Fatalf("message count = %d, want %d", len(hist), len(contents))
|
|
}
|
|
for i, c := range contents {
|
|
if hist[i].Content != c {
|
|
t.Errorf("msg[%d] = %q, want %q", i, hist[i].Content, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestStoreSession_ListPagedRich_TenantIsolation verifies ListPagedRich respects tenant.
|
|
func TestStoreSession_ListPagedRich_TenantIsolation(t *testing.T) {
|
|
db := testDB(t)
|
|
pg.InitSqlx(db)
|
|
tenantA, agentA := seedTenantAgent(t, db)
|
|
tenantB, _ := seedTenantAgent(t, db)
|
|
ctxA := tenantCtx(tenantA)
|
|
ctxB := tenantCtx(tenantB)
|
|
ss := pg.NewPGSessionStore(db)
|
|
|
|
kA := "agent:" + agentA.String() + ":rich:" + uuid.New().String()[:8]
|
|
ss.GetOrCreate(ctxA, kA)
|
|
_ = ss.Save(ctxA, kA)
|
|
t.Cleanup(func() {
|
|
db.Exec("DELETE FROM sessions WHERE session_key = $1 AND tenant_id = $2", kA, tenantA)
|
|
})
|
|
|
|
resultB := ss.ListPagedRich(ctxB, store.SessionListOpts{Limit: 100})
|
|
for _, s := range resultB.Sessions {
|
|
if s.Key == kA {
|
|
t.Error("tenant B sees tenant A rich session — isolation broken")
|
|
}
|
|
}
|
|
|
|
resultA := ss.ListPagedRich(ctxA, store.SessionListOpts{Limit: 100})
|
|
found := false
|
|
for _, s := range resultA.Sessions {
|
|
if s.Key == kA {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Errorf("tenant A cannot see its own rich session %q", kA)
|
|
}
|
|
}
|