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(); } }