Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/Taskboard/FillWorkToolTests.cs
T
RichardandClaude Opus 5.5 183e922278 Fuellarbeit: zeitlich flexible Arbeit fuellt ungenutztes Budget (#717, Stufe 1)
Eine einmalige Aufgabe mit fill_by (spaetestens bis) statt Termin laeuft,
wenn der Tag Budget uebrig laesst: nach regulaerer und nachgeholter Arbeit,
nur wenn nichts vom Agenten wartet, bevorzugt ab 18 Uhr, und nur wenn ihre
eigene Schaetzung in den Spielraum aus #716 passt. Kurz vor der Frist wird
einmal ein Mensch gefragt; nach der Frist laeuft sie als regulaere Arbeit.

Ehrliche Schaetzung (TaskCostEstimator): eigene Laeufe, dann Geschwister
unter derselben Elternaufgabe, dann die Angabe des Autors (als Schaetzung
gekennzeichnet), zuletzt das Mittel des Agenten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:34:15 +02:00

87 lines
3.3 KiB
C#

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;
/// <summary>#717: An agent hands out fill work — a deadline instead of an appointment, and its estimate.</summary>
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<ToolResult> Run(object input)
=> _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), new AgentToolContext(
"lead", "inst", new Dictionary<string, object?>(), 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();
}
}