package session import ( "os" "path/filepath" "strings" "testing" "agent-native/runtime/llm" ) func appendMsgs(t *testing.T, s *Store, msgs ...llm.Message) { t.Helper() for _, m := range msgs { if err := s.Append(m); err != nil { t.Fatal(err) } } } func TestLoadSkipsCrashResidueTail(t *testing.T) { dir := t.TempDir() s, err := Open(filepath.Join(dir, "t.jsonl")) if err != nil { t.Fatal(err) } appendMsgs(t, s, llm.Message{Role: llm.RoleSystem, Content: "sys"}, llm.Message{Role: llm.RoleUser, Content: "hi"}, ) // 模拟崩溃半行:无换行结尾的残缺 JSON。 f, _ := os.OpenFile(s.Path(), os.O_APPEND|os.O_WRONLY, 0o644) _, _ = f.WriteString(`{"role":"assistant","content":"trunca`) _ = f.Close() msgs, err := s.Load() if err != nil { t.Fatalf("crash residue must not poison the session: %v", err) } if len(msgs) != 2 || msgs[0].Role != llm.RoleSystem || msgs[1].Content != "hi" { t.Fatalf("want sys+hi, got %+v", msgs) } } func TestLoadStillFailsOnMidFileCorruption(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "t.jsonl") if err := os.WriteFile(path, []byte("{\"bad json}\n{\"role\":\"user\",\"content\":\"ok\"}\n"), 0o644); err != nil { t.Fatal(err) } s := &Store{path: path} if _, err := s.Load(); err == nil { t.Fatal("mid-file corruption must still error") } } func TestStatsAndMeta(t *testing.T) { dir := t.TempDir() s, _ := Open(filepath.Join(dir, "t.jsonl")) appendMsgs(t, s, llm.Message{Role: llm.RoleSystem, Content: "sys"}, llm.Message{Role: llm.RoleUser, Content: "q"}, ) st := s.Stats() if st.Lines != 2 || st.Bytes <= 0 { t.Fatalf("stats wrong: %+v", st) } if err := s.SaveMeta(map[string]string{"model": "m1"}); err != nil { t.Fatal(err) } meta := s.ReadMeta() if meta == nil || meta["model"] != "m1" { t.Fatalf("meta roundtrip failed: %v", meta) } if other := (&Store{path: filepath.Join(dir, "missing.jsonl")}).ReadMeta(); other != nil { t.Fatalf("missing meta must return nil, got %v", other) } } func TestCompactRewritesTranscript(t *testing.T) { dir := t.TempDir() s, _ := Open(filepath.Join(dir, "t.jsonl")) appendMsgs(t, s, llm.Message{Role: llm.RoleSystem, Content: "sys"}) for i := 0; i < 10; i++ { appendMsgs(t, s, llm.Message{Role: llm.RoleUser, Content: "early" + string(rune('a'+i))}, llm.Message{Role: llm.RoleAssistant, Content: "reply"}, ) } var seenHead []llm.Message done, err := s.Compact(3, func(messages []llm.Message) (string, error) { seenHead = messages return "这是摘要", nil }) if err != nil || !done { t.Fatalf("compact failed: %v done=%v", err, done) } if len(seenHead) != 17 { // 20 - system - 3 tail t.Fatalf("summarizer should receive 17 head messages, got %d", len(seenHead)) } msgs, err := s.Load() if err != nil { t.Fatal(err) } if len(msgs) != 5 { // system + summary + 3 tail t.Fatalf("want 5 messages after compact, got %d", len(msgs)) } if msgs[0].Role != llm.RoleSystem { t.Fatal("system message must be preserved") } if !strings.Contains(msgs[1].Content, "[conversation summary]") || !strings.Contains(msgs[1].Content, "这是摘要") { t.Fatalf("summary message wrong: %+v", msgs[1]) } if msgs[2].Content != "reply" || msgs[3].Content != "earlyj" || msgs[4].Content != "reply" { t.Fatalf("tail order wrong: %+v", msgs[2:]) } entries, err := s.Entries() if err != nil { t.Fatal(err) } if len(entries) <= 5 { t.Fatalf("compaction must append, not rewrite; got %d entries", len(entries)) } } func TestCompactNoopWhenShort(t *testing.T) { dir := t.TempDir() s, _ := Open(filepath.Join(dir, "t.jsonl")) appendMsgs(t, s, llm.Message{Role: llm.RoleSystem, Content: "sys"}, llm.Message{Role: llm.RoleUser, Content: "q"}, ) done, err := s.Compact(8, func([]llm.Message) (string, error) { return "x", nil }) if err != nil || done { t.Fatalf("short transcript must be a no-op: %v %v", done, err) } msgs, _ := s.Load() if len(msgs) != 2 { t.Fatal("no-op compact must not touch the file") } } func TestCompactSummarizerErrorAborts(t *testing.T) { dir := t.TempDir() s, _ := Open(filepath.Join(dir, "t.jsonl")) appendMsgs(t, s, llm.Message{Role: llm.RoleSystem, Content: "sys"}) for i := 0; i < 6; i++ { appendMsgs(t, s, llm.Message{Role: llm.RoleUser, Content: "m"}) } done, err := s.Compact(2, func([]llm.Message) (string, error) { return "", errStringT("boom") }) if err == nil || done { t.Fatal("summarizer error must abort compaction") } msgs, _ := s.Load() if len(msgs) != 7 { t.Fatal("failed compaction must leave the transcript untouched") } } type errStringT string func (e errStringT) Error() string { return string(e) }