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."); } }