using System.Text.Json;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tasks;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Tools.Taskboard;
using ClawdDotNet.Tools.Tests.FileRW;
using Microsoft.Extensions.Logging.Abstractions;
using Shouldly;
namespace ClawdDotNet.Tools.Tests.Taskboard;
/// #717: An agent hands out fill work — a deadline instead of an appointment, and its estimate.
public sealed class FillWorkToolTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
private readonly SqliteTaskRepository _repo;
private readonly TaskboardTool _tool = new();
public FillWorkToolTests()
{
_repo = new SqliteTaskRepository(new SqliteStorage(Path.Combine(_directory, "state.db")));
}
public void Dispose()
{
SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* cleanup is secondary */ }
}
private Task Run(object input)
=> _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), new AgentToolContext(
"lead", "inst", new Dictionary(), new NullStateStore(), NullLogger.Instance,
CancellationToken.None, SharedWorkspacePath: Path.Combine(_directory, "SharedWorkspace"), Tasks: _repo),
CancellationToken.None);
private static string IdOf(ToolResult result) => result.Content.Split(' ', 4)[2];
private static string InAWeek => DateTime.UtcNow.AddDays(7).ToString("yyyy-MM-dd");
[Fact]
public async Task Fill_work_is_created_with_its_deadline_and_estimate()
{
var created = await Run(new { action = "task_create", title = "Rewrite all 400 listings", assignee = "@ads", fill_by = InAWeek, estimate_usd = 1.5 });
created.Success.ShouldBeTrue(created.ErrorMessage);
var task = (await _repo.GetAsync(IdOf(created), default))!;
task.IsFillWork.ShouldBeTrue();
task.EstimateUsd.ShouldBe(1.5m);
var list = (await Run(new { action = "task_list" })).Content;
list.ShouldContain("Fill work: runs when the budget has room");
list.ShouldContain("Author's estimate: 1.50 USD per run (not measured)");
}
[Fact]
public async Task A_recurring_task_cannot_be_fill_work()
{
var result = await Run(new
{
action = "task_create", title = "Hourly check", fill_by = InAWeek,
when = new { kind = "every", value = "1h", tz = "UTC" }
});
result.Success.ShouldBeFalse();
result.ErrorMessage!.ShouldContain("one-time");
}
[Fact]
public async Task A_deadline_in_the_past_is_refused()
{
var result = await Run(new { action = "task_create", title = "Too late", fill_by = "2020-01-01" });
result.ErrorMessage!.ShouldContain("lies in the past");
}
[Fact]
public async Task An_empty_deadline_turns_fill_work_back_into_regular_work()
{
var id = IdOf(await Run(new { action = "task_create", title = "Rewrite all 400 listings", assignee = "@lead", fill_by = InAWeek }));
(await Run(new { action = "task_update", id, fill_by = "" })).Success.ShouldBeTrue();
(await _repo.GetAsync(id, default))!.IsFillWork.ShouldBeFalse();
}
}