merge origin main updates

This commit is contained in:
LPF
2026-05-10 17:43:06 +08:00
58 changed files with 7574 additions and 1192 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,10 @@ import (
"path/filepath"
"strings"
"testing"
"time"
"github.com/YspCoder/clawgo/pkg/jsonlog"
"github.com/YspCoder/clawgo/pkg/providers"
)
func TestLoadSessionsReturnsScannerErrorForOversizedLine(t *testing.T) {
@@ -38,3 +42,197 @@ func TestFromJSONLLineParsesOpenClawToolResult(t *testing.T) {
}
}
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{
activeSegmentFilename(key),
filepath.Base(sm.sessionMetaPath(key)),
filepath.Base(sm.sessionIndexPath(key)),
} {
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(sm.sessionMetaPath(key)); err != nil {
t.Fatalf("remove meta: %v", err)
}
if err := os.Remove(sm.sessionIndexPath(key)); 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, archivedSegmentFilename(key, 1))); err != nil {
t.Fatalf("expected archived segment: %v", err)
}
if _, err := os.Stat(filepath.Join(storage, activeSegmentFilename(key))); 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(sm.sessionIndexPath(key), &index); err != nil {
t.Fatalf("read index: %v", err)
}
size, err := jsonlog.FileSize(filepath.Join(storage, activeSegmentFilename(key)))
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 != activeSegmentFilename(key) {
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(sm.sessionIndexPath(key)); 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())
}
}