From 3ea2992d3ad796022ef00c4db052b2a62e40f4fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= <6334612+rbuergi@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:01:41 +0200 Subject: [PATCH 1/2] fix(todo sample), test(compiler): review follow-ups of #5973 and #5975 Todo sample (#5973): the planning area hands out OPEN work only. A member's 'Assign' carried every unassigned todo and assigned the first, so a completed or cancelled one could be assigned, and the button showed with nothing open left. Planning, Backlog and TodaysFocus now share one definition of open (TodoProjections.IsOpen: neither Completed nor Cancelled) and one ordered list (OpenUnassigned); the planning backlog title counts the todos it hands out. In-mesh impersonation check (#5975): a case for the statement-shaped write (this.UserId = ... in a derived request's constructor - the member-access target of an assignment); the test class moves to namespace MeshWeaver.Compiler.Pipeline.Test; the doc states that neither MeshQueryRequest nor AccessContext has a positional constructor. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../LayoutAreas/TodoProjections.cs | 48 ++++++++----- .../Data/Architecture/InMeshImpersonation.md | 2 + .../InMeshImpersonationReferencesTest.cs | 7 +- .../TodoRowScopedActionsTest.cs | 71 +++++++++++++++++++ 4 files changed, 108 insertions(+), 20 deletions(-) diff --git a/samples/Todo/MeshWeaver.Todo/LayoutAreas/TodoProjections.cs b/samples/Todo/MeshWeaver.Todo/LayoutAreas/TodoProjections.cs index d16d58f68d..edbf968163 100644 --- a/samples/Todo/MeshWeaver.Todo/LayoutAreas/TodoProjections.cs +++ b/samples/Todo/MeshWeaver.Todo/LayoutAreas/TodoProjections.cs @@ -139,7 +139,9 @@ public static IEnumerable Planning(IReadOnlyCollection todo } var today = DateTime.Now.Date; - var unassigned = todos.Where(t => t.ResponsiblePerson == Unassigned).ToList(); + // What can still be handed out: the SAME list the backlog section below shows. A member's + // "Assign" takes its first entry, so a finished or cancelled todo is never the one assigned. + var open = OpenUnassigned(todos); yield return Section("workload", x.T("todo.planning.workload", todos.Count(t => t.ResponsiblePerson != Unassigned)), DefaultColor, "20px"); @@ -153,7 +155,7 @@ public static IEnumerable Planning(IReadOnlyCollection todo var line = x.T("todo.planning.member", load, DisplayName(person.Key, x.Localize), active) + (overdue > 0 ? x.T("todo.planning.memberOverdue", overdue) : ""); var entry = Line($"member-{person.Key}", line, TodoStyles.Stat); - yield return unassigned.Count == 0 + yield return open.Count == 0 ? entry : entry with { @@ -161,21 +163,19 @@ public static IEnumerable Planning(IReadOnlyCollection todo GroupIcon = "user-plus", GroupAction = TodoGroupActions.AssignFirst, GroupStyle = TodoStyles.Menu, - Group = [.. unassigned], + Group = [.. open], Person = person.Key, }; } - if (unassigned.Count == 0) + if (open.Count == 0) yield break; - var open = unassigned.Where(t => t.Status != TodoStatus.Completed).ToList(); - yield return open.Count == 0 - ? Section("backlog", x.T("todo.planning.unassigned", unassigned.Count), DefaultColor) - : Section("backlog", x.T("todo.planning.unassigned", unassigned.Count), DefaultColor, "16px", - x.T("todo.group.autoAssign"), "shuffle", TodoGroupActions.AutoAssign, open); + // The title counts what Auto-Assign hands out; the rows below are its first eight. + yield return Section("backlog", x.T("todo.planning.unassigned", open.Count), DefaultColor, "16px", + x.T("todo.group.autoAssign"), "shuffle", TodoGroupActions.AutoAssign, open); - foreach (var task in open.OrderBy(t => t.DueDate ?? DateTime.MaxValue).Take(8)) + foreach (var task in open.Take(8)) { var urgency = task.DueDate?.Date <= today ? "🚨" : task.DueDate?.Date <= today.AddDays(1) ? "⏰" : "📅"; var line = x.T("todo.planning.task", urgency, task.Title, task.Category) @@ -238,11 +238,7 @@ public static IEnumerable MyTasks(IReadOnlyCollection todos /// Open tasks nobody is assigned to, each with its assignment menu. public static IEnumerable Backlog(IReadOnlyCollection todos, TodoViewText x) { - var open = todos - .Where(t => t.ResponsiblePerson == Unassigned - && t.Status != TodoStatus.Completed && t.Status != TodoStatus.Cancelled) - .OrderBy(t => t.DueDate ?? DateTime.MaxValue).ThenBy(t => t.CreatedAt) - .ToList(); + var open = OpenUnassigned(todos); if (open.Count == 0) { @@ -260,13 +256,12 @@ public static IEnumerable Backlog(IReadOnlyCollection todos public static IEnumerable TodaysFocus(IReadOnlyCollection todos, TodoViewText x) { var today = DateTime.Now.Date; - bool Open(TodoItem t) => t.Status != TodoStatus.Completed && t.Status != TodoStatus.Cancelled; IOrderedEnumerable MineFirst(IEnumerable items) => items.OrderBy(t => ResponsiblePersons.IsCurrentUser(t.ResponsiblePerson) ? 0 : 1).ThenBy(t => t.Status); - var overdue = MineFirst(todos.Where(t => t.DueDate?.Date < today && Open(t))).ThenBy(t => t.CreatedAt).ToList(); - var dueToday = MineFirst(todos.Where(t => t.DueDate?.Date == today && Open(t))).ThenBy(t => t.CreatedAt).ToList(); - var ongoing = MineFirst(todos.Where(t => t.ResponsiblePerson != Unassigned && Open(t) + var overdue = MineFirst(todos.Where(t => t.DueDate?.Date < today && IsOpen(t))).ThenBy(t => t.CreatedAt).ToList(); + var dueToday = MineFirst(todos.Where(t => t.DueDate?.Date == today && IsOpen(t))).ThenBy(t => t.CreatedAt).ToList(); + var ongoing = MineFirst(todos.Where(t => t.ResponsiblePerson != Unassigned && IsOpen(t) && (t.DueDate is null || t.DueDate.Value.Date > today))) .ThenBy(t => t.DueDate ?? DateTime.MaxValue).ThenBy(t => t.CreatedAt).ToList(); @@ -299,6 +294,21 @@ IOrderedEnumerable MineFirst(IEnumerable items) // ── Rows ─────────────────────────────────────────────────────────────────────────────────── + /// Work that is still to be done: neither completed nor cancelled. The ONE definition + /// the planning, backlog and focus areas share, so a todo is open in all of them or in none. + /// The todo to judge. + /// true while the todo can still be worked on. + public static bool IsOpen(TodoItem todo) => + todo.Status is not (TodoStatus.Completed or TodoStatus.Cancelled); + + /// The open todos nobody is assigned to, soonest due first (then oldest) — what the + /// backlog lists and what an assignment action hands out, in that order. + /// All todos. + /// The open unassigned todos, ordered. + public static IReadOnlyList OpenUnassigned(IEnumerable todos) => + [.. todos.Where(t => t.ResponsiblePerson == Unassigned && IsOpen(t)) + .OrderBy(t => t.DueDate ?? DateTime.MaxValue).ThenBy(t => t.CreatedAt)]; + /// A section heading, optionally with a group action over . public static TodoEntry Section(string key, string title, string color, string marginTop = "16px", string? groupTitle = null, string groupIcon = "", string groupAction = "", IEnumerable? group = null) diff --git a/src/MeshWeaver.Documentation/Data/Architecture/InMeshImpersonation.md b/src/MeshWeaver.Documentation/Data/Architecture/InMeshImpersonation.md index 10eb663880..ad587125c9 100644 --- a/src/MeshWeaver.Documentation/Data/Architecture/InMeshImpersonation.md +++ b/src/MeshWeaver.Documentation/Data/Architecture/InMeshImpersonation.md @@ -159,6 +159,8 @@ It reports a reference, not a call. That is why it closes three things the runti 3. **Reflection by name** (`GetMethod("ImpersonateAsSystem")`). Only option D closes that. That is limit 5 below. 4. **A reference to `ModulesAssemblyLoadContext`** is not reported, so limit 4 below (a platform load context that in-mesh code instantiates) stays open as well. +**What is NOT on this list: a constructor.** `MeshQueryRequest` and `AccessContext` are body-only records with no positional constructor, so an identity can only arrive through a written member (object initializer, `with`, or an assignment such as a derived request's `this.UserId = …`), a factory (`FromQuery` / `FromQueries` with a `userId`) or a surface call, and all three are reported. If either type ever gains a positional parameter for the identity, `new MeshQueryRequest(…)` becomes a new shape the check must learn. + ### Gate passthrough bound to the request it answers (option E, Plugins) Under the same `Grpc:GateIdentityMode` switch, the trusted port records each request it forwards to a gate under an ordinary user: the request id and that user. A carried user on a gate delivery is **bound**, and passes through, in two cases: diff --git a/test/MeshWeaver.Compiler.Pipeline.Test/InMeshImpersonationReferencesTest.cs b/test/MeshWeaver.Compiler.Pipeline.Test/InMeshImpersonationReferencesTest.cs index 24ddf42883..37acf381e8 100644 --- a/test/MeshWeaver.Compiler.Pipeline.Test/InMeshImpersonationReferencesTest.cs +++ b/test/MeshWeaver.Compiler.Pipeline.Test/InMeshImpersonationReferencesTest.cs @@ -9,7 +9,7 @@ using Microsoft.CodeAnalysis.CSharp; using Xunit; -namespace MeshWeaver.Graph.Test; +namespace MeshWeaver.Compiler.Pipeline.Test; /// /// 🚨 Option C of Doc/Architecture/InMeshImpersonation: the compiler finds every reference in @@ -89,6 +89,11 @@ private static string[] Found(string body) => "MeshWeaver.Mesh.Services.MeshQueryRequest.UserId (written)")] [InlineData("public static MeshQueryRequest M() => new() { UserId = \"alice\" };", "MeshWeaver.Mesh.Services.MeshQueryRequest.UserId (written)")] + // The STATEMENT-shaped write: the member is the Name of a member access that is the left side + // of an assignment (`this.UserId = …`). UserId is init-only, so a derived request's constructor + // is where in-mesh code can write it that way. + [InlineData("public record Mine : MeshQueryRequest { public Mine(string who) { this.UserId = who; } }", + "MeshWeaver.Mesh.Services.MeshQueryRequest.UserId (written)")] // The factories that install a query identity with NO written member — positional and named. [InlineData("public static MeshQueryRequest M(string who) => MeshQueryRequest.FromQuery(\"nodeType:X\", who);", "MeshWeaver.Mesh.Services.MeshQueryRequest.FromQuery (userId supplied)")] diff --git a/test/MeshWeaver.Layout.Test/TodoRowScopedActionsTest.cs b/test/MeshWeaver.Layout.Test/TodoRowScopedActionsTest.cs index 4dca272712..0b27b2f792 100644 --- a/test/MeshWeaver.Layout.Test/TodoRowScopedActionsTest.cs +++ b/test/MeshWeaver.Layout.Test/TodoRowScopedActionsTest.cs @@ -254,6 +254,77 @@ public void TheRowOffersTheActionsItsStatusAllows(TodoStatus status, bool comple row.HeadingStyle.Should().Be(TodoStyles.Hidden, "an item row shows no heading"); } + /// + /// The planning area hands out OPEN work only. A member's "Assign" takes the first entry of its + /// group and Auto-Assign takes all of them, so a completed or cancelled unassigned todo in either + /// group would be assigned to someone; and the backlog title must count the rows under it. + /// + [Fact] + public void Planning_HandsOutOnlyOpenUnassignedWork_AndCountsWhatItLists() + { + var todos = ImmutableList.Create( + Todo("a"), + Todo("p", "Unassigned", TodoStatus.Completed), + Todo("q", "Unassigned", TodoStatus.Cancelled), + Todo("r", "Unassigned"), + Todo("s", "Unassigned", TodoStatus.InProgress)); + + var rows = TodoProjections.Planning(todos, English).ToList(); + + var member = rows.Single(r => r.Key == $"member-{Assignee}"); + member.GroupAction.Should().Be(TodoGroupActions.AssignFirst); + member.Group.Select(t => t.Id).Should().Equal(["r", "s"], + "Assign takes the first of these: the finished 'p' and the cancelled 'q' must not be offered"); + + var backlog = rows.Single(r => r.Key == "backlog"); + backlog.GroupAction.Should().Be(TodoGroupActions.AutoAssign); + backlog.Group.Select(t => t.Id).Should().Equal(["r", "s"], + "Auto-Assign hands out open work only"); + var listed = rows.Where(r => r.Item is not null).Select(r => TodoOf(r).Id).ToList(); + listed.Should().Equal(["r", "s"], "the backlog lists the open unassigned todos, soonest due first"); + backlog.Heading.Should().Be($"##### {English.T("todo.planning.unassigned", listed.Count)}", + "the title counts the rows shown under it, not every unassigned todo"); + } + + /// With nothing open left to hand out, a member row offers no "Assign" and the planning + /// area shows no backlog — finished and cancelled todos are not a backlog. + [Fact] + public void Planning_WithNoOpenUnassignedWork_OffersNoAssignment() + { + var todos = ImmutableList.Create( + Todo("a"), + Todo("p", "Unassigned", TodoStatus.Completed), + Todo("q", "Unassigned", TodoStatus.Cancelled)); + + var rows = TodoProjections.Planning(todos, English).ToList(); + + var member = rows.Single(r => r.Key == $"member-{Assignee}"); + member.GroupAction.Should().BeEmpty("there is nothing open to assign"); + member.GroupStyle.Should().Be(TodoStyles.Hidden); + member.Group.Should().BeEmpty(); + rows.Should().NotContain(r => r.Key == "backlog"); + rows.Should().NotContain(r => r.Item != null); + } + + /// One definition of "open" for the planning, backlog and focus areas: a cancelled todo + /// is finished work in all three. + [Theory] + [InlineData(TodoStatus.Pending, true)] + [InlineData(TodoStatus.InProgress, true)] + [InlineData(TodoStatus.Completed, false)] + [InlineData(TodoStatus.Cancelled, false)] + public void OpenMeansNeitherCompletedNorCancelled_InEveryArea(TodoStatus status, bool open) + { + var todo = Todo("x", "Unassigned", status); + ImmutableList todos = [Todo("a"), todo]; + + TodoProjections.IsOpen(todo).Should().Be(open); + TodoProjections.Planning(todos, English).Any(r => r.Item?.Id == "x").Should().Be(open); + TodoProjections.Backlog(todos, English).Any(r => r.Item?.Id == "x").Should().Be(open); + TodoProjections.TodaysFocus([todo with { DueDate = DateTime.Now.Date }], English) + .Any(r => r.Item?.Id == "x").Should().Be(open); + } + private static readonly TodoViewText English = new((key, args) => LocalizationCatalog.Get(key, "en", args), System.Globalization.CultureInfo.InvariantCulture); From 91f5237d404e276e6ecd6688c6ba51690f2c2d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20B=C3=BCrgi?= <6334612+rbuergi@users.noreply.github.com> Date: Fri, 2 Oct 2026 20:23:38 +0200 Subject: [PATCH 2/2] test(compiler): the impersonation probe references only assemblies still on disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the class in its project's own namespace it runs after classes that compile node assemblies into temp folders, load them and delete the folder. Those stay in the AppDomain with a Location that no longer exists, and the probe compilation referenced every loaded assembly: all 26 cases failed in CI with FileNotFoundException on /tmp/mesh-image-census-… and /tmp/two-generations-…. The reference set now skips an assembly whose file is gone, so it no longer depends on which classes ran first. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../InMeshImpersonationReferencesTest.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/MeshWeaver.Compiler.Pipeline.Test/InMeshImpersonationReferencesTest.cs b/test/MeshWeaver.Compiler.Pipeline.Test/InMeshImpersonationReferencesTest.cs index 37acf381e8..05177c53d2 100644 --- a/test/MeshWeaver.Compiler.Pipeline.Test/InMeshImpersonationReferencesTest.cs +++ b/test/MeshWeaver.Compiler.Pipeline.Test/InMeshImpersonationReferencesTest.cs @@ -38,8 +38,13 @@ private static Compilation Compile(string body) // Touch every assembly the snippets bind against so it is loaded and has a Location. _ = new[] { typeof(AccessService), typeof(WellKnownUsers), typeof(MeshQueryRequest), typeof(ContentImportBuilder), typeof(System.Reactive.Linq.Observable) }; + // 🚨 Only assemblies whose file is STILL on disk. Other classes of this project compile + // node assemblies into temp folders, load them and delete the folder when they finish; such + // an assembly stays in the AppDomain with a Location that no longer exists, and referencing + // it fails the compilation with FileNotFoundException. Which classes ran before this one is + // a matter of class order, so the reference set must not depend on it. var references = AppDomain.CurrentDomain.GetAssemblies() - .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) + .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location) && System.IO.File.Exists(a.Location)) .Select(a => MetadataReference.CreateFromFile(a.Location)); var compilation = CSharpCompilation.Create("InMeshProbe", [CSharpSyntaxTree.ParseText($"{Usings}\npublic static class Probe {{\n{body}\n}}", path: "Probe.cs")],