feat(prime): ImportTickets runs the ticket import before filling MyDay
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Worker;
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.Prime;
|
||||
using ClaudeDo.Worker.Runner;
|
||||
using ClaudeDo.Worker.Tests.Infrastructure;
|
||||
using ClaudeDo.Worker.Tickets;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Prime;
|
||||
@@ -9,7 +12,25 @@ namespace ClaudeDo.Worker.Tests.Prime;
|
||||
public class PrimeRunnerTests : IDisposable
|
||||
{
|
||||
private readonly DbFixture _db = new();
|
||||
public void Dispose() => _db.Dispose();
|
||||
private readonly string _patDir = Path.Combine(Path.GetTempPath(), "cdo-prime-pat-" + Guid.NewGuid().ToString("N"));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_db.Dispose();
|
||||
try { Directory.Delete(_patDir, recursive: true); } catch { /* best effort */ }
|
||||
}
|
||||
|
||||
private TicketSystemConfig Tickets(bool configured)
|
||||
{
|
||||
var pat = new TicketPatStore(new DpapiTokenStore(Path.Combine(_patDir, "ticket.pat")));
|
||||
var worker = new WorkerConfig();
|
||||
if (configured)
|
||||
{
|
||||
worker.TicketApiBaseUrl = "http://api.local";
|
||||
pat.Store.Save("tsp_x");
|
||||
}
|
||||
return new TicketSystemConfig(worker, pat);
|
||||
}
|
||||
|
||||
private sealed class FakeClock : IPrimeClock
|
||||
{
|
||||
@@ -22,16 +43,20 @@ public class PrimeRunnerTests : IDisposable
|
||||
private readonly int _exitCode;
|
||||
private readonly string[] _emitLines;
|
||||
private readonly string? _result;
|
||||
private readonly Func<IReadOnlyList<string>, int>? _exitCodeFor;
|
||||
|
||||
public IReadOnlyList<string>? LastArguments { get; private set; }
|
||||
public string? LastPrompt { get; private set; }
|
||||
public List<IReadOnlyList<string>> AllArguments { get; } = [];
|
||||
|
||||
public FakeClaudeProcess(TimeSpan delay = default, int exitCode = 0, string[]? emitLines = null, string? result = null)
|
||||
public FakeClaudeProcess(TimeSpan delay = default, int exitCode = 0, string[]? emitLines = null, string? result = null,
|
||||
Func<IReadOnlyList<string>, int>? exitCodeFor = null)
|
||||
{
|
||||
_delay = delay;
|
||||
_exitCode = exitCode;
|
||||
_emitLines = emitLines ?? [];
|
||||
_result = result;
|
||||
_exitCodeFor = exitCodeFor;
|
||||
}
|
||||
|
||||
public async Task<RunResult> RunAsync(
|
||||
@@ -41,6 +66,7 @@ public class PrimeRunnerTests : IDisposable
|
||||
Func<string, Task> onStdoutLine,
|
||||
CancellationToken ct)
|
||||
{
|
||||
AllArguments.Add(arguments);
|
||||
LastArguments = arguments;
|
||||
LastPrompt = prompt;
|
||||
|
||||
@@ -50,7 +76,8 @@ public class PrimeRunnerTests : IDisposable
|
||||
foreach (var line in _emitLines)
|
||||
await onStdoutLine(line);
|
||||
|
||||
return new RunResult { ExitCode = _exitCode, ResultMarkdown = _result ?? (_exitCode == 0 ? "ok" : null) };
|
||||
var exitCode = _exitCodeFor?.Invoke(arguments) ?? _exitCode;
|
||||
return new RunResult { ExitCode = exitCode, ResultMarkdown = _result ?? (exitCode == 0 ? "ok" : null) };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,13 +97,14 @@ public class PrimeRunnerTests : IDisposable
|
||||
private PrimeRunner NewRunner(TimeSpan claudeDelay = default, int exitCode = 0) =>
|
||||
NewRunner(new FakeClaudeProcess(claudeDelay, exitCode), new RecordingPrimeBroadcaster());
|
||||
|
||||
private PrimeRunner NewRunner(FakeClaudeProcess claude, IPrimeBroadcaster broadcaster) =>
|
||||
private PrimeRunner NewRunner(FakeClaudeProcess claude, IPrimeBroadcaster broadcaster, bool ticketsConfigured = false) =>
|
||||
new PrimeRunner(
|
||||
claude,
|
||||
_db.CreateFactory(),
|
||||
new FakeClock(),
|
||||
NullLogger<PrimeRunner>.Instance,
|
||||
broadcaster);
|
||||
broadcaster,
|
||||
Tickets(ticketsConfigured));
|
||||
|
||||
private static PrimeScheduleDto DefaultSchedule() =>
|
||||
new(Guid.Empty, 0, TimeSpan.Zero, true, null, null, PrimeActionKind.FillMyDay);
|
||||
@@ -241,6 +269,66 @@ public class PrimeRunnerTests : IDisposable
|
||||
Assert.Contains(DailyPrepPrompt.SetMyDayTool, claude.LastArguments!);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportTickets_runs_the_import_then_the_day_plan_in_one_prep_session()
|
||||
{
|
||||
var broadcaster = new RecordingPrimeBroadcaster();
|
||||
var claude = new FakeClaudeProcess(exitCode: 0, result: "ok");
|
||||
var runner = NewRunner(claude, broadcaster, ticketsConfigured: true);
|
||||
|
||||
var outcome = await runner.FireAsync(Schedule(PrimeActionKind.ImportTickets), CancellationToken.None);
|
||||
|
||||
Assert.True(outcome.Success);
|
||||
Assert.Equal(2, claude.AllArguments.Count);
|
||||
Assert.Contains(TicketImportPrompt.ImportTool, claude.AllArguments[0]);
|
||||
Assert.Contains(DailyPrepPrompt.SetMyDayTool, claude.AllArguments[1]);
|
||||
Assert.Equal(1, broadcaster.StartedCount);
|
||||
Assert.Equal([true], broadcaster.FinishedResults);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportTickets_failed_import_still_plans_the_day_and_reports_the_failure()
|
||||
{
|
||||
var broadcaster = new RecordingPrimeBroadcaster();
|
||||
var claude = new FakeClaudeProcess(exitCodeFor: args => args.Contains(TicketImportPrompt.ImportTool) ? 1 : 0);
|
||||
var runner = NewRunner(claude, broadcaster, ticketsConfigured: true);
|
||||
|
||||
var outcome = await runner.FireAsync(Schedule(PrimeActionKind.ImportTickets), CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, claude.AllArguments.Count);
|
||||
Assert.False(outcome.Success);
|
||||
Assert.Contains("Ticket import failed", outcome.Message);
|
||||
Assert.Contains("Daily prep complete", outcome.Message);
|
||||
Assert.Equal([false], broadcaster.FinishedResults);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportTickets_without_a_ticket_system_is_plain_fill_my_day()
|
||||
{
|
||||
var claude = new FakeClaudeProcess(exitCode: 0, result: "ok");
|
||||
var runner = NewRunner(claude, new RecordingPrimeBroadcaster(), ticketsConfigured: false);
|
||||
|
||||
var outcome = await runner.FireAsync(Schedule(PrimeActionKind.ImportTickets), CancellationToken.None);
|
||||
|
||||
Assert.True(outcome.Success);
|
||||
var only = Assert.Single(claude.AllArguments);
|
||||
Assert.Contains(DailyPrepPrompt.SetMyDayTool, only);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ImportTickets_writes_both_runs_into_one_prep_log()
|
||||
{
|
||||
var path = DailyPrepPrompt.LogPath();
|
||||
if (File.Exists(path)) File.Delete(path);
|
||||
var claude = new FakeClaudeProcess(emitLines: ["step-line"], exitCode: 0, result: "ok");
|
||||
var runner = NewRunner(claude, new RecordingPrimeBroadcaster(), ticketsConfigured: true);
|
||||
|
||||
await runner.FireAsync(Schedule(PrimeActionKind.ImportTickets), CancellationToken.None);
|
||||
|
||||
var lines = (await File.ReadAllLinesAsync(path)).Count(l => l.Contains("step-line"));
|
||||
Assert.Equal(2, lines);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gate_is_shared_across_kinds()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user