Files
ClawdDotNet/tests/ClawdDotNet.Tools.Tests/Taskboard/DoctrineTests.cs
T
RichardandClaude Opus 5.5 679333b382 Doctrine: die Leitung steuert ihre Einheit, ohne jeden Auftragstext anzufassen (#698)
- SqliteDoctrineStore: je Einheit versioniert (Tabelle Doctrines: Text, Autor, Lauf, Grund),
  hoechstens 2000 Zeichen, leer entfernt; im Speicher gehalten fuer jeden Lauf
- OrgBriefing: die Doctrine der eigenen Einheit, nur eine Ebene tief, mit dem Satz,
  dass ein Widerspruch zur Aufgabe eskaliert wird; Leitungen erfahren doctrine_set
- Escalation: doctrine_read, doctrine_set (nur die Leitung, Grund Pflicht)
- IAgentTool.Vorgabe(operation, context): Einstufung mit dem Aufrufkontext; eine Leitung
  der Fremdinhalt-Stufe legt doctrine_set zur Freigabe vor
- API GET/PUT /organisation/units/{id}/doctrine; WebUI-Karte mit Verlauf und Markierung
  haeufiger Aenderungen; i18n en/de
- Tests: DoctrineTests (5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:38:44 +02:00

112 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Organisation;
using ClawdDotNet.Core.Staging;
using ClawdDotNet.Core.Storage;
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>
/// #698: The lead steers its unit without touching every task text — one level deep, short,
/// every version kept, and not unasked from a lead that reads foreign content.
/// </summary>
public sealed class DoctrineTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), "clawd-tests", Guid.NewGuid().ToString("N"));
private readonly SqliteDoctrineStore _store;
private readonly EscalationTool _tool = new();
/// <summary>purchasing (buy-lead; buyer) › team (team-lead; clerk).</summary>
private readonly OrgChart _org = new(
[
new UnitConfig { Id = "purchasing", Name = "Purchasing", LeaderAgentId = "buy-lead" },
new UnitConfig { Id = "team", Name = "Team", ParentId = "purchasing", LeaderAgentId = "team-lead" }
],
[
new AgentConfig { AgentId = "buy-lead", UnitId = "purchasing" },
new AgentConfig { AgentId = "buyer", UnitId = "purchasing" },
new AgentConfig { AgentId = "team-lead", UnitId = "team" },
new AgentConfig { AgentId = "clerk", UnitId = "team" }
]);
public DoctrineTests() => _store = new SqliteDoctrineStore(new SqliteStorage(Path.Combine(_directory, "state.db")));
public void Dispose()
{
SqliteStorage.ReleasePooledConnections(_directory);
try { Directory.Delete(_directory, recursive: true); }
catch { /* cleanup is secondary */ }
}
private AgentToolContext Context(string agent, AgentTrust? trust = null)
=> new(agent, "inst", new Dictionary<string, object?>(), new NullStateStore(), NullLogger.Instance, CancellationToken.None)
{
Organisation = _org,
Doctrines = _store,
RunId = "run-" + agent,
Trust = trust ?? AgentTrust.Standard
};
private Task<ToolResult> Run(string agent, object input)
=> _tool.ExecuteAsync(JsonSerializer.SerializeToElement(input), Context(agent), CancellationToken.None);
[Fact]
public async Task The_lead_sets_the_doctrine_and_its_members_read_it()
{
var set = await Run("buy-lead", new { action = "doctrine_set", text = "Every supplier contact goes into the database first.", reason = "Contacts got lost." });
set.Success.ShouldBeTrue(set.ErrorMessage);
(await Run("buyer", new { action = "doctrine_read" })).Content.ShouldContain("supplier contact");
_store.Current("purchasing")!.SetBy.ShouldBe("buy-lead");
}
[Fact]
public async Task Only_a_lead_sets_it_with_a_reason_and_within_the_length()
{
(await Run("buyer", new { action = "doctrine_set", text = "x", reason = "y" })).ErrorMessage!.ShouldContain("Only the lead");
(await Run("buy-lead", new { action = "doctrine_set", text = "x" })).ErrorMessage!.ShouldContain("'reason'");
(await Run("buy-lead", new { action = "doctrine_set", text = new string('x', 2001), reason = "y" })).ErrorMessage!.ShouldContain("2000");
}
[Fact]
public async Task It_reaches_one_level_deep_and_no_further()
{
await _store.SetAsync("purchasing", "Purchasing rule.", "buy-lead", null, "r", default);
await _store.SetAsync("team", "Team rule.", "team-lead", null, "r", default);
var buyer = OrgBriefing.For(_org, "buyer", DateTime.UtcNow, doctrine: _store.Current)!;
var clerk = OrgBriefing.For(_org, "clerk", DateTime.UtcNow, doctrine: _store.Current)!;
buyer.ShouldContain("Purchasing rule.");
clerk.ShouldContain("Team rule.");
clerk.ShouldNotContain("Purchasing rule.", customMessage: "the team has its own lead and its own doctrine");
}
[Fact]
public async Task Every_version_is_kept_and_an_empty_text_removes_it()
{
await Run("buy-lead", new { action = "doctrine_set", text = "First.", reason = "a" });
await Run("buy-lead", new { action = "doctrine_set", text = "", reason = "no longer needed" });
_store.Current("purchasing").ShouldBeNull();
var history = await _store.HistoryAsync("purchasing", default);
history.Select(d => d.Version).ShouldBe([2, 1]);
history[1].Text.ShouldBe("First.");
history[0].RunId.ShouldBe("run-buy-lead");
}
[Fact]
public void A_lead_that_reads_foreign_content_needs_an_approval_to_set_it()
{
_tool.Vorgabe("doctrine_set", Context("buy-lead", new AgentTrust(TrustTier.ReadsForeignContent, ["Mail"])))
.ShouldBe(StagingDecision.Approve);
_tool.Vorgabe("doctrine_set", Context("buy-lead")).ShouldBeNull("a lead of the standard tier sets it without asking");
_tool.Vorgabe("doctrine_read", Context("buy-lead", new AgentTrust(TrustTier.ReadsForeignContent, ["Mail"]))).ShouldBeNull();
}
}