using ClaudeDo.Worker.Prime; using ClaudeDo.Worker.Runner; using ClaudeDo.Worker.Tests.Infrastructure; using Microsoft.Extensions.Logging.Abstractions; namespace ClaudeDo.Worker.Tests.Prime; public class PrimeRunnerTests : IDisposable { private readonly DbFixture _db = new(); public void Dispose() => _db.Dispose(); private sealed class FakeClock : IPrimeClock { public DateTimeOffset Now { get; set; } = new DateTimeOffset(2026, 6, 3, 8, 0, 0, TimeSpan.FromHours(2)); } private sealed class FakeClaudeProcess : IClaudeProcess { private readonly TimeSpan _delay; private readonly int _exitCode; public FakeClaudeProcess(TimeSpan delay = default, int exitCode = 0) { _delay = delay; _exitCode = exitCode; } public async Task RunAsync( string arguments, string prompt, string workingDirectory, Func onStdoutLine, CancellationToken ct) { if (_delay > TimeSpan.Zero) await Task.Delay(_delay, ct); return new RunResult { ExitCode = _exitCode, ResultMarkdown = _exitCode == 0 ? "ok" : null }; } } private PrimeRunner NewRunner(TimeSpan claudeDelay = default, int exitCode = 0) => new PrimeRunner( new FakeClaudeProcess(claudeDelay, exitCode), _db.CreateFactory(), new FakeClock(), NullLogger.Instance); private static PrimeScheduleDto DefaultSchedule() => new(Guid.Empty, 0, TimeSpan.Zero, true, null, null); [Fact] public async Task FireAsync_returns_success_when_claude_exits_zero() { var runner = NewRunner(); var outcome = await runner.FireAsync(DefaultSchedule(), CancellationToken.None); Assert.True(outcome.Success); Assert.Equal("Daily prep complete", outcome.Message); } [Fact] public async Task FireAsync_returns_failure_when_claude_exits_nonzero() { var runner = NewRunner(exitCode: 1); var outcome = await runner.FireAsync(DefaultSchedule(), CancellationToken.None); Assert.False(outcome.Success); Assert.Contains("exit code", outcome.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task FireAsync_returns_already_running_when_gate_held() { var runner = NewRunner(claudeDelay: TimeSpan.FromSeconds(2)); var schedule = DefaultSchedule(); var first = runner.FireAsync(schedule, CancellationToken.None); var second = await runner.FireAsync(schedule, CancellationToken.None); Assert.False(second.Success); Assert.Contains("already running", second.Message, StringComparison.OrdinalIgnoreCase); await first; } }