/ test / persist_integration_test.go
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  }