Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/Taskboard/TaskOriginTests.cs
T
RichardandClaude Opus 5.5 f812dbd856 Verweis statt Nacherzaehlung: die Engine stempelt den Ursprung (#697, #677)
- Werkzeugkontext kennt Lauf und Aufgabe des Laufs (RunId, RunTaskId); die
  Delegationskette traegt den Lauf des Aufrufers weiter (RunId, CallerRunId)
- Aufgaben tragen origin_run / origin_task (Datei, DB, Taskboard "Arose from"),
  gestempelt, nie vom Agenten behauptet
- Kommentare nennen den Lauf, in dem sie geschrieben wurden
- Laufbeleg: CallerRunId; Laufansicht fuehrt zu "Delegiert von" und "Delegiert an"
- #677: task_update ersetzt den Rumpf nicht mehr, Abnahmekriterien aendert nur der
  Ersteller; Kommentare der Fremdinhalt-Stufe auf fremden Aufgaben kommen gerahmt an
- WebUI: Herkunft auf der Aufgabenkarte mit Link zum Lauf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 09:27:47 +02:00

104 lines
4.5 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>#697: A task an agent hands out carries where it came from — stamped, not claimed.</summary>
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<ToolResult> Run(object input, string? runId = "run-lead", string? runTaskId = "t-1")
=> _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)
{
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<string, object?>(), 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.");
}
}