persist_integration_test.go
1 package test 2 3 import ( 4 "context" 5 "os" 6 "path/filepath" 7 "strings" 8 "testing" 9 10 "github.com/Kocoro-lab/ShanClaw/internal/client" 11 "github.com/Kocoro-lab/ShanClaw/internal/config" 12 ctxwin "github.com/Kocoro-lab/ShanClaw/internal/context" 13 ) 14 15 // TestPersistLearningsIntegration tests PersistLearnings with a real LLM call. 16 // Requires SHANNON_API_KEY or valid config. Skip if not available. 17 func TestPersistLearningsIntegration(t *testing.T) { 18 cfg, err := config.Load() 19 if err != nil || cfg.APIKey == "" { 20 t.Skip("skipping: no API key configured") 21 } 22 23 gw := client.NewGatewayClient(cfg.Endpoint, cfg.APIKey) 24 25 // Simulate a conversation with content worth remembering 26 messages := []client.Message{ 27 {Role: "system", Content: client.NewTextContent("You are an assistant.")}, 28 {Role: "user", Content: client.NewTextContent("I prefer Go over Python for backend services. Also, our deployment pipeline uses ArgoCD with Helm charts. The staging cluster is at k8s-staging.internal.company.com.")}, 29 {Role: "assistant", Content: client.NewTextContent("Got it! I'll keep those preferences in mind. Go for backend, ArgoCD+Helm for deployments, and staging at k8s-staging.internal.company.com.")}, 30 {Role: "user", Content: client.NewTextContent("One more thing - never use fmt.Println in production code, always use structured logging with zerolog.")}, 31 {Role: "assistant", Content: client.NewTextContent("Understood - zerolog for structured logging, no fmt.Println in production.")}, 32 } 33 34 t.Run("extracts and writes learnings", func(t *testing.T) { 35 dir := t.TempDir() 36 37 _, err := ctxwin.PersistLearnings(context.Background(), gw, messages, dir) 38 if err != nil { 39 t.Fatalf("PersistLearnings failed: %v", err) 40 } 41 42 data, err := os.ReadFile(filepath.Join(dir, "MEMORY.md")) 43 if err != nil { 44 t.Fatalf("MEMORY.md not created: %v", err) 45 } 46 47 content := string(data) 48 t.Logf("MEMORY.md content:\n%s", content) 49 50 // Should contain at least some of the key facts 51 if !strings.Contains(strings.ToLower(content), "go") && !strings.Contains(strings.ToLower(content), "argocd") && !strings.Contains(strings.ToLower(content), "zerolog") { 52 t.Error("should contain at least one extracted learning (Go, ArgoCD, or zerolog)") 53 } 54 }) 55 56 t.Run("avoids duplicating existing memory", func(t *testing.T) { 57 dir := t.TempDir() 58 59 // Pre-seed with existing memory 60 existing := "# Memory\n\n- User prefers Go over Python for backend\n- Deployment uses ArgoCD with Helm\n" 61 os.WriteFile(filepath.Join(dir, "MEMORY.md"), []byte(existing), 0644) 62 63 _, err := ctxwin.PersistLearnings(context.Background(), gw, messages, dir) 64 if err != nil { 65 t.Fatalf("PersistLearnings failed: %v", err) 66 } 67 68 data, _ := os.ReadFile(filepath.Join(dir, "MEMORY.md")) 69 content := string(data) 70 t.Logf("MEMORY.md content (with existing):\n%s", content) 71 72 // Count occurrences of "Go" preference — should not be duplicated heavily 73 goCount := strings.Count(strings.ToLower(content), "go over python") 74 if goCount > 1 { 75 t.Errorf("should not duplicate existing memory, found 'go over python' %d times", goCount) 76 } 77 }) 78 79 t.Run("overflows to detail file when near limit", func(t *testing.T) { 80 dir := t.TempDir() 81 82 // Create a large MEMORY.md near the limit 83 var lines []string 84 for i := 0; i < 148; i++ { 85 lines = append(lines, "- existing fact line") 86 } 87 os.WriteFile(filepath.Join(dir, "MEMORY.md"), []byte(strings.Join(lines, "\n")), 0644) 88 89 _, err := ctxwin.PersistLearnings(context.Background(), gw, messages, dir) 90 if err != nil { 91 t.Fatalf("PersistLearnings failed: %v", err) 92 } 93 94 // Check for detail file 95 entries, _ := os.ReadDir(dir) 96 var detailFiles []string 97 for _, e := range entries { 98 if strings.HasPrefix(e.Name(), "auto-") { 99 detailFiles = append(detailFiles, e.Name()) 100 } 101 } 102 103 if len(detailFiles) == 0 { 104 t.Error("should have created a detail file when MEMORY.md is near limit") 105 } else { 106 t.Logf("Detail file created: %s", detailFiles[0]) 107 detailData, _ := os.ReadFile(filepath.Join(dir, detailFiles[0])) 108 t.Logf("Detail file content:\n%s", string(detailData)) 109 } 110 111 // MEMORY.md should have a pointer to the detail file 112 data, _ := os.ReadFile(filepath.Join(dir, "MEMORY.md")) 113 content := string(data) 114 if len(detailFiles) > 0 && !strings.Contains(content, detailFiles[0]) { 115 t.Error("MEMORY.md should reference the detail file") 116 } 117 }) 118 }