using System.Text.Json; using ClawdDotNet.Core.Storage; using ClawdDotNet.Core.Tasks; using ClawdDotNet.Core.Tests.Infrastructure; using ClawdDotNet.Core.Tools; using Shouldly; namespace ClawdDotNet.Core.Tests.Engine; /// /// #697: No whispering down the lane. Whatever an agent hands on carries a reference to where /// it came from — stamped by the engine, not claimed by the agent — so whoever decides can go /// back to the original without asking anyone. /// public sealed class OriginStampTests { /// Remembers the context of each call — what the engine told the tool. private sealed class ContextProbe : IAgentTool { public List Seen { get; } = []; public string Name => "TestTool"; public string Description => "probe"; public JsonElement InputSchema { get; } = JsonDocument.Parse("""{ "type": "object" }""").RootElement.Clone(); public Task ExecuteAsync(JsonElement input, AgentToolContext context, CancellationToken ct) { Seen.Add(context); return Task.FromResult(ToolResult.Ok("ok")); } } [Fact] public async Task A_tool_is_told_the_run_and_the_task_it_works_for() { var probe = new ContextProbe(); var audit = new InMemoryAuditRepository(); var fixture = new EngineFixture(audit: audit).WithTool(probe); var agent = fixture.AddAgent("ads", "TestTool"); fixture.Client.RespondsWithToolCall("TestTool").RespondsWithText("done"); await fixture.Engine.ChatAsync(agent, "work", "test-instance", default, taskId: "t-7"); var context = probe.Seen.ShouldHaveSingleItem(); context.RunTaskId.ShouldBe("t-7"); context.RunId.ShouldBe(audit.Receipts.Single().RunId); context.Delegation!.RunId.ShouldBe(context.RunId, "the chain carries the run, for a delegation to name it"); } [Fact] public void Handing_on_carries_the_caller_s_run_to_the_next() { var chain = DelegationChain.Root("lead").WithRun("run-lead").Append("ads"); chain.CallerRunId.ShouldBe("run-lead"); chain.RunId.ShouldBeNull("the new agent's run does not exist yet"); chain.WithRun("run-ads").CallerRunId.ShouldBe("run-lead"); } [Fact] public async Task A_delegated_run_names_the_run_it_came_from() { var tool = new DelegatingTool(new Dictionary { ["lead"] = "ads" }); var audit = new InMemoryAuditRepository(); var fixture = new EngineFixture(audit: audit).WithTool(tool); var lead = fixture.AddAgent("lead", "TestTool"); fixture.AddAgent("ads", "TestTool"); fixture.Client.RespondsContextually(); await fixture.Engine.ChatAsync(lead, "Please sort this out", "test-instance", default); var leadRun = audit.Receipts.Single(r => r.AgentId == "lead"); var adsRun = audit.Receipts.Single(r => r.AgentId == "ads"); adsRun.CallerRunId.ShouldBe(leadRun.RunId); leadRun.CallerRunId.ShouldBeNull(); (await audit.ListReceiptsCalledFromAsync(leadRun.RunId, default)).ShouldHaveSingleItem().RunId.ShouldBe(adsRun.RunId); } [Fact] public async Task The_origin_of_a_task_survives_the_file_and_a_re_import() { var directory = Path.Combine(Path.GetTempPath(), "clawd-tests", "origin-" + Guid.NewGuid().ToString("N")[..8]); try { var repo = new SqliteTaskRepository(new SqliteStorage(Path.Combine(directory, "state.db"))); var board = new TaskboardService(repo, Path.Combine(directory, "tasks")); await board.CreateAsync(new TaskItem { Id = "t-2", Title = "Below", OriginRunId = "run-1", OriginTaskId = "t-1" }, default); var file = Directory.GetFiles(Path.Combine(directory, "tasks")).Single(); File.ReadAllText(file).ShouldContain("origin_run: run-1"); // A file edited by hand without the lines keeps what the database knows. File.WriteAllText(file, File.ReadAllText(file).Replace("origin_run: run-1\n", "").Replace("origin_task: t-1\n", "")); await board.ImportAllAsync(default); var task = (await repo.GetAsync("t-2", default))!; task.OriginRunId.ShouldBe("run-1"); task.OriginTaskId.ShouldBe("t-1"); } finally { SqliteStorage.ReleasePooledConnections(directory); try { Directory.Delete(directory, recursive: true); } catch { /* cleaning up is secondary */ } } } [Fact] public async Task A_comment_names_the_run_it_was_written_in() { var directory = Path.Combine(Path.GetTempPath(), "clawd-tests", "origin-" + Guid.NewGuid().ToString("N")[..8]); try { var repo = new SqliteTaskRepository(new SqliteStorage(Path.Combine(directory, "state.db"))); var board = new TaskboardService(repo, Path.Combine(directory, "tasks")); await board.CreateAsync(new TaskItem { Id = "t-1", Title = "Top" }, default); var task = await board.AddCommentAsync("t-1", "ads", "Listing is live.", default, runId: "run-9"); task!.Body.ShouldContain(", run run-9):"); } finally { SqliteStorage.ReleasePooledConnections(directory); try { Directory.Delete(directory, recursive: true); } catch { /* cleaning up is secondary */ } } } }