Queuing (update_task_status, batch_update_task_status) and run_task_now now surface a non-blocking baseDirty warning (separate modified/untracked counts) when the list's working dir has uncommitted changes at enqueue time, since a new worktree forks from the commit tip and silently misses them. BaseDirtyChecker caches per working dir for a few seconds so a batch queue over many tasks in one list only shells out to git once. The UI surfaces the same warning via the footer error strip on queue actions.
182 lines
15 KiB
C#
182 lines
15 KiB
C#
using System.ComponentModel;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Ui.Services;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Ui.Tests;
|
|
|
|
/// <summary>
|
|
/// No-op <see cref="IWorkerClient"/> base for tests. Override only the members a
|
|
/// test actually exercises; everything else returns a benign default. Keeping the
|
|
/// full interface surface here means new interface members only break this file.
|
|
/// </summary>
|
|
public abstract class StubWorkerClient : IWorkerClient
|
|
{
|
|
#pragma warning disable CS0067 // interface-mandated events; tests don't raise them
|
|
public event 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>>? HandoffRequestedEvent;
|
|
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>? PlanningMergeStartedEvent;
|
|
public event Action<string, string>? PlanningSubtaskMergedEvent;
|
|
public event Action<string, string, IReadOnlyList<string>, bool>? PlanningMergeConflictEvent;
|
|
public event Action<string>? PlanningMergeAbortedEvent;
|
|
public event Action<string>? PlanningCompletedEvent;
|
|
public event Action<PrimeFiredEvent>? PrimeFired;
|
|
public event Action<UsageSnapshotDto>? UsageUpdatedEvent;
|
|
#pragma warning restore CS0067
|
|
|
|
public int ClearMyDayCalls { get; private set; }
|
|
public int RunDailyPrepNowCalls { get; private set; }
|
|
|
|
public virtual IReadOnlyList<ActiveTask> GetActiveTasks() => System.Array.Empty<ActiveTask>();
|
|
|
|
public void RaiseTaskStarted(string slot, string taskId, DateTime startedAt) => TaskStartedEvent?.Invoke(slot, taskId, startedAt);
|
|
public void RaiseTaskFinished(string slot, string taskId, string status, DateTime finishedAt) => TaskFinishedEvent?.Invoke(slot, taskId, status, finishedAt);
|
|
public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line);
|
|
public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId);
|
|
public void RaiseConnectionRestored() => ConnectionRestoredEvent?.Invoke();
|
|
public void RaiseTaskQuestionAsked(string taskId, string questionId, string question) => TaskQuestionAskedEvent?.Invoke(taskId, questionId, question);
|
|
public void RaiseTaskQuestionResolved(string taskId, string questionId) => TaskQuestionResolvedEvent?.Invoke(taskId, questionId);
|
|
public void RaiseHandoffRequested(string taskId, IReadOnlyList<string> survivingTaskIds) => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds);
|
|
public void RaisePlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList<string> files, bool externallyDriven)
|
|
=> PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, files, externallyDriven);
|
|
public void RaisePlanningMergeStarted(string planningTaskId, string targetBranch) => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch);
|
|
public void RaisePlanningMergeAborted(string planningTaskId) => PlanningMergeAbortedEvent?.Invoke(planningTaskId);
|
|
public void RaisePlanningCompleted(string planningTaskId) => PlanningCompletedEvent?.Invoke(planningTaskId);
|
|
|
|
public void RaisePrepStarted() => PrepStartedEvent?.Invoke();
|
|
public void RaisePrepLine(string line) => PrepLineEvent?.Invoke(line);
|
|
public void RaisePrepFinished(bool ok) => PrepFinishedEvent?.Invoke(ok);
|
|
|
|
public virtual bool IsConnected => false;
|
|
public virtual bool IsReconnecting => false;
|
|
public virtual string? LastApproveTarget => null;
|
|
|
|
public virtual Task WakeQueueAsync() => Task.CompletedTask;
|
|
public virtual Task RunNowAsync(string taskId) => Task.CompletedTask;
|
|
public virtual Task ContinueTaskAsync(string taskId, string followUpPrompt) => Task.CompletedTask;
|
|
public (string TaskId, string QuestionId, string Answer)? LastAnswer { get; private set; }
|
|
public virtual Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer)
|
|
{
|
|
LastAnswer = (taskId, questionId, answer);
|
|
return Task.CompletedTask;
|
|
}
|
|
public PendingQuestionDto? PendingQuestion;
|
|
public virtual Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId) => Task.FromResult(PendingQuestion);
|
|
public virtual Task ResetTaskAsync(string taskId) => Task.CompletedTask;
|
|
public virtual Task CancelTaskAsync(string taskId) => Task.CompletedTask;
|
|
public virtual Task<(bool Ok, string? Error)> DeleteTaskAsync(string taskId) => Task.FromResult<(bool, string?)>((true, null));
|
|
public virtual Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
|
public virtual Task RefreshAgentsAsync() => Task.CompletedTask;
|
|
public virtual Task<SeedResultDto?> RestoreDefaultAgentsAsync() => Task.FromResult<SeedResultDto?>(null);
|
|
public virtual Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
|
public virtual Task<List<string>> GetRepoImportFoldersAsync() => Task.FromResult(new List<string>());
|
|
public virtual Task SetRepoImportFoldersAsync(List<string> folders) => Task.CompletedTask;
|
|
public virtual Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
|
public virtual Task<List<SessionSkillDto>> GetSessionSkillsAsync() => Task.FromResult(new List<SessionSkillDto>());
|
|
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 virtual Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
|
public virtual Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
|
public virtual Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
|
public virtual Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask;
|
|
public virtual Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
|
|
public virtual Task CancelReviewAsync(string taskId) => Task.CompletedTask;
|
|
public virtual Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty<string>(), null));
|
|
public virtual Task<MergeConflictDocumentsDto> GetMergeConflictDocumentsAsync(string taskId) => Task.FromResult(new MergeConflictDocumentsDto(taskId, System.Array.Empty<ConflictDocumentDto>()));
|
|
public virtual Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
|
|
public virtual Task<MergeResultDto> ContinueConflictMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
|
public virtual Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;
|
|
public virtual Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
|
public virtual Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
|
public virtual Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
|
public virtual Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
|
|
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
|
public virtual Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, CancellationToken ct = default)
|
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
|
public virtual Task<string> CreateMergeHelperTaskAsync(
|
|
IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
|
|
=> Task.FromResult(Guid.NewGuid().ToString());
|
|
public virtual Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
|
|
string taskId, IReadOnlyList<string> survivingTaskIds, CancellationToken ct = default)
|
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
|
public virtual Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
|
public virtual Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
|
|
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
|
|
public virtual Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
|
public virtual Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
|
|
=> Task.FromResult(new DiscardPlanningOutcome(DiscardPlanningResult.Discarded, 0, 0));
|
|
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)
|
|
=> Task.FromResult<CombinedDiffResultDto?>(null);
|
|
public virtual Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
|
|
public virtual Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
|
|
public virtual Task<IReadOnlyList<PlanningMergeConflictStateDto>> GetActiveExternalPlanningMergeConflictsAsync()
|
|
=> Task.FromResult<IReadOnlyList<PlanningMergeConflictStateDto>>(Array.Empty<PlanningMergeConflictStateDto>());
|
|
public virtual Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask;
|
|
public virtual Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult<string?>(null);
|
|
public virtual Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult("");
|
|
public virtual Task<bool> RunDailyPrepNowAsync() { RunDailyPrepNowCalls++; return Task.FromResult(false); }
|
|
public virtual Task ClearMyDayAsync() { ClearMyDayCalls++; return Task.CompletedTask; }
|
|
public virtual Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult<AppSettingsDto?>(null);
|
|
public virtual Task UpdateAppSettingsAsync(AppSettingsDto dto) => Task.CompletedTask;
|
|
public virtual Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync() => Task.FromResult(new List<PrimeScheduleDto>());
|
|
public virtual Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto) => Task.FromResult<PrimeScheduleDto?>(null);
|
|
public virtual Task DeletePrimeScheduleAsync(Guid id) => Task.CompletedTask;
|
|
public virtual Task UpdateListAsync(UpdateListDto dto) => Task.CompletedTask;
|
|
public virtual Task UpdateListConfigAsync(UpdateListConfigDto dto) => Task.CompletedTask;
|
|
public virtual Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null) => Task.FromResult<WorktreeCleanupDto?>(null);
|
|
public virtual Task<WorktreeResetDto?> ResetAllWorktreesAsync() => Task.FromResult<WorktreeResetDto?>(null);
|
|
public virtual Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId) => Task.FromResult(new List<WorktreeOverviewDto>());
|
|
public virtual Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState) => Task.FromResult((true, (string?)null));
|
|
public virtual Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId) => Task.FromResult<ForceRemoveResultDto?>(null);
|
|
public virtual Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day) => Task.FromResult(new List<DailyNoteDto>());
|
|
public virtual Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text) => Task.FromResult<DailyNoteDto?>(null);
|
|
public virtual Task UpdateDailyNoteAsync(string id, string text) => Task.CompletedTask;
|
|
public virtual Task DeleteDailyNoteAsync(string id) => Task.CompletedTask;
|
|
public string LastPrepLog = "";
|
|
public virtual Task<string> GetLastPrepLogAsync() => Task.FromResult(LastPrepLog);
|
|
public WorkerBuildInfoDto? WorkerBuildInfo;
|
|
public virtual Task<WorkerBuildInfoDto?> GetWorkerBuildInfoAsync() => Task.FromResult(WorkerBuildInfo);
|
|
public virtual Task RefineTaskAsync(string taskId) => Task.CompletedTask;
|
|
|
|
public virtual Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync() => Task.FromResult<OnlineInboxStateDto?>(null);
|
|
public virtual Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask;
|
|
public virtual Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask;
|
|
public virtual Task ClearOnlineInboxAuthAsync() => Task.CompletedTask;
|
|
|
|
public virtual Task<UsageSnapshotDto?> GetUsageSnapshotAsync() => Task.FromResult<UsageSnapshotDto?>(null);
|
|
public virtual Task<UsageSnapshotDto?> RefreshUsageAsync() => Task.FromResult<UsageSnapshotDto?>(null);
|
|
public virtual Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to)
|
|
=> Task.FromResult<IReadOnlyList<ModelUsageRowDto>>(Array.Empty<ModelUsageRowDto>());
|
|
public virtual Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to)
|
|
=> Task.FromResult<IReadOnlyList<TaskUsageRowDto>>(Array.Empty<TaskUsageRowDto>());
|
|
public void RaiseUsageUpdated(UsageSnapshotDto snapshot) => UsageUpdatedEvent?.Invoke(snapshot);
|
|
|
|
protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
|
}
|