using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Organisation;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tasks;
using ClawdDotNet.Core.Tests.Infrastructure;
using Shouldly;
namespace ClawdDotNet.Core.Tests.Tasks;
///
/// #714: Every task can say what it serves, up to the goal of its unit — and the run of a
/// task is told so.
///
public sealed class GoalChainTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", "goal-" + Guid.NewGuid().ToString("N")[..8]);
private readonly SqliteTaskRepository _repo;
public GoalChainTests()
{
_repo = new SqliteTaskRepository(new SqliteStorage(Path.Combine(_directory, "state.db")));
}
public void Dispose()
{
SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* cleaning up is secondary */ }
}
private Task Save(string id, string title, string parent = "", string assignee = "@ads")
=> _repo.UpsertAsync(new TaskItem { Id = id, Title = title, ParentId = parent, Assignee = assignee }, default);
private static OrgChart Shop(string marketingGoal = "Grow revenue profitably.", string amazonGoal = "") => new(
[
new UnitConfig { Id = "marketing", Name = "Marketing", Goal = marketingGoal },
new UnitConfig { Id = "amazon", Name = "Amazon", ParentId = "marketing", Goal = amazonGoal }
],
[new AgentConfig { AgentId = "ads", UnitId = "amazon" }]);
[Fact]
public async Task The_parents_are_walked_nearest_first()
{
await Save("t-1", "Spring collection");
await Save("t-2", "Listings for the collection", "t-1");
var leaf = await Save("t-3", "Text for chair A-100", "t-2");
var parents = await GoalChain.ParentsAsync(leaf, _repo.GetAsync, default);
parents.Select(p => p.Id).ShouldBe(["t-2", "t-1"]);
}
[Fact]
public async Task A_cycle_in_the_data_does_not_hang_the_walk()
{
await Save("t-1", "A", "t-2");
var b = await Save("t-2", "B", "t-1");
(await GoalChain.ParentsAsync(b, _repo.GetAsync, default)).Select(p => p.Id).ShouldBe(["t-1"]);
}
[Fact]
public async Task A_task_cannot_be_put_below_itself()
{
await Save("t-1", "Top");
await Save("t-2", "Middle", "t-1");
await Save("t-3", "Bottom", "t-2");
(await GoalChain.WouldCycleAsync("t-1", "t-3", _repo.GetAsync, default)).ShouldBeTrue();
(await GoalChain.WouldCycleAsync("t-1", "t-1", _repo.GetAsync, default)).ShouldBeTrue();
(await GoalChain.WouldCycleAsync("t-3", "t-1", _repo.GetAsync, default)).ShouldBeFalse();
}
[Fact]
public async Task The_description_ends_in_the_goal_of_the_nearest_unit_that_has_one()
{
var top = await Save("t-1", "Spring collection");
var leaf = await Save("t-2", "Text for chair A-100", "t-1");
var text = GoalChain.Describe(leaf, [top], Shop())!;
text.ShouldContain("t-1 \"Spring collection\"");
// amazon has no goal of its own — the one of marketing above it counts.
text.ShouldContain("the goal of the unit \"Marketing\": Grow revenue profitably.");
}
[Fact]
public void Without_parents_and_without_a_goal_nothing_is_added()
{
GoalChain.Describe(new TaskItem { Id = "t-9", Assignee = "@ads" }, [], Shop(marketingGoal: "")).ShouldBeNull();
GoalChain.Describe(new TaskItem { Id = "t-9", Assignee = "@ads" }, [], null).ShouldBeNull();
}
[Fact]
public async Task The_parent_survives_the_file_and_a_re_import()
{
var tasksDir = Path.Combine(_directory, "tasks");
var board = new TaskboardService(_repo, tasksDir);
await board.CreateAsync(new TaskItem { Id = "t-1", Title = "Top" }, default);
await board.CreateAsync(new TaskItem { Id = "t-2", Title = "Below", ParentId = "t-1" }, default);
Directory.GetFiles(tasksDir).Select(File.ReadAllText).Single(text => text.Contains("id: t-2"))
.ShouldContain("parent: t-1");
await board.ImportAllAsync(default);
(await _repo.GetAsync("t-2", default))!.ParentId.ShouldBe("t-1");
}
[Fact]
public async Task The_run_of_a_task_is_told_why()
{
var fixture = new EngineFixture();
var agent = fixture.AddAgent("ads");
agent.UnitId = "amazon";
fixture.Engine.OrganisationProvider = () => new OrgChart(
[
new UnitConfig { Id = "marketing", Name = "Marketing", Goal = "Grow revenue profitably." },
new UnitConfig { Id = "amazon", Name = "Amazon", ParentId = "marketing" }
],
fixture.Agents);
fixture.Client.RespondsWithText("ERGEBNIS: ok");
await Save("t-1", "Spring collection", assignee: "@human");
var leaf = await Save("t-2", "Text for chair A-100", "t-1");
var dispatcher = new EngineTaskDispatcher(
fixture.Engine, () => fixture.Agents, "test-instance", fixture.Registry, fixture.StateStore, TestLogging.Factory,
tasks: _repo);
await dispatcher.DispatchAsync(leaf, default);
var message = fixture.Client.ReceivedRequests[0].Messages.Last(m => m.Role == "user").Content!;
message.ShouldContain("Why this task exists");
message.ShouldContain("t-1 \"Spring collection\"");
message.ShouldContain("Grow revenue profitably.");
}
}