package main import ( "context" "encoding/json" "net/http" "os" "path/filepath" "strings" "testing" "time" "github.com/stretchr/testify/require" ) func TestRebuildMergesDatesAndKeepsAgentFiles(t *testing.T) { agent := newRebuildTestAgent(t) old := []DatedMessage{ { Kind: "note", Text: "middle", Date: "2025-03-02T12:00:00-03:00", }, { Kind: "user", Text: "last", Date: "2025-03-03T12:00:00Z", }, } rebuildTestLog(t, agent.dir, old) require.NoError(t, os.MkdirAll(filepath.Join(agent.dir, "subagents", "BlueBird"), 0o700)) files := map[string]string{ "instructions.md": "Keep exact answers.", "schedules.json": "[]\n", "telegram-offset.json": "123\n", "work.json": "{\"running\":[]}\n", "usage.jsonl": "{\"model\":\"old\"}\n", "subagents/BlueBird/report": "Finished.", } for name, text := range files { require.NoError(t, os.WriteFile(filepath.Join(agent.dir, name), []byte(text), 0o600)) } before := rebuildTestSnapshot(t, agent.dir) server := newModelFake(t) client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(t.TempDir(), "usage.jsonl")) var report strings.Builder added := []DatedMessage{ { Kind: "work", Text: strings.Repeat("Earlier message. ", 80), Date: "2025-03-01T10:00:00-03:00", }, { Kind: "note", Text: "equal date comes after old", Date: old[0].Date, }, } backup, err := rebuild(t.Context(), agent, added, client, newCompactor(client, agent.Prompt, defaultConfig().Compactor, agent.Tools()), &report) require.NoError(t, err) require.Equal(t, before, rebuildTestSnapshot(t, backup)) entries, err := rebuildReadLog(t.Context(), agent.dir) require.NoError(t, err) require.Equal(t, []DatedMessage{added[0], old[0], added[1], old[1]}, entries) for name, text := range files { var data []byte data, err = os.ReadFile(filepath.Join(agent.dir, name)) require.NoError(t, err) if name == "usage.jsonl" { require.True(t, strings.HasPrefix(string(data), text)) continue } require.Equal(t, text, string(data)) } require.Positive(t, server.Calls()) call := server.Call(0) require.Equal(t, agent.CacheKey(), call.body["prompt_cache_key"]) prompt, err := agent.Prompt() require.NoError(t, err) require.Contains(t, string(call.raw), strings.Trim(string(rebuildTestJSON(t, prompt)), "\"")) require.Contains(t, report.String(), "cached (") memory, err := openMemory(t.Context(), agent.dir, time.Now, newFakeSummarizer(), discardLog()) require.NoError(t, err) require.NoError(t, memory.Settle(t.Context())) require.NoError(t, memory.Close()) } func TestRebuildFailureAndCancellationKeepOriginal(t *testing.T) { testCases := []struct { name string cancel bool }{ { name: "failed call keeps the original", }, { name: "cancelled run keeps the original", cancel: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { agent := newRebuildTestAgent(t) rebuildTestLog(t, agent.dir, []DatedMessage{{ Kind: "user", Text: "Keep this.", Date: "2026-01-01T00:00:00Z", }}) before := rebuildTestSnapshot(t, agent.dir) server := newModelFake(t) server.reply = func(w http.ResponseWriter, _ ModelFakeCall) { w.WriteHeader(http.StatusBadRequest) } client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(t.TempDir(), "usage.jsonl")) ctx, cancel := context.WithCancel(t.Context()) defer cancel() if tc.cancel { cancel() } backup, err := rebuild(ctx, agent, []DatedMessage{{ Kind: "work", Text: strings.Repeat("long ", 200), Date: "2025-01-01T00:00:00Z", }}, client, newCompactor(client, agent.Prompt, defaultConfig().Compactor, agent.Tools()), &strings.Builder{}) require.Error(t, err) require.Empty(t, backup) require.Equal(t, before, rebuildTestSnapshot(t, agent.dir)) pending, err := filepath.Glob(filepath.Join(filepath.Dir(agent.dir), ".agent-rebuild-*")) require.NoError(t, err) require.Empty(t, pending) }) } } func TestRebuildKeepsOriginalWhenAnInvisibleMergeFails(t *testing.T) { agent := newRebuildTestAgent(t) old := []DatedMessage{ { Kind: "user", Text: strings.Repeat("a", 300), Date: "2025-01-01T00:00:00Z", }, { Kind: "reply", Text: strings.Repeat("b", 300), Date: "2025-01-01T00:01:00Z", }, } rebuildTestLog(t, agent.dir, old) before := rebuildTestSnapshot(t, agent.dir) server := newModelFake(t) server.reply = func(w http.ResponseWriter, _ ModelFakeCall) { modelWriteEvents(w, modelCompletedEvent([]any{modelMessageItem("")}, 100, 0, 5)) } client := newModelClient(server.URL, ModelAPIKey(modelTestKey), agent.CacheKey(), filepath.Join(t.TempDir(), "usage.jsonl")) _, err := rebuild(t.Context(), agent, nil, client, newCompactor(client, agent.Prompt, defaultConfig().Compactor, agent.Tools()), &strings.Builder{}) require.ErrorContains(t, err, "summary tree has unfinished nodes") require.Equal(t, before, rebuildTestSnapshot(t, agent.dir)) } func TestRebuildRefusesUnfinishedSubagents(t *testing.T) { agent := newRebuildTestAgent(t) require.NoError(t, os.WriteFile(filepath.Join(agent.dir, "work.json"), []byte(`{"running":["BlueBird"]}`), 0o600)) require.ErrorContains(t, rebuildIdle(agent.dir), "unfinished work") } func TestRebuildReadDoesNotRepairIncompleteLog(t *testing.T) { agent := newRebuildTestAgent(t) require.NoError(t, os.Mkdir(filepath.Join(agent.dir, "main"), 0o700)) path := filepath.Join(agent.dir, "main", "2026-01-01.jsonl") require.NoError(t, os.WriteFile(path, []byte("incomplete"), 0o600)) _, err := rebuildReadLog(t.Context(), agent.dir) require.ErrorContains(t, err, "incomplete line") data, err := os.ReadFile(path) require.NoError(t, err) require.Equal(t, "incomplete", string(data)) } func TestRebuildReadStopsWhenTheContextEnds(t *testing.T) { agent := newRebuildTestAgent(t) rebuildTestLog(t, agent.dir, []DatedMessage{{ Kind: "note", Text: "x", Date: "2026-01-01T00:00:00Z", }}) ctx, cancel := context.WithCancel(t.Context()) defer cancel() cancel() _, err := rebuildReadLog(ctx, agent.dir) require.ErrorIs(t, err, context.Canceled) } func rebuildTestJSON(t *testing.T, value string) []byte { t.Helper() data, err := json.Marshal(value) require.NoError(t, err) return data }