Repository navigation
Pin #1127: the User NodeType declaration is never treated as an account #1157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
1d0496d
Pin #1127: the User NodeType declaration is never treated as an account
rbuergi 6763710
Merge main (green at 0e4cc931) — prior CI red was main's StaleStampRo…
rbuergi 18cdf8d
Merge remote-tracking branch 'origin/main' into HEAD
rbuergi cc2d39e
Address review: pin the phantom's ABSENCE against the node, not the i…
rbuergi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
25 changes: 25 additions & 0 deletions
25
...Data/WhatsNew/2026-08-10-ai-settings-no-longer-written-for-a-phantom-account.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| --- | ||
| Name: AI settings are no longer written for a phantom account | ||
| Category: Fix | ||
| Description: Package installs and portal starts no longer try to register AI agent and skill sources for a non-existent account named "User", which failed on every start and filled the logs with database errors. | ||
| Icon: Sparkle | ||
| Order: -20260810 | ||
| --- | ||
|
|
||
| # AI settings are no longer written for a phantom account | ||
|
|
||
| When a plugin package is installed — and again on every portal start, as part of the | ||
| routine repair pass — the platform registers the package's agents and skills in each | ||
| user's AI settings, so new content shows up in everyone's pickers right away. | ||
|
|
||
| The list of "each user" accidentally included one entry that is not a user at all: the | ||
| built-in definition of the User node type itself. Treated as an account, it produced a | ||
| write into a storage area named after it that no portal provisions, which failed every | ||
| single time — once per installed package, on every pod, on every start. The failures were | ||
| caught and retried on the next start, so nothing a real person entered was ever touched | ||
| or lost, but the logs filled with recurring database errors and an incident was filed for | ||
| what looked like missing storage. | ||
|
|
||
| The phantom entry is now filtered out where the user list is built, and a regression test | ||
| pins it there: real accounts keep receiving every package's sources, and the type | ||
| definition is never mistaken for an account again. |
112 changes: 112 additions & 0 deletions
112
test/MeshWeaver.Hosting.Monolith.Test/AiSourcesInstallHookTest.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| using System; | ||
| using System.Linq; | ||
| using System.Reactive; | ||
| using System.Reactive.Linq; | ||
| using System.Reactive.Threading.Tasks; | ||
| using System.Threading.Tasks; | ||
| using MeshWeaver.AI; | ||
| using MeshWeaver.Data; | ||
| using MeshWeaver.Graph; | ||
| using MeshWeaver.Graph.Configuration; | ||
| using MeshWeaver.Hosting.Monolith.TestBase; | ||
| using MeshWeaver.Mesh; | ||
| using MeshWeaver.Messaging; | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using Xunit; | ||
|
|
||
| namespace MeshWeaver.Hosting.Monolith.Test; | ||
|
|
||
| /// <summary> | ||
| /// 🚨 #1127: pins <see cref="AiSourcesInstallHook"/>'s user enumeration against the SELF-TYPED | ||
| /// <c>User</c> NodeType declaration node. | ||
| /// | ||
| /// <para><c>User</c> is deliberately self-typed (<see cref="UserNodeType.CreateMeshNode"/> sets | ||
| /// <c>NodeType = "User"</c> on the declaration at path <c>User</c>), so the hook's pathless | ||
| /// <c>nodeType:User</c> enumeration returns the DECLARATION alongside the real accounts. Pre-fix, | ||
| /// the declaration's Id (<c>"User"</c>) was treated as an account id, and every install / boot | ||
| /// repair pass tried to write <c>User/_Memex/AiSettings</c> — a partition literally named | ||
| /// <c>User</c> that no instance provisions. On PostgreSQL that failed each time with | ||
| /// <c>42P01: relation "user.mesh_nodes" does not exist</c> (27× on memex-cloud: 3 pod boots × | ||
| /// ~9 installed packages, the per-user Catch reducing it to log noise); on stores without | ||
| /// partition schemas it silently created a phantom node. The failing writes carried nothing a | ||
| /// real user ever entered — no user data was lost — but the noise re-fired on every boot.</para> | ||
| /// | ||
| /// <para>The fix is in the enumeration (<c>AiSourcesInstallHook.Users()</c>): the declaration id | ||
| /// is dropped before reconciliation. This test seeds real accounts, runs the hook end-to-end on | ||
| /// the monolith mesh (where the phantom write would SUCCEED and leave evidence), and asserts the | ||
| /// package sources land on real users while <c>User/_Memex/AiSettings</c> is never created.</para> | ||
| /// </summary> | ||
| public class AiSourcesInstallHookTest(ITestOutputHelper output) : MonolithMeshTestBase(output) | ||
| { | ||
| private const string Package = "HookPkg"; | ||
|
|
||
| protected override MeshBuilder ConfigureMesh(MeshBuilder builder) | ||
| => base.ConfigureMesh(builder) | ||
| .AddAI() | ||
| .AddSampleUsers(); | ||
|
|
||
| [Fact(Timeout = 60_000)] | ||
| public async Task InstallHook_RegistersSourcesForRealUsers_NeverForTheUserTypeDeclaration() | ||
| { | ||
| var hook = Mesh.ServiceProvider.GetServices<IPartitionInstallHook>() | ||
| .OfType<AiSourcesInstallHook>() | ||
| .Single(); | ||
|
|
||
| await hook.OnPartitionInstalled(Package) | ||
| .Timeout(TimeSpan.FromSeconds(30)) | ||
| .FirstAsync() | ||
| .ToTask(); | ||
|
|
||
| // A real account got the package's agent + skill sources — proves the hook enumerated | ||
| // users and wrote (the filter must not over-filter real accounts). | ||
| var realUserPath = AiSettingsNodeType.PathFor("TestUser"); | ||
| var settingsNodes = await Mesh.GetWorkspace() | ||
| .GetQuery($"{AiSettingsNodeType.NodeType}|TestUser", | ||
| $"path:{realUserPath} nodeType:{AiSettingsNodeType.NodeType} select:path,id,name,nodeType,content") | ||
| .Where(nodes => nodes.Any(n => | ||
| string.Equals(n.NodeType, AiSettingsNodeType.NodeType, StringComparison.OrdinalIgnoreCase))) | ||
| .FirstAsync() | ||
| .Timeout(TimeSpan.FromSeconds(15)) | ||
| .ToTask(); | ||
|
|
||
| var settings = AiSettingsNodeType.Effective( | ||
| settingsNodes.First(n => | ||
| string.Equals(n.NodeType, AiSettingsNodeType.NodeType, StringComparison.OrdinalIgnoreCase)), | ||
| new AiSettings(), | ||
| Mesh.JsonSerializerOptions); | ||
| settings.AgentQueries.Should().Contain( | ||
| q => q.Contains($"{Package}/", StringComparison.OrdinalIgnoreCase), | ||
| "the install hook must register the package's agent source on every real account"); | ||
| settings.SkillQueries.Should().Contain( | ||
| q => q.Contains($"{Package}/", StringComparison.OrdinalIgnoreCase), | ||
| "the install hook must register the package's skill source on every real account"); | ||
|
|
||
| // The self-typed `User` NodeType DECLARATION must never be reconciled as an account: | ||
| // no node at User/_Memex/AiSettings. Pre-fix this write happened once per package per | ||
| // boot — 42P01 noise on PostgreSQL, a phantom node here (#1127). | ||
| var phantomPath = AiSettingsNodeType.PathFor(UserNodeType.NodeType); | ||
|
|
||
| // 🚨 Read the NODE, not the index. This assertion pins an ABSENCE immediately after a write | ||
| // pass, which is the one case a query cannot settle: GetQuery is eventually consistent, so | ||
| // an empty first emission is equally consistent with "the phantom was never written" and | ||
| // "the phantom was written and the index has not caught up yet". The second reading is the | ||
| // regression this test exists to catch, so a query here can only ever pass — including on | ||
| // the pre-fix code. GetMeshNodeStream is authoritative, and an absent node surfaces as a | ||
| // routing NotFound (OnError/DeliveryFailureException), which is a DIFFERENT signal from a | ||
| // present one (OnNext) rather than the same empty result. Same shape as | ||
| // CompileActivityNoPhantomPathTest, which pins the identical "leaf never created under a | ||
| // real ancestor" phantom. | ||
| var probe = await Mesh.GetMeshNodeStream(phantomPath) | ||
| .Where(n => n?.Content is not null) | ||
| .Materialize() | ||
| .Should().Within(TimeSpan.FromSeconds(15)).Match( | ||
| n => n.Kind == NotificationKind.OnError, | ||
| $"the `User` NodeType declaration node is not an account — anything served at " | ||
| + $"{phantomPath} means the enumeration treated the self-typed declaration as a " | ||
| + "user again (#1127)"); | ||
|
|
||
| probe.Exception.Should().BeOfType<DeliveryFailureException>( | ||
| "the absence must be the routing NotFound for a node that was never created — any other " | ||
| + "fault would mean this assertion passed for a reason unrelated to #1127"); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.