test(worker): consolidate fakes into Infrastructure/, drop tag-era names

- Extract FakeClaudeProcess to Infrastructure/FakeClaudeProcess.cs (was
  defined inline in QueueServiceTests #region); all consumers updated
- Replace duplicate FakeHubContext/FakeHubClients/FakeClientProxy
  (QueueServiceTests) with existing CapturingHubContext from Infrastructure
  across all 7 affected files; Planning's file-local FakeHubContext kept
- Rename SeedListWithAgentTag → SeedListAsync (return Task<string>, drop
  unused agentTagId tuple element) and SeedListWithAgentTagAsync → SeedListAsync
- PrimeRunnerTests keeps its private nested FakeClaudeProcess: constructor
  API (delay/exitCode/lines/result params) differs from the shared one and
  replacement would require rewriting every test in that file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-09 23:04:59 +02:00
parent ddeded988a
commit d4af345ac3
9 changed files with 50 additions and 84 deletions

View File

@@ -5,7 +5,7 @@ xUnit integration tests for the Worker and Data layers. One of six test projects
## Framework ## Framework
- xUnit 2.5.3 with `xunit.runner.visualstudio` - xUnit 2.5.3 with `xunit.runner.visualstudio`
- No mocking library — custom sealed fakes (FakeClaudeProcess, FakeHubContext, FakeHubClients, FakeClientProxy) - No mocking library — custom sealed fakes (`Infrastructure/FakeClaudeProcess`, `CapturingHubContext/CapturingHubClients/CapturingClientProxy` for SignalR; file-local fakes for scoped tests)
- Real SQLite databases per test via `DbFixture` - Real SQLite databases per test via `DbFixture`
- Real git repos for worktree tests via `GitRepoFixture` - Real git repos for worktree tests via `GitRepoFixture`
- coverlet for coverage collection - coverlet for coverage collection
@@ -32,7 +32,7 @@ Tests are organized by Worker area (mirroring the source folders); 30+ test file
## Conventions ## Conventions
- Test classes implement `IDisposable` and create fixtures in constructor - Test classes implement `IDisposable` and create fixtures in constructor
- Helper factory methods for entities: `MakeTask()`, `CreateListAsync()`, `SeedListWithAgentTag()` - Helper factory methods for entities: `MakeTask()`, `CreateListAsync()`, `SeedListAsync()`
- Concurrency tests use `TaskCompletionSource` as gates for deterministic ordering - Concurrency tests use `TaskCompletionSource` as gates for deterministic ordering
- Git-dependent tests are conditionally skipped via `Skip = ...` when git is not available - Git-dependent tests are conditionally skipped via `Skip = ...` when git is not available

View File

@@ -7,7 +7,6 @@ using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Worker.Tests.Infrastructure;
using ClaudeDo.Worker.Tests.Services;
using ClaudeDo.Worker.Worktrees; using ClaudeDo.Worker.Worktrees;
using ClaudeDo.Worker.Config; using ClaudeDo.Worker.Config;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@@ -62,7 +61,7 @@ public sealed class AddSubtaskToolTests : IDisposable
QueueBackstopIntervalMs = 50, QueueBackstopIntervalMs = 50,
}; };
var dbFactory = _db.CreateFactory(); var dbFactory = _db.CreateFactory();
var hubCtx = new FakeHubContext(); var hubCtx = new CapturingHubContext();
var broadcaster = new HubBroadcaster(hubCtx); var broadcaster = new HubBroadcaster(hubCtx);
var git = new ClaudeDo.Data.Git.GitService(); var git = new ClaudeDo.Data.Git.GitService();
var wtManager = new WorktreeManager(git, dbFactory, cfg, NullLogger<WorktreeManager>.Instance); var wtManager = new WorktreeManager(git, dbFactory, cfg, NullLogger<WorktreeManager>.Instance);

View File

