Files
ClaudeDo/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs
T
Mika Kuns 08d1797ead chore(claude-do): Mission Control: allgemeiner Session-Handoff an eine frische
## Ziel

Jede Mission-Control-Session (Ad-hoc UND Task-Session) soll ihre Arbeit an eine frische Session
uebergeben koennen: "gib das ab an eine neue Session". Die alte Kachel wird geschlossen, an
derselben Position oeffnet eine frische Session im selben cwd, die mit einem Brief startet, den
die abgebende Session selbst geschrieben hat.

Motivation: eine lange Session mit vollem/verrottetem Kontex

ClaudeDo-Task: 8b846deb-ad10-4243-9272-9aea2a01ec93
2026-09-28 11:57:37 +02:00

230 lines
20 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>, string>? HandoffRequestedEvent;
public event Action<string, string>? SessionHandoffRequestedEvent;
public event Action<string, IReadOnlyList<string>>? ListHandlerStartRequestedEvent;
public event Action<string, string>? HandlerUsageWarningEvent;
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>? 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 int ClearMyDayCalls { get; private set; }
public int RunDailyPrepNowCalls { get; private set; }
public virtual IReadOnlyList<ActiveTaskDto> GetActiveTasks() => System.Array.Empty<ActiveTaskDto>();
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 RaiseListUpdated(string listId) => ListUpdatedEvent?.Invoke(listId);
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 RaiseListHandlerStartRequested(string listId, IReadOnlyList<string> taskIds) => ListHandlerStartRequestedEvent?.Invoke(listId, taskIds);
public void RaiseHandoffRequested(string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase = "wait") => HandoffRequestedEvent?.Invoke(taskId, survivingTaskIds, nextPhase);
public void RaiseSessionHandoffRequested(string paneId, string briefPath) => SessionHandoffRequestedEvent?.Invoke(paneId, briefPath);
public void RaiseHandlerUsageWarning(string taskId, string message) => HandlerUsageWarningEvent?.Invoke(taskId, message);
public void RaisePlanningMergeConflict(string planningTaskId, string subtaskId, IReadOnlyList<string> files, bool externallyDriven)
=> PlanningMergeConflictEvent?.Invoke(planningTaskId, subtaskId, files, externallyDriven);
public void RaiseOperationProgress(string opKey, string phase, int current, int total) => OperationProgressEvent?.Invoke(opKey, phase, current, total);
public void RaisePlanningMergeStarted(string planningTaskId, string targetBranch) => PlanningMergeStartedEvent?.Invoke(planningTaskId, targetBranch);
public void RaisePlanningMergeAborted(string planningTaskId, string? reason = null) => PlanningMergeAbortedEvent?.Invoke(planningTaskId, reason);
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 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;
public virtual Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch, bool autostash = false) => 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, bool autostash = false) => 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, string paneId, CancellationToken ct = default)
=> Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
public virtual Task<LaunchSpec> GetSessionHandoffLaunchSpecAsync(string paneId, string cwd, string briefPath, string? taskId, CancellationToken ct = default)
=> Task.FromResult(new LaunchSpec(cwd, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
public virtual Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, string? model = null, 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, string nextPhase, CancellationToken ct = default)
=> Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
public virtual Task ReportInteractiveSessionExitedAsync(string taskId, int exitCode, CancellationToken ct = default)
=> Task.CompletedTask;
public virtual Task<IReadOnlyList<HandlerOpenTaskDto>> GetHandlerOpenTasksAsync(string handlerTaskId, CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<HandlerOpenTaskDto>>(Array.Empty<HandlerOpenTaskDto>());
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<List<DailyNoteDto>> GetDailyNotesBetweenAsync(DateOnly start, DateOnly end) => 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 virtual Task RefineTaskAsync(string taskId) => Task.CompletedTask;
public virtual Task<ClaudeBinDto?> GetClaudeBinAsync() => Task.FromResult<ClaudeBinDto?>(null);
public virtual Task<ClaudeBinDto?> SetClaudeBinAsync(string? value) => Task.FromResult<ClaudeBinDto?>(null);
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>());
// "Not installed, but Node is fine" — the state that exercises the hint card and the
// install button without ever touching a real CLI.
public virtual 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 virtual Task<TokenTrackerStatusDto?> RefreshTokenTrackerAsync() => GetTokenTrackerStatusAsync();
public virtual Task<TokenTrackerStatusDto?> InstallTokenTrackerAsync() => GetTokenTrackerStatusAsync();
public virtual Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync() =>
Task.FromResult<TokenTrackerDashboardDto?>(new TokenTrackerDashboardDto(true, "http://127.0.0.1:7681/", null));
public virtual Task<TicketSettingsDto?> GetTicketSettingsAsync() => Task.FromResult<TicketSettingsDto?>(null);
public virtual Task SetTicketApiBaseUrlAsync(string? baseUrl) => Task.CompletedTask;
public virtual Task SetTicketTokenAsync(string token) => Task.CompletedTask;
public virtual Task ClearTicketTokenAsync() => Task.CompletedTask;
public virtual Task<TicketConnectionDto?> TestTicketConnectionAsync() => Task.FromResult<TicketConnectionDto?>(null);
public virtual Task<List<TicketProjectDto>> GetTicketProjectsAsync() => Task.FromResult(new List<TicketProjectDto>());
public virtual Task<TicketImportResultDto> ImportTicketsAsync(string listId) => Task.FromResult(new TicketImportResultDto(0, 0));
public void RaiseUsageUpdated(UsageSnapshotDto snapshot) => UsageUpdatedEvent?.Invoke(snapshot);
protected void RaisePropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}