mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-22 09:07:36 +08:00
239 lines
7.2 KiB
Go
239 lines
7.2 KiB
Go
package session
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/YspCoder/clawgo/pkg/jsonlog"
|
|
"github.com/YspCoder/clawgo/pkg/providers"
|
|
)
|
|
|
|
func TestLoadSessionsReturnsScannerErrorForOversizedLine(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
storage := t.TempDir()
|
|
line := `{"role":"user","content":"` + strings.Repeat("x", 2*1024*1024) + `"}`
|
|
if err := os.WriteFile(filepath.Join(storage, "huge.jsonl"), []byte(line+"\n"), 0644); err != nil {
|
|
t.Fatalf("write session file failed: %v", err)
|
|
}
|
|
|
|
sm := &SessionManager{
|
|
sessions: map[string]*Session{},
|
|
storage: storage,
|
|
}
|
|
if err := sm.loadSessions(); err != nil {
|
|
t.Fatalf("expected oversized line to load with expanded scanner buffer, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFromJSONLLineParsesOpenClawToolResult(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
line := []byte(`{"type":"message","message":{"role":"toolResult","content":[{"type":"text","text":"done"}],"toolCallId":"call-1"}}`)
|
|
msg, ok := fromJSONLLine(line)
|
|
if !ok {
|
|
t.Fatal("expected line to parse")
|
|
}
|
|
if msg.Role != "tool" || msg.ToolCallID != "call-1" || msg.Content != "done" {
|
|
t.Fatalf("unexpected parsed message: %+v", msg)
|
|
}
|
|
}
|
|
|
|
func TestSessionManagerWritesSidecarsAndSearches(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
storage := t.TempDir()
|
|
sm := NewSessionManager(storage)
|
|
key := "cli:default"
|
|
|
|
sm.AddMessage(key, "user", "deploy project alpha")
|
|
sm.AddMessage(key, "assistant", "deployment failed with timeout after contacting api gateway")
|
|
|
|
for _, name := range []string{
|
|
key + ".active.jsonl",
|
|
key + ".meta.json",
|
|
key + ".index.json",
|
|
} {
|
|
if _, err := os.Stat(filepath.Join(storage, name)); err != nil {
|
|
t.Fatalf("expected artifact %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
results := sm.Search("deploy timeout", nil, "", 5)
|
|
if len(results) != 1 {
|
|
t.Fatalf("expected one search result, got %#v", results)
|
|
}
|
|
if results[0].Key != key || len(results[0].Snippets) == 0 {
|
|
t.Fatalf("unexpected search result: %#v", results[0])
|
|
}
|
|
}
|
|
|
|
func TestSessionManagerRebuildsMissingSidecarsFromJSONL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
storage := t.TempDir()
|
|
sm := NewSessionManager(storage)
|
|
key := "cli:summary"
|
|
|
|
sm.AddMessage(key, "user", "remember previous deploy steps")
|
|
sm.SetSummary(key, "Key Facts\n- Previous deploy steps were discussed.")
|
|
|
|
if err := os.Remove(filepath.Join(storage, key+".meta.json")); err != nil {
|
|
t.Fatalf("remove meta: %v", err)
|
|
}
|
|
if err := os.Remove(filepath.Join(storage, key+".index.json")); err != nil {
|
|
t.Fatalf("remove index: %v", err)
|
|
}
|
|
|
|
reloaded := NewSessionManager(storage)
|
|
if got := reloaded.GetSummary(key); !strings.Contains(got, "Previous deploy steps") {
|
|
t.Fatalf("expected summary recovered from fallback index, got %q", got)
|
|
}
|
|
results := reloaded.Search("deploy", nil, "", 5)
|
|
if len(results) != 1 || results[0].Key != key {
|
|
t.Fatalf("expected rebuilt search result, got %#v", results)
|
|
}
|
|
}
|
|
|
|
func TestSessionManagerRollsOverActiveSegment(t *testing.T) {
|
|
t.Setenv("CLAWGO_SESSION_SEGMENT_MAX_MESSAGES", "2")
|
|
storage := t.TempDir()
|
|
sm := NewSessionManager(storage)
|
|
key := "cli:rollover"
|
|
|
|
sm.AddMessage(key, "user", "one")
|
|
sm.AddMessage(key, "assistant", "two")
|
|
sm.AddMessage(key, "user", "three")
|
|
|
|
if _, err := os.Stat(filepath.Join(storage, key+".0001.jsonl")); err != nil {
|
|
t.Fatalf("expected archived segment: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(storage, key+".active.jsonl")); err != nil {
|
|
t.Fatalf("expected new active segment: %v", err)
|
|
}
|
|
history := sm.GetHistory(key)
|
|
if len(history) != 3 {
|
|
t.Fatalf("expected full history across segments, got %d", len(history))
|
|
}
|
|
}
|
|
|
|
func TestSessionManagerHistoryWindowAndIncrementalIndex(t *testing.T) {
|
|
t.Setenv("CLAWGO_SESSION_SEGMENT_MAX_MESSAGES", "2")
|
|
storage := t.TempDir()
|
|
sm := NewSessionManager(storage)
|
|
key := "cli:window"
|
|
|
|
sm.AddMessage(key, "user", "one")
|
|
sm.AddMessage(key, "assistant", "two")
|
|
sm.AddMessage(key, "user", "three")
|
|
sm.AddMessage(key, "assistant", "four")
|
|
|
|
window := sm.GetHistoryWindow(key, 0, 0, 2, 2)
|
|
if len(window) != 2 || window[0].Content != "three" || window[1].Content != "four" {
|
|
t.Fatalf("unexpected history window: %#v", window)
|
|
}
|
|
|
|
var index sessionIndexFile
|
|
if err := jsonlog.ReadJSON(filepath.Join(storage, key+".index.json"), &index); err != nil {
|
|
t.Fatalf("read index: %v", err)
|
|
}
|
|
size, err := jsonlog.FileSize(filepath.Join(storage, key+".active.jsonl"))
|
|
if err != nil {
|
|
t.Fatalf("file size: %v", err)
|
|
}
|
|
if index.LastSeq != 4 {
|
|
t.Fatalf("expected last seq 4, got %d", index.LastSeq)
|
|
}
|
|
if index.LastOffset != size {
|
|
t.Fatalf("expected last offset %d, got %d", size, index.LastOffset)
|
|
}
|
|
if index.Segment != key+".active.jsonl" {
|
|
t.Fatalf("unexpected index segment %q", index.Segment)
|
|
}
|
|
}
|
|
|
|
func TestSessionManagerSearchSupportsChineseBigrams(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
storage := t.TempDir()
|
|
sm := NewSessionManager(storage)
|
|
key := "cli:zh"
|
|
|
|
sm.AddMessage(key, "user", "之前讨论过发布回滚方案")
|
|
sm.AddMessage(key, "assistant", "回滚需要先确认数据库版本")
|
|
|
|
results := sm.Search("回滚方案", nil, "", 5)
|
|
if len(results) != 1 || results[0].Key != key {
|
|
t.Fatalf("expected chinese query to hit sidecar index, got %#v", results)
|
|
}
|
|
|
|
if err := os.Remove(filepath.Join(storage, key+".index.json")); err != nil {
|
|
t.Fatalf("remove index: %v", err)
|
|
}
|
|
reloaded := NewSessionManager(storage)
|
|
results = reloaded.Search("回滚方案", nil, "", 5)
|
|
if len(results) != 1 || results[0].Key != key {
|
|
t.Fatalf("expected chinese query to hit scan fallback, got %#v", results)
|
|
}
|
|
}
|
|
|
|
func TestApplyCompactionIfUnchangedRejectsChangedSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sm := NewSessionManager(t.TempDir())
|
|
key := "cli:guard"
|
|
sm.AddMessage(key, "user", "one")
|
|
sm.AddMessage(key, "assistant", "two")
|
|
snapshot := sm.CompactionSnapshot(key)
|
|
sm.AddMessage(key, "user", "three")
|
|
|
|
applied := sm.ApplyCompactionIfUnchanged(key, snapshot.NextSeq, snapshot.Summary, []providers.Message{{Role: "assistant", Content: "two"}}, "Key Facts\n- compacted")
|
|
if applied {
|
|
t.Fatal("expected stale compaction application to be rejected")
|
|
}
|
|
history := sm.GetPromptHistory(key)
|
|
if len(history) != 3 || history[2].Content != "three" {
|
|
t.Fatalf("expected newer message to remain, got %#v", history)
|
|
}
|
|
}
|
|
|
|
func TestSessionManagerAppendDoesNotRewriteSessionsIndex(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
storage := t.TempDir()
|
|
sm := NewSessionManager(storage)
|
|
key := "cli:index"
|
|
|
|
sm.AddMessage(key, "user", "first")
|
|
indexPath := filepath.Join(storage, "sessions.json")
|
|
before, err := os.ReadFile(indexPath)
|
|
if err != nil {
|
|
t.Fatalf("read sessions index: %v", err)
|
|
}
|
|
statBefore, err := os.Stat(indexPath)
|
|
if err != nil {
|
|
t.Fatalf("stat sessions index: %v", err)
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
sm.AddMessage(key, "assistant", "second")
|
|
|
|
after, err := os.ReadFile(indexPath)
|
|
if err != nil {
|
|
t.Fatalf("read sessions index after append: %v", err)
|
|
}
|
|
statAfter, err := os.Stat(indexPath)
|
|
if err != nil {
|
|
t.Fatalf("stat sessions index after append: %v", err)
|
|
}
|
|
if string(before) != string(after) {
|
|
t.Fatalf("expected sessions.json to stay unchanged for hot-path append")
|
|
}
|
|
if !statAfter.ModTime().Equal(statBefore.ModTime()) {
|
|
t.Fatalf("expected sessions.json mtime to stay unchanged, before=%s after=%s", statBefore.ModTime(), statAfter.ModTime())
|
|
}
|
|
}
|