- 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>
112 lines
5.1 KiB
C#
112 lines
5.1 KiB
C#
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();
|
||
}
|
||
}
|