Freigabe durch die Leitung innerhalb von Grenzen (#687)

- UnitConfig.Approvals: Operationen, Hoechstzahl je Tag, Stunden je Freigabe; nur der
  Mensch setzt sie (OrgAdministration.SetApprovals mit Audit, Teilmengenregel nach oben,
  Senken oben senkt unten mit; Agentenaenderungen/Sonderbudget nie freigebbar)
- LeadApprovals: Vorlage geht an die naechste Leitung, deren Einheit die Operation
  erlaubt; nie eigene Aufrufe, nie eine Leitung mit Fremdinhalt, Tagesdeckel, Frist.
  Entscheidung ueber Escalation approve_call/reject_call, im Audit als lead:<id>
- Der Mensch sieht alles und kann zuerst entscheiden; danach wird die Leitungsaufgabe
  geschlossen. Nach der Frist nur noch der Mensch.
- StagedCalls: Approver, ApproverUntil, ApproverTaskId (Nachruestung)
- WebUI: Hinweis "bei Leitung" in der Freigabestelle, Zaehler ohne diese, Quote je
  Leitung; Karte in der Organisationsansicht; i18n en/de
- Tests: LeadApprovalsTests (13), OrgAdministrationTests (+2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-30 20:12:18 +02:00
co-authored by Claude Opus 5.5
parent dafb8241a0
commit f51186269c
27 changed files with 948 additions and 20 deletions
@@ -251,6 +251,43 @@ public sealed class OrgAdministrationTests : IDisposable
Find("newbie").UnitId.ShouldBe("amazon");
}
// ═══════════════════ approvals by the lead (#687) ═══════════════════
private UnitConfig Unit(string id) => _test.Host.Instance.Units.Single(u => u.Id == id);
[Fact]
public void A_unit_grants_its_lead_approvals_but_never_changes_to_agents()
{
BuildMarketing();
Org.SetApprovals("marketing", ["AgentEditor.update_soul"]).Message.ShouldContain("always stays with a human");
Org.SetApprovals("marketing", ["AgentEditor"]).Ok.ShouldBeFalse("a whole tool with such an operation reads as a grant for it");
Org.SetApprovals("marketing", [".x"]).Ok.ShouldBeFalse();
var set = Org.SetApprovals("marketing", ["Database.insert", "FileRW.delete"], perDay: 5, hours: 2, reason: "night shift");
set.Ok.ShouldBeTrue(set.Message);
Unit("marketing").Approvals!.Operations.ShouldBe(["Database.insert", "FileRW.delete"]);
Unit("marketing").Approvals!.PerDay.ShouldBe(5);
Org.SetApprovals("marketing", []).Ok.ShouldBeTrue();
Unit("marketing").Approvals.ShouldBeNull();
}
[Fact]
public void A_sub_unit_grants_no_more_than_the_units_above_and_shrinks_with_them()
{
BuildMarketing();
Org.SetApprovals("marketing", ["Database"]).Ok.ShouldBeTrue();
Org.SetApprovals("amazon", ["Mail.send"]).Message.ShouldContain("Marketing");
Org.SetApprovals("amazon", ["Database.insert", "Database.update"]).Ok.ShouldBeTrue("'Database' above covers each of its operations");
var lowered = Org.SetApprovals("marketing", ["Database.insert"]);
lowered.Message.ShouldContain("Amazon");
Unit("amazon").Approvals!.Operations.ShouldBe(["Database.insert"]);
}
// ═══════════════════ the top (#690) ═══════════════════
private void MakeDecider(string id)