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; /// /// #683: A task for another agent is a way of reaching it. The address book that holds for /// messages holds here too — otherwise it could be walked around with task_create. /// public sealed class AssignmentReachTests : IDisposable { private readonly string _directory; private readonly SqliteStorage _storage; private readonly SqliteTaskRepository _repo; private readonly TaskboardTool _tool = new(); private readonly OrgChart _org = new( [ new UnitConfig { Id = "amazon", Name = "Amazon", LeaderAgentId = "amazon-lead" }, new UnitConfig { Id = "accounting", Name = "Accounting", LeaderAgentId = "accounting-lead" } ], [ new AgentConfig { AgentId = "amazon-lead", UnitId = "amazon" }, new AgentConfig { AgentId = "ads", UnitId = "amazon" }, new AgentConfig { AgentId = "accounting-lead", UnitId = "accounting" }, new AgentConfig { AgentId = "bookkeeper", UnitId = "accounting" } ]); public AssignmentReachTests() { _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N")); _storage = new SqliteStorage(Path.Combine(_directory, "state.db")); _repo = new SqliteTaskRepository(_storage); } public void Dispose() { Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools(); try { Directory.Delete(_directory, recursive: true); } catch { /* cleanup is secondary */ } } private Task Run(string agentId, string json) { var context = new AgentToolContext( agentId, "inst", new Dictionary(), new NullStateStore(), NullLogger.Instance, CancellationToken.None, SharedWorkspacePath: Path.Combine(_directory, "SharedWorkspace"), Tasks: _repo) { Organisation = _org }; return _tool.ExecuteAsync(JsonDocument.Parse(json).RootElement, context, CancellationToken.None); } [Fact] public async Task A_task_for_an_agent_outside_the_address_book_is_refused() { var result = await Run("ads", """{"action":"task_create","title":"Rechnung buchen","assignee":"@bookkeeper"}"""); result.Success.ShouldBeFalse(); result.ErrorMessage.ShouldNotBeNull().ShouldContain("not in your address book"); result.ErrorMessage.ShouldContain("amazon-lead"); } [Fact] public async Task A_fresh_run_of_an_agent_outside_is_refused_too() { var result = await Run("ads", """{"action":"task_create","title":"Rechnung buchen","assignee":"@new:bookkeeper"}"""); result.Success.ShouldBeFalse(); } [Fact] public async Task A_task_for_the_own_lead_or_the_human_goes_through() { (await Run("ads", """{"action":"task_create","title":"Frage an die Leitung","assignee":"@amazon-lead"}""")).Success.ShouldBeTrue(); (await Run("ads", """{"action":"task_create","title":"Frage an den Menschen","assignee":"@human"}""")).Success.ShouldBeTrue(); (await Run("ads", """{"action":"task_create","title":"Eigene Aufgabe","assignee":"@ads"}""")).Success.ShouldBeTrue(); } [Fact] public async Task Reassigning_is_held_to_the_same_rule() { var created = await Run("ads", """{"action":"task_create","title":"Eigene Aufgabe","assignee":"@ads"}"""); var id = created.Content!.Split(' ', ':', '\n').First(w => w.StartsWith("t-")); var result = await Run("ads", $$"""{"action":"task_update","id":"{{id}}","assignee":"@bookkeeper"}"""); result.Success.ShouldBeFalse(); } }