using ClaudeDo.Worker.Report; namespace ClaudeDo.Worker.Tests.Report; public class ClaudeHistoryReaderTests : IDisposable { private readonly string _root; public ClaudeHistoryReaderTests() { _root = Path.Combine(Path.GetTempPath(), $"cdh_{Guid.NewGuid():N}"); Directory.CreateDirectory(_root); } public void Dispose() { try { Directory.Delete(_root, true); } catch { } } private void WriteSession(string projectDir, string file, params string[] lines) { var dir = Path.Combine(_root, projectDir); Directory.CreateDirectory(dir); File.WriteAllLines(Path.Combine(dir, file), lines); } private static string UserLine(string cwd, string ts, string text) => $$$"""{"type":"user","cwd":{{{Json(cwd)}}},"timestamp":"{{{ts}}}","message":{"role":"user","content":[{"type":"text","text":{{{Json(text)}}}}]}}"""; private static string AssistantLine(string cwd, string ts, string text) => $$$"""{"type":"assistant","cwd":{{{Json(cwd)}}},"timestamp":"{{{ts}}}","message":{"role":"assistant","content":[{"type":"text","text":{{{Json(text)}}}}]}}"""; private static string Json(string s) => System.Text.Json.JsonSerializer.Serialize(s); [Fact] public async Task SessionSpanningTwoRepos_KeepsLastSummaryPerRepo() { WriteSession("proj", "s.jsonl", AssistantLine(@"C:\Dev\Repos\A", "2026-06-01T08:00:00Z", "summary A"), AssistantLine(@"C:\Dev\Repos\B", "2026-06-01T09:00:00Z", "summary B")); var reader = new ClaudeHistoryReader(_root); var result = await reader.ReadAsync(new DateOnly(2026, 6, 1), new DateOnly(2026, 6, 3), Array.Empty()); Assert.Equal(2, result.Count); Assert.Equal(new[] { "summary A" }, result.Single(r => r.RepoPath.EndsWith("A")).Days.Single().Summaries); Assert.Equal(new[] { "summary B" }, result.Single(r => r.RepoPath.EndsWith("B")).Days.Single().Summaries); } [Fact] public async Task Extracts_Prompts_And_Last_Assistant_Summary_GroupedByRepoAndDay() { WriteSession("proj", "s1.jsonl", UserLine(@"C:\Dev\Repos\App", "2026-06-01T08:00:00Z", "Add login"), AssistantLine(@"C:\Dev\Repos\App", "2026-06-01T08:05:00Z", "first summary"), AssistantLine(@"C:\Dev\Repos\App", "2026-06-01T08:30:00Z", "final summary")); var reader = new ClaudeHistoryReader(_root); var result = await reader.ReadAsync(new DateOnly(2026, 6, 1), new DateOnly(2026, 6, 3), Array.Empty()); var repo = Assert.Single(result); Assert.Equal(@"C:\Dev\Repos\App", repo.RepoPath); var day = Assert.Single(repo.Days); Assert.Equal(new[] { "Add login" }, day.Prompts); Assert.Equal(new[] { "final summary" }, day.Summaries); } [Fact] public async Task Drops_Sessions_Under_Excluded_Prefix_CaseInsensitive() { WriteSession("priv", "s.jsonl", UserLine(@"C:\Private\Secret", "2026-06-01T08:00:00Z", "private work")); var reader = new ClaudeHistoryReader(_root); var result = await reader.ReadAsync(new DateOnly(2026, 6, 1), new DateOnly(2026, 6, 3), new[] { @"c:\private" }); Assert.Empty(result); } [Fact] public async Task Filters_By_Date_Window_And_Skips_Noise_And_Malformed() { WriteSession("proj", "s.jsonl", "this is not json", UserLine(@"C:\Dev\App", "2026-05-01T08:00:00Z", "too old"), UserLine(@"C:\Dev\App", "2026-06-02T08:00:00Z", "in range"), UserLine(@"C:\Dev\App", "2026-06-02T09:00:00Z", "noise blah")); var reader = new ClaudeHistoryReader(_root); var result = await reader.ReadAsync(new DateOnly(2026, 6, 1), new DateOnly(2026, 6, 3), Array.Empty()); var day = Assert.Single(Assert.Single(result).Days); Assert.Equal(new[] { "in range" }, day.Prompts); } }