Ablage je Einheit, Stufe 2: Verwaltung und Organisationsseite (#684)
- OrgAdministration: eine neue Einheit bekommt ihren Bereich (Mitglieder: die Einheit samt Untereinheiten, schreiben); reservierte Namen und belegte Bereichs-Ids werden als Einheiten-Id abgelehnt. Uebergreifende Bereiche anlegen (mit Eigentuemer), Name/Zweck/geschuetzte Ordner aendern, Mitgliederliste ersetzen, nur leere Bereiche loeschen, nie den eigenen einer Einheit. Eine geloeschte Einheit verlaesst alle Mitgliederlisten; ihr Bereich geht nur, wenn er leer ist, sonst bleibt er ohne Eigentuemer. Nachruesten des eigenen Bereichs fuer Einheiten von vor #684. Alles im AuditLog. - API unter /api/organisation/workspaces; die Uebersicht liefert je Bereich Mitglieder, Dateizahl und - von der Instanz berechnet - wer hineinkommt. - Organisationsseite: Bereiche unter der Struktur, Bearbeitung mit Mitglieder-Editor, "Wer hineinkommt", Bereichskarte an der Einheit, Bereiche am Agenten. Sprachschluessel en/de. - Die Nachschau nach einem Absturz bekommt die Organisation mit, sonst fände sie einen Aufruf in einem benannten Bereich nicht. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ff20ce2563
commit
609298dd7a
@@ -0,0 +1,191 @@
|
||||
using ClawdDotNet.App.Services;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.App.Tests.Services;
|
||||
|
||||
/// <summary>
|
||||
/// #684: Workspaces in the administration. A unit gets its own with it; a workspace across
|
||||
/// units is created on purpose, with an owner; nothing filed is ever deleted from here.
|
||||
/// </summary>
|
||||
public sealed class WorkspaceAdministrationTests : IDisposable
|
||||
{
|
||||
private readonly string _agents = Path.Combine(Path.GetTempPath(), "clawd-tests", "ws-" + Guid.NewGuid().ToString("N")[..8], "Agents");
|
||||
private readonly TestHost _test;
|
||||
|
||||
public WorkspaceAdministrationTests()
|
||||
{
|
||||
_test = new TestHost(Agent("buyer"), Agent("ads"), Agent("bookkeeper"));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_test.Dispose();
|
||||
try { Directory.Delete(Path.GetDirectoryName(_agents)!, recursive: true); }
|
||||
catch { /* cleaning up is secondary */ }
|
||||
}
|
||||
|
||||
private AgentConfig Agent(string id) => new()
|
||||
{
|
||||
AgentId = id,
|
||||
DisplayName = id,
|
||||
SharedWorkspacePath = Path.Combine(_agents, "SharedWorkspace")
|
||||
};
|
||||
|
||||
private OrgAdministration Org => _test.Host.Organisation;
|
||||
|
||||
private List<WorkspaceConfig> Workspaces => _test.Host.Instance.Workspaces;
|
||||
|
||||
private void Setup()
|
||||
{
|
||||
Org.CreateUnit("purchasing", "Purchasing", null).Ok.ShouldBeTrue();
|
||||
Org.CreateUnit("marketing", "Marketing", null).Ok.ShouldBeTrue();
|
||||
Org.MoveAgent("buyer", "purchasing", "setup").Ok.ShouldBeTrue();
|
||||
Org.MoveAgent("ads", "marketing", "setup").Ok.ShouldBeTrue();
|
||||
}
|
||||
|
||||
private void File(string workspaceId, string name)
|
||||
{
|
||||
var folder = Org.WorkspaceFolder(workspaceId)!;
|
||||
Directory.CreateDirectory(folder);
|
||||
System.IO.File.WriteAllText(Path.Combine(folder, name), "x");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_new_unit_comes_with_its_own_workspace_for_itself_and_below()
|
||||
{
|
||||
Setup();
|
||||
|
||||
var own = Workspaces.Single(w => w.Id == "purchasing");
|
||||
own.OwnerUnitId.ShouldBe("purchasing");
|
||||
own.Members.ShouldHaveSingleItem().UnitId.ShouldBe("purchasing");
|
||||
own.Members[0].Access.ShouldBe(WorkspaceAccess.Write);
|
||||
own.Members[0].IncludeSubUnits.ShouldBeTrue();
|
||||
Org.Current().AccessTo("buyer", "purchasing").ShouldBe(WorkspaceAccess.Write);
|
||||
Org.Current().AccessTo("ads", "purchasing").ShouldBe(WorkspaceAccess.None);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_unit_cannot_take_a_reserved_name_or_the_id_of_an_existing_workspace()
|
||||
{
|
||||
Org.CreateUnit("shared", "Shared", null).Ok.ShouldBeFalse();
|
||||
Org.CreateWorkspace("articles", "Articles", null, null, []).Ok.ShouldBeTrue();
|
||||
Org.CreateUnit("articles", "Articles team", null).Ok.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_workspace_across_units_has_an_owner_and_levels()
|
||||
{
|
||||
Setup();
|
||||
|
||||
var result = Org.CreateWorkspace("articles", "Article master", "One file per article.", "purchasing",
|
||||
[
|
||||
new WorkspaceMemberInput("purchasing", null, WorkspaceAccess.Manage),
|
||||
new WorkspaceMemberInput("marketing", null, WorkspaceAccess.Read)
|
||||
], "purchasing maintains, marketing reads");
|
||||
|
||||
result.Ok.ShouldBeTrue(result.Message);
|
||||
Org.Current().AccessTo("buyer", "articles").ShouldBe(WorkspaceAccess.Manage);
|
||||
Org.Current().AccessTo("ads", "articles").ShouldBe(WorkspaceAccess.Read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ids_of_units_are_kept_for_their_own_workspaces()
|
||||
{
|
||||
Setup();
|
||||
|
||||
Org.CreateWorkspace("marketing", "Other", null, null, []).Ok.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("nowhere", null, WorkspaceAccess.Read)]
|
||||
[InlineData(null, "ghost", WorkspaceAccess.Read)]
|
||||
[InlineData("marketing", "ads", WorkspaceAccess.Read)]
|
||||
[InlineData(null, null, WorkspaceAccess.Read)]
|
||||
[InlineData("marketing", null, WorkspaceAccess.None)]
|
||||
public void Members_must_be_one_existing_unit_or_agent_with_a_level(string? unit, string? agent, WorkspaceAccess access)
|
||||
{
|
||||
Setup();
|
||||
|
||||
Org.CreateWorkspace("x", "X", null, null, [new WorkspaceMemberInput(unit, agent, access)]).Ok.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_member_list_is_replaced_and_the_old_one_is_audited_away()
|
||||
{
|
||||
Setup();
|
||||
|
||||
Org.SetWorkspaceMembers("purchasing",
|
||||
[
|
||||
new WorkspaceMemberInput("purchasing", null, WorkspaceAccess.Write),
|
||||
new WorkspaceMemberInput(null, "ads", WorkspaceAccess.Read)
|
||||
], "ads may look at the orders").Ok.ShouldBeTrue();
|
||||
|
||||
Org.Current().AccessTo("ads", "purchasing").ShouldBe(WorkspaceAccess.Read);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Protected_folders_are_folders_inside_the_workspace()
|
||||
{
|
||||
Setup();
|
||||
|
||||
Org.UpdateWorkspace("purchasing", null, null, ["prices/", "/archive"]).Ok.ShouldBeTrue();
|
||||
Workspaces.Single(w => w.Id == "purchasing").ProtectedPaths.ShouldBe(["prices", "archive"]);
|
||||
Org.UpdateWorkspace("purchasing", null, null, ["../outside"]).Ok.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Only_an_empty_workspace_can_be_deleted_and_never_a_unit_s_own()
|
||||
{
|
||||
Setup();
|
||||
Org.CreateWorkspace("articles", "Articles", null, "purchasing", []).Ok.ShouldBeTrue();
|
||||
File("articles", "a-100.md");
|
||||
|
||||
Org.DeleteWorkspace("articles").Ok.ShouldBeFalse("it holds a file");
|
||||
Org.DeleteWorkspace("purchasing").Ok.ShouldBeFalse("it goes with its unit");
|
||||
|
||||
System.IO.File.Delete(Path.Combine(Org.WorkspaceFolder("articles")!, "a-100.md"));
|
||||
Org.DeleteWorkspace("articles").Ok.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deleting_a_unit_keeps_what_its_workspace_holds()
|
||||
{
|
||||
Setup();
|
||||
Org.CreateWorkspace("articles", "Articles", null, "purchasing",
|
||||
[new WorkspaceMemberInput("purchasing", null, WorkspaceAccess.Manage)]).Ok.ShouldBeTrue();
|
||||
File("purchasing", "orders.md");
|
||||
Org.MoveAgent("buyer", null, "leaving").Ok.ShouldBeTrue();
|
||||
|
||||
var result = Org.DeleteUnit("purchasing", "closed down");
|
||||
|
||||
result.Ok.ShouldBeTrue(result.Message);
|
||||
result.Message.ShouldContain("'purchasing'");
|
||||
var kept = Workspaces.Single(w => w.Id == "purchasing");
|
||||
kept.OwnerUnitId.ShouldBeNull();
|
||||
kept.Members.ShouldBeEmpty();
|
||||
Workspaces.Single(w => w.Id == "articles").Members.ShouldBeEmpty("the unit left every member list");
|
||||
System.IO.File.Exists(Path.Combine(Org.WorkspaceFolder("purchasing")!, "orders.md")).ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deleting_a_unit_with_an_empty_workspace_removes_both()
|
||||
{
|
||||
Setup();
|
||||
Org.MoveAgent("ads", null, "leaving").Ok.ShouldBeTrue();
|
||||
|
||||
Org.DeleteUnit("marketing").Ok.ShouldBeTrue();
|
||||
|
||||
Workspaces.ShouldNotContain(w => w.Id == "marketing");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_unit_from_before_workspaces_can_get_its_own()
|
||||
{
|
||||
_test.Host.Instance.Units.Add(new UnitConfig { Id = "legacy", Name = "Legacy" });
|
||||
|
||||
Org.CreateUnitWorkspace("legacy").Ok.ShouldBeTrue();
|
||||
Org.Current().UnitWorkspace("legacy").ShouldNotBeNull();
|
||||
Org.CreateUnitWorkspace("legacy").Message.ShouldContain("already");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user