Aufgabentyp meeting mit Teilnehmerliste (Frontmatter, SQLite, Taskboard-Werkzeug, WebUI). MeetingRunner: jeder Teilnehmer einmal in Reihenfolge, Beitraege gerahmt als Daten des Verfassers, danach schliesst die Leitung mit Ergebnis und Aufgaben. Protokoll schreibt die Instanz in den Bereich der Einheit der Leitung. Teilnehmer nur aus dem Adressbuch der Leitung, hoechstens 12. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
5.3 KiB
C#
133 lines
5.3 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>
|
|
/// #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.
|
|
/// </summary>
|
|
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<ToolResult> Run(string agentId, string json)
|
|
{
|
|
var context = new AgentToolContext(
|
|
agentId, "inst", new Dictionary<string, object?>(), 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");
|
|
}
|
|
}
|