13 "github.com/stretchr/testify/require"
16 func TestRebuildMergesDatesAndKeepsAgentFiles(t *testing.T) {
17 agent := newRebuildTestAgent(t)
18 old := []DatedMessage{
22 Date: "2025-03-02T12:00:00-03:00",
27 Date: "2025-03-03T12:00:00Z",
31 rebuildTestLog(t, agent.dir, old)
32 require.NoError(t, os.MkdirAll(filepath.Join(agent.dir, "subagents", "BlueBird"), 0o700))
34 files := map[string]string{
35 "instructions.md": "Keep exact answers.",
36 "schedules.json": "[]\n",
37 "telegram-offset.json": "123\n",
38 "work.json": "{\"running\":[]}\n",
39 "usage.jsonl": "{\"model\":\"old\"}\n",
40 "subagents/BlueBird/report": "Finished.",
43 for name, text := range files {
44 require.NoError(t, os.WriteFile(filepath.Join(agent.dir, name), []byte(text), 0o600))
47 before := rebuildTestSnapshot(t, agent.dir)
48 server := newModelFake(t)
49 client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(t.TempDir(), "usage.jsonl"))
50 var report strings.Builder
51 added := []DatedMessage{
54 Text: strings.Repeat("Earlier message. ", 80),
55 Date: "2025-03-01T10:00:00-03:00",
59 Text: "equal date comes after old",
64 backup, err := rebuild(t.Context(), agent, added, client, newCompactor(client, agent.Prompt, defaultConfig().Compactor), &report)
65 require.NoError(t, err)
66 require.Equal(t, before, rebuildTestSnapshot(t, backup))
68 entries, err := rebuildReadLog(t.Context(), agent.dir)
69 require.NoError(t, err)
70 require.Equal(t, []DatedMessage{added[0], old[0], added[1], old[1]}, entries)
72 for name, text := range files {
75 data, err = os.ReadFile(filepath.Join(agent.dir, name))
76 require.NoError(t, err)
78 if name == "usage.jsonl" {
79 require.True(t, strings.HasPrefix(string(data), text))
84 require.Equal(t, text, string(data))
87 require.Positive(t, server.Calls())
89 call := server.Call(0)
90 require.Equal(t, agent.CacheKey(), call.body["prompt_cache_key"])
92 prompt, err := agent.Prompt()
93 require.NoError(t, err)
94 require.Contains(t, string(call.raw), strings.Trim(string(rebuildTestJSON(t, prompt)), "\""))
95 require.Contains(t, report.String(), "cached (")
97 memory, err := openMemory(t.Context(), agent.dir, time.Now, newFakeSummarizer(), discardLog())
98 require.NoError(t, err)
99 require.NoError(t, memory.Settle(t.Context()))
100 require.NoError(t, memory.Close())
103 func TestRebuildFailureAndCancellationKeepOriginal(t *testing.T) {
104 testCases := []struct {
109 name: "failed call keeps the original",
112 name: "cancelled run keeps the original",
117 for _, tc := range testCases {
118 t.Run(tc.name, func(t *testing.T) {
119 agent := newRebuildTestAgent(t)
120 rebuildTestLog(t, agent.dir, []DatedMessage{{
123 Date: "2026-01-01T00:00:00Z",
126 before := rebuildTestSnapshot(t, agent.dir)
127 server := newModelFake(t)
128 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
129 w.WriteHeader(http.StatusBadRequest)
132 client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(t.TempDir(), "usage.jsonl"))
133 ctx, cancel := context.WithCancel(t.Context())
140 backup, err := rebuild(ctx, agent, []DatedMessage{{
142 Text: strings.Repeat("long ", 200),
143 Date: "2025-01-01T00:00:00Z",
144 }}, client, newCompactor(client, agent.Prompt, defaultConfig().Compactor), &strings.Builder{})
145 require.Error(t, err)
146 require.Empty(t, backup)
147 require.Equal(t, before, rebuildTestSnapshot(t, agent.dir))
149 pending, err := filepath.Glob(filepath.Join(filepath.Dir(agent.dir), ".agent-rebuild-*"))
150 require.NoError(t, err)
151 require.Empty(t, pending)
156 func TestRebuildKeepsOriginalWhenAnInvisibleMergeFails(t *testing.T) {
157 agent := newRebuildTestAgent(t)
158 old := []DatedMessage{
161 Text: strings.Repeat("a", 300),
162 Date: "2025-01-01T00:00:00Z",
166 Text: strings.Repeat("b", 300),
167 Date: "2025-01-01T00:01:00Z",
171 rebuildTestLog(t, agent.dir, old)
172 before := rebuildTestSnapshot(t, agent.dir)
173 server := newModelFake(t)
174 server.reply = func(w http.ResponseWriter, _ ModelFakeCall) {
175 modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5))
178 client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(t.TempDir(), "usage.jsonl"))
179 _, err := rebuild(t.Context(), agent, nil, client, newCompactor(client, agent.Prompt, defaultConfig().Compactor), &strings.Builder{})
180 require.ErrorContains(t, err, "summary tree has unfinished nodes")
181 require.Equal(t, before, rebuildTestSnapshot(t, agent.dir))
184 func TestRebuildRefusesUnfinishedSubagents(t *testing.T) {
185 agent := newRebuildTestAgent(t)
186 require.NoError(t, os.WriteFile(filepath.Join(agent.dir, "work.json"), []byte(`{"running":["BlueBird"]}`), 0o600))
188 require.ErrorContains(t, rebuildIdle(agent.dir), "unfinished work")
191 func TestRebuildReadDoesNotRepairIncompleteLog(t *testing.T) {
192 agent := newRebuildTestAgent(t)
193 require.NoError(t, os.Mkdir(filepath.Join(agent.dir, "main"), 0o700))
195 path := filepath.Join(agent.dir, "main", "2026-01-01.jsonl")
196 require.NoError(t, os.WriteFile(path, []byte("incomplete"), 0o600))
198 _, err := rebuildReadLog(t.Context(), agent.dir)
199 require.ErrorContains(t, err, "incomplete line")
201 data, err := os.ReadFile(path)
202 require.NoError(t, err)
203 require.Equal(t, "incomplete", string(data))
206 func TestRebuildReadStopsWhenTheContextEnds(t *testing.T) {
207 agent := newRebuildTestAgent(t)
208 rebuildTestLog(t, agent.dir, []DatedMessage{{
211 Date: "2026-01-01T00:00:00Z",
214 ctx, cancel := context.WithCancel(t.Context())
219 _, err := rebuildReadLog(ctx, agent.dir)
220 require.ErrorIs(t, err, context.Canceled)
223 func rebuildTestJSON(t *testing.T, value string) []byte {
226 data, err := json.Marshal(value)
227 require.NoError(t, err)