package llmfs import ( "io" "strings" "testing" "github.com/NERVsystems/llm9p/internal/llm" ) // newTestSession creates a SessionManager with a mock backend and a fresh session. // Returns the manager and the session ID. func newTestSession() (*llm.SessionManager, int) { mock := NewMockBackend() mock.askResponse = "This is a compact summary of the conversation." sm := llm.NewSessionManager(mock) id := sm.Create() return sm, id } // ---- SessionCompactFile ---- func TestSessionCompactFile_Read(t *testing.T) { sm, id := newTestSession() f := NewSessionCompactFile(sm, id) buf := make([]byte, 100) n, err := f.Read(buf, 0) if err != nil { t.Fatalf("Read() error: %v", err) } content := string(buf[:n]) if content == "" { t.Error("Read() returned empty content") } } func TestSessionCompactFile_Read_EOF(t *testing.T) { sm, id := newTestSession() f := NewSessionCompactFile(sm, id) buf := make([]byte, 100) n, err := f.Read(buf, 10000) if err != io.EOF { t.Errorf("Read(large offset) = %v, want io.EOF", err) } if n != 0 { t.Errorf("Read(large offset) n = %d, want 0", n) } } func TestSessionCompactFile_Write_NoOp_ShortHistory(t *testing.T) { sm, id := newTestSession() session := sm.Get(id) session.AddMessage("user", "hello") session.AddMessage("assistant", "hi") f := NewSessionCompactFile(sm, id) n, err := f.Write([]byte("compact"), 0) if err != nil { t.Fatalf("Write() error: %v", err) } if n != 7 { t.Errorf("Write() n = %d, want 7", n) } // Session with < 4 messages: compaction is a no-op (messages unchanged) msgs := session.Messages() if len(msgs) != 2 { t.Errorf("after no-op compact: messages = %d, want 2", len(msgs)) } } func TestSessionCompactFile_Write_Compacts(t *testing.T) { sm, id := newTestSession() session := sm.Get(id) // Add 3 turns (6 messages) to pass the threshold for i := 0; i < 3; i++ { session.AddMessage("user", "question") session.AddMessage("assistant", "answer") } f := NewSessionCompactFile(sm, id) _, err := f.Write([]byte("compact"), 0) if err != nil { t.Fatalf("Write() error: %v", err) } // After compaction messages should be condensed msgs := session.Messages() if len(msgs) >= 6 { t.Errorf("after compact: messages = %d, expected fewer than 6", len(msgs)) } } func TestSessionCompactFile_Stat(t *testing.T) { sm, id := newTestSession() f := NewSessionCompactFile(sm, id) stat := f.Stat() if stat.Length == 0 { t.Error("Stat().Length should be non-zero") } } // ---- SessionUsageFile ---- func TestSessionUsageFile_Read_Format(t *testing.T) { sm, id := newTestSession() session := sm.Get(id) // 400 chars → 100 estimated tokens session.AddMessage("user", strings.Repeat("x", 400)) f := NewSessionUsageFile(sm, id) buf := make([]byte, 64) n, err := f.Read(buf, 0) if err != nil { t.Fatalf("Read() error: %v", err) } content := string(buf[:n]) // Should be "100/200000\n" if content != "100/200000\n" { t.Errorf("Read() = %q, want %q", content, "100/200000\n") } } func TestSessionUsageFile_Read_EOF(t *testing.T) { sm, id := newTestSession() f := NewSessionUsageFile(sm, id) buf := make([]byte, 64) n, err := f.Read(buf, 10000) if err != io.EOF { t.Errorf("Read(large offset) = %v, want io.EOF", err) } if n != 0 { t.Errorf("Read(large offset) n = %d, want 0", n) } } func TestSessionUsageFile_Write_ReadOnly(t *testing.T) { sm, id := newTestSession() f := NewSessionUsageFile(sm, id) n, err := f.Write([]byte("anything"), 0) if err == nil { t.Error("Write() should return error (read-only)") } if n != 0 { t.Errorf("Write() n = %d, want 0", n) } } func TestSessionUsageFile_Stat_Length(t *testing.T) { sm, id := newTestSession() f := NewSessionUsageFile(sm, id) stat := f.Stat() // "0/200000\n" = 9 chars if stat.Length != 9 { t.Errorf("Stat().Length = %d, want 9 (for '0/200000\\n')", stat.Length) } } func TestSessionUsageFile_Reflects_Content(t *testing.T) { sm, id := newTestSession() session := sm.Get(id) f := NewSessionUsageFile(sm, id) buf := make([]byte, 64) // Initially empty n, _ := f.Read(buf, 0) if string(buf[:n]) != "0/200000\n" { t.Errorf("initial: got %q, want '0/200000\\n'", string(buf[:n])) } // Add 800 chars → 200 estimated tokens session.AddMessage("user", strings.Repeat("a", 800)) n, _ = f.Read(buf, 0) if string(buf[:n]) != "200/200000\n" { t.Errorf("after add: got %q, want '200/200000\\n'", string(buf[:n])) } } // ---- SessionDir includes compact and usage ---- func TestSessionDir_HasCompactAndUsage(t *testing.T) { sm, id := newTestSession() dir := NewSessionDir(sm, id) children := dir.Children() names := make(map[string]bool) for _, f := range children { names[f.Stat().Name] = true } for _, want := range []string{"compact", "usage"} { if !names[want] { t.Errorf("SessionDir.Children() missing %q", want) } } } func TestSessionDir_LookupCompact(t *testing.T) { sm, id := newTestSession() dir := NewSessionDir(sm, id) f, err := dir.Lookup("compact") if err != nil { t.Fatalf("Lookup('compact') error: %v", err) } if _, ok := f.(*SessionCompactFile); !ok { t.Errorf("Lookup('compact') returned %T, want *SessionCompactFile", f) } } func TestSessionDir_LookupUsage(t *testing.T) { sm, id := newTestSession() dir := NewSessionDir(sm, id) f, err := dir.Lookup("usage") if err != nil { t.Fatalf("Lookup('usage') error: %v", err) } if _, ok := f.(*SessionUsageFile); !ok { t.Errorf("Lookup('usage') returned %T, want *SessionUsageFile", f) } }