package runtime // Tests for checkpoint persistence (#480): the write→load round-trip, the // missing-file sentinel, and the summarize→Checkpoint bridge. They drive the // real filesystem via t.TempDir(), matching the session/memory test style. import ( "context" "errors" "os" "path/filepath" "strings" "testing" "time" "github.com/smallnest/pigo/internal/agentcore" ) // TestCheckpointRoundTrip is the core acceptance check: a written checkpoint // loads back with watermark, summary, createdAt, and covered count preserved. func TestCheckpointRoundTrip(t *testing.T) { root := t.TempDir() created := time.Date(2026, 8, 1, 9, 30, 0, 0, time.UTC) cp := Checkpoint{ Watermark: 42, Summary: "The user asked to refactor foo().\nWe extracted a helper and added tests.", CreatedAt: created, CoveredMessages: 42, } if err := WriteCheckpoint("sess-abc", root, cp); err != nil { t.Fatalf("WriteCheckpoint: %v", err) } got, ok, err := LoadCheckpoint("sess-abc", root) if err != nil { t.Fatalf("LoadCheckpoint: %v", err) } if !ok { t.Fatal("LoadCheckpoint: found = false, want true") } if got.Watermark != cp.Watermark { t.Errorf("Watermark = %d, want %d", got.Watermark, cp.Watermark) } if got.CoveredMessages != cp.CoveredMessages { t.Errorf("CoveredMessages = %d, want %d", got.CoveredMessages, cp.CoveredMessages) } if got.Summary != cp.Summary { t.Errorf("Summary = %q, want %q", got.Summary, cp.Summary) } if !got.CreatedAt.Equal(cp.CreatedAt) { t.Errorf("CreatedAt = %v, want %v", got.CreatedAt, cp.CreatedAt) } } // TestCheckpointFileLayoutAndFrontmatter verifies the file lands at the expected // path and carries the repo's name/description/metadata.type=checkpoint convention. func TestCheckpointFileLayoutAndFrontmatter(t *testing.T) { root := t.TempDir() cp := Checkpoint{Watermark: 3, Summary: "body text", CreatedAt: time.Now().UTC(), CoveredMessages: 3} if err := WriteCheckpoint("s1", root, cp); err != nil { t.Fatalf("WriteCheckpoint: %v", err) } want := filepath.Join(root, "sessions", "s1", "checkpoint.md") if want != CheckpointPath("s1", root) { t.Fatalf("CheckpointPath = %q, want %q", CheckpointPath("s1", root), want) } content, err := os.ReadFile(want) if err != nil { t.Fatalf("read checkpoint file: %v", err) } fm, _, err := splitFrontmatter(content) if err != nil { t.Fatalf("splitFrontmatter: %v", err) } s := string(fm) for _, needle := range []string{"name: checkpoint", "type: checkpoint", "watermark: 3"} { if !strings.Contains(s, needle) { t.Errorf("frontmatter missing %q; got:\n%s", needle, s) } } } // TestLoadCheckpointMissing verifies a missing checkpoint is the (nil,false,nil) // sentinel, not an error — callers treat "no checkpoint yet" as normal. func TestLoadCheckpointMissing(t *testing.T) { root := t.TempDir() cp, ok, err := LoadCheckpoint("nope", root) if err != nil { t.Fatalf("LoadCheckpoint on missing file: err = %v, want nil", err) } if ok { t.Error("found = true, want false") } if cp != nil { t.Errorf("checkpoint = %+v, want nil", cp) } } // TestBuildCheckpoint verifies the summarize bridge tags the result with the // watermark, covered count, and a UTC createdAt. func TestBuildCheckpoint(t *testing.T) { msgs := []agentcore.Message{ agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent("hi")}}, agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, Content: agentcore.ContentList{agentcore.NewTextContent("hello")}}, } now := time.Date(2026, 8, 1, 10, 0, 0, 0, time.FixedZone("x", 3600)) summarize := func(ctx context.Context, m []agentcore.Message) (string, error) { if len(m) != len(msgs) { t.Errorf("summarize got %d msgs, want %d", len(m), len(msgs)) } return "distilled", nil } cp, err := BuildCheckpoint(context.Background(), msgs, 2, now, summarize) if err != nil { t.Fatalf("BuildCheckpoint: %v", err) } if cp.Summary != "distilled" { t.Errorf("Summary = %q, want %q", cp.Summary, "distilled") } if cp.Watermark != 2 || cp.CoveredMessages != 2 { t.Errorf("Watermark/Covered = %d/%d, want 2/2", cp.Watermark, cp.CoveredMessages) } if cp.CreatedAt.Location() != time.UTC { t.Errorf("CreatedAt not UTC: %v", cp.CreatedAt) } } // TestBuildCheckpointPropagatesError verifies a summarization failure surfaces // as an error rather than a partial checkpoint (write path never runs). func TestBuildCheckpointPropagatesError(t *testing.T) { boom := errors.New("summarize failed") _, err := BuildCheckpoint(context.Background(), nil, 0, time.Now(), func(context.Context, []agentcore.Message) (string, error) { return "", boom }) if !errors.Is(err, boom) { t.Fatalf("err = %v, want wrapping %v", err, boom) } }