Skip to content

Commit 5d6ed2b

Browse files
committed
tests: add edge-case tests for session recall pipeline
New tests: - TestSessionSearch_DeepSearchTwoTokens: single common word ('changes') must not match; two distinct tokens must match. - TestSessionSearch_GetReturnsSessionMessages: get returns the full session_messages array with correct role+content. - TestSessionSearch_PreSavePersistence: a freshly saved session is findable immediately (validates pre-agent-loop save fix). - TestSessionSearch_DeepSearchEdgeCases: empty messages don't panic, system-only messages excluded, unicode content works.
1 parent 3d43ef8 commit 5d6ed2b

1 file changed

Lines changed: 314 additions & 0 deletions

File tree

cmd/odek/session_search_tool_test.go

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,3 +565,317 @@ func TestSessionSearch_SearchNoDeepFallback(t *testing.T) {
565565
t.Error("expected native-tools session in task-only search results")
566566
}
567567
}
568+
569+
// ── Extended Edge-Case Tests ──────────────────────────────────────────
570+
571+
func TestSessionSearch_DeepSearchTwoTokens(t *testing.T) {
572+
// Verify deepSearch requires >= 2 distinct tokens to match.
573+
// A single common word in 107 messages must not qualify.
574+
store, cleanup := seedSessionStore(t)
575+
defer cleanup()
576+
577+
// Add a session where a single common word "changes" appears in messages
578+
// but nothing else from the query matches. This simulates the false
579+
// positive where "go-vector changes" matched the events fetcher session.
580+
singleWordSess := &session.Session{
581+
ID: "test-single-word",
582+
Task: "unrelated task about events",
583+
CreatedAt: time.Now().Add(-1 * time.Hour),
584+
UpdatedAt: time.Now().Add(-30 * time.Minute),
585+
Model: "deepseek-v4-flash",
586+
Turns: 3,
587+
Messages: []llm.Message{
588+
{Role: "user", Content: "Analyze the current output"},
589+
{Role: "assistant", Content: "Events changed: +8 -8 = 10 total"},
590+
},
591+
Buffer: []string{"13:00 user analyzed output"},
592+
}
593+
if err := store.Save(singleWordSess); err != nil {
594+
t.Fatalf("save single-word session: %v", err)
595+
}
596+
597+
// Add a session with TWO distinct tokens matching
598+
twoTokenSess := &session.Session{
599+
ID: "test-two-tokens",
600+
Task: "go-vector project updates",
601+
CreatedAt: time.Now().Add(-2 * time.Hour),
602+
UpdatedAt: time.Now().Add(-1 * time.Hour),
603+
Model: "deepseek-v4-flash",
604+
Turns: 5,
605+
Messages: []llm.Message{
606+
{Role: "user", Content: "Review our latest changes in go-vector"},
607+
{Role: "assistant", Content: "The SaveEmbedder API is now persistent"},
608+
},
609+
Buffer: []string{"14:00 user asked about go-vector changes"},
610+
}
611+
if err := store.Save(twoTokenSess); err != nil {
612+
t.Fatalf("save two-token session: %v", err)
613+
}
614+
615+
tool := newSessionSearchTool(store)
616+
617+
// Query with "changes" + novel term — only two-token session should match
618+
result := callSessionSearch(t, tool, `{"action":"search","query":"go-vector changes","limit":10}`)
619+
620+
var resp struct {
621+
Sessions []struct {
622+
ID string `json:"id"`
623+
} `json:"sessions"`
624+
}
625+
if err := json.Unmarshal([]byte(result), &resp); err != nil {
626+
t.Fatalf("unmarshal: %v\nraw: %s", err, result)
627+
}
628+
629+
// The single-word session should NOT appear
630+
for _, s := range resp.Sessions {
631+
if s.ID == "test-single-word" {
632+
t.Error("single-word session matched — 'changes' alone should not qualify")
633+
}
634+
}
635+
636+
// The two-token session SHOULD appear
637+
found := false
638+
for _, s := range resp.Sessions {
639+
if s.ID == "test-two-tokens" {
640+
found = true
641+
break
642+
}
643+
}
644+
if !found {
645+
t.Error("two-token session should match 'go-vector changes'")
646+
}
647+
}
648+
649+
func TestSessionSearch_GetReturnsSessionMessages(t *testing.T) {
650+
// Verify get returns the full session_messages array with role+content.
651+
store, cleanup := seedSessionStore(t)
652+
defer cleanup()
653+
654+
// Add a session with known messages
655+
testSess := &session.Session{
656+
ID: "test-msg-content",
657+
Task: "test session for message content",
658+
CreatedAt: time.Now().Add(-1 * time.Hour),
659+
UpdatedAt: time.Now(),
660+
Model: "deepseek-v4-flash",
661+
Turns: 2,
662+
Messages: []llm.Message{
663+
{Role: "user", Content: "First user message about go-vector"},
664+
{Role: "assistant", Content: "Here is the response about SaveEmbedder"},
665+
{Role: "user", Content: "Second question about vector dimensions"},
666+
{Role: "assistant", Content: "256 dimensions for RandomProjections"},
667+
},
668+
}
669+
if err := store.Save(testSess); err != nil {
670+
t.Fatalf("save test session: %v", err)
671+
}
672+
673+
tool := newSessionSearchTool(store)
674+
result := callSessionSearch(t, tool, `{"action":"get","query":"test-msg-content"}`)
675+
676+
var resp struct {
677+
SessionMessages []struct {
678+
Role string `json:"role"`
679+
Content string `json:"content"`
680+
} `json:"session_messages"`
681+
Messages int `json:"messages"`
682+
Error string `json:"error"`
683+
}
684+
if err := json.Unmarshal([]byte(result), &resp); err != nil {
685+
t.Fatalf("unmarshal: %v\nraw: %s", err, result)
686+
}
687+
688+
if resp.Error != "" {
689+
t.Fatalf("error: %s", resp.Error)
690+
}
691+
692+
// Check messages count matches
693+
if resp.Messages != 4 {
694+
t.Errorf("messages count = %d, want 4", resp.Messages)
695+
}
696+
697+
// Check all 4 messages are present with correct role+content
698+
if len(resp.SessionMessages) != 4 {
699+
t.Fatalf("got %d session_messages, want 4", len(resp.SessionMessages))
700+
}
701+
702+
checks := []struct {
703+
role string
704+
content string
705+
}{
706+
{"user", "First user message about go-vector"},
707+
{"assistant", "Here is the response about SaveEmbedder"},
708+
{"user", "Second question about vector dimensions"},
709+
{"assistant", "256 dimensions for RandomProjections"},
710+
}
711+
for i, c := range checks {
712+
if resp.SessionMessages[i].Role != c.role {
713+
t.Errorf("msg[%d] role = %q, want %q", i, resp.SessionMessages[i].Role, c.role)
714+
}
715+
if resp.SessionMessages[i].Content != c.content {
716+
t.Errorf("msg[%d] content = %q, want %q", i, resp.SessionMessages[i].Content, c.content)
717+
}
718+
}
719+
}
720+
721+
func TestSessionSearch_PreSavePersistence(t *testing.T) {
722+
// Verify that a session saved immediately (simulating pre-agent-loop save)
723+
// is findable by session_search right after. This tests the core of the
724+
// v0.58.5 fix: save user message before agent loop runs.
725+
store, cleanup := seedSessionStore(t)
726+
defer cleanup()
727+
728+
// Simulate: user sends a message, handler saves it immediately
729+
uniqueContent := "specific-unique-go-vector-context-xyz"
730+
freshSess := &session.Session{
731+
ID: "test-fresh-save",
732+
Task: "current conversation",
733+
CreatedAt: time.Now(),
734+
UpdatedAt: time.Now(),
735+
Model: "deepseek-v4-flash",
736+
Turns: 0,
737+
Messages: []llm.Message{
738+
{Role: "user", Content: uniqueContent},
739+
},
740+
}
741+
// This is what Store.Save does — immediate persistence to disk + vector index
742+
if err := store.Save(freshSess); err != nil {
743+
t.Fatalf("save fresh session: %v", err)
744+
}
745+
746+
tool := newSessionSearchTool(store)
747+
748+
// Search should find it immediately (both vector and deepSearch paths)
749+
result := callSessionSearch(t, tool, `{"action":"search","query":"specific-unique-go-vector-context-xyz","limit":5}`)
750+
751+
var resp struct {
752+
Sessions []struct {
753+
ID string `json:"id"`
754+
} `json:"sessions"`
755+
Count int `json:"count"`
756+
}
757+
if err := json.Unmarshal([]byte(result), &resp); err != nil {
758+
t.Fatalf("unmarshal: %v\nraw: %s", err, result)
759+
}
760+
761+
if resp.Count == 0 {
762+
t.Fatal("freshly saved session should be findable immediately")
763+
}
764+
765+
found := false
766+
for _, s := range resp.Sessions {
767+
if s.ID == "test-fresh-save" {
768+
found = true
769+
break
770+
}
771+
}
772+
if !found {
773+
t.Error("fresh-save session not found in search results")
774+
}
775+
}
776+
777+
func TestSessionSearch_DeepSearchEdgeCases(t *testing.T) {
778+
store, cleanup := seedSessionStore(t)
779+
defer cleanup()
780+
781+
// Edge case 1: session with empty messages
782+
emptySess := &session.Session{
783+
ID: "test-empty-msgs",
784+
Task: "empty session",
785+
CreatedAt: time.Now().Add(-1 * time.Hour),
786+
UpdatedAt: time.Now(),
787+
Model: "deepseek-v4-flash",
788+
Turns: 0,
789+
Messages: []llm.Message{},
790+
}
791+
if err := store.Save(emptySess); err != nil {
792+
t.Fatalf("save empty session: %v", err)
793+
}
794+
795+
// Edge case 2: session with only system messages (should be skipped)
796+
systemOnlySess := &session.Session{
797+
ID: "test-system-only",
798+
Task: "system messages only",
799+
CreatedAt: time.Now().Add(-1 * time.Hour),
800+
UpdatedAt: time.Now(),
801+
Model: "deepseek-v4-flash",
802+
Turns: 1,
803+
Messages: []llm.Message{
804+
{Role: "system", Content: "You are a helpful assistant"},
805+
{Role: "system", Content: "Memory context block"},
806+
},
807+
}
808+
if err := store.Save(systemOnlySess); err != nil {
809+
t.Fatalf("save system-only session: %v", err)
810+
}
811+
812+
// Edge case 3: session with unicode content
813+
unicodeSess := &session.Session{
814+
ID: "test-unicode",
815+
Task: "unicode test session",
816+
CreatedAt: time.Now().Add(-1 * time.Hour),
817+
UpdatedAt: time.Now(),
818+
Model: "deepseek-v4-flash",
819+
Turns: 2,
820+
Messages: []llm.Message{
821+
{Role: "user", Content: "Check the 🚀 emoji and café content"},
822+
{Role: "assistant", Content: "Found go-vector persistência in the code"},
823+
},
824+
}
825+
if err := store.Save(unicodeSess); err != nil {
826+
t.Fatalf("save unicode session: %v", err)
827+
}
828+
829+
tool := newSessionSearchTool(store)
830+
831+
t.Run("empty messages dont cause panic", func(t *testing.T) {
832+
result := callSessionSearch(t, tool, `{"action":"search","query":"go-vector changes","limit":5}`)
833+
// Should not panic — just return whatever it finds
834+
var r sessionSearchBasicResult
835+
if err := json.Unmarshal([]byte(result), &r); err != nil {
836+
t.Fatalf("unmarshal: %v", err)
837+
}
838+
if r.Error != "" && r.Error != "null" {
839+
t.Errorf("unexpected error: %s", r.Error)
840+
}
841+
})
842+
843+
t.Run("system-only messages not matched", func(t *testing.T) {
844+
result := callSessionSearch(t, tool, `{"action":"search","query":"helpful assistant memory","limit":5}`)
845+
var resp struct {
846+
Sessions []struct {
847+
ID string `json:"id"`
848+
} `json:"sessions"`
849+
}
850+
if err := json.Unmarshal([]byte(result), &resp); err != nil {
851+
t.Fatalf("unmarshal: %v", err)
852+
}
853+
for _, s := range resp.Sessions {
854+
if s.ID == "test-system-only" {
855+
t.Error("system-only session should not match — system messages are excluded from deepSearch")
856+
}
857+
}
858+
})
859+
860+
t.Run("unicode content with two tokens matches", func(t *testing.T) {
861+
result := callSessionSearch(t, tool, `{"action":"search","query":"go-vector persistência","limit":5}`)
862+
var resp struct {
863+
Sessions []struct {
864+
ID string `json:"id"`
865+
} `json:"sessions"`
866+
}
867+
if err := json.Unmarshal([]byte(result), &resp); err != nil {
868+
t.Fatalf("unmarshal: %v", err)
869+
}
870+
found := false
871+
for _, s := range resp.Sessions {
872+
if s.ID == "test-unicode" {
873+
found = true
874+
break
875+
}
876+
}
877+
if !found {
878+
t.Error("unicode session should match 'go-vector persistência'")
879+
}
880+
})
881+
}

0 commit comments

Comments
 (0)