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; /// #697: A task an agent hands out carries where it came from — stamped, not claimed. public sealed class TaskOriginTests : 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 TaskOriginTests() { _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, string? runId = "run-lead", string? runTaskId = "t-1") => _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), new AgentToolContext( "lead", "inst", new Dictionary(), new NullStateStore(), NullLogger.Instance, CancellationToken.None, SharedWorkspacePath: Path.Combine(_directory, "SharedWorkspace"), Tasks: _repo) { RunId = runId, RunTaskId = runTaskId }, CancellationToken.None); private static string IdOf(ToolResult result) => result.Content.Split(' ', 4)[2]; [Fact] public async Task A_task_handed_out_in_a_run_names_that_run_and_its_task() { var created = await Run(new { action = "task_create", title = "Write the listing", assignee = "@ads" }); var task = (await _repo.GetAsync(IdOf(created), default))!; task.OriginRunId.ShouldBe("run-lead"); task.OriginTaskId.ShouldBe("t-1"); (await Run(new { action = "task_list" })).Content.ShouldContain("Arose from: lead, while working on t-1, run run-lead"); } [Fact] public async Task The_agent_cannot_claim_an_origin() { var created = await Run(new { action = "task_create", title = "Write the listing", origin_run = "run-forged", origin_task = "t-99" }); var task = (await _repo.GetAsync(IdOf(created), default))!; task.OriginRunId.ShouldBe("run-lead"); task.OriginTaskId.ShouldBe("t-1"); } [Fact] public async Task A_comment_names_the_run_it_was_written_in() { var id = IdOf(await Run(new { action = "task_create", title = "Write the listing", assignee = "@lead" })); await Run(new { action = "task_comment", id, text = "Price checked." }); (await _repo.GetAsync(id, default))!.Body.ShouldContain("run run-lead"); } [Fact] public async Task The_body_is_not_replaced_so_the_trail_back_stays() { var id = IdOf(await Run(new { action = "task_create", title = "Write the listing", body = "Use the spring photos." })); await Run(new { action = "task_comment", id, text = "Photos are in the unit workspace." }); var result = await Run(new { action = "task_update", id, body = "Something else entirely." }); result.Success.ShouldBeFalse(); result.ErrorMessage!.ShouldContain("task_comment"); var body = (await _repo.GetAsync(id, default))!.Body; body.ShouldContain("Use the spring photos."); body.ShouldContain("Photos are in the unit workspace."); } [Fact] public async Task Only_the_creator_moves_the_acceptance_criteria() { var id = IdOf(await Run(new { action = "task_create", title = "Write the listing", assignee = "@ads", acceptance = "Three bullet points." })); (await Run(new { action = "task_update", id, acceptance = "Four bullet points." })).Success.ShouldBeTrue("the lead created it"); var byAssignee = await _tool.ExecuteAsync( JsonSerializer.SerializeToElement(new { action = "task_update", id, acceptance = "Any text will do." }), new AgentToolContext("ads", "inst", new Dictionary(), new NullStateStore(), NullLogger.Instance, CancellationToken.None, SharedWorkspacePath: Path.Combine(_directory, "SharedWorkspace"), Tasks: _repo), CancellationToken.None); byAssignee.Success.ShouldBeFalse(); (await _repo.GetAsync(id, default))!.Acceptance.ShouldBe("Four bullet points."); } }