chore: merge main into task branch
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace ClaudeDo.Data.Tests;
|
||||
|
||||
// Sibling migrations branched off the same parent have silently dropped each other's columns
|
||||
// during a SQLite table rebuild before (see docs/explore-notes). Every other Data.Tests fixture
|
||||
// uses EnsureCreated, which skips the migration chain entirely, so nothing else in the suite
|
||||
// would catch that. This compares the schema the migration chain actually produces against the
|
||||
// schema EnsureCreated builds from today's model.
|
||||
public sealed class MigrationSchemaDriftTests : IDisposable
|
||||
{
|
||||
private readonly string _migratedDbPath;
|
||||
private readonly string _modelDbPath;
|
||||
|
||||
public MigrationSchemaDriftTests()
|
||||
{
|
||||
_migratedDbPath = Path.Combine(Path.GetTempPath(), $"claudedo_drift_migrated_{Guid.NewGuid():N}.db");
|
||||
_modelDbPath = Path.Combine(Path.GetTempPath(), $"claudedo_drift_model_{Guid.NewGuid():N}.db");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
SqliteConnection.ClearAllPools();
|
||||
foreach (var path in new[] { _migratedDbPath, _modelDbPath })
|
||||
foreach (var suffix in new[] { "", "-wal", "-shm" })
|
||||
try { File.Delete(path + suffix); } catch { }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Migrated_schema_has_the_same_columns_as_the_current_model()
|
||||
{
|
||||
using (var migrated = new ClaudeDoDbContext(OptionsFor(_migratedDbPath)))
|
||||
ClaudeDoDbContext.MigrateAndConfigure(migrated);
|
||||
using (var model = new ClaudeDoDbContext(OptionsFor(_modelDbPath)))
|
||||
model.Database.EnsureCreated();
|
||||
|
||||
var migratedColumns = ReadColumnsByTable(_migratedDbPath);
|
||||
var modelColumns = ReadColumnsByTable(_modelDbPath);
|
||||
|
||||
var mismatches = new List<string>();
|
||||
foreach (var (table, expectedColumns) in modelColumns)
|
||||
{
|
||||
if (!migratedColumns.TryGetValue(table, out var actualColumns))
|
||||
{
|
||||
mismatches.Add($"{table}: missing from migrated schema entirely");
|
||||
continue;
|
||||
}
|
||||
|
||||
var missingInMigrated = expectedColumns.Except(actualColumns).ToList();
|
||||
var extraInMigrated = actualColumns.Except(expectedColumns).ToList();
|
||||
if (missingInMigrated.Count > 0 || extraInMigrated.Count > 0)
|
||||
{
|
||||
mismatches.Add(
|
||||
$"{table}: missing-in-migrated=[{string.Join(", ", missingInMigrated)}] " +
|
||||
$"extra-in-migrated=[{string.Join(", ", extraInMigrated)}]");
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(mismatches.Count == 0, "Schema drift between migrated DB and current model:\n" + string.Join("\n", mismatches));
|
||||
}
|
||||
|
||||
private static DbContextOptions<ClaudeDoDbContext> OptionsFor(string dbPath) =>
|
||||
new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
||||
.UseSqlite($"Data Source={dbPath}")
|
||||
.Options;
|
||||
|
||||
private static Dictionary<string, HashSet<string>> ReadColumnsByTable(string dbPath)
|
||||
{
|
||||
using var conn = new SqliteConnection($"Data Source={dbPath}");
|
||||
conn.Open();
|
||||
|
||||
var tables = new List<string>();
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.CommandText = "SELECT name FROM sqlite_master WHERE type='table' AND name != '__EFMigrationsHistory' AND name NOT LIKE 'sqlite_%'";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
tables.Add(reader.GetString(0));
|
||||
}
|
||||
|
||||
var result = new Dictionary<string, HashSet<string>>();
|
||||
foreach (var table in tables)
|
||||
{
|
||||
var columns = new HashSet<string>();
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText = $"PRAGMA table_info(\"{table}\")";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
columns.Add(reader.GetString(reader.GetOrdinal("name")));
|
||||
result[table] = columns;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -221,11 +221,14 @@ public class PromptFilesTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultFor_merge_helper_merge_never_reads_diffs_itself()
|
||||
public void DefaultFor_merge_helper_merge_gates_the_diff_subagent_on_a_file_count_threshold()
|
||||
{
|
||||
// Below 5 changed files a subagent hop is pure context-protection overhead for a tiny
|
||||
// diff; the threshold keeps the subagent mandatory once a diff is actually large.
|
||||
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperMerge);
|
||||
Assert.Contains("Never read a task's diff yourself", d);
|
||||
Assert.Contains("Below 5 changed files", d);
|
||||
Assert.Contains("sonnet subagent", d);
|
||||
Assert.Contains("changedFileCount", d);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using ClaudeDo.Localization;
|
||||
|
||||
namespace ClaudeDo.Localization.Tests;
|
||||
@@ -16,10 +17,34 @@ public class EnJsonCoverageTests
|
||||
throw new DirectoryNotFoundException("Could not locate src/ClaudeDo.Localization/locales");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_locale_files_parse_as_valid_json()
|
||||
{
|
||||
var localesDir = LocalesDir();
|
||||
foreach (var path in Directory.EnumerateFiles(localesDir, "*.json"))
|
||||
{
|
||||
try
|
||||
{
|
||||
JsonDocument.Parse(File.ReadAllText(path));
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Assert.Fail($"{Path.GetFileName(path)} is not valid JSON: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_locale_has_same_keys_as_english()
|
||||
{
|
||||
var store = LocaleStore.Load(LocalesDir());
|
||||
var localesDir = LocalesDir();
|
||||
var jsonFileCount = Directory.EnumerateFiles(localesDir, "*.json").Count();
|
||||
var store = LocaleStore.Load(localesDir);
|
||||
Assert.True(store.Available.Count == jsonFileCount,
|
||||
$"Expected {jsonFileCount} locale(s) to load from {localesDir}, but only {store.Available.Count} loaded " +
|
||||
$"(loaded: {string.Join(", ", store.Available.Select(l => l.Code))}). A *.json file failed to parse.");
|
||||
Assert.True(store.TryGet("de", out _), "Locale 'de' did not load — de.json failed to parse.");
|
||||
|
||||
Assert.True(store.TryGet("en", out var en));
|
||||
var enKeys = en!.Strings.Keys.ToHashSet();
|
||||
|
||||
|
||||
@@ -98,7 +98,12 @@ public abstract class StubWorkerClient : IWorkerClient
|
||||
public virtual Task<List<string>> InstallSessionSkillAsync(string url) => Task.FromResult(new List<string>());
|
||||
public virtual Task UpdateSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
|
||||
public virtual Task RemoveSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
|
||||
public virtual Task<BaseDirtyWarningDto?> SetTaskStatusAsync(string taskId, TaskStatus status) => Task.FromResult<BaseDirtyWarningDto?>(null);
|
||||
public List<(string TaskId, TaskStatus Status)> SetTaskStatusCalls { get; } = new();
|
||||
public virtual Task<BaseDirtyWarningDto?> SetTaskStatusAsync(string taskId, TaskStatus status)
|
||||
{
|
||||
SetTaskStatusCalls.Add((taskId, status));
|
||||
return Task.FromResult<BaseDirtyWarningDto?>(null);
|
||||
}
|
||||
public virtual Task SetTaskDoneAsync(string taskId) => Task.CompletedTask;
|
||||
public virtual Task UnsetTaskDoneAsync(string taskId) => Task.CompletedTask;
|
||||
public virtual Task DequeueTaskAsync(string taskId) => Task.CompletedTask;
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using Xunit;
|
||||
|
||||
// Loc.Current (src/ClaudeDo.Ui/Localization/Loc.cs) is a process-wide mutable static that many
|
||||
// ViewModel test constructors assign — parallel collections race on which localizer is active.
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
|
||||
namespace ClaudeDo.Ui.Tests;
|
||||
|
||||
@@ -17,4 +24,17 @@ internal static class TestSetup
|
||||
var path = Path.Combine(Path.GetTempPath(), "claudedo-ui-tests", $"operation-timing-{Environment.ProcessId}.ndjson");
|
||||
OperationTiming.Shared = new OperationTiming(path);
|
||||
}
|
||||
|
||||
// Installs the real localizer before any test runs, so a class that reads Loc.T/Loc.Current
|
||||
// without setting it up itself (e.g. via a ViewModel error message) doesn't fall back to
|
||||
// Loc's key-echo default depending on which test class happens to run first.
|
||||
[ModuleInitializer]
|
||||
internal static void InstallRealLocalizer()
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Agent;
|
||||
@@ -9,6 +10,18 @@ namespace ClaudeDo.Ui.Tests.ViewModels;
|
||||
|
||||
public class AgentConfigEditorViewModelTests
|
||||
{
|
||||
public AgentConfigEditorViewModelTests()
|
||||
{
|
||||
// Loc.T(...) is read below without installing a localizer via a ViewModel constructor
|
||||
// (unlike its sibling test classes) — install one directly so this class doesn't depend
|
||||
// on whichever neighbour test happened to run first and set Loc.Current.
|
||||
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");
|
||||
}
|
||||
|
||||
private sealed class FakeWorker : StubWorkerClient
|
||||
{
|
||||
public AppSettingsDto? App;
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
<Using Include="ClaudeDo.Data.Wire" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\ClaudeDo.Ui.Tests\StubWorkerClient.cs" Link="TestDoubles\StubWorkerClient.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ClaudeDo.Worker\ClaudeDo.Worker.csproj" />
|
||||
<ProjectReference Include="..\..\src\ClaudeDo.Data\ClaudeDo.Data.csproj" />
|
||||
|
||||
@@ -623,4 +623,38 @@ public sealed class BatchMcpToolsTests : IDisposable
|
||||
Assert.Equal(2, progress.Reports.Count);
|
||||
Assert.Equal(2, progress.Reports[1].Total);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BatchGetTasks_FailedTaskWithFailureReason_ReturnsReasonWhenRequested()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, "failed", TaskStatus.Failed);
|
||||
task.FailureReason = "max-turns-exceeded";
|
||||
await _tasks.UpdateAsync(task);
|
||||
var sut = BuildSut();
|
||||
|
||||
var results = await sut.BatchGetTasks(
|
||||
new[] { task.Id }, includeDescription: true, fields: new[] { "failureReason" },
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
var found = results.Single(r => r.Id == task.Id).TaskFull!;
|
||||
Assert.Equal("max-turns-exceeded", found.FailureReason);
|
||||
Assert.Null(found.Description);
|
||||
Assert.Null(found.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BatchGetTasks_IdleTaskWithoutFailureReason_ReturnsNullWhenRequested()
|
||||
{
|
||||
var listId = await SeedListAsync();
|
||||
var task = await SeedTaskAsync(listId, "idle", TaskStatus.Idle);
|
||||
var sut = BuildSut();
|
||||
|
||||
var results = await sut.BatchGetTasks(
|
||||
new[] { task.Id }, includeDescription: true, fields: new[] { "failureReason" },
|
||||
cancellationToken: CancellationToken.None);
|
||||
|
||||
var found = results.Single(r => r.Id == task.Id).TaskFull!;
|
||||
Assert.Null(found.FailureReason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Lifecycle;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Lifecycle;
|
||||
|
||||
public sealed class UntrackedWorktreeRecoveryTests : IDisposable
|
||||
{
|
||||
private readonly DbFixture _db = new();
|
||||
|
||||
public void Dispose() => _db.Dispose();
|
||||
|
||||
[Fact]
|
||||
public void FindUntrackedPaths_PathNotInDb_IsReturned()
|
||||
{
|
||||
var registered = new[] { @"C:\repo\.worktrees\abc123" };
|
||||
var db = new[] { @"C:\repo\.worktrees\def456" };
|
||||
|
||||
var untracked = UntrackedWorktreeRecovery.FindUntrackedPaths(registered, db);
|
||||
|
||||
Assert.Equal(registered, untracked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindUntrackedPaths_PathInDb_IsExcluded()
|
||||
{
|
||||
var registered = new[] { @"C:\repo\.worktrees\abc123" };
|
||||
var db = new[] { @"C:\repo\.worktrees\abc123" };
|
||||
|
||||
var untracked = UntrackedWorktreeRecovery.FindUntrackedPaths(registered, db);
|
||||
|
||||
Assert.Empty(untracked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindUntrackedPaths_DifferentCasingAndSeparators_StillMatches()
|
||||
{
|
||||
var registered = new[] { @"C:\repo\.worktrees\abc123\" };
|
||||
var db = new[] { "c:/repo/.worktrees/ABC123" };
|
||||
|
||||
var untracked = UntrackedWorktreeRecovery.FindUntrackedPaths(registered, db);
|
||||
|
||||
Assert.Empty(untracked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FindUntrackedPaths_EmptyDb_ReturnsAllRegistered()
|
||||
{
|
||||
var registered = new[] { @"C:\repo\.worktrees\abc123", @"C:\repo\.worktrees\def456" };
|
||||
|
||||
var untracked = UntrackedWorktreeRecovery.FindUntrackedPaths(registered, Array.Empty<string>());
|
||||
|
||||
Assert.Equal(registered, untracked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StartAsync_NoLists_DoesNotThrow()
|
||||
{
|
||||
var hubContext = new CapturingHubContext();
|
||||
var sut = new UntrackedWorktreeRecovery(
|
||||
_db.CreateFactory(),
|
||||
new ClaudeDo.Data.Git.GitService(),
|
||||
new HubBroadcaster(hubContext),
|
||||
NullLogger<UntrackedWorktreeRecovery>.Instance);
|
||||
|
||||
await sut.StartAsync(CancellationToken.None);
|
||||
await sut.StopAsync(CancellationToken.None);
|
||||
|
||||
Assert.Contains(hubContext.Proxy.Calls, c =>
|
||||
c.Method == "OperationProgress" && (string)c.Args[0]! == "startup-recovery");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using ClaudeDo.Localization;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests;
|
||||
@@ -16,4 +18,20 @@ internal static class TestSetup
|
||||
var path = Path.Combine(Path.GetTempPath(), "claudedo-worker-tests", $"operation-timing-{Environment.ProcessId}.ndjson");
|
||||
OperationTiming.Shared = new OperationTiming(path);
|
||||
}
|
||||
|
||||
// Installs the real localizer before any test runs, mirroring ClaudeDo.Ui.Tests' TestSetup,
|
||||
// so a UiVm test that reads Loc.T without setting up its own localizer doesn't depend on
|
||||
// ambient state left by whichever test class happened to run first. Deliberately does NOT
|
||||
// also disable test parallelization here (unlike Ui.Tests) — measured 14m49s -> 18m46s
|
||||
// (+27%) on this project's real-SQLite/git suite, too costly for a test-only ordering issue
|
||||
// that the module initializer alone already closes.
|
||||
[ModuleInitializer]
|
||||
internal static void InstallRealLocalizer()
|
||||
{
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
using System.Collections.Generic;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using Xunit;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.UiVm;
|
||||
|
||||
// Characterization test for TaskRowViewModel's PropertyChanged fan-out. For every source
|
||||
// property that today drives dependent notifications (via a handwritten partial OnXChanged),
|
||||
// this pins down the exact SET of names raised when that property changes. Order is
|
||||
// deliberately not asserted: a NotifyPropertyChangedFor-based rewrite raises the source
|
||||
// property's own notification before its dependents instead of after, which is an unobservable
|
||||
// reordering, not a behavior change. This test must stay green across that rewrite.
|
||||
public class TaskRowViewModelNotificationsTests
|
||||
{
|
||||
private static HashSet<string?> Raised(TaskRowViewModel vm, System.Action mutate)
|
||||
{
|
||||
var raised = new HashSet<string?>();
|
||||
vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName);
|
||||
mutate();
|
||||
return raised;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StatusChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.Status = TaskStatus.Running);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.Status),
|
||||
nameof(TaskRowViewModel.StatusChipClass),
|
||||
nameof(TaskRowViewModel.StatusLabel),
|
||||
nameof(TaskRowViewModel.ShowStatusChip),
|
||||
nameof(TaskRowViewModel.IsRunning),
|
||||
nameof(TaskRowViewModel.IsWaitingForReview),
|
||||
nameof(TaskRowViewModel.IsParked),
|
||||
nameof(TaskRowViewModel.IsQueued),
|
||||
nameof(TaskRowViewModel.IsWaiting),
|
||||
nameof(TaskRowViewModel.HasFailureReason),
|
||||
nameof(TaskRowViewModel.FailureReasonTooltip),
|
||||
nameof(TaskRowViewModel.StatusChipTooltip),
|
||||
nameof(TaskRowViewModel.IsDraft),
|
||||
nameof(TaskRowViewModel.IsPlanned),
|
||||
nameof(TaskRowViewModel.CanOpenPlanningSession),
|
||||
nameof(TaskRowViewModel.CanRemoveFromQueue),
|
||||
nameof(TaskRowViewModel.CanSendToQueue),
|
||||
nameof(TaskRowViewModel.CanRefine),
|
||||
nameof(TaskRowViewModel.SendToQueueDisabledReason),
|
||||
nameof(TaskRowViewModel.CancelDisabledReason),
|
||||
nameof(TaskRowViewModel.RefineDisabledReason),
|
||||
nameof(TaskRowViewModel.PlanningDisabledReason),
|
||||
nameof(TaskRowViewModel.MarkAsDisabledReason),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentTaskIdChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.ParentTaskId = "parent");
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.ParentTaskId),
|
||||
nameof(TaskRowViewModel.IsChild),
|
||||
nameof(TaskRowViewModel.IsAgentSuggested),
|
||||
nameof(TaskRowViewModel.ShowAsChild),
|
||||
nameof(TaskRowViewModel.IsDraft),
|
||||
nameof(TaskRowViewModel.IsPlanned),
|
||||
nameof(TaskRowViewModel.CanSendToQueue),
|
||||
nameof(TaskRowViewModel.CanOpenPlanningSession),
|
||||
nameof(TaskRowViewModel.SendToQueueDisabledReason),
|
||||
nameof(TaskRowViewModel.PlanningDisabledReason),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentInViewChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.ParentInView = false);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.ParentInView),
|
||||
nameof(TaskRowViewModel.ShowAsChild),
|
||||
nameof(TaskRowViewModel.IsDraft),
|
||||
nameof(TaskRowViewModel.IsPlanned),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreatedByChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.CreatedBy = "someone");
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.CreatedBy),
|
||||
nameof(TaskRowViewModel.IsAgentSuggested),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentFinalizedChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.ParentFinalized = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.ParentFinalized),
|
||||
nameof(TaskRowViewModel.IsDraft),
|
||||
nameof(TaskRowViewModel.IsPlanned),
|
||||
nameof(TaskRowViewModel.CanSendToQueue),
|
||||
nameof(TaskRowViewModel.SendToQueueDisabledReason),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlanningPhaseChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.PlanningPhase = PlanningPhase.Active);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.PlanningPhase),
|
||||
nameof(TaskRowViewModel.IsPlanningParent),
|
||||
nameof(TaskRowViewModel.PlanningBadge),
|
||||
nameof(TaskRowViewModel.ShowStatusChip),
|
||||
nameof(TaskRowViewModel.IsPlanActive),
|
||||
nameof(TaskRowViewModel.IsPlanFinalized),
|
||||
nameof(TaskRowViewModel.CanOpenPlanningSession),
|
||||
nameof(TaskRowViewModel.CanResumeOrDiscardPlanning),
|
||||
nameof(TaskRowViewModel.CanQueuePlan),
|
||||
nameof(TaskRowViewModel.CanSendToQueue),
|
||||
nameof(TaskRowViewModel.CanFinalizePlanning),
|
||||
nameof(TaskRowViewModel.CanRefine),
|
||||
nameof(TaskRowViewModel.SendToQueueDisabledReason),
|
||||
nameof(TaskRowViewModel.RefineDisabledReason),
|
||||
nameof(TaskRowViewModel.PlanningDisabledReason),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsRefiningChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.IsRefining = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.IsRefining),
|
||||
nameof(TaskRowViewModel.CanRefine),
|
||||
nameof(TaskRowViewModel.RefineDisabledReason),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsManualChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.IsManual = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.IsManual),
|
||||
nameof(TaskRowViewModel.ManualBadge),
|
||||
nameof(TaskRowViewModel.CanRefine),
|
||||
nameof(TaskRowViewModel.CanSendToQueue),
|
||||
nameof(TaskRowViewModel.CanOpenPlanningSession),
|
||||
nameof(TaskRowViewModel.SendToQueueDisabledReason),
|
||||
nameof(TaskRowViewModel.RefineDisabledReason),
|
||||
nameof(TaskRowViewModel.PlanningDisabledReason),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasInteractiveSessionChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.HasInteractiveSession = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.HasInteractiveSession),
|
||||
nameof(TaskRowViewModel.IsParked),
|
||||
nameof(TaskRowViewModel.StatusChipClass),
|
||||
nameof(TaskRowViewModel.StatusLabel),
|
||||
nameof(TaskRowViewModel.ShowStatusChip),
|
||||
nameof(TaskRowViewModel.StatusChipTooltip),
|
||||
nameof(TaskRowViewModel.CanSendToQueue),
|
||||
nameof(TaskRowViewModel.SendToQueueDisabledReason),
|
||||
nameof(TaskRowViewModel.CanDeleteTask),
|
||||
nameof(TaskRowViewModel.DeleteDisabledReason),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailureReasonChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.FailureReason = "error");
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.FailureReason),
|
||||
nameof(TaskRowViewModel.HasFailureReason),
|
||||
nameof(TaskRowViewModel.FailureReasonLabel),
|
||||
nameof(TaskRowViewModel.FailureReasonTooltip),
|
||||
nameof(TaskRowViewModel.StatusChipTooltip),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailureTurnsUsedChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.FailureTurnsUsed = 5);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.FailureTurnsUsed),
|
||||
nameof(TaskRowViewModel.FailureReasonTooltip),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailureMaxTurnsChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.FailureMaxTurns = 10);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.FailureMaxTurns),
|
||||
nameof(TaskRowViewModel.FailureReasonTooltip),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasQueuedSubtasksChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.HasQueuedSubtasks = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.HasQueuedSubtasks),
|
||||
nameof(TaskRowViewModel.CanRemoveFromQueue),
|
||||
nameof(TaskRowViewModel.CanSendToQueue),
|
||||
nameof(TaskRowViewModel.CanQueuePlan),
|
||||
nameof(TaskRowViewModel.SendToQueueDisabledReason),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHoveredChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.IsHovered = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.IsHovered),
|
||||
nameof(TaskRowViewModel.ShowRowActions),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsSelectedChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.IsSelected = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.IsSelected),
|
||||
nameof(TaskRowViewModel.ShowRowActions),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlockedByTaskIdChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.BlockedByTaskId = "blocker");
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.BlockedByTaskId),
|
||||
nameof(TaskRowViewModel.IsQueued),
|
||||
nameof(TaskRowViewModel.IsWaiting),
|
||||
nameof(TaskRowViewModel.StatusChipClass),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasPlanningChildrenChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.HasPlanningChildren = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.HasPlanningChildren),
|
||||
nameof(TaskRowViewModel.IsPlanningParent),
|
||||
nameof(TaskRowViewModel.CanQueuePlan),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumberChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.Number = 5);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.Number),
|
||||
nameof(TaskRowViewModel.ShowNumberBadge),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BranchChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.Branch = "main");
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.Branch),
|
||||
nameof(TaskRowViewModel.HasBranch),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorktreePathChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.WorktreePath = @"C:\repo\worktrees\t");
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.WorktreePath),
|
||||
nameof(TaskRowViewModel.CanOpenWorktree),
|
||||
nameof(TaskRowViewModel.OpenWorktreeDisabledReason),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WorktreeStateChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.WorktreeState = ClaudeDo.Data.Models.WorktreeState.Active);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.WorktreeState),
|
||||
nameof(TaskRowViewModel.IsParked),
|
||||
nameof(TaskRowViewModel.StatusLabel),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoneChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.Done = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.Done),
|
||||
nameof(TaskRowViewModel.IsOverdue),
|
||||
nameof(TaskRowViewModel.CanAddToMyDay),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsMyDayChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.IsMyDay = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.IsMyDay),
|
||||
nameof(TaskRowViewModel.CanAddToMyDay),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScheduledForChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.ScheduledFor = DateTime.UtcNow);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.ScheduledFor),
|
||||
nameof(TaskRowViewModel.IsOverdue),
|
||||
nameof(TaskRowViewModel.HasSchedule),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiffAdditionsChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.DiffAdditions = 5);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.DiffAdditions),
|
||||
nameof(TaskRowViewModel.HasDiff),
|
||||
nameof(TaskRowViewModel.DiffAdditionsText),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DiffDeletionsChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.DiffDeletions = 5);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.DiffDeletions),
|
||||
nameof(TaskRowViewModel.HasDiff),
|
||||
nameof(TaskRowViewModel.DiffDeletionsText),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoadblockCountChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.RoadblockCount = 3);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.RoadblockCount),
|
||||
nameof(TaskRowViewModel.HasRoadblock),
|
||||
nameof(TaskRowViewModel.RoadblockTooltip),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropHintAboveChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.DropHintAbove = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.DropHintAbove),
|
||||
nameof(TaskRowViewModel.IsDropTarget),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropHintBelowChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.DropHintBelow = true);
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.DropHintBelow),
|
||||
nameof(TaskRowViewModel.IsDropTarget),
|
||||
}, raised);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreationPhaseChanged_Notifies_Dependents()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
var raised = Raised(vm, () => vm.CreationPhase = "creating_worktree");
|
||||
Assert.Equal(new HashSet<string?>
|
||||
{
|
||||
nameof(TaskRowViewModel.CreationPhase),
|
||||
nameof(TaskRowViewModel.HasCreationPhase),
|
||||
nameof(TaskRowViewModel.CreationPhaseLabel),
|
||||
}, raised);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.Tests;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
@@ -13,188 +14,7 @@ namespace ClaudeDo.Worker.Tests.UiVm;
|
||||
|
||||
// ── Fake worker client ────────────────────────────────────────────────────────
|
||||
|
||||
sealed class FakeWorkerClient : IWorkerClient
|
||||
{
|
||||
public int StartPlanningCalls { get; private set; }
|
||||
public int ResumePlanningCalls { get; private set; }
|
||||
public int DiscardPlanningCalls { get; private set; }
|
||||
public int FinalizePlanningCalls { get; private set; }
|
||||
public int WakeQueueCalls { get; private set; }
|
||||
public List<(string TaskId, TaskStatus Status)> SetTaskStatusCalls { get; } = new();
|
||||
|
||||
public bool IsConnected => false;
|
||||
public bool IsReconnecting => false;
|
||||
public string? LastApproveTarget => null;
|
||||
#pragma warning disable CS0067 // events required by IWorkerClient but not exercised by this fake
|
||||
public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
|
||||
public event Action<string, string, DateTime>? TaskStartedEvent;
|
||||
public event Action<string, string, string, DateTime>? TaskFinishedEvent;
|
||||
public event Action<string>? TaskUpdatedEvent;
|
||||
public event Action? ConnectionRestoredEvent;
|
||||
public event Action<string>? WorktreeUpdatedEvent;
|
||||
public event Action<string>? ListUpdatedEvent;
|
||||
public event Action<string, string>? TaskMessageEvent;
|
||||
public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
|
||||
public event Action<string, string, string>? TaskQuestionAskedEvent;
|
||||
public event Action<string, string>? TaskQuestionResolvedEvent;
|
||||
public event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
|
||||
public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId);
|
||||
public void RaiseWorktreeUpdated(string taskId) => WorktreeUpdatedEvent?.Invoke(taskId);
|
||||
public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line);
|
||||
public void RaiseOperationProgress(string opKey, string phase, int current, int total) => OperationProgressEvent?.Invoke(opKey, phase, current, total);
|
||||
|
||||
public Task RunNowAsync(string taskId) => Task.CompletedTask;
|
||||
public Task ContinueTaskAsync(string taskId, string followUpPrompt) => Task.CompletedTask;
|
||||
public Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer) => Task.CompletedTask;
|
||||
public Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId) => Task.FromResult<PendingQuestionDto?>(null);
|
||||
public Task ResetTaskAsync(string taskId) => Task.CompletedTask;
|
||||
public Task CancelTaskAsync(string taskId) => Task.CompletedTask;
|
||||
public Task<(bool Ok, string? Error)> DeleteTaskAsync(string taskId) => Task.FromResult<(bool, string?)>((true, null));
|
||||
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
||||
public Task RefreshAgentsAsync() => Task.CompletedTask;
|
||||
public Task<SeedResultDto?> RestoreDefaultAgentsAsync() => Task.FromResult<SeedResultDto?>(null);
|
||||
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
||||
public Task<List<string>> GetRepoImportFoldersAsync() => Task.FromResult(new List<string>());
|
||||
public Task SetRepoImportFoldersAsync(List<string> folders) => Task.CompletedTask;
|
||||
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
||||
public Task<List<SessionSkillDto>> GetSessionSkillsAsync() => Task.FromResult(new List<SessionSkillDto>());
|
||||
public Task<List<string>> InstallSessionSkillAsync(string url) => Task.FromResult(new List<string>());
|
||||
public Task UpdateSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
|
||||
public Task RemoveSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
|
||||
public Task<BaseDirtyWarningDto?> SetTaskStatusAsync(string taskId, TaskStatus status)
|
||||
{
|
||||
SetTaskStatusCalls.Add((taskId, status));
|
||||
return Task.FromResult<BaseDirtyWarningDto?>(null);
|
||||
}
|
||||
public Task SetTaskDoneAsync(string taskId) => Task.CompletedTask;
|
||||
public Task UnsetTaskDoneAsync(string taskId) => Task.CompletedTask;
|
||||
public Task DequeueTaskAsync(string taskId) => Task.CompletedTask;
|
||||
public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
||||
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
||||
public Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||
public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty<string>(), null));
|
||||
public Task<MergeConflictDocumentsDto> GetMergeConflictDocumentsAsync(string taskId) => Task.FromResult(new MergeConflictDocumentsDto(taskId, System.Array.Empty<ConflictDocumentDto>()));
|
||||
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
|
||||
public Task<MergeResultDto> ContinueConflictMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||
public Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;
|
||||
public Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask;
|
||||
public Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
|
||||
public Task CancelReviewAsync(string taskId) => Task.CompletedTask;
|
||||
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
|
||||
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
|
||||
public Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public Task<string> CreateMergeHelperTaskAsync(
|
||||
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
||||
=> Task.FromResult(Guid.NewGuid().ToString());
|
||||
public Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
||||
string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
||||
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
||||
public int PlanningStartSpecCalls { get; private set; }
|
||||
public int PlanningResumeSpecCalls { get; private set; }
|
||||
public Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
{ PlanningStartSpecCalls++; return Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>())); }
|
||||
public Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
||||
{ PlanningResumeSpecCalls++; return Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>())); }
|
||||
public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
|
||||
public Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
||||
{
|
||||
DiscardPlanningCalls++;
|
||||
return Task.FromResult(new DiscardPlanningOutcome(DiscardPlanningResult.Discarded, 0, 0));
|
||||
}
|
||||
public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) { FinalizePlanningCalls++; return Task.CompletedTask; }
|
||||
public Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
|
||||
public Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync() => Task.FromResult<IReadOnlyList<WorkerLogEntry>>(System.Array.Empty<WorkerLogEntry>());
|
||||
|
||||
public event Action? NotesUpdatedEvent;
|
||||
public event Action? PrepStartedEvent;
|
||||
public event Action<string>? PrepLineEvent;
|
||||
public event Action<bool>? PrepFinishedEvent;
|
||||
public event Action<string>? RefineStartedEvent;
|
||||
public event Action<string, bool, string?>? RefineFinishedEvent;
|
||||
public event Action<string, string, int, int>? OperationProgressEvent;
|
||||
public event Action<string, string, int>? MergeProgressEvent;
|
||||
public event Action<string, string>? PlanningMergeStartedEvent;
|
||||
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
||||
public event Action<string, string, IReadOnlyList<string>, bool>? PlanningMergeConflictEvent;
|
||||
public event Action<string, string?>? PlanningMergeAbortedEvent;
|
||||
public event Action<string>? PlanningCompletedEvent;
|
||||
public event Action<PrimeFiredEvent>? PrimeFired;
|
||||
public event Action<UsageSnapshotDto>? UsageUpdatedEvent;
|
||||
#pragma warning restore CS0067
|
||||
|
||||
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId) => Task.FromResult<MergeTargetsDto?>(null);
|
||||
public Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId) => Task.FromResult<IReadOnlyList<SubtaskDiffDto>>(Array.Empty<SubtaskDiffDto>());
|
||||
public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => Task.FromResult<CombinedDiffResultDto?>(null);
|
||||
public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
|
||||
public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
|
||||
public Task<IReadOnlyList<PlanningMergeConflictStateDto>> GetActiveExternalPlanningMergeConflictsAsync()
|
||||
=> Task.FromResult<IReadOnlyList<PlanningMergeConflictStateDto>>(Array.Empty<PlanningMergeConflictStateDto>());
|
||||
|
||||
public Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult<AppSettingsDto?>(null);
|
||||
public Task UpdateAppSettingsAsync(AppSettingsDto dto) => Task.CompletedTask;
|
||||
public Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync() => Task.FromResult(new List<PrimeScheduleDto>());
|
||||
public Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto) => Task.FromResult<PrimeScheduleDto?>(null);
|
||||
public Task DeletePrimeScheduleAsync(Guid id) => Task.CompletedTask;
|
||||
public Task UpdateListAsync(UpdateListDto dto) => Task.CompletedTask;
|
||||
public Task UpdateListConfigAsync(UpdateListConfigDto dto) => Task.CompletedTask;
|
||||
public Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null) => Task.FromResult<WorktreeCleanupDto?>(null);
|
||||
public Task<WorktreeResetDto?> ResetAllWorktreesAsync() => Task.FromResult<WorktreeResetDto?>(null);
|
||||
public Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId) => Task.FromResult(new List<WorktreeOverviewDto>());
|
||||
public Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState) => Task.FromResult((true, (string?)null));
|
||||
public Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId) => Task.FromResult<ForceRemoveResultDto?>(null);
|
||||
public Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult<string?>(null);
|
||||
public Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult("");
|
||||
public Task<bool> RunDailyPrepNowAsync() => Task.FromResult(false);
|
||||
public Task ClearMyDayAsync() => Task.CompletedTask;
|
||||
public Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day) => Task.FromResult(new List<DailyNoteDto>());
|
||||
public Task<List<DailyNoteDto>> GetDailyNotesBetweenAsync(DateOnly start, DateOnly end) => Task.FromResult(new List<DailyNoteDto>());
|
||||
public Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text) => Task.FromResult<DailyNoteDto?>(null);
|
||||
public Task UpdateDailyNoteAsync(string id, string text) => Task.CompletedTask;
|
||||
public Task DeleteDailyNoteAsync(string id) => Task.CompletedTask;
|
||||
public Task<string> GetLastPrepLogAsync() => Task.FromResult(string.Empty);
|
||||
public Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
||||
public Task<ClaudeBinDto?> GetClaudeBinAsync() => Task.FromResult<ClaudeBinDto?>(null);
|
||||
public Task<ClaudeBinDto?> SetClaudeBinAsync(string? value) => Task.FromResult<ClaudeBinDto?>(null);
|
||||
public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync() => Task.FromResult<OnlineInboxStateDto?>(null);
|
||||
public Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask;
|
||||
public Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask;
|
||||
public Task ClearOnlineInboxAuthAsync() => Task.CompletedTask;
|
||||
public IReadOnlyList<ActiveTaskDto> GetActiveTasks() => System.Array.Empty<ActiveTaskDto>();
|
||||
|
||||
public Task<UsageSnapshotDto?> GetUsageSnapshotAsync() => Task.FromResult<UsageSnapshotDto?>(null);
|
||||
public Task<UsageSnapshotDto?> RefreshUsageAsync() => Task.FromResult<UsageSnapshotDto?>(null);
|
||||
public Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to)
|
||||
=> Task.FromResult<IReadOnlyList<ModelUsageRowDto>>(System.Array.Empty<ModelUsageRowDto>());
|
||||
public Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to)
|
||||
=> Task.FromResult<IReadOnlyList<TaskUsageRowDto>>(System.Array.Empty<TaskUsageRowDto>());
|
||||
|
||||
public Task<TokenTrackerStatusDto?> GetTokenTrackerStatusAsync() =>
|
||||
Task.FromResult<TokenTrackerStatusDto?>(new TokenTrackerStatusDto(
|
||||
Installed: false, Version: null, NodeOk: true, NodeVersion: "v22.0.0",
|
||||
LastFetchedUtc: null, LastError: null, FormatVersion: null, SessionCount: 0));
|
||||
|
||||
public Task<TokenTrackerStatusDto?> RefreshTokenTrackerAsync() => GetTokenTrackerStatusAsync();
|
||||
|
||||
public Task<TokenTrackerStatusDto?> InstallTokenTrackerAsync() => GetTokenTrackerStatusAsync();
|
||||
|
||||
public Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync() =>
|
||||
Task.FromResult<TokenTrackerDashboardDto?>(new TokenTrackerDashboardDto(false, null, "not configured"));
|
||||
|
||||
public Task<TicketSettingsDto?> GetTicketSettingsAsync() => Task.FromResult<TicketSettingsDto?>(null);
|
||||
public Task SetTicketApiBaseUrlAsync(string? baseUrl) => Task.CompletedTask;
|
||||
public Task SetTicketTokenAsync(string token) => Task.CompletedTask;
|
||||
public Task ClearTicketTokenAsync() => Task.CompletedTask;
|
||||
public Task<TicketConnectionDto?> TestTicketConnectionAsync() => Task.FromResult<TicketConnectionDto?>(null);
|
||||
public Task<List<TicketProjectDto>> GetTicketProjectsAsync() => Task.FromResult(new List<TicketProjectDto>());
|
||||
public Task<TicketImportResultDto> ImportTicketsAsync(string listId) => Task.FromResult(new TicketImportResultDto(0, 0));
|
||||
}
|
||||
sealed class FakeWorkerClient : StubWorkerClient;
|
||||
|
||||
// ── Helper to build VM with pre-seeded Items ──────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user