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() { ClawdDotNet.Core.Storage.SqliteStorage.ReleasePooledConnections(_directory); 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(); } // ─── #686: meetings ─── [Fact] public async Task A_lead_calls_a_meeting_with_whom_it_reaches() { var result = await Run("amazon-lead", """{"action":"task_create","title":"Leads round","type":"meeting","assignee":"@new:amazon-lead","participants":["ads","accounting-lead"],"body":"Late shipments"}"""); result.Success.ShouldBeTrue(result.ErrorMessage); var meeting = (await _repo.ListAsync(new TaskQuery(), default)).Single(t => t.Title == "Leads round"); meeting.Type.ShouldBe(TaskItemType.Meeting); meeting.Participants.ShouldBe(["ads", "accounting-lead"]); } [Fact] public async Task A_meeting_with_someone_outside_the_chairs_reach_is_refused() { var result = await Run("amazon-lead", """{"action":"task_create","title":"Round","type":"meeting","assignee":"@new:amazon-lead","participants":["bookkeeper"]}"""); result.Success.ShouldBeFalse(); result.ErrorMessage!.ShouldContain("cannot address 'bookkeeper'"); } [Fact] public async Task Participants_without_a_meeting_are_a_mistake() { var result = await Run("amazon-lead", """{"action":"task_create","title":"Plain","assignee":"@ads","participants":["ads"]}"""); result.Success.ShouldBeFalse(); result.ErrorMessage!.ShouldContain("only belongs to a meeting"); } }