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>
87 lines
3.3 KiB
C#
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();
|
|
}
|
|
}
|