mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-07-12 21:03:59 +00:00
348 lines
9.4 KiB
Go
348 lines
9.4 KiB
Go
//go:build sqliteonly
|
|
|
|
package integration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store/sqlitestore"
|
|
)
|
|
|
|
// TestSQLiteSmokeTest boots a SQLite gateway with all stores and exercises each new store.
|
|
func TestSQLiteSmokeTest(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dbPath := filepath.Join(tmpDir, "smoke.db")
|
|
skillDir := filepath.Join(tmpDir, "skills")
|
|
os.MkdirAll(skillDir, 0o755)
|
|
|
|
stores, err := sqlitestore.NewSQLiteStores(store.StoreConfig{
|
|
SQLitePath: dbPath,
|
|
SkillsStorageDir: skillDir,
|
|
EncryptionKey: "test-key-32-bytes-long-for-aes!!", // 32 bytes for AES-256
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteStores: %v", err)
|
|
}
|
|
defer stores.DB.Close()
|
|
|
|
// Verify all 9 previously-nil stores are now non-nil.
|
|
t.Run("FactoryAllStoresNonNil", func(t *testing.T) {
|
|
if stores.AgentLinks == nil {
|
|
t.Error("AgentLinks is nil")
|
|
}
|
|
if stores.SubagentTasks == nil {
|
|
t.Error("SubagentTasks is nil")
|
|
}
|
|
if stores.SecureCLI == nil {
|
|
t.Error("SecureCLI is nil")
|
|
}
|
|
if stores.SecureCLIGrants == nil {
|
|
t.Error("SecureCLIGrants is nil")
|
|
}
|
|
if stores.Episodic == nil {
|
|
t.Error("Episodic is nil")
|
|
}
|
|
if stores.EvolutionMetrics == nil {
|
|
t.Error("EvolutionMetrics is nil")
|
|
}
|
|
if stores.EvolutionSuggestions == nil {
|
|
t.Error("EvolutionSuggestions is nil")
|
|
}
|
|
if stores.KnowledgeGraph == nil {
|
|
t.Error("KnowledgeGraph is nil")
|
|
}
|
|
if stores.Vault == nil {
|
|
t.Error("Vault is nil")
|
|
}
|
|
})
|
|
|
|
// Seed a tenant + agent for FK satisfaction.
|
|
tenantID := uuid.Must(uuid.NewV7())
|
|
agentID := uuid.Must(uuid.NewV7())
|
|
agentKey := "smoke-" + agentID.String()[:8]
|
|
userID := "smoke-user"
|
|
|
|
_, err = stores.DB.Exec(
|
|
`INSERT INTO tenants (id, name, slug, status) VALUES (?, 'Smoke', 'smoke', 'active')`,
|
|
tenantID.String())
|
|
if err != nil {
|
|
t.Fatalf("seed tenant: %v", err)
|
|
}
|
|
_, err = stores.DB.Exec(
|
|
`INSERT INTO agents (id, agent_key, display_name, status, tenant_id, owner_id, model, provider)
|
|
VALUES (?, ?, 'Smoke Agent', 'active', ?, 'smoke-owner', 'gpt-4o', 'openai')`,
|
|
agentID.String(), agentKey, tenantID.String())
|
|
if err != nil {
|
|
t.Fatalf("seed agent: %v", err)
|
|
}
|
|
|
|
ctx := store.WithTenantID(context.Background(), tenantID)
|
|
|
|
// --- SubagentTasks round-trip ---
|
|
t.Run("SubagentTasks", func(t *testing.T) {
|
|
taskID := uuid.Must(uuid.NewV7())
|
|
task := &store.SubagentTaskData{
|
|
ParentAgentKey: agentKey,
|
|
Subject: "smoke task",
|
|
Description: "test",
|
|
Status: "running",
|
|
Depth: 1,
|
|
Metadata: map[string]any{"key": "val"},
|
|
}
|
|
task.ID = taskID
|
|
task.TenantID = tenantID
|
|
if err := stores.SubagentTasks.Create(ctx, task); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
got, err := stores.SubagentTasks.Get(ctx, taskID)
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if got == nil || got.Subject != "smoke task" {
|
|
t.Fatalf("unexpected: %+v", got)
|
|
}
|
|
})
|
|
|
|
// --- EvolutionMetrics round-trip ---
|
|
t.Run("EvolutionMetrics", func(t *testing.T) {
|
|
metric := store.EvolutionMetric{
|
|
ID: uuid.Must(uuid.NewV7()),
|
|
TenantID: tenantID,
|
|
AgentID: agentID,
|
|
SessionKey: "s1",
|
|
MetricType: "tool",
|
|
MetricKey: "exec",
|
|
Value: json.RawMessage(`{"success":"true"}`),
|
|
}
|
|
if err := stores.EvolutionMetrics.RecordMetric(ctx, metric); err != nil {
|
|
t.Fatalf("RecordMetric: %v", err)
|
|
}
|
|
metrics, err := stores.EvolutionMetrics.QueryMetrics(ctx, agentID, "tool", time.Time{}, 10)
|
|
if err != nil {
|
|
t.Fatalf("QueryMetrics: %v", err)
|
|
}
|
|
if len(metrics) == 0 {
|
|
t.Fatal("expected metrics")
|
|
}
|
|
})
|
|
|
|
// --- EpisodicStore round-trip ---
|
|
t.Run("Episodic", func(t *testing.T) {
|
|
summary := &store.EpisodicSummary{
|
|
ID: uuid.Must(uuid.NewV7()),
|
|
TenantID: tenantID,
|
|
AgentID: agentID,
|
|
UserID: userID,
|
|
SessionKey: "s1",
|
|
Summary: "discussed machine learning algorithms",
|
|
L0Abstract: "ML discussion",
|
|
KeyTopics: []string{"ml", "algorithms"},
|
|
SourceType: "session",
|
|
TurnCount: 5,
|
|
TokenCount: 100,
|
|
}
|
|
if err := stores.Episodic.Create(ctx, summary); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
results, err := stores.Episodic.Search(ctx, "machine", agentID.String(), userID, store.EpisodicSearchOptions{MaxResults: 5})
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(results) == 0 {
|
|
t.Fatal("expected search results")
|
|
}
|
|
})
|
|
|
|
// --- KnowledgeGraph round-trip ---
|
|
t.Run("KnowledgeGraph", func(t *testing.T) {
|
|
entityA := &store.Entity{
|
|
ID: uuid.Must(uuid.NewV7()).String(),
|
|
AgentID: agentID.String(),
|
|
UserID: userID,
|
|
ExternalID: "ext-a",
|
|
Name: "Alice",
|
|
EntityType: "person",
|
|
Confidence: 0.9,
|
|
Properties: map[string]string{"role": "engineer"},
|
|
}
|
|
entityB := &store.Entity{
|
|
ID: uuid.Must(uuid.NewV7()).String(),
|
|
AgentID: agentID.String(),
|
|
UserID: userID,
|
|
ExternalID: "ext-b",
|
|
Name: "Bob",
|
|
EntityType: "person",
|
|
Confidence: 0.9,
|
|
}
|
|
if err := stores.KnowledgeGraph.UpsertEntity(ctx, entityA); err != nil {
|
|
t.Fatalf("UpsertEntity A: %v", err)
|
|
}
|
|
if err := stores.KnowledgeGraph.UpsertEntity(ctx, entityB); err != nil {
|
|
t.Fatalf("UpsertEntity B: %v", err)
|
|
}
|
|
|
|
rel := &store.Relation{
|
|
ID: uuid.Must(uuid.NewV7()).String(),
|
|
AgentID: agentID.String(),
|
|
UserID: userID,
|
|
SourceEntityID: entityA.ID,
|
|
TargetEntityID: entityB.ID,
|
|
RelationType: "knows",
|
|
Confidence: 0.8,
|
|
}
|
|
if err := stores.KnowledgeGraph.UpsertRelation(ctx, rel); err != nil {
|
|
t.Fatalf("UpsertRelation: %v", err)
|
|
}
|
|
|
|
results, err := stores.KnowledgeGraph.Traverse(ctx, agentID.String(), userID, entityA.ID, 3)
|
|
if err != nil {
|
|
t.Fatalf("Traverse: %v", err)
|
|
}
|
|
if len(results) == 0 {
|
|
t.Fatal("expected traversal results")
|
|
}
|
|
})
|
|
|
|
// --- VaultStore round-trip ---
|
|
t.Run("Vault", func(t *testing.T) {
|
|
aid := agentID.String()
|
|
doc := &store.VaultDocument{
|
|
TenantID: tenantID.String(),
|
|
AgentID: &aid,
|
|
Scope: "personal",
|
|
Path: "notes/meeting.md",
|
|
Title: "Meeting Notes",
|
|
DocType: "note",
|
|
ContentHash: "abc123",
|
|
Metadata: map[string]any{"tags": []string{"meeting"}},
|
|
}
|
|
if err := stores.Vault.UpsertDocument(ctx, doc); err != nil {
|
|
t.Fatalf("UpsertDocument: %v", err)
|
|
}
|
|
if doc.ID == "" {
|
|
t.Fatal("expected ID set after upsert")
|
|
}
|
|
|
|
got, err := stores.Vault.GetDocument(ctx, tenantID.String(), agentID.String(), "notes/meeting.md")
|
|
if err != nil {
|
|
t.Fatalf("GetDocument: %v", err)
|
|
}
|
|
if got.Title != "Meeting Notes" {
|
|
t.Fatalf("unexpected title: %s", got.Title)
|
|
}
|
|
|
|
// Search
|
|
results, err := stores.Vault.Search(ctx, store.VaultSearchOptions{
|
|
Query: "meeting",
|
|
AgentID: agentID.String(),
|
|
TenantID: tenantID.String(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Search: %v", err)
|
|
}
|
|
if len(results) == 0 {
|
|
t.Fatal("expected vault search results")
|
|
}
|
|
|
|
// Links
|
|
doc2 := &store.VaultDocument{
|
|
TenantID: tenantID.String(),
|
|
AgentID: &aid,
|
|
Scope: "personal",
|
|
Path: "notes/followup.md",
|
|
Title: "Follow-up",
|
|
DocType: "note",
|
|
ContentHash: "def456",
|
|
}
|
|
if err := stores.Vault.UpsertDocument(ctx, doc2); err != nil {
|
|
t.Fatalf("UpsertDocument2: %v", err)
|
|
}
|
|
|
|
link := &store.VaultLink{
|
|
FromDocID: doc.ID,
|
|
ToDocID: doc2.ID,
|
|
LinkType: "wikilink",
|
|
Context: "see also",
|
|
}
|
|
if err := stores.Vault.CreateLink(ctx, link); err != nil {
|
|
t.Fatalf("CreateLink: %v", err)
|
|
}
|
|
|
|
outLinks, err := stores.Vault.GetOutLinks(ctx, tenantID.String(), doc.ID)
|
|
if err != nil {
|
|
t.Fatalf("GetOutLinks: %v", err)
|
|
}
|
|
if len(outLinks) != 1 {
|
|
t.Fatalf("expected 1 outlink, got %d", len(outLinks))
|
|
}
|
|
})
|
|
|
|
// --- SecureCLI encrypt/decrypt ---
|
|
t.Run("SecureCLIEncryptDecrypt", func(t *testing.T) {
|
|
binID := uuid.Must(uuid.NewV7())
|
|
env := map[string]string{"API_KEY": "secret123"}
|
|
envBytes, _ := json.Marshal(env)
|
|
|
|
bin := &store.SecureCLIBinary{
|
|
BinaryName: "smoke-cli",
|
|
Description: "test binary",
|
|
EncryptedEnv: envBytes,
|
|
DenyArgs: json.RawMessage(`[]`),
|
|
DenyVerbose: json.RawMessage(`[]`),
|
|
TimeoutSeconds: 30,
|
|
IsGlobal: true,
|
|
Enabled: true,
|
|
}
|
|
bin.ID = binID
|
|
if err := stores.SecureCLI.Create(ctx, bin); err != nil {
|
|
t.Fatalf("Create: %v", err)
|
|
}
|
|
|
|
got, err := stores.SecureCLI.Get(ctx, binID)
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if got == nil {
|
|
t.Fatal("expected non-nil binary")
|
|
}
|
|
// Verify decrypted env matches original.
|
|
var gotEnv map[string]string
|
|
if err := json.Unmarshal(got.EncryptedEnv, &gotEnv); err != nil {
|
|
t.Fatalf("unmarshal env: %v", err)
|
|
}
|
|
if gotEnv["API_KEY"] != "secret123" {
|
|
t.Fatalf("expected API_KEY=secret123, got %s", gotEnv["API_KEY"])
|
|
}
|
|
})
|
|
|
|
// --- F15: SecureCLI nil when no encryption key ---
|
|
t.Run("SecureCLINilWithoutKey", func(t *testing.T) {
|
|
tmpDir2 := t.TempDir()
|
|
dbPath2 := filepath.Join(tmpDir2, "nokey.db")
|
|
skillDir2 := filepath.Join(tmpDir2, "skills")
|
|
os.MkdirAll(skillDir2, 0o755)
|
|
|
|
stores2, err := sqlitestore.NewSQLiteStores(store.StoreConfig{
|
|
SQLitePath: dbPath2,
|
|
SkillsStorageDir: skillDir2,
|
|
EncryptionKey: "", // empty = disabled
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewSQLiteStores: %v", err)
|
|
}
|
|
defer stores2.DB.Close()
|
|
|
|
if stores2.SecureCLI != nil {
|
|
t.Error("expected SecureCLI to be nil when EncryptionKey is empty")
|
|
}
|
|
})
|
|
}
|