- WorkspaceConfig in der Instanzkonfiguration: Id, Name, Zweck, Eigentuemer-Einheit, Mitglieder (Einheit oder Agent, Stufe Read/Write/ Manage, Untereinheiten ein/aus), geschuetzte Pfade. Das Recht steht am Bereich, nicht am Agenten. - OrgChart: AccessTo, WorkspacesFor, UnitWorkspace; Problems nennt ungueltige Ids und tote Verweise. WorkspaceLayout: Bereiche liegen unter Agents/SharedWorkspaces/<id>/, neben dem unveraenderten SharedWorkspace. - FileRW: workspace ist ein Name - personal, shared (wie bisher ueber sharedAccessLevel), unit (eigene Einheit) oder eine Bereichs-Id. Ein unbekannter und ein nicht erlaubter Bereich bekommen dieselbe Antwort. Loeschen braucht Manage wie frueher Admin. Der #662-Hinweis sucht in allen erlaubten Bereichen. - Mit Einheiten ist shared:/tasks/ fuer FileRW gesperrt: Aufgaben laufen ueber das Brett, sonst umginge eine direkt geschriebene Datei Brett- und Adressbuchpruefung. Ohne Einheiten unveraendert. - Organisationsabschnitt im Prompt nennt die Bereiche mit Stufe und Zweck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
134 lines
6.1 KiB
C#
134 lines
6.1 KiB
C#
using ClawdDotNet.Core.Config;
|
|
using ClawdDotNet.Core.Organisation;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Core.Tests.Organisation;
|
|
|
|
/// <summary>
|
|
/// #684: Who may use which workspace. The right stands at the workspace — a member list of
|
|
/// units and agents with a level — and the tree decides what a unit entry covers.
|
|
/// </summary>
|
|
public sealed class WorkspaceAccessTests
|
|
{
|
|
private static readonly UnitConfig[] Units =
|
|
[
|
|
new() { Id = "purchasing", Name = "Purchasing", LeaderAgentId = "head" },
|
|
new() { Id = "suppliers", Name = "Suppliers", ParentId = "purchasing" },
|
|
new() { Id = "marketing", Name = "Marketing" }
|
|
];
|
|
|
|
private static readonly AgentConfig[] Agents =
|
|
[
|
|
new() { AgentId = "head", UnitId = "purchasing" },
|
|
new() { AgentId = "scout", UnitId = "suppliers" },
|
|
new() { AgentId = "ads", UnitId = "marketing" },
|
|
new() { AgentId = "ceo" }
|
|
];
|
|
|
|
private static OrgChart Chart(params WorkspaceConfig[] workspaces) => new(Units, Agents, workspaces);
|
|
|
|
private static WorkspaceConfig Workspace(string id, string? owner, params WorkspaceMember[] members)
|
|
=> new() { Id = id, Name = id, OwnerUnitId = owner, Members = [.. members] };
|
|
|
|
[Fact]
|
|
public void A_unit_entry_covers_the_units_below()
|
|
{
|
|
var org = Chart(Workspace("purchasing", "purchasing", new WorkspaceMember { UnitId = "purchasing", Access = WorkspaceAccess.Write }));
|
|
|
|
org.AccessTo("head", "purchasing").ShouldBe(WorkspaceAccess.Write);
|
|
org.AccessTo("scout", "purchasing").ShouldBe(WorkspaceAccess.Write);
|
|
org.AccessTo("ads", "purchasing").ShouldBe(WorkspaceAccess.None);
|
|
org.AccessTo("ceo", "purchasing").ShouldBe(WorkspaceAccess.None, "the top level is not a member of every unit");
|
|
}
|
|
|
|
[Fact]
|
|
public void Without_sub_units_the_entry_covers_the_unit_alone()
|
|
{
|
|
var org = Chart(Workspace("round", null,
|
|
new WorkspaceMember { UnitId = "purchasing", Access = WorkspaceAccess.Read, IncludeSubUnits = false }));
|
|
|
|
org.AccessTo("head", "round").ShouldBe(WorkspaceAccess.Read);
|
|
org.AccessTo("scout", "round").ShouldBe(WorkspaceAccess.None);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_highest_level_of_all_matching_entries_counts()
|
|
{
|
|
var org = Chart(Workspace("articles", "purchasing",
|
|
new WorkspaceMember { UnitId = "purchasing", Access = WorkspaceAccess.Read },
|
|
new WorkspaceMember { AgentId = "scout", Access = WorkspaceAccess.Manage }));
|
|
|
|
org.AccessTo("scout", "articles").ShouldBe(WorkspaceAccess.Manage);
|
|
org.AccessTo("head", "articles").ShouldBe(WorkspaceAccess.Read);
|
|
}
|
|
|
|
[Fact]
|
|
public void An_agent_on_the_top_level_can_be_named_directly()
|
|
=> Chart(Workspace("board", null, new WorkspaceMember { AgentId = "ceo", Access = WorkspaceAccess.Write }))
|
|
.AccessTo("ceo", "board").ShouldBe(WorkspaceAccess.Write);
|
|
|
|
[Fact]
|
|
public void The_own_unit_s_workspace_comes_first_and_only_visible_ones_are_listed()
|
|
{
|
|
var org = Chart(
|
|
Workspace("articles", "purchasing", new WorkspaceMember { UnitId = "marketing", Access = WorkspaceAccess.Read }),
|
|
Workspace("marketing", "marketing", new WorkspaceMember { UnitId = "marketing", Access = WorkspaceAccess.Write }),
|
|
Workspace("purchasing", "purchasing", new WorkspaceMember { UnitId = "purchasing", Access = WorkspaceAccess.Write }));
|
|
|
|
var grants = org.WorkspacesFor("ads");
|
|
|
|
grants.Select(g => g.Workspace.Id).ShouldBe(["marketing", "articles"]);
|
|
grants[0].IsOwnUnit.ShouldBeTrue();
|
|
org.UnitWorkspace("marketing")!.Id.ShouldBe("marketing");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_workspace_carrying_a_unit_s_id_but_owned_by_another_is_not_that_unit_s()
|
|
=> Chart(Workspace("marketing", "purchasing")).UnitWorkspace("marketing").ShouldBeNull();
|
|
|
|
[Fact]
|
|
public void Broken_references_and_reserved_names_are_named()
|
|
{
|
|
var problems = Chart(
|
|
Workspace("shared", null),
|
|
Workspace("orphan", "gone", new WorkspaceMember { UnitId = "nowhere" }, new WorkspaceMember { AgentId = "ghost" })).Problems();
|
|
|
|
problems.ShouldContain(p => p.Contains("'shared' has an invalid id"));
|
|
problems.ShouldContain(p => p.Contains("'gone'"));
|
|
problems.ShouldContain(p => p.Contains("'nowhere'"));
|
|
problems.ShouldContain(p => p.Contains("'ghost'"));
|
|
}
|
|
|
|
[Fact]
|
|
public void Workspaces_live_next_to_the_one_shared_folder()
|
|
{
|
|
var shared = Path.Combine(Path.GetTempPath(), "inst", "Agents", "SharedWorkspace");
|
|
|
|
WorkspaceLayout.PathOf(shared, "purchasing")
|
|
.ShouldBe(Path.Combine(Path.GetTempPath(), "inst", "Agents", "SharedWorkspaces", "purchasing"));
|
|
WorkspaceLayout.PathOf(shared + Path.DirectorySeparatorChar, "purchasing")
|
|
.ShouldBe(Path.Combine(Path.GetTempPath(), "inst", "Agents", "SharedWorkspaces", "purchasing"), "a trailing separator changes nothing");
|
|
WorkspaceLayout.PathOf(shared, "../escape").ShouldBeNull();
|
|
WorkspaceLayout.PathOf(null, "purchasing").ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void The_briefing_names_the_workspaces_and_what_the_agent_may_do_there()
|
|
{
|
|
var org = Chart(
|
|
Workspace("marketing", "marketing", new WorkspaceMember { UnitId = "marketing", Access = WorkspaceAccess.Write }),
|
|
new WorkspaceConfig
|
|
{
|
|
Id = "articles", Name = "Article master", OwnerUnitId = "purchasing", Purpose = "One file per article.",
|
|
Members = [new WorkspaceMember { UnitId = "marketing", Access = WorkspaceAccess.Read }]
|
|
});
|
|
|
|
var section = OrgBriefing.For(org, "ads", DateTime.UtcNow)!;
|
|
|
|
section.ShouldContain("\"unit\" — your unit's workspace (also \"marketing\"), read and write");
|
|
section.ShouldContain("\"articles\" (Article master), read only — One file per article.");
|
|
OrgBriefing.For(org, "scout", DateTime.UtcNow)!.ShouldNotContain("workspaces", Case.Insensitive,
|
|
"the scout is in none of them — and must not learn that they exist");
|
|
}
|
|
}
|