first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
package tui
import (
"testing"
"time"
"github.com/smallnest/pigo/internal/agentcore"
"github.com/smallnest/pigo/internal/session"
)
// newTestStore opens a session store rooted at a temp dir so persistence/resume
// can be exercised without touching ~/.pigo.
func newTestStore(t *testing.T) *session.Store {
t.Helper()
store, err := session.NewStore(t.TempDir())
if err != nil {
t.Fatalf("NewStore: %v", err)
}
return store
}
// saveSession writes a linear session with the given messages and returns its id.
func saveSession(t *testing.T, store *session.Store, msgs agentcore.MessageList) string {
t.Helper()
now := time.Now().UTC()
header := session.SessionHeader{
ID: session.NewID(now),
CreatedAt: now,
UpdatedAt: now,
Model: "test-model",
Provider: "test-provider",
}
if err := store.Save(header, msgs); err != nil {
t.Fatalf("Save: %v", err)
}
return header.ID
}
// TestResumeSeedsTranscript constructs a session with a few messages, resumes it
// through newRunSessionWithStore, seeds a transcript with the returned history,
// and asserts the initial transcript blocks carry those messages (FR-16 resume).
func TestResumeSeedsTranscript(t *testing.T) {
store := newTestStore(t)
id := saveSession(t, store, agentcore.MessageList{
agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("hello, world")}},
agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, Content: agentcore.ContentList{agentcore.NewTextContent("hello back")}},
agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("second question")}},
})
s, history, err := newRunSessionWithStore(store, Options{ResumeID: id})
if err != nil {
t.Fatalf("newRunSessionWithStore: %v", err)
}
if len(history) != 3 {
t.Fatalf("history len = %d, want 3", len(history))
}
// The persisted cursor must cover the full resumed history so the first new
// turn appends only fresh messages, not a re-save of history.
if s.persisted != 3 {
t.Errorf("persisted = %d, want 3", s.persisted)
}
if s.curLeaf == "" {
t.Error("curLeaf should be the resumed leaf, got empty")
}
tr := newTranscript(DefaultTheme())
seedTranscript(&tr, history)
wantTexts := []string{"hello, world", "hello back", "second question"}
if len(tr.blocks) != len(wantTexts) {
t.Fatalf("transcript blocks = %d, want %d", len(tr.blocks), len(wantTexts))
}
for i, want := range wantTexts {
if tr.blocks[i].text != want {
t.Errorf("block[%d] = %q, want %q", i, tr.blocks[i].text, want)
}
}
}
// TestBuildConfigAssembly asserts the run-config assembly maps the live config
// onto RunConfig without a live provider: the model/provider/thinking/window
// fields flow through, compaction is enabled, and the tool registry is wired.
func TestBuildConfigAssembly(t *testing.T) {
store := newTestStore(t)
s, _, err := newRunSessionWithStore(store, Options{
Model: "opus-test",
ProviderName: "anthropic",
ThinkingLevel: agentcore.ThinkingLevel("high"),
})
if err != nil {
t.Fatalf("newRunSessionWithStore: %v", err)
}
cfg := s.buildConfig()
if cfg.Model != "opus-test" {
t.Errorf("cfg.Model = %q, want opus-test", cfg.Model)
}
if cfg.Provider != "anthropic" {
t.Errorf("cfg.Provider = %q, want anthropic", cfg.Provider)
}
if cfg.ThinkingLevel != agentcore.ThinkingLevel("high") {
t.Errorf("cfg.ThinkingLevel = %q, want high", cfg.ThinkingLevel)
}
if cfg.ContextWindow <= 0 {
t.Errorf("cfg.ContextWindow = %d, want a positive default", cfg.ContextWindow)
}
if !cfg.Compaction.Enabled {
t.Error("cfg.Compaction.Enabled = false, want true (DefaultCompactionSettings)")
}
if cfg.Batch.Registry == nil {
t.Error("cfg.Batch.Registry is nil, want the assembled tool registry")
}
if cfg.Stream == nil {
t.Error("cfg.Stream is nil, want a stream fn derived from the provider")
}
}
// TestFreshSessionPersists starts a fresh session, appends a turn to the context,
// persists it, and confirms it round-trips back through the store (FR-16 persist).
func TestFreshSessionPersists(t *testing.T) {
store := newTestStore(t)
s, history, err := newRunSessionWithStore(store, Options{Model: "m", ProviderName: "p"})
if err != nil {
t.Fatalf("newRunSessionWithStore: %v", err)
}
if history != nil {
t.Fatalf("fresh session history = %v, want nil", history)
}
s.agentCtx.Messages = append(s.agentCtx.Messages,
agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("hi")}},
agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, Content: agentcore.ContentList{agentcore.NewTextContent("yo")}},
)
if err := s.persist(); err != nil {
t.Fatalf("persist: %v", err)
}
if s.persisted != 2 {
t.Errorf("persisted = %d, want 2", s.persisted)
}
_, msgs, err := store.Load(s.header.ID)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(msgs) != 2 {
t.Fatalf("persisted messages = %d, want 2", len(msgs))
}
// A second persist with no new messages is a no-op.
before := s.curLeaf
if err := s.persist(); err != nil {
t.Fatalf("persist (no-op): %v", err)
}
if s.curLeaf != before {
t.Errorf("curLeaf changed on no-op persist: %q -> %q", before, s.curLeaf)
}
}
// TestPersistAfterCompaction reproduces the crash where an automatic compaction
// shrinks agentCtx.Messages below the persisted cursor: an incremental
// Messages[persisted:] would panic with a slice-bounds error. persist() must
// instead re-save the flattened context and reset the cursor to the new length.
func TestPersistAfterCompaction(t *testing.T) {
store := newTestStore(t)
s, _, err := newRunSessionWithStore(store, Options{Model: "m", ProviderName: "p"})
if err != nil {
t.Fatalf("newRunSessionWithStore: %v", err)
}
// Persist a few turns so the cursor advances past what compaction will keep.
for i := 0; i < 4; i++ {
s.agentCtx.Messages = append(s.agentCtx.Messages,
agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("q")}},
agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, Content: agentcore.ContentList{agentcore.NewTextContent("a")}},
)
}
if err := s.persist(); err != nil {
t.Fatalf("persist: %v", err)
}
if s.persisted != 8 {
t.Fatalf("persisted = %d, want 8 before compaction", s.persisted)
}
// Simulate the run loop compacting: Messages is rewritten to a shorter
// summary + tail (here just a 2-message tail), and the loop signalled it via
// compactionMsg (which sets s.compacted).
s.agentCtx.Messages = agentcore.MessageList{
agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("recent q")}},
agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, Content: agentcore.ContentList{agentcore.NewTextContent("recent a")}},
}
s.compacted = true
if err := s.persist(); err != nil {
t.Fatalf("persist after compaction: %v", err)
}
if s.compacted {
t.Error("compacted flag should be cleared after persist")
}
if s.persisted != 2 {
t.Errorf("persisted = %d, want 2 (the compacted length)", s.persisted)
}
_, msgs, err := store.Load(s.header.ID)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(msgs) != 2 {
t.Fatalf("persisted messages = %d, want 2 (flattened compacted context)", len(msgs))
}
}