fix(worker): kill cancelled runs' processes and make MCP approve actually merge
- CancelAsync now signals the running Claude process of the cancelled task and its cascaded children via the new RunCancellationRegistry (queue + override slots register their CTS there) instead of only flipping DB state. - external MCP review_task 'approve' now mirrors the hub's ApproveReview: unit merge for parents, ApproveAndMergeAsync for childless tasks, optional targetBranch; ReviewTaskResult carries mergeStatus/conflicts.
This commit is contained in:
@@ -4,6 +4,7 @@ using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.External;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Lifecycle;
|
||||
using ClaudeDo.Worker.Planning;
|
||||
using ClaudeDo.Worker.Queue;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
@@ -72,14 +73,18 @@ public sealed class AddSubtaskToolTests : IDisposable
|
||||
NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry(), new AttachmentStore(), new FakeSessionSkillSeeder());
|
||||
var waker = new ClaudeDo.Worker.Queue.QueueWaker();
|
||||
var picker = new ClaudeDo.Worker.Queue.QueuePicker(dbFactory);
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance);
|
||||
var queue = new QueueService(dbFactory, runner, cfg, NullLogger<QueueService>.Instance, waker, picker, overrideSlot, state);
|
||||
var runCancels = new RunCancellationRegistry();
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance, runCancels);
|
||||
var queue = new QueueService(dbFactory, runner, cfg, NullLogger<QueueService>.Instance, waker, picker, overrideSlot, state, runCancels);
|
||||
var maintenance = new WorktreeMaintenanceService(dbFactory, git, NullLogger<WorktreeMaintenanceService>.Instance);
|
||||
var merge = new TaskMergeService(dbFactory, git, broadcaster, TaskStateServiceBuilder.Build(dbFactory).State, NullLogger<TaskMergeService>.Instance);
|
||||
var merge = new TaskMergeService(dbFactory, git, broadcaster, state, NullLogger<TaskMergeService>.Instance);
|
||||
var aggregator = new PlanningAggregator(dbFactory, git, NullLogger<PlanningAggregator>.Instance);
|
||||
var planningMerge = new PlanningMergeOrchestrator(
|
||||
dbFactory, merge, aggregator, broadcaster, git, state, NullLogger<PlanningMergeOrchestrator>.Instance);
|
||||
return new ExternalMcpService(
|
||||
_tasks, _lists, queue, broadcaster,
|
||||
state,
|
||||
git, dbFactory, maintenance, merge);
|
||||
git, dbFactory, maintenance, merge, planningMerge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+11
-5
@@ -6,6 +6,7 @@ using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.External;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Lifecycle;
|
||||
using ClaudeDo.Worker.Planning;
|
||||
using ClaudeDo.Worker.Queue;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
@@ -64,11 +65,15 @@ public sealed class BatchMcpToolsTests : IDisposable
|
||||
var git = new GitService();
|
||||
var factory = _db.CreateFactory();
|
||||
var maintenance = new WorktreeMaintenanceService(factory, git, NullLogger<WorktreeMaintenanceService>.Instance);
|
||||
var merge = new TaskMergeService(factory, git, _broadcaster, TaskStateServiceBuilder.Build(factory).State, NullLogger<TaskMergeService>.Instance);
|
||||
var state = TaskStateServiceBuilder.Build(factory).State;
|
||||
var merge = new TaskMergeService(factory, git, _broadcaster, state, NullLogger<TaskMergeService>.Instance);
|
||||
var aggregator = new PlanningAggregator(factory, git, NullLogger<PlanningAggregator>.Instance);
|
||||
var planningMerge = new PlanningMergeOrchestrator(
|
||||
factory, merge, aggregator, _broadcaster, git, state, NullLogger<PlanningMergeOrchestrator>.Instance);
|
||||
var svc = new ExternalMcpService(
|
||||
_tasks, _lists, CreateQueue(), _broadcaster,
|
||||
TaskStateServiceBuilder.Build(factory).State,
|
||||
git, factory, maintenance, merge);
|
||||
state,
|
||||
git, factory, maintenance, merge, planningMerge);
|
||||
return new BatchMcpTools(svc);
|
||||
}
|
||||
|
||||
@@ -88,9 +93,10 @@ public sealed class BatchMcpToolsTests : IDisposable
|
||||
var state = TaskStateServiceBuilder.Build(dbFactory).State;
|
||||
var runner = new TaskRunner(new FakeClaudeProcess(), dbFactory, broadcaster, wtManager, new ClaudeArgsBuilder(), cfg,
|
||||
NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry(), new AttachmentStore(), new FakeSessionSkillSeeder());
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance);
|
||||
var runCancels = new RunCancellationRegistry();
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance, runCancels);
|
||||
return new QueueService(dbFactory, runner, cfg, NullLogger<QueueService>.Instance,
|
||||
new QueueWaker(), new QueuePicker(dbFactory), overrideSlot, state);
|
||||
new QueueWaker(), new QueuePicker(dbFactory), overrideSlot, state, runCancels);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
+90
-11
@@ -6,6 +6,7 @@ using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.External;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
using ClaudeDo.Worker.Lifecycle;
|
||||
using ClaudeDo.Worker.Planning;
|
||||
using ClaudeDo.Worker.Queue;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
@@ -126,11 +127,15 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var git = new GitService();
|
||||
var factory = _db.CreateFactory();
|
||||
var maintenance = new WorktreeMaintenanceService(factory, git, NullLogger<WorktreeMaintenanceService>.Instance);
|
||||
var merge = new TaskMergeService(factory, git, _broadcaster, TaskStateServiceBuilder.Build(factory).State, NullLogger<TaskMergeService>.Instance);
|
||||
var state = TaskStateServiceBuilder.Build(factory).State;
|
||||
var merge = new TaskMergeService(factory, git, _broadcaster, state, NullLogger<TaskMergeService>.Instance);
|
||||
var aggregator = new PlanningAggregator(factory, git, NullLogger<PlanningAggregator>.Instance);
|
||||
var planningMerge = new PlanningMergeOrchestrator(
|
||||
factory, merge, aggregator, _broadcaster, git, state, NullLogger<PlanningMergeOrchestrator>.Instance);
|
||||
return new ExternalMcpService(
|
||||
_tasks, _lists, queue, _broadcaster,
|
||||
TaskStateServiceBuilder.Build(factory).State,
|
||||
git, factory, maintenance, merge);
|
||||
state,
|
||||
git, factory, maintenance, merge, planningMerge);
|
||||
}
|
||||
|
||||
private QueueService CreateQueue()
|
||||
@@ -154,8 +159,9 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry(), new AttachmentStore(), new FakeSessionSkillSeeder());
|
||||
var waker = new ClaudeDo.Worker.Queue.QueueWaker();
|
||||
var picker = new ClaudeDo.Worker.Queue.QueuePicker(dbFactory);
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance);
|
||||
return new QueueService(dbFactory, runner, cfg, NullLogger<QueueService>.Instance, waker, picker, overrideSlot, state);
|
||||
var runCancels = new RunCancellationRegistry();
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance, runCancels);
|
||||
return new QueueService(dbFactory, runner, cfg, NullLogger<QueueService>.Instance, waker, picker, overrideSlot, state, runCancels);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -210,9 +216,10 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.WaitingForReview);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var dto = await sut.ReviewTask(task.Id, "approve", null, CancellationToken.None);
|
||||
var result = await sut.ReviewTask(task.Id, "approve", null, null, CancellationToken.None);
|
||||
|
||||
Assert.Equal("Done", dto.Status);
|
||||
Assert.Equal("Done", result.Task.Status);
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.MergeStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -223,7 +230,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
sut.ReviewTask(task.Id, "reject_rerun", null, CancellationToken.None));
|
||||
sut.ReviewTask(task.Id, "reject_rerun", null, null, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -233,9 +240,9 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var task = await SeedTaskAsync(listId, status: TaskStatus.WaitingForReview);
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
var dto = await sut.ReviewTask(task.Id, "reject_rerun", "fix it", CancellationToken.None);
|
||||
var result = await sut.ReviewTask(task.Id, "reject_rerun", "fix it", null, CancellationToken.None);
|
||||
|
||||
Assert.Equal("Queued", dto.Status);
|
||||
Assert.Equal("Queued", result.Task.Status);
|
||||
var loaded = await new TaskRepository(_db.CreateContext()).GetByIdAsync(task.Id);
|
||||
Assert.Equal("fix it", loaded!.ReviewFeedback);
|
||||
}
|
||||
@@ -248,7 +255,79 @@ public sealed class ExternalMcpServiceTests : IDisposable
|
||||
var sut = BuildSut(CreateQueue());
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
sut.ReviewTask(task.Id, "bogus", null, CancellationToken.None));
|
||||
sut.ReviewTask(task.Id, "bogus", null, null, CancellationToken.None));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReviewTask_Approve_WithActiveWorktree_MergesIntoTargetBranch()
|
||||
{
|
||||
if (!GitAvailable) return;
|
||||
var (task, list, wt) = await SeedWorktreeAsync(status: TaskStatus.WaitingForReview);
|
||||
|
||||
File.WriteAllText(Path.Combine(wt.WorktreePath, "feature.txt"), "x\n");
|
||||
GitRepoFixture.RunGit(wt.WorktreePath, "add", "feature.txt");
|
||||
GitRepoFixture.RunGit(wt.WorktreePath, "commit", "-m", "add feature");
|
||||
using (var ctx = _db.CreateContext())
|
||||
{
|
||||
var row = ctx.Worktrees.Single(w => w.TaskId == task.Id);
|
||||
row.HeadCommit = GitRepoFixture.RunGit(wt.WorktreePath, "rev-parse", "HEAD").Trim();
|
||||
ctx.SaveChanges();
|
||||
}
|
||||
|
||||
var sut = BuildSut(CreateQueue());
|
||||
var result = await sut.ReviewTask(task.Id, "approve", null, null, CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.MergeStatus);
|
||||
Assert.Equal("Done", result.Task.Status);
|
||||
Assert.True(File.Exists(Path.Combine(list.WorkingDir!, "feature.txt")));
|
||||
using var verify = _db.CreateContext();
|
||||
Assert.Equal(WorktreeState.Merged, verify.Worktrees.Single(w => w.TaskId == task.Id).State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReviewTask_Approve_ParentWithChildren_RunsUnitMerge()
|
||||
{
|
||||
if (!GitAvailable) return;
|
||||
var repo = new GitRepoFixture();
|
||||
_repos.Add(repo);
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "branch", "-m", "main");
|
||||
|
||||
var listId = Guid.NewGuid().ToString();
|
||||
var parentId = Guid.NewGuid().ToString();
|
||||
var childId = Guid.NewGuid().ToString();
|
||||
using (var ctx = _db.CreateContext())
|
||||
{
|
||||
ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", WorkingDir = repo.RepoDir, CreatedAt = DateTime.UtcNow });
|
||||
ctx.Tasks.Add(new TaskEntity { Id = parentId, ListId = listId, Title = "plan", CreatedAt = DateTime.UtcNow,
|
||||
Status = TaskStatus.WaitingForReview, PlanningPhase = PlanningPhase.Finalized });
|
||||
ctx.Tasks.Add(new TaskEntity { Id = childId, ListId = listId, Title = "child", CreatedAt = DateTime.UtcNow,
|
||||
ParentTaskId = parentId, Status = TaskStatus.Done, SortOrder = 1 });
|
||||
|
||||
var wtPath = Path.Combine(Path.GetTempPath(), $"wt_{Guid.NewGuid():N}");
|
||||
_worktreeCleanups.Add((repo.RepoDir, wtPath));
|
||||
var branch = $"claudedo/{childId[..8]}";
|
||||
GitRepoFixture.RunGit(repo.RepoDir, "worktree", "add", "-b", branch, wtPath, repo.BaseCommit);
|
||||
File.WriteAllText(Path.Combine(wtPath, "child.txt"), "c\n");
|
||||
GitRepoFixture.RunGit(wtPath, "add", "child.txt");
|
||||
GitRepoFixture.RunGit(wtPath, "commit", "-m", "add child.txt");
|
||||
ctx.Worktrees.Add(new WorktreeEntity
|
||||
{
|
||||
TaskId = childId, Path = wtPath, BranchName = branch,
|
||||
BaseCommit = repo.BaseCommit,
|
||||
HeadCommit = GitRepoFixture.RunGit(wtPath, "rev-parse", "HEAD").Trim(),
|
||||
State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var sut = BuildSut(CreateQueue());
|
||||
var result = await sut.ReviewTask(parentId, "approve", null, "main", CancellationToken.None);
|
||||
|
||||
Assert.Equal(TaskMergeService.StatusMerged, result.MergeStatus);
|
||||
Assert.Equal("Done", result.Task.Status);
|
||||
Assert.True(File.Exists(Path.Combine(repo.RepoDir, "child.txt")));
|
||||
using var verify = _db.CreateContext();
|
||||
Assert.Equal(WorktreeState.Merged, verify.Worktrees.Single(w => w.TaskId == childId).State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -17,13 +17,15 @@ public static class TaskStateServiceBuilder
|
||||
PlanningChainCoordinator Chain,
|
||||
CapturingHubContext Hub,
|
||||
Func<int> WakeCount,
|
||||
CountingQueueWaker Waker);
|
||||
CountingQueueWaker Waker,
|
||||
RunCancellationRegistry RunCancels);
|
||||
|
||||
public static Built Build(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||
{
|
||||
var hub = new CapturingHubContext();
|
||||
var broadcaster = new HubBroadcaster(hub);
|
||||
var waker = new CountingQueueWaker();
|
||||
var runCancels = new RunCancellationRegistry();
|
||||
|
||||
TaskStateService? state = null;
|
||||
var chain = new PlanningChainCoordinator(dbFactory, () => state!);
|
||||
@@ -32,9 +34,10 @@ public static class TaskStateServiceBuilder
|
||||
broadcaster,
|
||||
waker,
|
||||
chain,
|
||||
runCancels,
|
||||
NullLogger<TaskStateService>.Instance);
|
||||
|
||||
return new Built(state, chain, hub, () => waker.Count, waker);
|
||||
return new Built(state, chain, hub, () => waker.Count, waker, runCancels);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using ClaudeDo.Worker.Queue;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Queue;
|
||||
|
||||
public sealed class RunCancellationRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryCancel_RegisteredTask_CancelsAndReturnsTrue()
|
||||
{
|
||||
var sut = new RunCancellationRegistry();
|
||||
using var cts = new CancellationTokenSource();
|
||||
sut.Register("t1", cts);
|
||||
|
||||
Assert.True(sut.TryCancel("t1"));
|
||||
Assert.True(cts.IsCancellationRequested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCancel_UnknownTask_ReturnsFalse()
|
||||
{
|
||||
var sut = new RunCancellationRegistry();
|
||||
Assert.False(sut.TryCancel("nope"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unregister_RemovesOnlyTheGivenRegistration()
|
||||
{
|
||||
var sut = new RunCancellationRegistry();
|
||||
using var stale = new CancellationTokenSource();
|
||||
using var current = new CancellationTokenSource();
|
||||
|
||||
sut.Register("t1", stale);
|
||||
sut.Register("t1", current); // re-run replaced the registration
|
||||
sut.Unregister("t1", stale); // late cleanup of the old slot must not evict the new one
|
||||
|
||||
Assert.True(sut.TryCancel("t1"));
|
||||
Assert.True(current.IsCancellationRequested);
|
||||
Assert.False(stale.IsCancellationRequested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryCancel_DisposedCts_ReturnsFalse()
|
||||
{
|
||||
var sut = new RunCancellationRegistry();
|
||||
var cts = new CancellationTokenSource();
|
||||
sut.Register("t1", cts);
|
||||
cts.Dispose();
|
||||
|
||||
Assert.False(sut.TryCancel("t1"));
|
||||
}
|
||||
}
|
||||
@@ -52,13 +52,14 @@ public sealed class QueueServiceSlotGuardTests : IDisposable
|
||||
var dbFactory = _db.CreateFactory();
|
||||
var wtManager = new WorktreeManager(new ClaudeDo.Data.Git.GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
|
||||
var argsBuilder = new ClaudeArgsBuilder();
|
||||
var state = TaskStateServiceBuilder.Build(dbFactory).State;
|
||||
var built = TaskStateServiceBuilder.Build(dbFactory);
|
||||
var state = built.State;
|
||||
var runner = new TaskRunner(fake, dbFactory, broadcaster, wtManager, argsBuilder, _cfg,
|
||||
NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry(), new AttachmentStore(), new FakeSessionSkillSeeder());
|
||||
_waker = new QueueWaker();
|
||||
var picker = new QueuePicker(dbFactory);
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance);
|
||||
var service = new QueueService(dbFactory, runner, _cfg, NullLogger<QueueService>.Instance, _waker, picker, overrideSlot, state);
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance, built.RunCancels);
|
||||
var service = new QueueService(dbFactory, runner, _cfg, NullLogger<QueueService>.Instance, _waker, picker, overrideSlot, state, built.RunCancels);
|
||||
return (service, fake);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,13 +53,14 @@ public sealed class QueueServiceTests : IDisposable
|
||||
var dbFactory = _db.CreateFactory();
|
||||
var wtManager = new WorktreeManager(new GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
|
||||
var argsBuilder = new ClaudeArgsBuilder();
|
||||
var state = TaskStateServiceBuilder.Build(dbFactory).State;
|
||||
var built = TaskStateServiceBuilder.Build(dbFactory);
|
||||
var state = built.State;
|
||||
var runner = new TaskRunner(fake, dbFactory, broadcaster, wtManager, argsBuilder, _cfg,
|
||||
NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry(), new AttachmentStore(), new FakeSessionSkillSeeder());
|
||||
_waker = new QueueWaker();
|
||||
var picker = new QueuePicker(dbFactory);
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance);
|
||||
var service = new QueueService(dbFactory, runner, _cfg, NullLogger<QueueService>.Instance, _waker, picker, overrideSlot, state);
|
||||
var overrideSlot = new OverrideSlotService(dbFactory, runner, NullLogger<OverrideSlotService>.Instance, built.RunCancels);
|
||||
var service = new QueueService(dbFactory, runner, _cfg, NullLogger<QueueService>.Instance, _waker, picker, overrideSlot, state, built.RunCancels);
|
||||
return (service, fake);
|
||||
}
|
||||
|
||||
|
||||
@@ -214,6 +214,41 @@ public sealed class WaitingForChildrenLifecycleTests : IDisposable
|
||||
Assert.Null(queuedChild.BlockedByTaskId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancelling_WaitingForChildren_parent_stops_running_child_process()
|
||||
{
|
||||
using (var ctx = _db.CreateContext())
|
||||
{
|
||||
ctx.Lists.Add(new ListEntity { Id = "l1", Name = "L", CreatedAt = DateTime.UtcNow });
|
||||
ctx.Tasks.Add(new TaskEntity { Id = "par", ListId = "l1", Title = "Parent",
|
||||
Status = TaskStatus.WaitingForChildren, CreatedAt = DateTime.UtcNow });
|
||||
ctx.Tasks.Add(new TaskEntity { Id = "c_running", ListId = "l1", Title = "Running child",
|
||||
Status = TaskStatus.Running, ParentTaskId = "par", CreatedAt = DateTime.UtcNow });
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
using var childCts = new CancellationTokenSource();
|
||||
_built.RunCancels.Register("c_running", childCts);
|
||||
|
||||
var result = await _built.State.CancelAsync("par", DateTime.UtcNow, default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.True(childCts.IsCancellationRequested);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cancelling_running_task_signals_its_registered_run()
|
||||
{
|
||||
var id = await SeedRunningStandaloneAsync();
|
||||
using var cts = new CancellationTokenSource();
|
||||
_built.RunCancels.Register(id, cts);
|
||||
|
||||
var result = await _built.State.CancelAsync(id, DateTime.UtcNow, default);
|
||||
|
||||
Assert.True(result.Ok);
|
||||
Assert.True(cts.IsCancellationRequested);
|
||||
}
|
||||
|
||||
// ─── FinalizePlanningAsync ────────────────────────────────────────────
|
||||
|
||||
private async Task<string> SeedActivePlanningParentAsync(string id = "par")
|
||||
|
||||
Reference in New Issue
Block a user