Merge claudedo/8c1c213004574c4fad6beb75b84b70d7
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class DetailsIslandRoadblockReplyTests : IDisposable
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
|
||||
public DetailsIslandRoadblockReplyTests()
|
||||
{
|
||||
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_roadblock_reply_test_{Guid.NewGuid():N}.db");
|
||||
using var ctx = NewContext();
|
||||
ctx.Database.EnsureCreated();
|
||||
|
||||
// Loc is a process-wide ambient singleton other tests also mutate — pin it to
|
||||
// the real locale data so this test's assertions don't depend on run order.
|
||||
var dir = AppContext.BaseDirectory;
|
||||
while (dir is not null && !Directory.Exists(Path.Combine(dir, "src", "ClaudeDo.Localization", "locales")))
|
||||
dir = Path.GetDirectoryName(dir);
|
||||
Loc.Current = new Localizer(
|
||||
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { File.Delete(_dbPath); } catch { }
|
||||
try { File.Delete(_dbPath + "-wal"); } catch { }
|
||||
try { File.Delete(_dbPath + "-shm"); } catch { }
|
||||
}
|
||||
|
||||
private ClaudeDoDbContext NewContext()
|
||||
{
|
||||
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite($"Data Source={_dbPath}")
|
||||
.Options;
|
||||
return new ClaudeDoDbContext(opts);
|
||||
}
|
||||
|
||||
private sealed class TestDbFactory : IDbContextFactory<ClaudeDoDbContext>
|
||||
{
|
||||
private readonly Func<ClaudeDoDbContext> _create;
|
||||
public TestDbFactory(Func<ClaudeDoDbContext> create) => _create = create;
|
||||
public ClaudeDoDbContext CreateDbContext() => _create();
|
||||
}
|
||||
|
||||
private sealed class NullServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
|
||||
private sealed class StubNotesApi : ClaudeDo.Ui.Services.Interfaces.INotesApi
|
||||
{
|
||||
public Task<List<DailyNoteDto>> ListAsync(DateOnly day) =>
|
||||
Task.FromResult(new List<DailyNoteDto>());
|
||||
public Task<DailyNoteDto?> AddAsync(DateOnly day, string text) =>
|
||||
Task.FromResult<DailyNoteDto?>(null);
|
||||
public Task UpdateAsync(string id, string text) => Task.CompletedTask;
|
||||
public Task DeleteAsync(string id) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
private sealed class RecordingWorkerClient : StubWorkerClient
|
||||
{
|
||||
public override bool IsConnected => true;
|
||||
public (string TaskId, string Prompt)? LastContinue;
|
||||
public Exception? ThrowOnContinue;
|
||||
|
||||
public override Task ContinueTaskAsync(string taskId, string followUpPrompt)
|
||||
{
|
||||
if (ThrowOnContinue is not null) throw ThrowOnContinue;
|
||||
LastContinue = (taskId, followUpPrompt);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private DetailsIslandViewModel BuildVm(StubWorkerClient worker)
|
||||
{
|
||||
var factory = new TestDbFactory(NewContext);
|
||||
return new DetailsIslandViewModel(factory, worker, new NullServiceProvider(), new StubNotesApi(), new ClaudeDo.Ui.Services.MergeCoordinator());
|
||||
}
|
||||
|
||||
private static DetailsIslandViewModel PrepareDoneTaskWithRoadblock(DetailsIslandViewModel vm, string taskId = "task-rb-1")
|
||||
{
|
||||
vm.Bind(new TaskRowViewModel { Id = taskId, Status = TaskStatus.Done });
|
||||
vm.Monitor.ApplyState(TaskStatus.Done);
|
||||
vm.Roadblocks = "needs go-ahead to merge branches";
|
||||
return vm;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReplyToRoadblock_IsFalse_WithoutSessionId()
|
||||
{
|
||||
var vm = BuildVm(new RecordingWorkerClient());
|
||||
PrepareDoneTaskWithRoadblock(vm);
|
||||
|
||||
Assert.True(vm.ShowRoadblockCard);
|
||||
Assert.Null(vm.LatestRunSessionId);
|
||||
Assert.False(vm.CanReplyToRoadblock);
|
||||
Assert.False(vm.SendRoadblockReplyCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanReplyToRoadblock_BecomesTrue_OnceSessionIdIsPresent()
|
||||
{
|
||||
var vm = BuildVm(new RecordingWorkerClient());
|
||||
PrepareDoneTaskWithRoadblock(vm);
|
||||
|
||||
vm.LatestRunSessionId = "session-abc";
|
||||
|
||||
Assert.True(vm.CanReplyToRoadblock);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SendRoadblockReplyCommand_IsDisabled_WhenDraftIsEmptyOrWhitespace()
|
||||
{
|
||||
var vm = BuildVm(new RecordingWorkerClient());
|
||||
PrepareDoneTaskWithRoadblock(vm);
|
||||
vm.LatestRunSessionId = "session-abc";
|
||||
|
||||
Assert.False(vm.SendRoadblockReplyCommand.CanExecute(null));
|
||||
|
||||
vm.RoadblockReplyDraft = " ";
|
||||
Assert.False(vm.SendRoadblockReplyCommand.CanExecute(null));
|
||||
|
||||
vm.RoadblockReplyDraft = "go ahead, merge it";
|
||||
Assert.True(vm.SendRoadblockReplyCommand.CanExecute(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendRoadblockReply_PassesTypedText_NotTheHardcodedContinueConstant()
|
||||
{
|
||||
var worker = new RecordingWorkerClient();
|
||||
var vm = BuildVm(worker);
|
||||
PrepareDoneTaskWithRoadblock(vm);
|
||||
vm.LatestRunSessionId = "session-abc";
|
||||
vm.RoadblockReplyDraft = "yes, go ahead and merge";
|
||||
|
||||
await vm.SendRoadblockReplyCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(("task-rb-1", "yes, go ahead and merge"), worker.LastContinue);
|
||||
Assert.NotEqual("Continue working on this task.", worker.LastContinue?.Prompt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendRoadblockReply_ClearsDraft_OnSuccess()
|
||||
{
|
||||
var worker = new RecordingWorkerClient();
|
||||
var vm = BuildVm(worker);
|
||||
PrepareDoneTaskWithRoadblock(vm);
|
||||
vm.LatestRunSessionId = "session-abc";
|
||||
vm.RoadblockReplyDraft = "go ahead";
|
||||
|
||||
await vm.SendRoadblockReplyCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.Equal(string.Empty, vm.RoadblockReplyDraft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendRoadblockReply_WhenWorkerThrows_ReportsErrorAndKeepsDraft()
|
||||
{
|
||||
var worker = new RecordingWorkerClient { ThrowOnContinue = new Exception("override slot busy") };
|
||||
var vm = BuildVm(worker);
|
||||
PrepareDoneTaskWithRoadblock(vm);
|
||||
vm.LatestRunSessionId = "session-abc";
|
||||
vm.RoadblockReplyDraft = "go ahead";
|
||||
|
||||
string? reportedError = null;
|
||||
vm.ErrorReported += msg => reportedError = msg;
|
||||
|
||||
await vm.SendRoadblockReplyCommand.ExecuteAsync(null);
|
||||
|
||||
Assert.NotNull(reportedError);
|
||||
Assert.Contains("override slot busy", reportedError);
|
||||
Assert.Equal("go ahead", vm.RoadblockReplyDraft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_ClearsRoadblockReplyDraft_ForNewTask()
|
||||
{
|
||||
var vm = BuildVm(new RecordingWorkerClient());
|
||||
PrepareDoneTaskWithRoadblock(vm);
|
||||
vm.LatestRunSessionId = "session-abc";
|
||||
vm.RoadblockReplyDraft = "leftover text";
|
||||
|
||||
vm.Bind(new TaskRowViewModel { Id = "task-rb-2", Status = TaskStatus.Idle });
|
||||
|
||||
Assert.Equal(string.Empty, vm.RoadblockReplyDraft);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowContinueAndCanContinue_AreUnaffected_ByRoadblockReplyAddition()
|
||||
{
|
||||
// Regression: the existing Continue button (Failed/Cancelled only) must keep
|
||||
// working exactly as before — the roadblock reply field is an additional path,
|
||||
// not a replacement.
|
||||
var vm = BuildVm(new RecordingWorkerClient());
|
||||
vm.Bind(new TaskRowViewModel { Id = "task-rb-3", Status = TaskStatus.Failed });
|
||||
vm.Monitor.ApplyState(TaskStatus.Failed);
|
||||
|
||||
Assert.True(vm.ShowContinue);
|
||||
Assert.False(vm.ContinueCommand.CanExecute(null)); // no session id yet
|
||||
|
||||
vm.LatestRunSessionId = "session-xyz";
|
||||
Assert.True(vm.ContinueCommand.CanExecute(null));
|
||||
|
||||
vm.Monitor.ApplyState(TaskStatus.Done);
|
||||
Assert.False(vm.ShowContinue);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(TaskStatus.WaitingForReview, true)]
|
||||
[InlineData(TaskStatus.Done, true)]
|
||||
[InlineData(TaskStatus.Failed, true)]
|
||||
[InlineData(TaskStatus.Cancelled, true)]
|
||||
[InlineData(TaskStatus.Running, false)]
|
||||
[InlineData(TaskStatus.Queued, false)]
|
||||
[InlineData(TaskStatus.Idle, false)]
|
||||
[InlineData(TaskStatus.WaitingForChildren, false)]
|
||||
public void ShowRoadblockCard_MatchesStatusMatrix_WhenARoadblockWasReported(TaskStatus status, bool expectedVisible)
|
||||
{
|
||||
var vm = BuildVm(new RecordingWorkerClient());
|
||||
vm.Bind(new TaskRowViewModel { Id = "task-rb-matrix", Status = status });
|
||||
vm.Monitor.ApplyState(status);
|
||||
vm.Roadblocks = "needs a go-ahead";
|
||||
|
||||
Assert.Equal(expectedVisible, vm.ShowRoadblockCard);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShowRoadblockCard_IsFalse_WithoutARoadblock_RegardlessOfStatus()
|
||||
{
|
||||
var vm = BuildVm(new RecordingWorkerClient());
|
||||
vm.Bind(new TaskRowViewModel { Id = "task-rb-none", Status = TaskStatus.Done });
|
||||
vm.Monitor.ApplyState(TaskStatus.Done);
|
||||
|
||||
Assert.False(vm.ShowRoadblockCard);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user