Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/Taskboard/AssignmentReachTests.cs
T
RichardandClaude Opus 5.5 626fc8d593 Einheiten, Stufe 2: das Adressbuch durchsetzen und jedem Agenten seinen Platz sagen (#683)
Durchgesetzt vom Code, nicht nur im Prompt beschrieben:
- Nachricht und Spawn an einen Agenten ausserhalb des Adressbuchs werden vor
  jedem Modellaufruf abgelehnt; die Antwort nennt den offenen Weg (die eigene
  Leitung, sonst @human).
- list_agents zeigt nur noch das Adressbuch.
- task_create und task_update pruefen die Zuweisung an einen Agenten gegen
  dasselbe Adressbuch; sonst liesse es sich ueber eine Aufgabe umgehen.
- Die Delegationstiefe folgt der Tiefe des Baums (Tiefe + 1), der
  eingestellte Wert bleibt Untergrenze.

Unterrichtet werden, wie das Konzept verlangt:
- OrgBriefing erzeugt einen Abschnitt "# Organisation" im Systemprompt:
  Einheit mit Pfad und Ziel, seit wann (mit Hinweis, wenn frisch), die
  effektive Leitung oder "@human", als Leitung die Untereinheiten und neue
  Mitglieder, und wen man sonst erreicht. Er ist immer wahr, weil er aus dem
  Baum kommt; ein Umzug aendert ihn von selbst.
- In einem laufenden Chat wird nur dieser Abschnitt ersetzt; der uebrige
  Systemprompt bleibt, wie er war (#193).
- Ohne Einheiten gibt es keinen Abschnitt und keine Einschraenkung. Bestehende
  Instanzen behalten ihren Prompt und ihren Prompt-Cache.

AppHost setzt den Organisationsbaum als Provider (frisch je Frage, eine
Aenderung gilt ab dem naechsten Lauf) und protokolliert Probleme der Struktur
beim Start.

Tests: AddressBookEnforcementTests (10), AssignmentReachTests (4). 1.317 gruen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 12:43:10 +02:00

99 lines
3.9 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()
{
Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
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();
}
}