using ClawdDotNet.App.Services; using ClawdDotNet.Core.Config; using Shouldly; namespace ClawdDotNet.App.Tests.Services; /// /// #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. /// 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 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"); } }