Skip to content

fix(gate): the Tests probe of a partition-owning NodeType is created at the top level (#4602) - #4618

Merged
meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/4602-gate-probe-partition-owning
Sep 17, 2026
Merged

meshweaver-cloud[bot] merged 2 commits into
mainfrom
fix/4602-gate-probe-partition-owning

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

The defect

A NodeType whose definition declares ownsPartition has instances that ARE partition roots, so
their path is just their id. Since 693feae92 ("every partition-owning NodeType can be created at
the top level, not only Space") and ae0f706b3 (#4449 item 3) the create-validation chain refuses a
NESTED instance of every owning type — but PluginGateRunner.CreateTestsProbe still built
{typePath}/GateProbe unconditionally, so the probe could no longer be created for such a type:

could not create the Tests probe: InvalidOperationException: Cannot create 'Crm/Client/GateProbe':
a 'Crm/Client' owns its partition, so it must be created at the top level, where its path is just
its id ('GateProbe') — not under 'Crm/Client'.

Deterministic, so the transient-retry steward correctly declined it. Any module repo with a
partition-owning NodeType could not be green on main, publish-bake never ran, and merged work
became unshippable (MeshWeaver.Crm pinned at 79c5ceb2, 2026-09-15).

The fix

CreateTestsProbe asks PartitionOwningTypes.OwnsPartitionWithoutActivating — the same
resolution OwnsPartitionProvisioningValidator consults on a nested create
— and places the probe
at the top level when the answer is yes. Gate and validator therefore cannot disagree about a type,
whichever of the two routes (static registry, durable row) produced the answer. Exempting the gate
from validation was the alternative and is the wrong one: the probe exists to exercise the same
create a real instance gets.

Addressing the caveat raised on the issue: a top-level id has to carry the disambiguation the
namespace gave the constant GateProbe for free — one run covers every type of a repo in a single
in-process mesh, so two owning types would collide on one path. TestsProbeId embeds the type path,
sanitised so it satisfies PartitionDefinition.IsValidPartitionSegment by construction (the id
becomes the partition, and so the schema, name), and closes an over-long one with a stable SHA-256
prefix rather than a Guid, so two runs of the same repo stay readable side by side.

Evidence

PartitionOwningNodeType_TestsProbeIsCreatedAtTopLevel_AndTwoOfThemDoNotCollide ships a fixture
package with TWO partition-owning types, so it pins both halves. Measured on this worktree:

  • Fix reverted, test project rebuilt: fails with exactly the error above, for both types.
  • With the fix: GateProbeEstate_Tenant / GateProbeEstate_Branch created at the top level,
    both Tests areas execute green, ALL GREEN, exit 0.
  • Whole project: total: 395, failed: 0 (the non-owning fixtures still use the nested probe).
  • dotnet build -c Release -warnaserror clean on MeshWeaver.PluginTester and its test project.

Fixes #4602

Pairs-with: none — no public type or member leaves src/; the change is inside
tools/MeshWeaver.PluginTester plus a new What's New entry.

🤖 Generated with Claude Code

…at the top level

An instance of a NodeType whose definition declares `ownsPartition` IS a partition
root, so its path is just its id — and since 693feae / ae0f706 the create
validation chain refuses a nested one for EVERY owning type, not only `Space`.
`CreateTestsProbe` built `{typePath}/GateProbe` unconditionally, so every such type
came back

    could not create the Tests probe: InvalidOperationException:
    Cannot create 'Crm/Client/GateProbe': a 'Crm/Client' owns its partition, so it
    must be created at the top level, where its path is just its id ('GateProbe')
    — not under 'Crm/Client'.

The failure is deterministic, so "retry known transients" correctly declined it and
the repository's `main` stayed red: `publish-bake` never ran and every merged change
became unshippable.

The gate now asks `PartitionOwningTypes.OwnsPartitionWithoutActivating` — the SAME
resolution `OwnsPartitionProvisioningValidator` consults on a nested create — so the
two cannot disagree about a type, whichever route (static registry, durable row)
answered. Exempting the gate from validation was the alternative and is the wrong one:
the probe exists to exercise the create a real instance gets.

A top-level probe's id has to carry the disambiguation the namespace gave the constant
`GateProbe` for free: one run covers every type of a repo in one in-process mesh, so
two owning types would collide on one path. The id embeds the type path, sanitised to
satisfy `PartitionDefinition.IsValidPartitionSegment` (it becomes the partition, hence
the schema, name) and closed with a stable SHA-256 prefix past the 63-byte cap.

The test ships two partition-owning types in one fixture package, so it pins both
halves. Measured on this worktree: with the fix reverted it fails with exactly the
error above for both types; with it, both probes are created top-level as
`GateProbeEstate_Tenant` / `GateProbeEstate_Branch` and both Tests areas run green.

Fixes #4602

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 17, 2026 16:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The probe creation retains unsafe impersonation handling and its ID sanitization permits deterministic collisions for valid type paths.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates the plugin gate so partition-owning NodeTypes receive valid top-level test probes.

Changes:

  • Resolves partition ownership before creating probes.
  • Adds collision regression coverage for two owning types.
  • Adds a What's New fix entry.
File summaries
File Description
tools/MeshWeaver.PluginTester/PluginGateRunner.cs Creates probes at the correct scope and generates IDs.
test/MeshWeaver.PluginTester.Test/PluginGateRunnerTest.cs Tests multiple partition-owning types.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-09-17-a-package-whose-type-owns-a-partition-can-be-published-again.md Documents the user-visible fix.
Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +802 to +804
.SelectMany(probe => Observable.Using(
() => access.ImpersonateAsSystem(),
_ => meshService.CreateNode(probe)))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and fixed in 480c010. The site now uses the sealed access.RunAsSystem(() => meshService.CreateNode(probe)) helper, which enters the scope at Subscribe and leaves it on the way out of that same Subscribe, so the subscriber is never latched. It was this file's only such site, so its line is gone from test/ImpersonationScopeSites.allow and TotalBudget follows it 64 → 63 in the same change — the tidy the guard's STALE report asks for.

Comment on lines +853 to +856
var sanitized = string.Concat(typePath.Select(c =>
char.IsAsciiLetterOrDigit(c) || c is '.' or '-' or '_' ? c : '_'));
var candidate = "GateProbe" + sanitized;
if (Encoding.UTF8.GetByteCount(candidate) <= 63)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and fixed in 480c010 — Estate/A/B and a type literally named Estate/A_B both sanitise to GateProbeEstate_A_B, and both are valid literal type paths, so the collision was reachable. The digest is now UNCONDITIONAL and is taken over the UNSANITISED path, so the two stay apart whatever the sanitiser does to them; the head is cut to 45 chars so GateProbe(9) + head + _ + 8 hex still fits the 63-byte partition-name cap.

TestsProbeId_IsInjective_AcrossPathsThatSanitiseAlike (PluginGateRunnerTest) asserts exactly that pair, the long-shared-head pair, and that every id it produces satisfies PartitionDefinition.IsValidPartitionSegment. It is a direct unit test rather than a gate fixture because the gate fixture cannot reach the case — its two owning types differ before the collapse.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

264 tests  ±0   264 ✅ ±0   2m 14s ⏱️ -3s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 480c010. ± Comparison against base commit cf64edd.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

533 tests  ±0   533 ✅ ±0   58s ⏱️ -2s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 480c010. ± Comparison against base commit cf64edd.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

429 tests  +2   429 ✅ +2   1m 1s ⏱️ ±0s
  2 suites ±0     0 💤 ±0 
  2 files   ±0     0 ❌ ±0 

Results for commit 480c010. ± Comparison against base commit cf64edd.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    3 files  ±0      3 suites  ±0   5m 29s ⏱️ -23s
2 332 tests ±0  2 332 ✅ ±0  0 💤 ±0  0 ❌ ±0 
2 333 runs  ±0  2 333 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 480c010. ± Comparison against base commit cf64edd.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

1 496 tests  ±0   1 494 ✅ ±0   7m 37s ⏱️ +6s
    4 suites ±0       2 💤 ±0 
    4 files   ±0       0 ❌ ±0 

Results for commit 480c010. ± Comparison against base commit cf64edd.

♻️ This comment has been updated with latest results.

…he sealed scope

Both automatic-review findings, both valid.

1. `Observable.Using(() => access.ImpersonateAsSystem(), ...)` enters the AsyncLocal
   scope on the SUBSCRIBING thread and leaves it on whichever thread the create
   terminates on, so the subscriber stays latched as system-security (#1790). The
   site moves to the sealed `access.RunAsSystem(...)` helper. It was the file's only
   one, so its allow line goes and `TotalBudget` follows it 64 to 63 — the tidy the
   guard's STALE report asks for, in the change that makes it stale.

2. `TestsProbeId` was not injective. Every disallowed character collapses to `_`,
   so `Estate/A/B` and a type literally named `Estate/A_B` — both valid literal
   type paths — sanitise to the same string, and appending the digest only past
   the length cap left that collision open. It would surface as the SECOND owning
   type's probe failing already-exists: the failure this change exists to prevent,
   one door along. The digest is now unconditional and is taken over the
   UNSANITISED path, so the two stay apart whatever the sanitiser does to them;
   the head is cut to 45 so the 63-byte partition-name cap still holds.

`TestsProbeId_IsInjective_AcrossPathsThatSanitiseAlike` asserts the collision case
directly — the gate fixture cannot reach it, because its two types differ before
the collapse — plus the long-path case and that every id it produces satisfies
`PartitionDefinition.IsValidPartitionSegment`.

Measured: MeshWeaver.PluginTester.Test 396/396, the impersonation ratchet guard
3/3, `-c Release -warnaserror` clean on both projects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    6 files  ±0      6 suites  ±0   11m 27s ⏱️ -9s
3 186 tests ±0  2 994 ✅ ±0  192 💤 ±0  0 ❌ ±0 
3 190 runs  ±0  2 998 ✅ ±0  192 💤 ±0  0 ❌ ±0 

Results for commit 480c010. ± Comparison against base commit cf64edd.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   17 files  ±0     17 suites  ±0   28m 48s ⏱️ -31s
8 240 tests +2  8 046 ✅ +2  194 💤 ±0  0 ❌ ±0 
8 245 runs  +2  8 051 ✅ +2  194 💤 ±0  0 ❌ ±0 

Results for commit 480c010. ± Comparison against base commit cf64edd.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 6e21e91 Sep 17, 2026
38 of 39 checks passed
rbuergi added a commit that referenced this pull request Sep 17, 2026
…→ 62)

#4618 lowered it 64 → 63 by removing PluginGateRunner's line; this branch lowered it 64 → 63 by
taking InstanceAutoRegistrationService from 6 to 5. Both are real, so the merged budget is 62 —
exact against the merged allow file (33 entries, sum 62). Taking either side alone would have left
the constant one ABOVE the file, which is the free slot the exact seed exists to remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rbuergi
rbuergi deleted the fix/4602-gate-probe-partition-owning branch October 10, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The gate's Tests probe cannot be created for a partition-owning NodeType — every downstream main carrying one goes red

2 participants