@@ -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\n raw: %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\n raw: %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\n raw: %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