- Cases/Decisions in der Instanz-DB; CaseDesk fuehrt die Regeln, der Brett-Task traegt den Vorgang zum jeweiligen Halter - Eskalation nur nach oben, eine Ebene je Schritt; die Leitung entscheidet (mit Begruendung, bindet ihren Bereich), erledigt selbst oder reicht mit Einschaetzung weiter - Erst nachsehen: bindende Beschluesse werden vor dem Eskalieren gezeigt; dieselbe Frage aus derselben Einheit schliesst sich dem offenen Vorgang an - Tagesdeckel je Einheit; nach der Frist wird ein Mensch gerufen (Anschluss fuer #644) - Konflikt: zuerst die Gegenseite, dann Zustellung an die niedrigste gemeinsame Leitung; Ebenen dazwischen vermerkt, nicht befragt - Fremdinhalt-Stufe (#715): Eskalation ohne eigenen Text, kein Konflikt mit Position - Jede Ebene haengt an (#697), beim Halter gerahmt als Daten - Escalation ist ein implizites Werkzeug jedes Agenten; Briefing nennt den Weg - Scanner behaelt einen im Lauf gesetzten Status blockiert/abgebrochen Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
4.3 KiB
C#
104 lines
4.3 KiB
C#
using ClawdDotNet.Core.Cases;
|
||
using ClawdDotNet.Core.Config;
|
||
using ClawdDotNet.Core.Organisation;
|
||
using Shouldly;
|
||
|
||
namespace ClawdDotNet.Core.Tests.Cases;
|
||
|
||
/// <summary>#689: The rules of escalation, conflict and decision — read off the tree, none configured.</summary>
|
||
public sealed class CaseRulesTests
|
||
{
|
||
/// <summary>
|
||
/// purchasing (lead: buy-lead) › team-a (lead: a-lead; buyer-a), team-b (lead: b-lead; buyer-b);
|
||
/// devops (lead: ops-lead; dev). No lead above the top units — the top is a human.
|
||
/// </summary>
|
||
private static OrgChart House() => new(
|
||
[
|
||
new UnitConfig { Id = "purchasing", Name = "Purchasing", LeaderAgentId = "buy-lead" },
|
||
new UnitConfig { Id = "team-a", Name = "Team A", ParentId = "purchasing", LeaderAgentId = "a-lead" },
|
||
new UnitConfig { Id = "team-b", Name = "Team B", ParentId = "purchasing", LeaderAgentId = "b-lead" },
|
||
new UnitConfig { Id = "devops", Name = "DevOps", LeaderAgentId = "ops-lead" }
|
||
],
|
||
[
|
||
new AgentConfig { AgentId = "buy-lead", UnitId = "purchasing" },
|
||
new AgentConfig { AgentId = "a-lead", UnitId = "team-a" },
|
||
new AgentConfig { AgentId = "buyer-a", UnitId = "team-a" },
|
||
new AgentConfig { AgentId = "b-lead", UnitId = "team-b" },
|
||
new AgentConfig { AgentId = "buyer-b", UnitId = "team-b" },
|
||
new AgentConfig { AgentId = "ops-lead", UnitId = "devops" },
|
||
new AgentConfig { AgentId = "dev", UnitId = "devops" }
|
||
]);
|
||
|
||
[Fact]
|
||
public void An_escalation_goes_to_the_own_lead_one_level_at_a_time()
|
||
{
|
||
var org = House();
|
||
|
||
CaseRules.NextHolder(org, "buyer-a").ShouldBe("a-lead");
|
||
CaseRules.NextHolder(org, "a-lead").ShouldBe("buy-lead");
|
||
CaseRules.NextHolder(org, "buy-lead").ShouldBe(CaseRules.Human, "above the top is a human");
|
||
CaseRules.NextHolder(null, "anyone").ShouldBe(CaseRules.Human);
|
||
}
|
||
|
||
[Fact]
|
||
public void Two_teams_of_one_area_are_decided_by_that_area_s_lead_and_nobody_above_is_bothered()
|
||
{
|
||
var route = CaseRules.RouteConflict(House(), "a-lead", "b-lead");
|
||
|
||
route.Decider.ShouldBe("buy-lead");
|
||
route.Informed.ShouldBeEmpty();
|
||
}
|
||
|
||
[Fact]
|
||
public void Two_areas_go_to_the_top()
|
||
{
|
||
var route = CaseRules.RouteConflict(House(), "buy-lead", "ops-lead");
|
||
|
||
route.DecidedByHuman.ShouldBeTrue();
|
||
}
|
||
|
||
[Fact]
|
||
public void A_conflict_is_delivered_directly_and_the_levels_between_are_told_not_asked()
|
||
{
|
||
var route = CaseRules.RouteConflict(House(), "buyer-a", "dev");
|
||
|
||
route.DecidedByHuman.ShouldBeTrue("purchasing and devops meet only at the top");
|
||
route.Informed.ShouldBe(["a-lead", "buy-lead", "ops-lead"], ignoreOrder: true);
|
||
}
|
||
|
||
[Fact]
|
||
public void Someone_above_the_other_side_decides_it_that_is_no_conflict()
|
||
{
|
||
CaseRules.RouteConflict(House(), "a-lead", "buyer-a").Refusal.ShouldNotBeNull().ShouldContain("no conflict");
|
||
CaseRules.RouteConflict(House(), "buyer-a", "buy-lead").Refusal.ShouldNotBeNull().ShouldContain("escalate");
|
||
}
|
||
|
||
[Fact]
|
||
public void A_decision_binds_the_deciding_lead_s_area_and_everything_below()
|
||
{
|
||
var org = House();
|
||
var decision = new Decision { Id = "d-1", ScopeUnitId = CaseRules.ScopeOf(org, "buy-lead") };
|
||
|
||
decision.ScopeUnitId.ShouldBe("purchasing");
|
||
CaseRules.Binds(org, decision, "buyer-a").ShouldBeTrue();
|
||
CaseRules.Binds(org, decision, "dev").ShouldBeFalse();
|
||
CaseRules.Binds(org, decision with { ScopeUnitId = CaseRules.ScopeOf(org, CaseRules.Human) }, "dev")
|
||
.ShouldBeTrue("a human decides for the whole instance");
|
||
}
|
||
|
||
[Fact]
|
||
public void A_decision_is_found_by_its_subject_or_by_telling_words()
|
||
{
|
||
var decisions = new[]
|
||
{
|
||
new Decision { Id = "d-1", Subject = "Delivery date field in orders", Question = "Where do we record the delivery date?" },
|
||
new Decision { Id = "d-2", Subject = "Weekly report format" }
|
||
};
|
||
|
||
CaseRules.Find(decisions, "delivery date field orders").Select(d => d.Id).ShouldBe(["d-1"]);
|
||
CaseRules.Find(decisions, "Order confirmation", "The supplier confirmed a delivery date, where does it go?")
|
||
.Select(d => d.Id).ShouldBe(["d-1"]);
|
||
CaseRules.Find(decisions, "Holiday plan").ShouldBeEmpty();
|
||
}
|
||
}
|