using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using ClaudeDo.Data.Models; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels; using ClaudeDo.Ui.ViewModels.Modals; using Xunit; namespace ClaudeDo.Ui.Tests; public class IslandsShellViewModelWorkerLogTests { [Fact] public void Receiving_event_sets_text_level_and_visible() { var vm = new IslandsShellViewModel(); var at = new DateTime(2026, 4, 23, 14, 32, 0, DateTimeKind.Utc); vm.OnWorkerLogReceived(new WorkerLogEntry("Created worktree for \"X\"", WorkerLogLevel.Info, at)); Assert.True(vm.IsWorkerLogVisible); Assert.Equal(WorkerLogLevel.Info, vm.WorkerLogLevel); Assert.Contains("Created worktree for \"X\"", vm.WorkerLogText); } [Fact] public void Second_event_replaces_first() { var vm = new IslandsShellViewModel(); vm.OnWorkerLogReceived(new WorkerLogEntry("first", WorkerLogLevel.Info, DateTime.UtcNow)); vm.OnWorkerLogReceived(new WorkerLogEntry("second", WorkerLogLevel.Success, DateTime.UtcNow)); Assert.Contains("second", vm.WorkerLogText); Assert.Equal(WorkerLogLevel.Success, vm.WorkerLogLevel); } [Fact] public void ClearWorkerLog_hides_line() { var vm = new IslandsShellViewModel(); vm.OnWorkerLogReceived(new WorkerLogEntry("msg", WorkerLogLevel.Info, DateTime.UtcNow)); vm.ClearWorkerLog(); Assert.False(vm.IsWorkerLogVisible); Assert.Null(vm.WorkerLogText); } [Fact] public void Text_is_formatted_as_HHmm_dot_message_local_time() { var vm = new IslandsShellViewModel(); var utc = new DateTime(2026, 4, 23, 12, 0, 0, DateTimeKind.Utc); var expectedLocalHhmm = utc.ToLocalTime().ToString("HH:mm"); vm.OnWorkerLogReceived(new WorkerLogEntry("hello", WorkerLogLevel.Info, utc)); Assert.StartsWith(expectedLocalHhmm + " ยท ", vm.WorkerLogText); } [Fact] public async Task Visualizer_shows_flashed_and_received_footer_lines_with_their_levels() { var shell = new IslandsShellViewModel(); shell.FlashFooterInfo("claude 2.0 updating"); shell.OnWorkerLogReceived(new WorkerLogEntry("task merged", WorkerLogLevel.Success, DateTime.UtcNow.AddSeconds(1))); var vm = new LogVisualizerViewModel(new BufferClient(), shell.FooterHistorySnapshot); await vm.RefreshAsync(); Assert.Equal(new[] { ("task merged", WorkerLogLevel.Success), ("claude 2.0 updating", WorkerLogLevel.Info) }, vm.Rows.Select(r => (r.Message, r.Level))); } [Fact] public async Task Footer_line_arriving_while_visualizer_is_open_shows_on_next_refresh() { var shell = new IslandsShellViewModel(); var vm = new LogVisualizerViewModel(new BufferClient(), shell.FooterHistorySnapshot); await vm.RefreshAsync(); shell.FlashFooterError("boom"); await vm.RefreshAsync(); Assert.Equal(("boom", WorkerLogLevel.Error), (vm.Rows.Single().Message, vm.Rows.Single().Level)); } [Fact] public async Task Worker_warn_in_both_footer_history_and_buffer_appears_once() { var shell = new IslandsShellViewModel(); var warn = new WorkerLogEntry("disk low", WorkerLogLevel.Warn, DateTime.UtcNow); shell.OnWorkerLogReceived(warn); var vm = new LogVisualizerViewModel(new BufferClient(warn), shell.FooterHistorySnapshot); await vm.RefreshAsync(); Assert.Single(vm.Rows); } private sealed class BufferClient(params WorkerLogEntry[] logs) : StubWorkerClient { public override Task> GetRecentLogsAsync() => Task.FromResult>(logs); } }