返回 DeepSeek-Reasonix
options_test.go
根目录 / internal / checkpoint / options_test.go
1 package checkpoint
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "reasonix/internal/diff"
11 )
12
13 func TestOptionsOverrideRetentionDefaults(t *testing.T) {
14 s := New("", t.TempDir(), WithRetainCheckpoints(7), WithBlobQuota(1<<20))
15 if s.retainN != 7 {
16 t.Fatalf("retainN = %d, want 7", s.retainN)
17 }
18 if s.blobQuota != 1<<20 {
19 t.Fatalf("blobQuota = %d, want %d", s.blobQuota, int64(1<<20))
20 }
21 }
22
23 func TestOptionsIgnoreNonPositiveValues(t *testing.T) {
24 // Zero means "unset" in TOML, so it must not clobber the default with 0 and
25 // silently disable retention.
26 s := New("", t.TempDir(), WithRetainCheckpoints(0), WithBlobQuota(-1))
27 if s.retainN != DefaultRetainCheckpoints {
28 t.Fatalf("retainN = %d, want default %d", s.retainN, DefaultRetainCheckpoints)
29 }
30 if s.blobQuota != DefaultBlobQuotaBytes {
31 t.Fatalf("blobQuota = %d, want default %d", s.blobQuota, int64(DefaultBlobQuotaBytes))
32 }
33 }
34
35 func TestNewIgnoresNilOption(t *testing.T) {
36 s := New("", t.TempDir(), nil, WithRetainCheckpoints(3))
37 if s.retainN != 3 {
38 t.Fatalf("retainN = %d, want 3", s.retainN)
39 }
40 }
41
42 // seedTurns writes one edited file per turn and captures its preimage, so each
43 // turn ends up with a restorable payload on disk.
44 func seedTurns(t *testing.T, s *Store, root string, turns int, body string) {
45 t.Helper()
46 for i := range turns {
47 path := filepath.Join(root, fmt.Sprintf("f%d.txt", i))
48 original := fmt.Sprintf("turno %d\n%s", i, body)
49 if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
50 t.Fatal(err)
51 }
52 s.Begin(i, fmt.Sprintf("edit %d", i), 0)
53 s.Snapshot(diff.Change{Path: path, Kind: diff.Modify, OldText: original})
54 }
55 }
56
57 // checkpointsWithPayloads counts checkpoints still holding restorable content,
58 // which is what the retention GC trims.
59 func checkpointsWithPayloads(s *Store) int {
60 n := 0
61 for _, c := range s.all() {
62 if len(c.Files) > 0 || c.SchemaVersion >= SchemaV3 {
63 n++
64 }
65 }
66 return n
67 }
68
69 // The ordering guarantee: the startup GC inside New reads retainN (via
70 // pruneV3TurnsLocked), so a configured retention has to be applied before that
71 // prune. Otherwise reopening a session would trim using DefaultRetainCheckpoints
72 // and discard payloads the operator asked to keep.
73 func TestConfiguredRetentionAppliesToStartupGC(t *testing.T) {
74 root := t.TempDir()
75 dir := filepath.Join(t.TempDir(), "sess.ckpt")
76
77 const turns = 6
78 const retain = 2
79
80 s := New(dir, root)
81 seedTurns(t, s, root, turns, "")
82 if got := checkpointsWithPayloads(s); got != turns {
83 t.Fatalf("before reopen: %d checkpoints with payloads, want %d", got, turns)
84 }
85
86 reopened := New(dir, root, WithRetainCheckpoints(retain))
87 if reopened.retainN != retain {
88 t.Fatalf("retainN = %d, want %d", reopened.retainN, retain)
89 }
90 if got := checkpointsWithPayloads(reopened); got > retain {
91 t.Fatalf("startup GC kept %d checkpoints with payloads, want at most %d", got, retain)
92 }
93 }
94
95 // WithBlobQuota has to reach the prune decision, not just the struct field. The
96 // retain count here is deliberately generous so the turn-count path cannot
97 // trigger a prune: only the byte budget can.
98 func TestConfiguredBlobQuotaPrunesWithinRetainCount(t *testing.T) {
99 root := t.TempDir()
100 dir := filepath.Join(t.TempDir(), "sess.ckpt")
101
102 const turns = 5
103 body := strings.Repeat("x", 4096)
104
105 s := New(dir, root)
106 seedTurns(t, s, root, turns, body)
107 if got := checkpointsWithPayloads(s); got != turns {
108 t.Fatalf("before reopen: %d checkpoints with payloads, want %d", got, turns)
109 }
110
111 reopened := New(dir, root, WithRetainCheckpoints(turns+10), WithBlobQuota(1024))
112 if got := checkpointsWithPayloads(reopened); got >= turns {
113 t.Fatalf("byte-budget GC kept %d checkpoints with payloads, want fewer than %d", got, turns)
114 }
115 }
116
116 lines GO