using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Organisation; using Shouldly; namespace ClawdDotNet.Core.Tests.Organisation; /// /// #683: The organisation tree and the address book it cuts. The example is the online /// shop from the concept, trimmed: /// /// /// (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 /// /// public sealed class OrgChartTests { private static UnitConfig Unit(string id, string? parent = null, string? lead = null, List? 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()) }; 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(); }