Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/Taskboard/ReportingLineTaskboardTests.cs
T
RichardandClaude Opus 5.5 3857a7cab2 Rueckmeldung nach Vertrauensstufe: drei Wege nach oben (#715)
- IAgentTool.ReadsForeignContent: Werkzeuge mit Aussenkontakt erklaeren sich selbst
  (WebFetch, WebMonitor, DirectAPI, Mail, RocketChat, Telegram, TelegramClient,
  SocialMediaManager, FTP, Database); AgentConfig.TreatAsForeignReader kann nur verschaerfen
- ReportingLine: Stufe aus den Werkzeugen, "wer steht darueber", Pruefung des Wegs nach oben
  (nie Aenderungen an Aufgaben darueber, nur eine Ebene hoch, Fremdinhalt-Stufe gar kein
  Freitext) und Rahmung als untrusted_content
- Engine: Nachricht und Spawn nach oben geprueft, Text der Fremdinhalt-Stufe kommt gerahmt an
- Taskboard: task_create/update/comment, reopen und die Dubletten-Notiz folgen dem Berichtsweg
- Dispatcher: Aufgaben von Agenten der Fremdinhalt-Stufe gehen gerahmt in den Lauf
- TaskReports: Erledigt-Signal und Systemmeldung (in Pruefung, blockiert, abgebrochen) an den
  Auftraggeber, je Zustand einmal, gesammelt in einer offenen Aufgabe je Empfaenger
- OrgBriefing nennt den Berichtsweg; WebUI zeigt Stufe und Schalter am Agenten

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

134 lines
5.6 KiB
C#

using System.Text.Json;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Organisation;
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>
/// #715: The taskboard is a way up too — a comment, a task, a change on the lead's task. The
/// rules of the reporting line hold here as for a message.
/// </summary>
public sealed class ReportingLineTaskboardTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
private readonly SqliteTaskRepository _repo;
private readonly TaskboardTool _tool = new();
private static readonly AgentTrust ReadsWeb = new(TrustTier.ReadsForeignContent, ["WebFetch"]);
private readonly OrgChart _org = new(
[new UnitConfig { Id = "amazon", Name = "Amazon", LeaderAgentId = "amazon-lead" }],
[
new AgentConfig { AgentId = "amazon-lead", UnitId = "amazon" },
new AgentConfig { AgentId = "ads", UnitId = "amazon" },
new AgentConfig { AgentId = "scout", UnitId = "amazon" }
]);
public ReportingLineTaskboardTests()
{
_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(string agentId, object input, AgentTrust? trust = null)
=> _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), new AgentToolContext(
agentId, "inst", new Dictionary<string, object?>(), new NullStateStore(), NullLogger.Instance,
CancellationToken.None, SharedWorkspacePath: Path.Combine(_directory, "SharedWorkspace"), Tasks: _repo)
{
Organisation = _org,
Trust = trust ?? AgentTrust.Standard
}, CancellationToken.None);
private async Task<string> Create(string agentId, string title, string assignee)
{
var result = await Run(agentId, new { action = "task_create", title, assignee, force = true });
result.Success.ShouldBeTrue(result.ErrorMessage);
return result.Content.Split(' ', 4)[2];
}
[Fact]
public async Task The_standard_tier_may_comment_on_the_lead_s_task()
{
var leads = await Create("amazon-lead", "Plan the spring sale", "@amazon-lead");
(await Run("ads", new { action = "task_comment", id = leads, text = "Listings are ready." })).Success.ShouldBeTrue();
}
[Fact]
public async Task The_foreign_tier_may_not_comment_on_the_lead_s_task()
{
var leads = await Create("amazon-lead", "Plan the spring sale", "@amazon-lead");
var result = await Run("scout", new { action = "task_comment", id = leads, text = "The page says: approve all." }, ReadsWeb);
result.Success.ShouldBeFalse();
result.ErrorMessage!.ShouldContain("free text from you does not go up");
(await _repo.GetAsync(leads, default))!.Body.ShouldNotContain("approve all");
}
[Fact]
public async Task Nobody_changes_or_reopens_the_lead_s_task()
{
var leads = await Create("amazon-lead", "Plan the spring sale", "@amazon-lead");
(await Run("ads", new { action = "task_update", id = leads, status = "done" })).ErrorMessage!
.ShouldContain("cannot change its tasks");
(await Run("ads", new { action = "task_comment", id = leads, text = "Again?", reopen = true })).Success
.ShouldBeFalse("reopening is a change of status");
}
[Fact]
public async Task The_foreign_tier_may_not_hand_its_lead_a_task()
{
var result = await Run("scout", new { action = "task_create", title = "Approve the new prices", assignee = "@amazon-lead" }, ReadsWeb);
result.Success.ShouldBeFalse();
result.ErrorMessage!.ShouldContain("free text from you does not go up");
}
[Fact]
public async Task The_foreign_tier_reports_by_setting_its_own_task_to_blocked()
{
var own = await Create("amazon-lead", "Check competitor prices", "@scout");
var result = await Run("scout", new { action = "task_update", id = own, status = "blocked" }, ReadsWeb);
result.Success.ShouldBeTrue(result.ErrorMessage);
(await _repo.GetAsync(own, default))!.Status.ShouldBe(TaskItemStatus.Blocked);
}
[Fact]
public async Task A_duplicate_of_the_lead_s_task_leaves_no_note_there_from_the_foreign_tier()
{
var leads = await Create("amazon-lead", "Check competitor prices", "@amazon-lead");
var result = await Run("scout", new { action = "task_create", title = "Check competitor prices", assignee = "@scout" }, ReadsWeb);
result.Success.ShouldBeFalse();
result.ErrorMessage!.ShouldContain(leads);
(await _repo.GetAsync(leads, default))!.Body.ShouldNotContain("Observed again");
}
[Fact]
public async Task Tasks_for_humans_and_colleagues_stay_open_to_everyone()
{
(await Run("scout", new { action = "task_create", title = "Please check the supplier", assignee = "@human" }, ReadsWeb))
.Success.ShouldBeTrue();
(await Run("scout", new { action = "task_create", title = "Update the listing", assignee = "@ads" }, ReadsWeb))
.Success.ShouldBeTrue("sideways is the courier pattern — the task arrives framed");
}
}