1. ArgumentList (fix injection): ClaudeArgsBuilder.Build() now returns IReadOnlyList<string>; ClaudeProcess populates ProcessStartInfo.ArgumentList instead of Arguments, so values like system prompts are never shell-split. DailyPrepPrompt, RefinePrompt, and WeekReportService migrated similarly. All IClaudeProcess fakes updated. 2. ContinueAsync exception guard: wrap RunOnceAsync in try/catch matching the RunAsync pattern so an unexpected exception never leaves the task stuck in Running status. 3. Planning chain cascade: OnChildFinishedAsync now calls CancelAsync on the immediate blocked successor when a child fails or is cancelled, triggering a recursive cascade that clears the entire remaining chain instead of leaving it wedged. 4. FailAsync guard: restrict valid source states to Running and Queued; WaitingForReview -> Failed is now rejected, preventing an invalid transition that could corrupt the review workflow. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
82 lines
3.1 KiB
C#
82 lines
3.1 KiB
C#
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Worker.Report;
|
|
using ClaudeDo.Worker.Report.Interfaces;
|
|
using ClaudeDo.Worker.Runner;
|
|
using ClaudeDo.Worker.Tests.Infrastructure;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Report;
|
|
|
|
public class WeekReportServiceTests : IDisposable
|
|
{
|
|
private readonly DbFixture _db = new();
|
|
public void Dispose() => _db.Dispose();
|
|
|
|
private static readonly DateOnly Start = new(2026, 5, 28);
|
|
private static readonly DateOnly End = new(2026, 6, 3);
|
|
|
|
private sealed class FakeReader : IClaudeHistoryReader
|
|
{
|
|
public IReadOnlyList<RepoActivity> Result = Array.Empty<RepoActivity>();
|
|
public Task<IReadOnlyList<RepoActivity>> ReadAsync(
|
|
DateOnly s, DateOnly e, IReadOnlyList<string> ex, CancellationToken ct) => Task.FromResult(Result);
|
|
}
|
|
|
|
private sealed class FakeClaude : IClaudeProcess
|
|
{
|
|
public int Calls;
|
|
public RunResult Next = new() { ExitCode = 0, ResultMarkdown = "## Bericht" };
|
|
public Task<RunResult> RunAsync(IReadOnlyList<string> args, string prompt, string wd, Func<string, Task> onLine, CancellationToken ct)
|
|
{ Calls++; return Task.FromResult(Next); }
|
|
}
|
|
|
|
private WeekReportService Make(FakeReader reader, FakeClaude claude) =>
|
|
new(reader, _db.CreateFactory(), claude, NullLogger<WeekReportService>.Instance);
|
|
|
|
[Fact]
|
|
public async Task EmptyWindow_ProducesNoActivityReport_WithoutCallingClaude()
|
|
{
|
|
var claude = new FakeClaude();
|
|
var svc = Make(new FakeReader(), claude);
|
|
|
|
var md = await svc.GenerateAsync(Start, End);
|
|
|
|
Assert.Equal(0, claude.Calls);
|
|
Assert.Contains("No activity", md);
|
|
using var ctx = _db.CreateContext();
|
|
Assert.NotNull(await new WeekReportRepository(ctx).GetByRangeAsync(Start, End));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SuccessPath_StoresAndReturnsClaudeMarkdown()
|
|
{
|
|
var repo = new RepoActivity { RepoPath = @"C:\Dev\App" };
|
|
var day = new DayActivity { Date = new DateOnly(2026, 6, 1) };
|
|
day.Prompts.Add("Add login");
|
|
repo.Days.Add(day);
|
|
var claude = new FakeClaude { Next = new RunResult { ExitCode = 0, ResultMarkdown = "## Bericht\n- Habe Login umgesetzt" } };
|
|
var svc = Make(new FakeReader { Result = new[] { repo } }, claude);
|
|
|
|
var md = await svc.GenerateAsync(Start, End);
|
|
|
|
Assert.Equal(1, claude.Calls);
|
|
Assert.Contains("Habe Login umgesetzt", md);
|
|
Assert.Equal(md, await svc.GetStoredAsync(Start, End));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ClaudeFailure_Throws_AndDoesNotStore()
|
|
{
|
|
var repo = new RepoActivity { RepoPath = @"C:\Dev\App" };
|
|
var day = new DayActivity { Date = new DateOnly(2026, 6, 1) };
|
|
day.Prompts.Add("x");
|
|
repo.Days.Add(day);
|
|
var claude = new FakeClaude { Next = new RunResult { ExitCode = 1, ErrorMarkdown = "boom" } };
|
|
var svc = Make(new FakeReader { Result = new[] { repo } }, claude);
|
|
|
|
await Assert.ThrowsAsync<InvalidOperationException>(() => svc.GenerateAsync(Start, End));
|
|
Assert.Null(await svc.GetStoredAsync(Start, End));
|
|
}
|
|
}
|