Einheiten, Stufe 1: Organisationsbaum und Adressbuch als reine Rechnung (#683)
- UnitConfig (Id, Name, ParentId, LeaderAgentId, Goal, ToolCeiling, Defaults) in der Instanzkonfiguration neben den Agenten; AgentConfig.UnitId und UnitSinceUtc. Bewusst Konfiguration statt Datenbank: Die Werkzeugrechte der Agenten stehen schon dort, die Wahrheit ueber einen Agenten bleibt an einem Ort. Strukturaenderungen gehen ins Audit (Stufe 3). - OrgChart: Pfade, Tiefe, Leitung, effektive Leitung (die naechste Leitung darueber, wenn die eigene Einheit keine hat oder man sie selbst leitet), Adressbuch, Delegationstiefe aus dem Baum (Tiefe + 1, der eingestellte Wert bleibt Untergrenze), Rechte-Obergrenze als Schnittmenge entlang des Pfades, und eine Liste der Probleme (unbekannte Verweise, Zyklen, Leitung ist kein Mitglied, Werkzeuge ueber der Obergrenze). - Adressbuch: eigene Einheit und effektive Leitung; als Leitung die Leitungen der Untereinheiten (bei leitungslosen Untereinheiten deren Mitglieder), der Geschwistereinheiten und die eigene Leitung; die oberste Ebene und die Leitungen der obersten Einheiten sehen sich gegenseitig. Jede Linie ist in beide Richtungen begehbar. Ohne Einheiten sieht jeder jeden, wie bisher. Tests: OrgChartTests (20) am Onlinehandel-Beispiel aus dem Konzept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dc11961805
commit
d014a20c38
@@ -0,0 +1,226 @@
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Organisation;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Core.Tests.Organisation;
|
||||
|
||||
/// <summary>
|
||||
/// #683: The organisation tree and the address book it cuts. The example is the online
|
||||
/// shop from the concept, trimmed:
|
||||
///
|
||||
/// <code>
|
||||
/// (top level) ceo
|
||||
/// ├── purchasing lead: purchasing-lead
|
||||
/// │ ├── suppliers lead: suppliers-lead · negotiator
|
||||
/// │ └── quality (no lead) · sampler
|
||||
/// ├── marketing lead: marketing-lead
|
||||
/// │ └── marketplaces lead: marketplaces-lead
|
||||
/// │ └── amazon lead: amazon-lead · amazon-ads, amazon-listing
|
||||
/// └── accounting lead: accounting-lead · bookkeeper
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public sealed class OrgChartTests
|
||||
{
|
||||
private static UnitConfig Unit(string id, string? parent = null, string? lead = null, List<string>? ceiling = null)
|
||||
=> new() { Id = id, Name = char.ToUpperInvariant(id[0]) + id[1..], ParentId = parent, LeaderAgentId = lead, ToolCeiling = ceiling };
|
||||
|
||||
private static AgentConfig Agent(string id, string? unit = null, params string[] tools)
|
||||
=> new() { AgentId = id, DisplayName = id, UnitId = unit, Tools = tools.ToDictionary(t => t, _ => new Dictionary<string, object?>()) };
|
||||
|
||||
private static OrgChart Shop() => new(
|
||||
[
|
||||
Unit("purchasing", lead: "purchasing-lead"),
|
||||
Unit("suppliers", "purchasing", "suppliers-lead"),
|
||||
Unit("quality", "purchasing"),
|
||||
Unit("marketing", lead: "marketing-lead"),
|
||||
Unit("marketplaces", "marketing", "marketplaces-lead"),
|
||||
Unit("amazon", "marketplaces", "amazon-lead"),
|
||||
Unit("accounting", lead: "accounting-lead")
|
||||
],
|
||||
[
|
||||
Agent("ceo"),
|
||||
Agent("purchasing-lead", "purchasing"),
|
||||
Agent("suppliers-lead", "suppliers"),
|
||||
Agent("negotiator", "suppliers"),
|
||||
Agent("sampler", "quality"),
|
||||
Agent("marketing-lead", "marketing"),
|
||||
Agent("marketplaces-lead", "marketplaces"),
|
||||
Agent("amazon-lead", "amazon"),
|
||||
Agent("amazon-ads", "amazon"),
|
||||
Agent("amazon-listing", "amazon"),
|
||||
Agent("accounting-lead", "accounting"),
|
||||
Agent("bookkeeper", "accounting")
|
||||
]);
|
||||
|
||||
// ═══════════════════ the tree ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public void Paths_and_depths_follow_the_tree()
|
||||
{
|
||||
var org = Shop();
|
||||
|
||||
org.PathOf("amazon").ShouldBe("Marketing / Marketplaces / Amazon");
|
||||
org.Depth("marketing").ShouldBe(1);
|
||||
org.Depth("amazon").ShouldBe(3);
|
||||
org.TreeDepth.ShouldBe(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Delegation_may_reach_from_the_top_to_the_bottom_and_one_step_further()
|
||||
{
|
||||
Shop().DelegationDepth(configuredFloor: 3).ShouldBe(4, "before #683 it was a fixed 3");
|
||||
Shop().DelegationDepth(configuredFloor: 6).ShouldBe(6, "the configured value stays a floor");
|
||||
new OrgChart([], [Agent("a")]).DelegationDepth(3).ShouldBe(3);
|
||||
}
|
||||
|
||||
// ═══════════════════ leads ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public void A_member_answers_to_the_lead_of_its_unit()
|
||||
=> Shop().EffectiveLeaderOf("amazon-ads")!.AgentId.ShouldBe("amazon-lead");
|
||||
|
||||
[Fact]
|
||||
public void A_lead_answers_to_the_lead_above()
|
||||
=> Shop().EffectiveLeaderOf("amazon-lead")!.AgentId.ShouldBe("marketplaces-lead");
|
||||
|
||||
[Fact]
|
||||
public void In_a_unit_without_lead_the_nearest_lead_above_takes_over()
|
||||
=> Shop().EffectiveLeaderOf("sampler")!.AgentId.ShouldBe("purchasing-lead");
|
||||
|
||||
[Fact]
|
||||
public void Above_a_top_level_lead_there_is_only_the_human()
|
||||
=> Shop().EffectiveLeaderOf("marketing-lead").ShouldBeNull();
|
||||
|
||||
[Fact]
|
||||
public void A_lead_who_is_not_a_member_does_not_count()
|
||||
{
|
||||
var org = new OrgChart([Unit("team", lead: "outsider")], [Agent("outsider"), Agent("member", "team")]);
|
||||
|
||||
org.LeaderOf("team").ShouldBeNull();
|
||||
org.Problems().ShouldContain(p => p.Contains("not a member"));
|
||||
}
|
||||
|
||||
// ═══════════════════ the address book ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public void A_member_sees_its_unit_and_its_lead_and_nothing_else()
|
||||
{
|
||||
Shop().AddressBook("amazon-ads").ShouldBe(["amazon-lead", "amazon-listing"], ignoreOrder: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_logistician_does_not_learn_that_an_ads_agent_exists()
|
||||
{
|
||||
// The example from the item: with a flat list, everyone saw everyone.
|
||||
Shop().CanAddress("bookkeeper", "amazon-ads").ShouldBeFalse();
|
||||
Shop().CanAddress("amazon-ads", "bookkeeper").ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_lead_sees_its_team_its_sub_leads_its_sibling_leads_and_its_own_lead()
|
||||
{
|
||||
Shop().AddressBook("marketplaces-lead").ShouldBe(
|
||||
["amazon-lead", "marketing-lead"], ignoreOrder: true);
|
||||
|
||||
Shop().AddressBook("purchasing-lead").ShouldBe(
|
||||
// sub-lead; the lead-less quality team directly; sibling leads; the top level.
|
||||
["suppliers-lead", "sampler", "marketing-lead", "accounting-lead", "ceo"], ignoreOrder: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Members_of_a_lead_less_unit_and_the_lead_above_see_each_other()
|
||||
{
|
||||
var org = Shop();
|
||||
|
||||
org.CanAddress("sampler", "purchasing-lead").ShouldBeTrue();
|
||||
org.CanAddress("purchasing-lead", "sampler").ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_top_level_and_the_top_level_leads_see_each_other()
|
||||
{
|
||||
var org = Shop();
|
||||
|
||||
org.AddressBook("ceo").ShouldBe(["purchasing-lead", "marketing-lead", "accounting-lead"], ignoreOrder: true);
|
||||
org.CanAddress("marketing-lead", "ceo").ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_line_can_be_walked_both_ways()
|
||||
{
|
||||
// Whoever I can reach as my lead can reach me: escalation and delegation use the
|
||||
// same line.
|
||||
var org = Shop();
|
||||
foreach (var agent in org.Units.SelectMany(u => org.MembersOf(u.Id)).Concat(org.MembersOf(null)))
|
||||
{
|
||||
foreach (var other in org.AddressBook(agent.AgentId))
|
||||
org.CanAddress(other, agent.AgentId).ShouldBeTrue($"{agent.AgentId} → {other} but not back");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Without_units_everyone_reaches_everyone_as_before()
|
||||
{
|
||||
var org = new OrgChart([], [Agent("a"), Agent("b"), Agent("c")]);
|
||||
|
||||
org.HasUnits.ShouldBeFalse();
|
||||
org.AddressBook("a").ShouldBe(["b", "c"], ignoreOrder: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void An_unknown_agent_reaches_nobody()
|
||||
=> Shop().AddressBook("ghost").ShouldBeEmpty();
|
||||
|
||||
// ═══════════════════ tool ceiling ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public void A_ceiling_applies_along_the_whole_path_down()
|
||||
{
|
||||
var org = new OrgChart(
|
||||
[
|
||||
Unit("marketing", ceiling: ["WebFetch", "Taskboard", "AgentComm"]),
|
||||
Unit("amazon", "marketing", ceiling: ["WebFetch", "Taskboard", "FileRW"])
|
||||
],
|
||||
[Agent("ads", "amazon")]);
|
||||
|
||||
org.ToolCeiling("amazon")!.ShouldBe(["WebFetch", "Taskboard"], ignoreOrder: true,
|
||||
"FileRW is not allowed further up, AgentComm not here");
|
||||
org.ToolCeiling("marketing")!.Count.ShouldBe(3);
|
||||
org.ToolCeiling(null).ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Tools_beyond_the_ceiling_are_named_as_a_problem()
|
||||
{
|
||||
var org = new OrgChart(
|
||||
[Unit("marketing", ceiling: ["WebFetch"])],
|
||||
[Agent("ads", "marketing", "WebFetch", "Database")]);
|
||||
|
||||
org.Problems().ShouldContain(p => p.Contains("ads") && p.Contains("Database"));
|
||||
}
|
||||
|
||||
// ═══════════════════ broken structures ═══════════════════
|
||||
|
||||
[Fact]
|
||||
public void A_cycle_is_named_and_does_not_hang()
|
||||
{
|
||||
var org = new OrgChart([Unit("a", "b"), Unit("b", "a")], [Agent("x", "a")]);
|
||||
|
||||
org.Problems().ShouldContain(p => p.Contains("cycle"));
|
||||
org.Ancestors("a").Count.ShouldBeLessThan(3);
|
||||
org.AddressBook("x").ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unknown_references_are_named()
|
||||
{
|
||||
var org = new OrgChart([Unit("team", "nowhere")], [Agent("x", "missing")]);
|
||||
|
||||
org.Problems().ShouldContain(p => p.Contains("nowhere"));
|
||||
org.Problems().ShouldContain(p => p.Contains("missing"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_sound_structure_has_no_problems()
|
||||
=> Shop().Problems().ShouldBeEmpty();
|
||||
}
|
||||
Reference in New Issue
Block a user