feat(ui): Log Visualizer overlay reachable from a clickable footer log line

This commit is contained in:
Mika Kuns
2026-06-23 09:05:17 +02:00
parent 08a4f97a78
commit c4f74a7aea
14 changed files with 241 additions and 9 deletions

View File

@@ -77,6 +77,7 @@ public abstract class StubWorkerClient : IWorkerClient
public virtual Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) => Task.CompletedTask;
public virtual Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
public virtual Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId) => Task.FromResult<MergeTargetsDto?>(null);
public virtual Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync() => Task.FromResult<IReadOnlyList<WorkerLogEntry>>(Array.Empty<WorkerLogEntry>());
public virtual Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
=> Task.FromResult<IReadOnlyList<SubtaskDiffDto>>(Array.Empty<SubtaskDiffDto>());
public virtual Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)

View File

@@ -0,0 +1,51 @@
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<WorkerLogEntry> _logs;
public FakeClient(IReadOnlyList<WorkerLogEntry> logs) => _logs = logs;
public override Task<IReadOnlyList<WorkerLogEntry>> 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));
[Fact]
public async Task Refresh_populates_rows_from_worker()
{
var vm = new LogVisualizerViewModel(new FakeClient(new[] { E(WorkerLogLevel.Info, "a"), E(WorkerLogLevel.Error, "b") }));
await vm.RefreshAsync();
Assert.Equal(new[] { "a", "b" }, 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[] { "w", "e" }, 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<WorkerLogEntry>()));
await vm.RefreshAsync();
Assert.Empty(vm.Rows);
Assert.False(string.IsNullOrEmpty(vm.StatusText));
}
}