diff --git a/src/MeshWeaver.Graph/Configuration/CodeQueryResolver.cs b/src/MeshWeaver.Graph/Configuration/CodeQueryResolver.cs index e10dddf2a3..bc543af2a3 100644 --- a/src/MeshWeaver.Graph/Configuration/CodeQueryResolver.cs +++ b/src/MeshWeaver.Graph/Configuration/CodeQueryResolver.cs @@ -25,8 +25,10 @@ namespace MeshWeaver.Graph.Configuration; /// A namespace:X value with no / (e.g. bare Source /// or Test) is rebased onto the NodeType's own path so the defaults /// read as "my own Source / Test folder". -/// Every emitted query is ANDed with nodeType:Code so non-code -/// children never leak in. +/// A query that does not already name a node type is ANDed with +/// nodeType:Code, so non-code children never leak in. One that names its +/// own (nodeType:Scope, say) is left as authored — the filter is a +/// DEFAULT, not an override. /// /// public static class CodeQueryResolver diff --git a/test/MeshWeaver.Hosting.Monolith.Test/SourceDiscoveryUnavailableTest.cs b/test/MeshWeaver.Hosting.Monolith.Test/SourceDiscoveryUnavailableTest.cs index 701f03dc9a..94b96d3428 100644 --- a/test/MeshWeaver.Hosting.Monolith.Test/SourceDiscoveryUnavailableTest.cs +++ b/test/MeshWeaver.Hosting.Monolith.Test/SourceDiscoveryUnavailableTest.cs @@ -259,16 +259,38 @@ public sealed class StarvedSourceQueryProvider : IMeshQueryProvider /// produces it /// (namespace:{selfPath}/Source scope:subtree nodeType:Code). /// - /// 🚨 Matched on the namespace: FORM, not on the bare path, so only source - /// DISCOVERY starves. Node creation and single-node reads under the same subtree use - /// path:-shaped queries and must keep working — otherwise the test could not put a - /// healthy source node in the mesh in the first place, and "the code is fine, the read is - /// not" would be unprovable. Note the type's /Test query is deliberately NOT starved: - /// one leg dies while the other answers, which is precisely the PARTIAL source set that used - /// to reach Roslyn. + /// 🚨 Matched on the namespace: FORM **and** nodeType:Code, so only source + /// DISCOVERY starves. Node creation and single-node reads under the same subtree must keep + /// working — otherwise the test could not put a healthy source node in the mesh in the first + /// place, and "the code is fine, the read is not" would be unprovable. Note the type's + /// /Test query is deliberately NOT starved: one leg dies while the other answers, which + /// is precisely the PARTIAL source set that used to reach Roslyn. + /// + /// 🚨 The nodeType:Code half is load-bearing, and its absence was a real flake + /// (shard 1, 2026-08-13). The SECURITY layer reads its policy and grants with + /// namespace:-shaped queries over the SAME subtree — + /// namespace:…/Source id:_Policy nodeType:PartitionAccessPolicy and + /// namespace:…/Source/_Access nodeType:AccessAssignment — so a marker that matched the + /// namespace alone starved those too, and the CreateNode at the top of this test hung + /// (observed: Executing(CreateNodeRequest, 33014ms)) until the 30 s assertion gave up. + /// It passed whenever those security queries were already cached and failed when they were + /// cold, which is why it reproduced on a CI shard and never locally (10/10 green). + /// CodeQueryResolver appends nodeType:Code to a source query that does not already + /// name a node type, and this type declares no Sources — so its discovery runs the + /// DEFAULT query (namespace:{path}/Source scope:subtree) and does carry the clause, while + /// the security queries carry node types of their own. That separates them here. (A type whose + /// declared source query named some OTHER node type would not be starved by this marker; nothing + /// in this suite does, and the starve is asserted to still bite — see the test's own proof.) /// private const string StarveMarker = "namespace:SourceUnavailableTest/StarvedSourceType/Source"; + /// + /// The discriminator that keeps the starve to SOURCE DISCOVERY — see the note on + /// . Without it the security policy/grant reads over the same + /// namespace starve as well, and node creation into that subtree hangs. + /// + private const string SourceDiscoveryMarker = "nodeType:Code"; + /// public string Name => nameof(StarvedSourceQueryProvider); @@ -280,7 +302,8 @@ public IObservable> Query( MeshQueryRequest request, JsonSerializerOptions options) { var starve = request.EffectiveQueries - .Any(q => q?.Contains(StarveMarker, StringComparison.OrdinalIgnoreCase) == true); + .Any(q => q?.Contains(StarveMarker, StringComparison.OrdinalIgnoreCase) == true + && q.Contains(SourceDiscoveryMarker, StringComparison.OrdinalIgnoreCase)); return starve ? Observable.Throw>(new TimeoutException( "No response received for SubscribeRequest within 00:01:00 — the owning "