@@ -9,7 +9,6 @@ using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Worker.Tests.Infrastructure;
using ClaudeDo.Worker.Tests.Services;
using ClaudeDo.Worker.Worktrees; using ClaudeDo.Worker.Worktrees;
using Microsoft.AspNetCore.SignalR; using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@@ -143,7 +142,7 @@ public sealed class ExternalMcpServiceTests : IDisposable
QueueBackstopIntervalMs = 50, QueueBackstopIntervalMs = 50,
}; };
var fake = new FakeClaudeProcess(); var fake = new FakeClaudeProcess();
var hubCtx = new FakeHubContext(); var hubCtx = new CapturingHubContext();
var broadcaster = new HubBroadcaster(hubCtx); var broadcaster = new HubBroadcaster(hubCtx);
var dbFactory = _db.CreateFactory(); var dbFactory = _db.CreateFactory();
var wtManager = new WorktreeManager(new GitService(), dbFactory, cfg, NullLogger<WorktreeManager>.Instance); var wtManager = new WorktreeManager(new GitService(), dbFactory, cfg, NullLogger<WorktreeManager>.Instance);

View File

@@ -0,0 +1,25 @@
using ClaudeDo.Worker.Runner;
namespace ClaudeDo.Worker.Tests.Infrastructure;
internal sealed class FakeClaudeProcess : IClaudeProcess
{
private readonly Func<string, string, IReadOnlyList<string>, Func<string, Task>, CancellationToken, Task<RunResult>> _handler;
private int _callCount;
public int CallCount => _callCount;
public FakeClaudeProcess(
Func<string, string, IReadOnlyList<string>, Func<string, Task>, CancellationToken, Task<RunResult>>? handler = null)
{
_handler = handler ?? ((_, _, _, _, _) =>
Task.FromResult(new RunResult { ExitCode = 0, ResultMarkdown = "ok" }));
}
public async Task<RunResult> RunAsync(IReadOnlyList<string> arguments, string prompt, string workingDirectory,
Func<string, Task> onStdoutLine, CancellationToken ct)
{
Interlocked.Increment(ref _callCount);
return await _handler(prompt, workingDirectory, arguments, onStdoutLine, ct);
}
}

View File

