package seahorse import ( "context" "fmt" "os" "path/filepath" "strings" "sync" "testing" "time" ) // helper: open a test engine with in-memory DB func newTestEngine(t *testing.T) *Engine { t.Helper() db := openTestDB(t) if err := runSchema(db); err != nil { t.Fatalf("migration: %v", err) } store := &Store{db: db} return &Engine{ store: store, config: Config{}, } } // --- compileSessionPattern --- func TestCompileSessionPattern(t *testing.T) { tests := []struct { pattern string input string want bool }{ // Exact match {"agent:abc123", "agent:abc123", true}, {"agent:abc123", "agent:def456", false}, // Single * — matches non-colon chars {"agent:*", "agent:abc123", true}, {"agent:*", "agent:abc:def", false}, // * doesn't match colons // ** — matches everything including colons {"cron:**", "cron:backup", true}, {"cron:**", "cron:backup:daily", true}, {"cron:**", "agent:abc", false}, // Mixed {"agent:*:sub:**", "agent:abc:sub:def", true}, {"agent:*:sub:**", "agent:abc:sub:def:ghi", true}, {"agent:*:sub:**", "agent:abc:def", false}, // Empty pattern — matches nothing meaningful {"", "", true}, {"", "agent:abc", false}, } for _, tt := range tests { re := compileSessionPattern(tt.pattern) if re == nil && tt.pattern != "" { t.Fatalf("compileSessionPattern(%q) returned nil", tt.pattern) } if tt.pattern == "" { continue } got := re.MatchString(tt.input) if got != tt.want { t.Errorf("compileSessionPattern(%q).Match(%q) = %v, want %v", tt.pattern, tt.input, got, tt.want) } } } // --- Session Pattern Filtering --- func TestEngineShouldIgnoreSession(t *testing.T) { eng := &Engine{ ignorePatterns: compileSessionPatterns([]string{"cron:**", "test:*"}), } tests := []struct { key string want bool }{ {"cron:backup", true}, {"cron:backup:daily", true}, {"test:session", true}, {"agent:abc", false}, {"", false}, } for _, tt := range tests { got := eng.shouldIgnoreSession(tt.key) if got != tt.want { t.Errorf("shouldIgnoreSession(%q) = %v, want %v", tt.key, got, tt.want) } } } func TestEngineIsStatelessSession(t *testing.T) { eng := &Engine{ statelessPatterns: compileSessionPatterns([]string{"agent:*:sub:**"}), } tests := []struct { key string want bool }{ {"agent:abc:sub:def", true}, {"agent:abc:sub:def:ghi", true}, {"agent:abc", false}, {"cron:backup", false}, } for _, tt := range tests { got := eng.isStatelessSession(tt.key) if got != tt.want { t.Errorf("isStatelessSession(%q) = %v, want %v", tt.key, got, tt.want) } } } // --- NewEngine --- func TestNewEngine(t *testing.T) { dir := t.TempDir() dbPath := filepath.Join(dir, "short.db") eng, err := NewEngine(Config{DBPath: dbPath}, nil) if err != nil { t.Fatalf("NewEngine: %v", err) } defer eng.Close() // DB file should exist if _, pathErr := os.Stat(dbPath); os.IsNotExist(pathErr) { t.Error("expected DB file to be created") } // Store should be usable ctx := context.Background() conv, err := eng.store.GetOrCreateConversation(ctx, "test:session") if err != nil { t.Fatalf("store should work: %v", err) } if conv.ConversationID == 0 { t.Error("expected valid conversation ID") } // GetRetrieval should return non-nil RetrievalEngine retrieval := eng.GetRetrieval() if retrieval == nil { t.Error("expected GetRetrieval to return non-nil RetrievalEngine") } } func TestNewEngineWithPatterns(t *testing.T) { dir := t.TempDir() dbPath := filepath.Join(dir, "short.db") eng, err := NewEngine(Config{ DBPath: dbPath, IgnoreSessionPatterns: []string{"cron:**"}, StatelessSessionPatterns: []string{"agent:*:sub:**"}, }, nil) if err != nil { t.Fatalf("NewEngine: %v", err) } defer eng.Close() if !eng.shouldIgnoreSession("cron:backup") { t.Error("expected cron:backup to be ignored") } if !eng.isStatelessSession("agent:abc:sub:def") { t.Error("expected agent:abc:sub:def to be stateless") } } // --- Ingest --- func TestEngineIngest(t *testing.T) { eng := newTestEngine(t) ctx := context.Background() msgs := []Message{ {Role: "user", Content: "hello", TokenCount: 2}, {Role: "assistant", Content: "world", TokenCount: 2}, } result, err := eng.Ingest(ctx, "agent:test", msgs) if err != nil { t.Fatalf("Ingest: %v", err) } if result.MessageCount != 2 { t.Errorf("MessageCount = %d, want 2", result.MessageCount) } if result.TokenCount != 4 { t.Errorf("TokenCount = %d, want 4", result.TokenCount) } // Verify messages were stored conv, _ := eng.store.GetOrCreateConversation(ctx, "agent:test") stored, _ := eng.store.GetMessages(ctx, conv.ConversationID, 10, 0) if len(stored) != 2 { t.Fatalf("stored messages = %d, want 2", len(stored)) } if stored[0].Content != "hello" { t.Errorf("stored[0].Content = %q, want 'hello'", stored[0].Content) } // Verify context_items were populated items, _ := eng.store.GetContextItems(ctx, conv.ConversationID) if len(items) != 2 { t.Fatalf("context items = %d, want 2", len(items)) } if items[0].ItemType != "message" { t.Errorf("item[0].ItemType = %q, want 'message'", items[0].ItemType) } } func TestEngineIngestIgnoresSession(t *testing.T) { eng := newTestEngine(t) eng.ignorePatterns = compileSessionPatterns([]string{"cron:**"}) ctx := context.Background() msgs := []Message{{Role: "user", Content: "hello", TokenCount: 2}} result, err := eng.Ingest(ctx, "cron:backup", msgs) if err != nil { t.Fatalf("Ingest: %v", err) } if result != nil { t.Error("expected nil result for ignored session") } // Verify no data was stored conv, _ := eng.store.GetConversationBySessionKey(ctx, "cron:backup") if conv != nil { t.Error("expected no conversation for ignored session") } } func TestEngineIngestStatelessSession(t *testing.T) { eng := newTestEngine(t) eng.statelessPatterns = compileSessionPatterns([]string{"agent:*:ro"}) ctx := context.Background() msgs := []Message{{Role: "user", Content: "hello", TokenCount: 2}} result, err := eng.Ingest(ctx, "agent:abc:ro", msgs) if err != nil { t.Fatalf("Ingest: %v", err) } if result != nil { t.Error("expected nil result for stateless session") } } func TestEngineIngestIncremental(t *testing.T) { eng := newTestEngine(t) ctx := context.Background() // First ingest eng.Ingest(ctx, "agent:test", []Message{ {Role: "user", Content: "msg1", TokenCount: 1}, }) // Second ingest — should append, not replace eng.Ingest(ctx, "agent:test", []Message{ {Role: "assistant", Content: "msg2", TokenCount: 1}, }) conv, _ := eng.store.GetOrCreateConversation(ctx, "agent:test") stored, _ := eng.store.GetMessages(ctx, conv.ConversationID, 10, 0) if len(stored) != 2 { t.Errorf("stored messages = %d, want 2", len(stored)) } } func TestEngineIngestWithParts(t *testing.T) { eng := newTestEngine(t) ctx := context.Background() msgs := []Message{ { Role: "assistant", Content: "", TokenCount: 10, Parts: []MessagePart{ {Type: "tool_use", Name: "read_file", Arguments: `{"path":"/tmp/test"}`, ToolCallID: "tc_123"}, {Type: "text", Text: "here is the file content"}, }, }, } result, err := eng.Ingest(ctx, "agent:parts-test", msgs) if err != nil { t.Fatalf("Ingest with parts: %v", err) } if result.MessageCount != 1 { t.Errorf("MessageCount = %d, want 1", result.MessageCount) } // Verify message was stored WITH parts conv, _ := eng.store.GetOrCreateConversation(ctx, "agent:parts-test") stored, _ := eng.store.GetMessages(ctx, conv.ConversationID, 10, 0) if len(stored) != 1 { t.Fatalf("stored messages = %d, want 1", len(stored)) } if len(stored[0].Parts) != 2 { t.Fatalf("stored message parts = %d, want 2", len(stored[0].Parts)) } if stored[0].Parts[0].Type != "tool_use" { t.Errorf("part[0].Type = %q, want tool_use", stored[0].Parts[0].Type) } if stored[0].Parts[0].Name != "read_file" { t.Errorf("part[0].Name = %q, want read_file", stored[0].Parts[0].Name) } if stored[0].Parts[0].ToolCallID != "tc_123" { t.Errorf("part[0].ToolCallID = %q, want tc_123", stored[0].Parts[0].ToolCallID) } if stored[0].Parts[1].Type != "text" { t.Errorf("part[1].Type = %q, want text", stored[0].Parts[1].Type) } if stored[0].Parts[1].Text != "here is the file content" { t.Errorf("part[1].Text = %q, want 'here is the file content'", stored[0].Parts[1].Text) } } func TestEngineIngestAssemblePreservesParts(t *testing.T) { eng := newTestEngine(t) ctx := context.Background() // Ingest a message with tool_use parts eng.Ingest(ctx, "agent:parts-roundtrip", []Message{ {Role: "user", Content: "list files", TokenCount: 3}, { Role: "assistant", Content: "", TokenCount: 5, Parts: []MessagePart{ {Type: "tool_use", Name: "bash", Arguments: `{"cmd":"ls"}`, ToolCallID: "tc_1"}, {Type: "text", Text: "found 3 files"}, }, }, }) // Assemble should return messages with parts intact result, err := eng.Assemble(ctx, "agent:parts-roundtrip", AssembleInput{Budget: 1000}) if err != nil { t.Fatalf("Assemble: %v", err) } if len(result.Messages) != 2 { t.Fatalf("Assemble returned %d messages, want 2", len(result.Messages)) } // The second message should have Parts populated assistantMsg := result.Messages[1] if len(assistantMsg.Parts) != 2 { t.Fatalf("Assembled assistant message Parts = %d, want 2", len(assistantMsg.Parts)) } if assistantMsg.Parts[0].Type != "tool_use" { t.Errorf("part[0].Type = %q, want tool_use", assistantMsg.Parts[0].Type) } if assistantMsg.Parts[0].ToolCallID != "tc_1" { t.Errorf("part[0].ToolCallID = %q, want tc_1", assistantMsg.Parts[0].ToolCallID) } } // --- Session Mutex --- func TestEngineSessionMutex(t *testing.T) { eng := newTestEngine(t) mu1 := eng.getSessionMutex("agent:test") mu2 := eng.getSessionMutex("agent:test") mu3 := eng.getSessionMutex("agent:other") if mu1 != mu2 { t.Error("expected same mutex for same session key") } if mu1 == mu3 { t.Error("expected different mutex for different session key") } } // --- Close --- func TestEngineClose(t *testing.T) { eng := newTestEngine(t) if err := eng.Close(); err != nil { t.Errorf("Close: %v", err) } } // --- compileSessionPatterns (batch) --- func TestCompileSessionPatterns(t *testing.T) { patterns := compileSessionPatterns([]string{"cron:**", "agent:*:ro"}) if len(patterns) != 2 { t.Fatalf("expected 2 patterns, got %d", len(patterns)) } tests := []struct { input string want bool }{ {"cron:backup", true}, {"agent:abc:ro", true}, {"agent:abc:def", false}, {"", false}, } for _, tt := range tests { matched := false for _, p := range patterns { if p.MatchString(tt.input) { matched = true break } } if matched != tt.want { t.Errorf("patterns.Match(%q) = %v, want %v", tt.input, matched, tt.want) } } } func TestCompileSessionPatternsEmpty(t *testing.T) { patterns := compileSessionPatterns(nil) if len(patterns) != 0 { t.Errorf("expected 0 patterns for nil input, got %d", len(patterns)) } } // --- Bootstrap --- func TestEngineBootstrap(t *testing.T) { eng := newTestEngine(t) ctx := context.Background() msgs := []Message{ {Role: "user", Content: "hello", TokenCount: 3}, {Role: "assistant", Content: "world", TokenCount: 3}, {Role: "user", Content: "how are you", TokenCount: 5}, } err := eng.Bootstrap(ctx, "agent:boot1", msgs) if err != nil { t.Fatalf("Bootstrap: %v", err) } // Verify conversation was created conv, err := eng.store.GetConversationBySessionKey(ctx, "agent:boot1") if err != nil { t.Fatalf("GetConversation: %v", err) } if conv == nil { t.Fatal("expected conversation to exist after bootstrap") } // Verify messages were stored stored, err := eng.store.GetMessages(ctx, conv.ConversationID, 10, 0) if err != nil { t.Fatalf("GetMessages: %v", err) } if len(stored) != 3 { t.Fatalf("expected 3 stored messages, got %d", len(stored)) } if stored[0].Content != "hello" { t.Errorf("stored[0].Content = %q, want 'hello'", stored[0].Content) } // Verify context_items were populated items, err := eng.store.GetContextItems(ctx, conv.ConversationID) if err != nil { t.Fatalf("GetContextItems: %v", err) } if len(items) != 3 { t.Fatalf("expected 3 context items, got %d", len(items)) } } func TestEngineBootstrapEmpty(t *testing.T) { eng := newTestEngine(t) ctx := context.Background() err := eng.Bootstrap(ctx, "agent:empty", nil) if err != nil { t.Fatalf("Bootstrap empty: %v", err) } // No conversation should be created for empty messages conv, _ := eng.store.GetConversationBySessionKey(ctx, "agent:empty") if conv != nil { t.Error("expected no conversation for empty bootstrap") } } func TestEngineBootstrapIdempotent(t *testing.T) { eng := newTestEngine(t) ctx := context.Background() msgs := []Message{ {Role: "user", Content: "hello", TokenCount: 3}, {Role: "assistant", Content: "world", TokenCount: 3}, } // Bootstrap twice with same messages eng.Bootstrap(ctx, "agent:idem", msgs) eng.Bootstrap(ctx, "agent:idem", msgs) // Should still have exactly 2 messages (no duplicates) conv, _ := eng.store.GetConversationBySessionKey(ctx, "agent:idem") if conv == nil { t.Fatal("expected conversation") } stored, _ := eng.store.GetMessages(ctx, conv.ConversationID, 10, 0) if len(stored) != 2 { t.Errorf("expected 2 messages (idempotent), got %d", len(stored)) } } func TestEngineBootstrapDelta(t *testing.T) { eng := newTestEngine(t) ctx := context.Background() // First bootstrap with 2 messages msgs1 := []Message{ {Role: "user", Content: "hello", TokenCount: 3}, {Role: "assistant", Content: "world", TokenCount: 3}, } eng.Bootstrap(ctx, "agent:delta", msgs1) // Second bootstrap with 4 messages (2 existing + 2 new) msgs2 := []Message{ {Role: "user", Content: "hello", TokenCount: 3}, {Role: "assistant", Content: "world", TokenCount: 3}, {Role: "user", Content: "new question", TokenCount: 5}, {Role: "assistant", Content: "new answer", TokenCount: 5}, } eng.Bootstrap(ctx, "agent:delta", msgs2) conv, _ := eng.store.GetConversationBySessionKey(ctx, "agent:delta") if conv == nil { t.Fatal("expected conversation") } stored, _ := eng.store.GetMessages(ctx, conv.ConversationID, 10, 0) if len(stored) != 4 { t.Errorf("expected 4 messages (delta), got %d", len(stored)) } } func TestBootstrapPopulatesContextItems(t *testing.T) { // Bootstrap ingests messages and populates context_items e := newTestEngine(t) ctx := context.Background() messages := []Message{ {Role: "user", Content: "hello from bootstrap test", TokenCount: 10}, {Role: "assistant", Content: "hi there", TokenCount: 5}, {Role: "user", Content: "how are you", TokenCount: 5}, {Role: "assistant", Content: "doing well", TokenCount: 5}, {Role: "user", Content: "great news", TokenCount: 5}, {Role: "assistant", Content: "awesome", TokenCount: 5}, {Role: "user", Content: "lets code", TokenCount: 5}, {Role: "assistant", Content: "sure thing", TokenCount: 5}, } // Bootstrap should ingest and rebuild context_items err := e.Bootstrap(ctx, "test-bootstrap-rebuild", messages) if err != nil { t.Fatalf("Bootstrap: %v", err) } // After bootstrap, context_items should be populated conv, _ := e.store.GetOrCreateConversation(ctx, "test-bootstrap-rebuild") items, err := e.store.GetContextItems(ctx, conv.ConversationID) if err != nil { t.Fatalf("GetContextItems: %v", err) } if len(items) == 0 { t.Error("expected context_items to be populated after Bootstrap, got 0 items") } // Should have one item per message if len(items) != len(messages) { t.Errorf("expected %d context items, got %d", len(messages), len(items)) } } func TestBootstrapDeltaPreservesOrder(t *testing.T) { // When Bootstrap does delta ingest, context_items should maintain // correct order with new messages appended after anchor. e := newTestEngine(t) ctx := context.Background() sessionKey := "test-bootstrap-delta-order" // First: bootstrap with 4 messages initialMsgs := []Message{ {Role: "user", Content: "msg1", TokenCount: 5}, {Role: "assistant", Content: "msg2", TokenCount: 5}, {Role: "user", Content: "msg3", TokenCount: 5}, {Role: "assistant", Content: "msg4", TokenCount: 5}, } err := e.Bootstrap(ctx, sessionKey, initialMsgs) if err != nil { t.Fatalf("first Bootstrap: %v", err) } conv, _ := e.store.GetOrCreateConversation(ctx, sessionKey) items1, _ := e.store.GetContextItems(ctx, conv.ConversationID) if len(items1) != 4 { t.Fatalf("after first bootstrap: expected 4 items, got %d", len(items1)) } // Now bootstrap again with 6 messages (4 existing + 2 new) // The delta (msg5, msg6) should be appended updatedMsgs := []Message{ {Role: "user", Content: "msg1", TokenCount: 5}, {Role: "assistant", Content: "msg2", TokenCount: 5}, {Role: "user", Content: "msg3", TokenCount: 5}, {Role: "assistant", Content: "msg4", TokenCount: 5}, {Role: "user", Content: "msg5", TokenCount: 5}, {Role: "assistant", Content: "msg6", TokenCount: 5}, } err = e.Bootstrap(ctx, sessionKey, updatedMsgs) if err != nil { t.Fatalf("second Bootstrap: %v", err) } items2, _ := e.store.GetContextItems(ctx, conv.ConversationID) if len(items2) != 6 { t.Errorf("after delta bootstrap: expected 6 items, got %d", len(items2)) } } func TestBootstrapHistoryEditFirstMessageChanged(t *testing.T) { // When the first message changes (anchor = -1), Bootstrap should rebuild // from scratch without panicking (regression test for index out of range [-1]) e := newTestEngine(t) ctx := context.Background() sessionKey := "test-bootstrap-history-edit" // First: bootstrap with some messages initialMsgs := []Message{ {Role: "user", Content: "original first", TokenCount: 5}, {Role: "assistant", Content: "response", TokenCount: 5}, {Role: "user", Content: "question", TokenCount: 5}, } err := e.Bootstrap(ctx, sessionKey, initialMsgs) if err != nil { t.Fatalf("first Bootstrap: %v", err) } // Now bootstrap with completely different messages (first message changed) // This should NOT panic - it should rebuild from scratch editedMsgs := []Message{ {Role: "user", Content: "DIFFERENT first message", TokenCount: 5}, {Role: "assistant", Content: "DIFFERENT response", TokenCount: 5}, {Role: "user", Content: "DIFFERENT question", TokenCount: 5}, } err = e.Bootstrap(ctx, sessionKey, editedMsgs) if err != nil { t.Fatalf("second Bootstrap (history edit): %v", err) } conv, _ := e.store.GetOrCreateConversation(ctx, sessionKey) stored, _ := e.store.GetMessages(ctx, conv.ConversationID, 10, 0) // Should have the NEW messages (history was rebuilt) if len(stored) != 3 { t.Errorf("expected 3 messages after history edit, got %d", len(stored)) } if len(stored) > 0 && stored[0].Content != "DIFFERENT first message" { t.Errorf("first message = %q, want 'DIFFERENT first message'", stored[0].Content) } } func TestBootstrapSameContentDifferentTokenCountNoRebuild(t *testing.T) { // Bootstrap should NOT rebuild when content is identical but TokenCount differs. // This happens when TokenCount is re-estimated (e.g., via tokenizer.EstimateMessageTokens) // during bootstrap, which may give slightly different values. e := newTestEngine(t) ctx := context.Background() sessionKey := "test-bootstrap-token-diff" // First: bootstrap with some messages initialMsgs := []Message{ {Role: "user", Content: "hello world", TokenCount: 10}, {Role: "assistant", Content: "hi there", TokenCount: 5}, } err := e.Bootstrap(ctx, sessionKey, initialMsgs) if err != nil { t.Fatalf("first Bootstrap: %v", err) } conv, _ := e.store.GetOrCreateConversation(ctx, sessionKey) storedBefore, _ := e.store.GetMessages(ctx, conv.ConversationID, 10, 0) // Second: bootstrap with SAME content but DIFFERENT TokenCount // This should be a no-op (not rebuild) sameContentMsgs := []Message{ {Role: "user", Content: "hello world", TokenCount: 999}, // Different token count! {Role: "assistant", Content: "hi there", TokenCount: 888}, // Different token count! } err = e.Bootstrap(ctx, sessionKey, sameContentMsgs) if err != nil { t.Fatalf("second Bootstrap: %v", err) } storedAfter, _ := e.store.GetMessages(ctx, conv.ConversationID, 10, 0) // Should have same number of messages (no rebuild) if len(storedAfter) != len(storedBefore) { t.Errorf("expected %d messages (no rebuild), got %d", len(storedBefore), len(storedAfter)) } // Message IDs should be the same (no delete+re-ingest) for i := range storedBefore { if storedBefore[i].ID != storedAfter[i].ID { t.Errorf("message %d ID changed: before=%d, after=%d (should be no-op)", i, storedBefore[i].ID, storedAfter[i].ID) } } } // --- Session Mutex --- func TestEngineSessionMutexSharded(t *testing.T) { eng := newTestEngine(t) // Same session key should always return the same mutex (deterministic hash) mu1 := eng.getSessionMutex("agent:test") mu2 := eng.getSessionMutex("agent:test") if mu1 != mu2 { t.Error("expected same mutex for same session key") } // Different session keys may share the same shard (hash collision) // This is expected behavior - we just need bounded memory, not unique locks mu3 := eng.getSessionMutex("agent:other") // Both mutexes should be valid and usable mu1.Lock() mu1.Unlock() mu3.Lock() mu3.Unlock() } func TestEngineSessionMutexBoundedMemory(t *testing.T) { // Verify that session mutexes use bounded memory (256 shards) eng := newTestEngine(t) // Get mutexes for many different sessions seen := make(map[*sync.Mutex]bool) for i := 0; i < 1000; i++ { sessionKey := fmt.Sprintf("agent:session-%d", i) mu := eng.getSessionMutex(sessionKey) seen[mu] = true } // With 256 shards and 1000 sessions, we should see at most 256 unique mutexes // (likely fewer due to hash collisions) if len(seen) > 256 { t.Errorf("expected at most 256 unique mutexes (shards), got %d", len(seen)) } } func TestEngineSessionMutexConsistentHash(t *testing.T) { // Same session key should always hash to the same shard eng := newTestEngine(t) sessionKey := "agent:consistent-hash-test" mu1 := eng.getSessionMutex(sessionKey) mu2 := eng.getSessionMutex(sessionKey) mu3 := eng.getSessionMutex(sessionKey) if mu1 != mu2 || mu2 != mu3 { t.Error("hash function should be deterministic - same key must map to same shard") } } // --- Summary Role --- func TestAssemblerSummaryRoleNotUser(t *testing.T) { // Summaries should use "system" role, not "user" eng := newTestEngine(t) ctx := context.Background() // Ingest messages eng.Ingest(ctx, "agent:summary-role-test", []Message{ {Role: "user", Content: "hello", TokenCount: 5}, {Role: "assistant", Content: "world", TokenCount: 5}, }) conv, _ := eng.store.GetOrCreateConversation(ctx, "agent:summary-role-test") // Create a summary and add it to context sum, err := eng.store.CreateSummary(ctx, CreateSummaryInput{ ConversationID: conv.ConversationID, Content: "Test summary content", TokenCount: 10, Kind: SummaryKindCondensed, Depth: 1, }) if err != nil { t.Fatalf("CreateSummary: %v", err) } eng.store.AppendContextSummary(ctx, conv.ConversationID, sum.SummaryID) // Assemble and check summary message role result, err := eng.Assemble(ctx, "agent:summary-role-test", AssembleInput{Budget: 1000}) if err != nil { t.Fatalf("Assemble: %v", err) } // Find the summary message (should have XML content with ) for _, msg := range result.Messages { if strings.Contains(msg.Content, "= 5 // This tests the bug: when depth=2 is missing, the loop breaks and depth=3 is never checked // Need > FreshTailCount(32) summaries so they are not all in fresh tail // Depth 0: 3 summaries (not enough), Depth 1: 3 summaries (not enough) // Depth 2: 0 summaries (missing), Depth 3: 40 summaries (enough) depths := []int{0, 0, 0, 1, 1, 1} for i := 0; i < 40; i++ { depths = append(depths, 3) } now := time.Now().UTC() for i, depth := range depths { sum, createErr := e.store.CreateSummary(ctx, CreateSummaryInput{ ConversationID: conv.ConversationID, Kind: SummaryKindLeaf, Depth: depth, Content: fmt.Sprintf("summary depth %d #%d", depth, i), TokenCount: 10, EarliestAt: &now, LatestAt: &now, }) if createErr != nil { t.Fatalf("CreateSummary: %v", createErr) } // Add to context items (not in fresh tail) if appendErr := e.store.AppendContextSummary(ctx, conv.ConversationID, sum.SummaryID); appendErr != nil { t.Fatalf("AppendContextSummary: %v", appendErr) } } // Initialize compaction engine (lazy init) e.initCompactionOnce() // Call selectShallowestCondensationCandidate candidates, err := e.compaction.selectShallowestCondensationCandidate(ctx, conv.ConversationID, false) if err != nil { t.Fatalf("selectShallowestCondensationCandidate: %v", err) } // Should find depth=0 (shallowest) with 5 summaries if candidates == nil { t.Fatal("expected candidates, got nil") } if len(candidates) < CondensedMinFanout { t.Errorf("expected at least %d candidates, got %d", CondensedMinFanout, len(candidates)) } // Verify all returned summaries have the same depth if len(candidates) > 0 { expectedDepth := candidates[0].Depth for _, c := range candidates[1:] { if c.Depth != expectedDepth { t.Errorf("candidates have mixed depths: %d vs %d", expectedDepth, c.Depth) } } } }