using ClaudeDo.Data.Models; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Modals; namespace ClaudeDo.Ui.Tests.ViewModels; public class LogVisualizerViewModelTests { private sealed class FakeClient : StubWorkerClient { private readonly IReadOnlyList _logs; public FakeClient(IReadOnlyList logs) => _logs = logs; public override Task> GetRecentLogsAsync() => Task.FromResult(_logs); } private static WorkerLogEntry E(WorkerLogLevel lvl, string msg) => new(msg, lvl, new DateTime(2026, 6, 23, 8, 0, 0, DateTimeKind.Utc)); private static WorkerLogEntry E(WorkerLogLevel lvl, string msg, DateTime timestampUtc) => new(msg, lvl, timestampUtc); [Fact] public async Task Refresh_populates_rows_from_worker_newest_first() { var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Info, "a"), E(WorkerLogLevel.Error, "b") })); await vm.RefreshAsync(); Assert.Equal(new[] { "b", "a" }, vm.Rows.Select(r => r.Message)); } [Fact] public async Task WarnErrorOnly_filters_out_info() { var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Info, "a"), E(WorkerLogLevel.Warn, "w"), E(WorkerLogLevel.Error, "e") })); await vm.RefreshAsync(); vm.WarnErrorOnly = true; Assert.Equal(new[] { "e", "w" }, vm.Rows.Select(r => r.Message)); } [Fact] public async Task Empty_logs_yield_no_rows_and_a_status() { var vm = new LogVisualizerViewModel(new FakeClient(Array.Empty())); await vm.RefreshAsync(); Assert.Empty(vm.Rows); Assert.False(string.IsNullOrEmpty(vm.StatusText)); } [Fact] public async Task Refresh_orders_newest_entry_first() { var t0 = new DateTime(2026, 6, 23, 8, 0, 0, DateTimeKind.Utc); var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Info, "oldest", t0), E(WorkerLogLevel.Info, "middle", t0.AddMinutes(1)), E(WorkerLogLevel.Info, "newest", t0.AddMinutes(2)), })); await vm.RefreshAsync(); Assert.Equal(new[] { "newest", "middle", "oldest" }, vm.Rows.Select(r => r.Message)); } [Fact] public async Task Refresh_orders_newest_entry_first_with_warn_error_only() { var t0 = new DateTime(2026, 6, 23, 8, 0, 0, DateTimeKind.Utc); var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Warn, "oldest", t0), E(WorkerLogLevel.Info, "skipped", t0.AddMinutes(1)), E(WorkerLogLevel.Error, "middle", t0.AddMinutes(2)), E(WorkerLogLevel.Warn, "newest", t0.AddMinutes(3)), })); await vm.RefreshAsync(); vm.WarnErrorOnly = true; Assert.Equal(new[] { "newest", "middle", "oldest" }, vm.Rows.Select(r => r.Message)); } [Fact] public async Task CopyLast_orders_chronologically_oldest_first() { var t0 = new DateTime(2026, 6, 23, 8, 0, 0, DateTimeKind.Utc); var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Info, "oldest", t0), E(WorkerLogLevel.Warn, "middle", t0.AddMinutes(1)), E(WorkerLogLevel.Error, "newest", t0.AddMinutes(2)), })); await vm.RefreshAsync(); var text = vm.BuildCopyText(); var lines = text.Split('\n'); Assert.Equal(3, lines.Length); Assert.EndsWith("INFO oldest", lines[0]); Assert.EndsWith("WARN middle", lines[1]); Assert.EndsWith("ERROR newest", lines[2]); } [Fact] public async Task CopyLast_caps_at_40_most_recent_entries() { var t0 = new DateTime(2026, 6, 23, 8, 0, 0, DateTimeKind.Utc); var entries = Enumerable.Range(0, 50) .Select(i => E(WorkerLogLevel.Info, $"line{i}", t0.AddMinutes(i))) .ToArray(); var vm = new LogVisualizerViewModel(new FakeClient(entries)); await vm.RefreshAsync(); var lines = vm.BuildCopyText().Split('\n'); Assert.Equal(40, lines.Length); Assert.EndsWith("line10", lines[0]); Assert.EndsWith("line49", lines[^1]); } [Fact] public async Task CopyLast_respects_active_warn_error_filter() { var t0 = new DateTime(2026, 6, 23, 8, 0, 0, DateTimeKind.Utc); var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Info, "skipped", t0), E(WorkerLogLevel.Warn, "kept", t0.AddMinutes(1)), })); await vm.RefreshAsync(); vm.WarnErrorOnly = true; var lines = vm.BuildCopyText().Split('\n'); Assert.Single(lines); Assert.EndsWith("WARN kept", lines[0]); } [Fact] public async Task CopyLastCommand_disabled_when_no_rows() { var vm = new LogVisualizerViewModel(new FakeClient(Array.Empty())); await vm.RefreshAsync(); Assert.False(vm.CopyLastCommand.CanExecute(null)); } [Fact] public async Task CopyLastCommand_copies_and_reports_count_on_success() { string? copied = null; var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Info, "a") }), text => { copied = text; return Task.FromResult(true); }); await vm.RefreshAsync(); await vm.CopyLastCommand.ExecuteAsync(null); Assert.EndsWith("INFO a", copied); Assert.Contains("1", vm.StatusText); } [Fact] public async Task CopyLastCommand_reports_failure_when_clipboard_unavailable() { var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Info, "a") }), _ => Task.FromResult(false)); await vm.RefreshAsync(); await vm.CopyLastCommand.ExecuteAsync(null); Assert.False(string.IsNullOrEmpty(vm.StatusText)); } }