210 lines
6.7 KiB
C#
210 lines
6.7 KiB
C#
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Ui.Services;
|
|
using ClaudeDo.Ui.ViewModels.Modals.Settings;
|
|
|
|
namespace ClaudeDo.Ui.Tests.ViewModels;
|
|
|
|
public class PrimeClaudeTabViewModelTests
|
|
{
|
|
private sealed class FakeApi : IPrimeScheduleApi
|
|
{
|
|
public List<PrimeScheduleDto> Stored { get; } = new();
|
|
public List<PrimeScheduleDto> Upserts { get; } = new();
|
|
public List<Guid> Deletes { get; } = new();
|
|
public Task<List<PrimeScheduleDto>> ListAsync() => Task.FromResult(Stored.ToList());
|
|
public Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto)
|
|
{
|
|
Upserts.Add(dto);
|
|
return Task.FromResult<PrimeScheduleDto?>(dto);
|
|
}
|
|
public Task DeleteAsync(Guid id) { Deletes.Add(id); return Task.CompletedTask; }
|
|
}
|
|
|
|
private sealed class ThrowingApi : IPrimeScheduleApi
|
|
{
|
|
public string ExceptionMessage { get; init; } = "worker offline";
|
|
public Task<List<PrimeScheduleDto>> ListAsync() => Task.FromResult(new List<PrimeScheduleDto>());
|
|
public Task<PrimeScheduleDto?> UpsertAsync(PrimeScheduleDto dto) => throw new Exception(ExceptionMessage);
|
|
public Task DeleteAsync(Guid id) => Task.CompletedTask;
|
|
}
|
|
|
|
private static PrimeScheduleDto Dto(Guid id, int days, TimeSpan time,
|
|
PrimeActionKind kind = PrimeActionKind.FillMyDay, string? prompt = null) =>
|
|
new(id, days, time, true, null, prompt, kind);
|
|
|
|
[Fact]
|
|
public async Task Load_Populates_Rows()
|
|
{
|
|
var api = new FakeApi();
|
|
api.Stored.Add(Dto(Guid.NewGuid(), 31, new TimeSpan(7, 0, 0)));
|
|
var vm = new PrimeClaudeTabViewModel(api);
|
|
await vm.LoadAsync();
|
|
Assert.Single(vm.Rows);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddSchedule_Appends_Row_With_Defaults()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
vm.AddScheduleCommand.Execute(null);
|
|
Assert.Single(vm.Rows);
|
|
Assert.True(vm.Rows[0].Enabled);
|
|
Assert.True(vm.Rows[0].Monday);
|
|
Assert.True(vm.Rows[0].Friday);
|
|
Assert.False(vm.Rows[0].Saturday);
|
|
Assert.Equal(new TimeSpan(7, 0, 0), vm.Rows[0].TimeOfDay);
|
|
}
|
|
|
|
[Fact]
|
|
public void Row_Decomposes_And_Recomposes_Days()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
vm.AddScheduleCommand.Execute(null);
|
|
var row = vm.Rows[0];
|
|
Assert.Equal(31, row.DaysMask());
|
|
row.Saturday = true;
|
|
Assert.Equal(63, row.DaysMask());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Save_Diffs_New_And_Removed_Rows()
|
|
{
|
|
var api = new FakeApi();
|
|
var keptId = Guid.NewGuid();
|
|
var deletedId = Guid.NewGuid();
|
|
api.Stored.Add(Dto(keptId, 31, new TimeSpan(7, 0, 0)));
|
|
api.Stored.Add(Dto(deletedId, 31, new TimeSpan(8, 0, 0)));
|
|
|
|
var vm = new PrimeClaudeTabViewModel(api);
|
|
await vm.LoadAsync();
|
|
vm.RemoveScheduleCommand.Execute(vm.Rows.Single(r => r.Id == deletedId));
|
|
vm.AddScheduleCommand.Execute(null);
|
|
|
|
await vm.SaveAsync();
|
|
|
|
Assert.Contains(deletedId, api.Deletes);
|
|
Assert.Equal(2, api.Upserts.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Reports_No_Days_Selected()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
vm.AddScheduleCommand.Execute(null);
|
|
var row = vm.Rows[0];
|
|
row.Monday = row.Tuesday = row.Wednesday = row.Thursday = row.Friday = false;
|
|
Assert.NotNull(vm.Validate());
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Passes_With_One_Day()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
vm.AddScheduleCommand.Execute(null);
|
|
Assert.Null(vm.Validate());
|
|
}
|
|
|
|
// SettingsModalViewModel.Save() is the only place that catches Prime.SaveAsync's
|
|
// failures (via a try/catch around the whole settings save) and surfaces them as
|
|
// "Save failed: {message}". For that to work, SaveAsync must propagate the worker's
|
|
// exception rather than swallow it.
|
|
[Fact]
|
|
public async Task Save_WhenApiThrows_PropagatesException()
|
|
{
|
|
var api = new ThrowingApi();
|
|
var vm = new PrimeClaudeTabViewModel(api);
|
|
vm.AddScheduleCommand.Execute(null);
|
|
|
|
var ex = await Assert.ThrowsAsync<Exception>(() => vm.SaveAsync());
|
|
Assert.Equal(api.ExceptionMessage, ex.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void AddSchedule_Defaults_To_Ping()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
vm.AddScheduleCommand.Execute(null);
|
|
|
|
Assert.Equal(PrimeActionKind.Ping, vm.Rows[0].Kind);
|
|
Assert.True(vm.Rows[0].IsPing);
|
|
Assert.False(vm.Rows[0].IsFillMyDay);
|
|
Assert.False(vm.Rows[0].IsCustom);
|
|
}
|
|
|
|
[Fact]
|
|
public void Setting_IsCustom_Flips_Kind_And_Clears_The_Others()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
vm.AddScheduleCommand.Execute(null);
|
|
var row = vm.Rows[0];
|
|
|
|
row.IsCustom = true;
|
|
|
|
Assert.Equal(PrimeActionKind.Custom, row.Kind);
|
|
Assert.False(row.IsPing);
|
|
Assert.False(row.IsFillMyDay);
|
|
}
|
|
|
|
[Fact]
|
|
public void Row_RadioGroup_Is_Unique_Per_Row()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
vm.AddScheduleCommand.Execute(null);
|
|
vm.AddScheduleCommand.Execute(null);
|
|
|
|
Assert.NotEqual(vm.Rows[0].RadioGroup, vm.Rows[1].RadioGroup);
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Rejects_Custom_Without_A_Prompt()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
vm.AddScheduleCommand.Execute(null);
|
|
vm.Rows[0].IsCustom = true;
|
|
vm.Rows[0].PromptOverride = " ";
|
|
|
|
Assert.NotNull(vm.Validate());
|
|
}
|
|
|
|
[Fact]
|
|
public void Validate_Accepts_Custom_With_A_Prompt()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
vm.AddScheduleCommand.Execute(null);
|
|
vm.Rows[0].IsCustom = true;
|
|
vm.Rows[0].PromptOverride = "queue the oldest task";
|
|
|
|
Assert.Null(vm.Validate());
|
|
}
|
|
|
|
[Fact]
|
|
public void AnyFillMyDay_Tracks_Kind_And_Row_Changes()
|
|
{
|
|
var vm = new PrimeClaudeTabViewModel(new FakeApi());
|
|
Assert.False(vm.AnyFillMyDay);
|
|
|
|
vm.AddScheduleCommand.Execute(null);
|
|
Assert.False(vm.AnyFillMyDay);
|
|
|
|
vm.Rows[0].IsFillMyDay = true;
|
|
Assert.True(vm.AnyFillMyDay);
|
|
|
|
vm.RemoveScheduleCommand.Execute(vm.Rows[0]);
|
|
Assert.False(vm.AnyFillMyDay);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Save_Round_Trips_The_Kind()
|
|
{
|
|
var api = new FakeApi();
|
|
var vm = new PrimeClaudeTabViewModel(api);
|
|
vm.AddScheduleCommand.Execute(null);
|
|
vm.Rows[0].IsCustom = true;
|
|
vm.Rows[0].PromptOverride = "do the thing";
|
|
|
|
await vm.SaveAsync();
|
|
|
|
Assert.Equal(PrimeActionKind.Custom, api.Upserts[0].Kind);
|
|
}
|
|
}
|