@@ -5,7 +5,6 @@ using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Worker.Tests.Infrastructure;
using ClaudeDo.Worker.Tests.Services;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
@@ -33,7 +32,7 @@ public sealed class ContinueAsyncExceptionTests : IDisposable
private TaskRunner BuildRunner(IClaudeProcess claude, ClaudeDoDbContext ctx) private TaskRunner BuildRunner(IClaudeProcess claude, ClaudeDoDbContext ctx)
{ {
var dbFactory = _db.CreateFactory(); var dbFactory = _db.CreateFactory();
var broadcaster = new HubBroadcaster(new FakeHubContext()); var broadcaster = new HubBroadcaster(new CapturingHubContext());
var state = TaskStateServiceBuilder.Build(dbFactory).State; var state = TaskStateServiceBuilder.Build(dbFactory).State;
var wt = new WorktreeManager(new ClaudeDo.Data.Git.GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance); var wt = new WorktreeManager(new ClaudeDo.Data.Git.GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
return new TaskRunner(claude, dbFactory, broadcaster, wt, new ClaudeArgsBuilder(), _cfg, return new TaskRunner(claude, dbFactory, broadcaster, wt, new ClaudeArgsBuilder(), _cfg,

View File

@@ -5,7 +5,6 @@ using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Worker.Tests.Infrastructure;
using ClaudeDo.Worker.Tests.Services;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using Xunit; using Xunit;
@@ -41,7 +40,7 @@ public sealed class StandaloneChildrenRoutingTests : IDisposable
} }
var fake = new FakeClaudeProcess((_, _, _, _, _) => var fake = new FakeClaudeProcess((_, _, _, _, _) =>
Task.FromResult(new RunResult { ExitCode = 0, ResultMarkdown = "done" })); Task.FromResult(new RunResult { ExitCode = 0, ResultMarkdown = "done" }));
var broadcaster = new HubBroadcaster(new FakeHubContext()); var broadcaster = new HubBroadcaster(new CapturingHubContext());
var state = TaskStateServiceBuilder.Build(dbFactory).State; var state = TaskStateServiceBuilder.Build(dbFactory).State;
var wt = new WorktreeManager(new GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance); var wt = new WorktreeManager(new GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
var runner = new TaskRunner(fake, dbFactory, broadcaster, wt, new ClaudeArgsBuilder(), _cfg, var runner = new TaskRunner(fake, dbFactory, broadcaster, wt, new ClaudeArgsBuilder(), _cfg,
@@ -71,7 +70,7 @@ public sealed class StandaloneChildrenRoutingTests : IDisposable
Task.FromResult(new RunResult { ExitCode = 0, ResultMarkdown = "done" })); Task.FromResult(new RunResult { ExitCode = 0, ResultMarkdown = "done" }));
var state = TaskStateServiceBuilder.Build(dbFactory).State; var state = TaskStateServiceBuilder.Build(dbFactory).State;
var wt = new WorktreeManager(new GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance); var wt = new WorktreeManager(new GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
var runner = new TaskRunner(fake, dbFactory, new HubBroadcaster(new FakeHubContext()), wt, var runner = new TaskRunner(fake, dbFactory, new HubBroadcaster(new CapturingHubContext()), wt,
new ClaudeArgsBuilder(), _cfg, NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry()); new ClaudeArgsBuilder(), _cfg, NullLogger<TaskRunner>.Instance, state, new TaskRunTokenRegistry());
using (var ctx = _db.CreateContext()) using (var ctx = _db.CreateContext())

View File

@@ -6,7 +6,6 @@ using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
@@ -49,7 +48,7 @@ public sealed class QueueServiceSlotGuardTests : IDisposable
Func<string, string, IReadOnlyList<string>, Func<string, Task>, CancellationToken, Task<RunResult>>? handler = null) Func<string, string, IReadOnlyList<string>, Func<string, Task>, CancellationToken, Task<RunResult>>? handler = null)
{ {
var fake = new FakeClaudeProcess(handler); var fake = new FakeClaudeProcess(handler);
var broadcaster = new HubBroadcaster(new FakeHubContext()); var broadcaster = new HubBroadcaster(new CapturingHubContext());
var dbFactory = _db.CreateFactory(); var dbFactory = _db.CreateFactory();
var wtManager = new WorktreeManager(new ClaudeDo.Data.Git.GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance); var wtManager = new WorktreeManager(new ClaudeDo.Data.Git.GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
var argsBuilder = new ClaudeArgsBuilder(); var argsBuilder = new ClaudeArgsBuilder();
@@ -63,7 +62,7 @@ public sealed class QueueServiceSlotGuardTests : IDisposable
return (service, fake); return (service, fake);
} }
private async Task<string> SeedListWithAgentTagAsync() private async Task<string> SeedListAsync()
{ {
var listId = Guid.NewGuid().ToString(); var listId = Guid.NewGuid().ToString();
await _listRepo.AddAsync(new ListEntity { Id = listId, Name = "Test", CreatedAt = DateTime.UtcNow }); await _listRepo.AddAsync(new ListEntity { Id = listId, Name = "Test", CreatedAt = DateTime.UtcNow });
@@ -88,7 +87,7 @@ public sealed class QueueServiceSlotGuardTests : IDisposable
[Fact] [Fact]
public async Task RunNow_Throws_When_Task_Already_Running_In_Queue_Slot() public async Task RunNow_Throws_When_Task_Already_Running_In_Queue_Slot()
{ {
var listId = await SeedListWithAgentTagAsync(); var listId = await SeedListAsync();
var task = await SeedQueuedTaskAsync(listId); var task = await SeedQueuedTaskAsync(listId);
// Gate keeps the queue slot occupied indefinitely. // Gate keeps the queue slot occupied indefinitely.
@@ -119,7 +118,7 @@ public sealed class QueueServiceSlotGuardTests : IDisposable
[Fact] [Fact]
public async Task ContinueTask_Throws_When_Task_Already_Running_In_Queue_Slot() public async Task ContinueTask_Throws_When_Task_Already_Running_In_Queue_Slot()
{ {
var listId = await SeedListWithAgentTagAsync(); var listId = await SeedListAsync();
var task = await SeedQueuedTaskAsync(listId); var task = await SeedQueuedTaskAsync(listId);
var tcs = new TaskCompletionSource<RunResult>(); var tcs = new TaskCompletionSource<RunResult>();

View File

@@ -7,7 +7,6 @@ using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Queue; using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
@@ -50,7 +49,7 @@ public sealed class QueueServiceTests : IDisposable
Func<string, string, IReadOnlyList<string>, Func<string, Task>, CancellationToken, Task<RunResult>>? handler = null) Func<string, string, IReadOnlyList<string>, Func<string, Task>, CancellationToken, Task<RunResult>>? handler = null)
{ {
var fake = new FakeClaudeProcess(handler); var fake = new FakeClaudeProcess(handler);
var broadcaster = new HubBroadcaster(new FakeHubContext()); var broadcaster = new HubBroadcaster(new CapturingHubContext());
var dbFactory = _db.CreateFactory(); var dbFactory = _db.CreateFactory();
var wtManager = new WorktreeManager(new GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance); var wtManager = new WorktreeManager(new GitService(), dbFactory, _cfg, NullLogger<WorktreeManager>.Instance);
var argsBuilder = new ClaudeArgsBuilder(); var argsBuilder = new ClaudeArgsBuilder();
@@ -64,11 +63,11 @@ public sealed class QueueServiceTests : IDisposable
return (service, fake); return (service, fake);
} }
private async Task<(string listId, long agentTagId)> SeedListWithAgentTag() private async Task<string> SeedListAsync()
{ {
var listId = Guid.NewGuid().ToString(); var listId = Guid.NewGuid().ToString();
await _listRepo.AddAsync(new ListEntity { Id = listId, Name = "Test", CreatedAt = DateTime.UtcNow }); await _listRepo.AddAsync(new ListEntity { Id = listId, Name = "Test", CreatedAt = DateTime.UtcNow });
return (listId, 0L); return listId;
} }
private async Task<TaskEntity> SeedQueuedTask(string listId, DateTime? scheduledFor = null, DateTime? createdAt = null) private async Task<TaskEntity> SeedQueuedTask(string listId, DateTime? scheduledFor = null, DateTime? createdAt = null)
@@ -90,7 +89,7 @@ public sealed class QueueServiceTests : IDisposable
[Fact] [Fact]
public async Task RunNow_Throws_When_Override_Slot_Busy() public async Task RunNow_Throws_When_Override_Slot_Busy()
{ {
var (listId, _) = await SeedListWithAgentTag(); var listId = await SeedListAsync();
var tcs = new TaskCompletionSource<RunResult>(); var tcs = new TaskCompletionSource<RunResult>();
var (service, _) = CreateService((_, _, _, _, ct) => tcs.Task); var (service, _) = CreateService((_, _, _, _, ct) => tcs.Task);
@@ -116,7 +115,7 @@ public sealed class QueueServiceTests : IDisposable
[Fact] [Fact]
public async Task ReQueuedReviewTask_ResumesSession_WithFeedbackPrompt_AndClearsFeedback() public async Task ReQueuedReviewTask_ResumesSession_WithFeedbackPrompt_AndClearsFeedback()
{ {
var (listId, _) = await SeedListWithAgentTag(); var listId = await SeedListAsync();
IReadOnlyList<string>? capturedArgs = null; IReadOnlyList<string>? capturedArgs = null;
string? capturedPrompt = null; string? capturedPrompt = null;
@@ -181,7 +180,7 @@ public sealed class QueueServiceTests : IDisposable
[Fact] [Fact]
public async Task Schedule_Filter_Skips_Future_Tasks() public async Task Schedule_Filter_Skips_Future_Tasks()
{ {
var (listId, _) = await SeedListWithAgentTag(); var listId = await SeedListAsync();
await SeedQueuedTask(listId, scheduledFor: DateTime.UtcNow.AddHours(1)); await SeedQueuedTask(listId, scheduledFor: DateTime.UtcNow.AddHours(1));
var (service, fake) = CreateService((_, _, _, _, _) => var (service, fake) = CreateService((_, _, _, _, _) =>
@@ -202,7 +201,7 @@ public sealed class QueueServiceTests : IDisposable
[Fact] [Fact]
public async Task Queue_FIFO_Sequentiality() public async Task Queue_FIFO_Sequentiality()
{ {
var (listId, _) = await SeedListWithAgentTag(); var listId = await SeedListAsync();
var order = new List<string>(); var order = new List<string>();
var gate1 = new TaskCompletionSource(); var gate1 = new TaskCompletionSource();
@@ -249,7 +248,7 @@ public sealed class QueueServiceTests : IDisposable
[Fact] [Fact]
public async Task CancelTask_Triggers_Cancellation() public async Task CancelTask_Triggers_Cancellation()
{ {
var (listId, _) = await SeedListWithAgentTag(); var listId = await SeedListAsync();
var running = new TaskCompletionSource(); var running = new TaskCompletionSource();
var cancelled = false; var cancelled = false;
@@ -284,7 +283,7 @@ public sealed class QueueServiceTests : IDisposable
[Fact] [Fact]
public async Task RunNow_AutoRetries_On_Failure_With_SessionId() public async Task RunNow_AutoRetries_On_Failure_With_SessionId()
{ {
var (listId, agentTagId) = await SeedListWithAgentTag(); var listId = await SeedListAsync();
var task = await SeedQueuedTask(listId); var task = await SeedQueuedTask(listId);
var callCount = 0; var callCount = 0;
@@ -327,7 +326,7 @@ public sealed class QueueServiceTests : IDisposable
[Fact] [Fact]
public async Task GetActive_Returns_Running_Slots() public async Task GetActive_Returns_Running_Slots()
{ {
var (listId, _) = await SeedListWithAgentTag(); var listId = await SeedListAsync();
var tcs = new TaskCompletionSource<RunResult>(); var tcs = new TaskCompletionSource<RunResult>();
var (service, _) = CreateService((_, _, _, _, _) => tcs.Task); var (service, _) = CreateService((_, _, _, _, _) => tcs.Task);
@@ -343,55 +342,3 @@ public sealed class QueueServiceTests : IDisposable
tcs.SetResult(new RunResult { ExitCode = 0, ResultMarkdown = "ok" }); tcs.SetResult(new RunResult { ExitCode = 0, ResultMarkdown = "ok" });
} }
} }
#region Test doubles
internal sealed class FakeClaudeProcess : IClaudeProcess
{
private readonly Func<string, string, IReadOnlyList<string>, Func<string, Task>, CancellationToken, Task<RunResult>> _handler;
private int _callCount;
public int CallCount => _callCount;
public FakeClaudeProcess(
Func<string, string, IReadOnlyList<string>, Func<string, Task>, CancellationToken, Task<RunResult>>? handler = null)
{
_handler = handler ?? ((_, _, _, _, _) =>
Task.FromResult(new RunResult { ExitCode = 0, ResultMarkdown = "ok" }));
}
public async Task<RunResult> RunAsync(IReadOnlyList<string> arguments, string prompt, string workingDirectory,
Func<string, Task> onStdoutLine, CancellationToken ct)
{
Interlocked.Increment(ref _callCount);
return await _handler(prompt, workingDirectory, arguments, onStdoutLine, ct);
}
}
internal sealed class FakeHubContext : IHubContext<WorkerHub>
{
public IHubClients Clients { get; } = new FakeHubClients();
public IGroupManager Groups => throw new NotImplementedException();
}
internal sealed class FakeHubClients : IHubClients
{
private readonly FakeClientProxy _proxy = new();
public IClientProxy All => _proxy;
public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => _proxy;
public IClientProxy Client(string connectionId) => _proxy;
public IClientProxy Clients(IReadOnlyList<string> connectionIds) => _proxy;
public IClientProxy Group(string groupName) => _proxy;
public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => _proxy;
public IClientProxy Groups(IReadOnlyList<string> groupNames) => _proxy;
public IClientProxy User(string userId) => _proxy;
public IClientProxy Users(IReadOnlyList<string> userIds) => _proxy;
}
internal sealed class FakeClientProxy : IClientProxy
{
public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default) =>
Task.CompletedTask;
}
#endregion

View File

@@ -3,7 +3,6 @@ using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub; using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Tests.Infrastructure; using ClaudeDo.Worker.Tests.Infrastructure;
using ClaudeDo.Worker.Tests.Services;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus; using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using Xunit; using Xunit;
@@ -38,7 +37,7 @@ public sealed class SuggestImprovementTests : IDisposable
await SeedCallerAsync("caller", parentId: null); await SeedCallerAsync("caller", parentId: null);
using var ctx = _db.CreateContext(); using var ctx = _db.CreateContext();
var svc = new TaskRunMcpService(new TaskRepository(ctx), AccessorFor("caller"), var svc = new TaskRunMcpService(new TaskRepository(ctx), AccessorFor("caller"),
new HubBroadcaster(new FakeHubContext())); new HubBroadcaster(new CapturingHubContext()));
var dto = await svc.SuggestImprovement("Refactor X", "details", model: null, default); var dto = await svc.SuggestImprovement("Refactor X", "details", model: null, default);
var child = await new TaskRepository(ctx).GetByIdAsync(dto.ChildTaskId); var child = await new TaskRepository(ctx).GetByIdAsync(dto.ChildTaskId);
Assert.Equal("caller", child!.ParentTaskId); Assert.Equal("caller", child!.ParentTaskId);
@@ -54,7 +53,7 @@ public sealed class SuggestImprovementTests : IDisposable
await SeedCallerAsync("caller", parentId: null); await SeedCallerAsync("caller", parentId: null);
using var ctx = _db.CreateContext(); using var ctx = _db.CreateContext();
var svc = new TaskRunMcpService(new TaskRepository(ctx), AccessorFor("caller"), var svc = new TaskRunMcpService(new TaskRepository(ctx), AccessorFor("caller"),
new HubBroadcaster(new FakeHubContext())); new HubBroadcaster(new CapturingHubContext()));
var dto = await svc.SuggestImprovement("Refactor X", "details", model: "HAIKU", default); var dto = await svc.SuggestImprovement("Refactor X", "details", model: "HAIKU", default);
var child = await new TaskRepository(ctx).GetByIdAsync(dto.ChildTaskId); var child = await new TaskRepository(ctx).GetByIdAsync(dto.ChildTaskId);
Assert.Equal("haiku", child!.Model); Assert.Equal("haiku", child!.Model);
@@ -66,7 +65,7 @@ public sealed class SuggestImprovementTests : IDisposable
await SeedCallerAsync("caller", parentId: null); await SeedCallerAsync("caller", parentId: null);
using var ctx = _db.CreateContext(); using var ctx = _db.CreateContext();
var svc = new TaskRunMcpService(new TaskRepository(ctx), AccessorFor("caller"), var svc = new TaskRunMcpService(new TaskRepository(ctx), AccessorFor("caller"),
new HubBroadcaster(new FakeHubContext())); new HubBroadcaster(new CapturingHubContext()));
await Assert.ThrowsAsync<ArgumentException>( await Assert.ThrowsAsync<ArgumentException>(
() => svc.SuggestImprovement("x", "y", model: "gpt4", default)); () => svc.SuggestImprovement("x", "y", model: "gpt4", default));
} }
@@ -78,7 +77,7 @@ public sealed class SuggestImprovementTests : IDisposable
await SeedCallerAsync("child", parentId: "parent"); await SeedCallerAsync("child", parentId: "parent");
using var ctx = _db.CreateContext(); using var ctx = _db.CreateContext();
var svc = new TaskRunMcpService(new TaskRepository(ctx), AccessorFor("child"), var svc = new TaskRunMcpService(new TaskRepository(ctx), AccessorFor("child"),
new HubBroadcaster(new FakeHubContext())); new HubBroadcaster(new CapturingHubContext()));
await Assert.ThrowsAsync<InvalidOperationException>( await Assert.ThrowsAsync<InvalidOperationException>(
() => svc.SuggestImprovement("nested", "x", model: null, default)); () => svc.SuggestImprovement("nested", "x", model: null, default));
} }