Ein "Abandon run"-Knopf an Handler-Panes fragt HandlerRoundState.Get(handlerTaskId) nach den noch offenen (Queued/Running) Tasks des Laufs, bestaetigt mit Anzahl, cancelt jeden ueber den bestehenden CancelTaskAsync-Pfad und schliesst danach die Session -- ein Schritt statt Tile schliessen + Queue separat abbrechen. Ohne offene Tasks (oder ohne Round-Eintrag) schliesst die Aktion direkt, ohne Dialog. Nur an den beiden Handler-Panes sichtbar (IsListHandler), normale Task-/Ad-hoc- Panes bleiben unveraendert.
142 lines
6.1 KiB
C#
142 lines
6.1 KiB
C#
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Git;
|
|
using ClaudeDo.Data.Models;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Worker.Config;
|
|
using ClaudeDo.Worker.Hub;
|
|
using ClaudeDo.Worker.Lifecycle;
|
|
using ClaudeDo.Worker.Runner;
|
|
using ClaudeDo.Worker.Skills;
|
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
|
using Xunit;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Hub;
|
|
|
|
/// Covers WorkerHub.GetHandlerOpenTasks -- the Mission Control "abandon run" action's read side.
|
|
/// The round's TaskIds come from HandlerRoundState (structured store, not the handler task's
|
|
/// description); only rows still Queued or Running are returned, so WaitingForReview/Done/Failed/
|
|
/// Cancelled/Idle work is never touched by a subsequent cancel loop.
|
|
public sealed class HandlerOpenTasksHubTests : IDisposable
|
|
{
|
|
private readonly DbFixture _db = new();
|
|
private readonly ClaudeDoDbContext _ctx;
|
|
private readonly TaskRepository _tasks;
|
|
private readonly ListRepository _lists;
|
|
private readonly string _roundsPath = Path.Combine(Path.GetTempPath(), $"claudedo_rounds_{Guid.NewGuid():N}.json");
|
|
private readonly HandlerRoundState _rounds;
|
|
|
|
private sealed class FakeSessionSkillRegistry : ISessionSkillRegistry
|
|
{
|
|
public Task<IReadOnlyList<string>> InstallAsync(string url, CancellationToken ct) => throw new NotImplementedException();
|
|
public Task UpdateAsync(string sourceUrl, CancellationToken ct) => throw new NotImplementedException();
|
|
public Task RemoveAsync(string sourceUrl, CancellationToken ct) => throw new NotImplementedException();
|
|
public Task<IReadOnlyList<SessionSkillEntity>> ListAsync(CancellationToken ct)
|
|
=> Task.FromResult<IReadOnlyList<SessionSkillEntity>>(Array.Empty<SessionSkillEntity>());
|
|
}
|
|
|
|
public HandlerOpenTasksHubTests()
|
|
{
|
|
_ctx = _db.CreateContext();
|
|
_tasks = new TaskRepository(_ctx);
|
|
_lists = new ListRepository(_ctx);
|
|
_rounds = new HandlerRoundState(_roundsPath);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_ctx.Dispose();
|
|
_db.Dispose();
|
|
try { File.Delete(_roundsPath); } catch { /* best effort */ }
|
|
}
|
|
|
|
private WorkerHub CreateHub()
|
|
{
|
|
var factory = _db.CreateFactory();
|
|
var git = new GitService();
|
|
var wtManager = new WorktreeManager(git, factory, new WorkerConfig(), Microsoft.Extensions.Logging.Abstractions.NullLogger<WorktreeManager>.Instance);
|
|
var interactiveLaunchSpec = new InteractiveLaunchSpecService(
|
|
factory, new FakeSessionSkillSeeder(), new FakeSessionSkillRegistry(), wtManager, git,
|
|
new WorkerConfig { ClaudeBin = "claude" }, new ClaudeDo.Worker.External.HandoffRoundTracker(), _rounds);
|
|
var built = TaskStateServiceBuilder.Build(factory);
|
|
|
|
var hub = new WorkerHub(
|
|
null!, null!, null!, null!, null!, factory, null!, null!, null!,
|
|
null!, null!, null!, null!, null!, null!, null!, built.State, null!, null!,
|
|
null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
|
|
new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!,
|
|
logBuffer: null, interactiveLaunchSpec: interactiveLaunchSpec, worktreeManager: wtManager, git: git,
|
|
roundState: _rounds);
|
|
return hub;
|
|
}
|
|
|
|
private async Task<string> SeedListAsync(string name = "L")
|
|
{
|
|
var listId = Guid.NewGuid().ToString();
|
|
await _lists.AddAsync(new ListEntity { Id = listId, Name = name, WorkingDir = Path.GetTempPath(), CreatedAt = DateTime.UtcNow });
|
|
return listId;
|
|
}
|
|
|
|
private async Task<TaskEntity> SeedTaskAsync(string listId, TaskStatus status, string title = "T")
|
|
{
|
|
var task = new TaskEntity
|
|
{
|
|
Id = Guid.NewGuid().ToString(),
|
|
ListId = listId,
|
|
Title = title,
|
|
Status = status,
|
|
CreatedAt = DateTime.UtcNow,
|
|
};
|
|
await _tasks.AddAsync(task);
|
|
return task;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task NoRoundEntry_ReturnsEmpty()
|
|
{
|
|
var hub = CreateHub();
|
|
var result = await hub.GetHandlerOpenTasks("no-such-handler-task");
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RoundWithOnlyTerminalTasks_ReturnsEmpty()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var done = await SeedTaskAsync(listId, TaskStatus.Done, "Done task");
|
|
var review = await SeedTaskAsync(listId, TaskStatus.WaitingForReview, "Review task");
|
|
var failed = await SeedTaskAsync(listId, TaskStatus.Failed, "Failed task");
|
|
var cancelled = await SeedTaskAsync(listId, TaskStatus.Cancelled, "Cancelled task");
|
|
var idle = await SeedTaskAsync(listId, TaskStatus.Idle, "Idle task");
|
|
var handlerTaskId = "handler-1";
|
|
_rounds.Save(new HandlerRound(handlerTaskId, "wait",
|
|
new[] { done.Id, review.Id, failed.Id, cancelled.Id, idle.Id }, DateTime.UtcNow));
|
|
|
|
var hub = CreateHub();
|
|
var result = await hub.GetHandlerOpenTasks(handlerTaskId);
|
|
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task RoundWithMixedTasks_ReturnsOnlyQueuedAndRunning()
|
|
{
|
|
var listId = await SeedListAsync();
|
|
var queued = await SeedTaskAsync(listId, TaskStatus.Queued, "Queued task");
|
|
var running = await SeedTaskAsync(listId, TaskStatus.Running, "Running task");
|
|
var done = await SeedTaskAsync(listId, TaskStatus.Done, "Done task");
|
|
var notInRound = await SeedTaskAsync(listId, TaskStatus.Queued, "Unrelated queued task");
|
|
var handlerTaskId = "handler-2";
|
|
_rounds.Save(new HandlerRound(handlerTaskId, "wait",
|
|
new[] { queued.Id, running.Id, done.Id }, DateTime.UtcNow));
|
|
|
|
var hub = CreateHub();
|
|
var result = await hub.GetHandlerOpenTasks(handlerTaskId);
|
|
|
|
Assert.Equal(2, result.Count);
|
|
Assert.Contains(result, r => r.Id == queued.Id && r.Title == "Queued task");
|
|
Assert.Contains(result, r => r.Id == running.Id && r.Title == "Running task");
|
|
Assert.DoesNotContain(result, r => r.Id == notInRound.Id);
|
|
}
|
|
}
|