Composing modules into the in-mesh test lane
Measured on MeshWeaver.Plugins at beeacfa5 (2026-09-01), against the pinned tester digest
sha256:36eb4296… — the same image CI runs.
The test estate is moving onto the product's own lane: take the image, install the plugin, execute
the test code. mw-plugin-test build already IS that lane — it compiles every package as a
dependency cascade and executes each NodeType's Test/*.cs cases, inside the image, against the
assemblies a portal loads. What stopped it was one gap, and this page is what closed it.
The gap, measured
Since MeshWeaver#2276 took the AI engine out of the platform repo, the tester image ships no module at all. Not "fewer modules" — none:
$ docker create meshweaver.azurecr.io/mw-plugin-test@sha256:36eb4296… && docker export … | grep '^app/'
27 MeshWeaver.*.dll at /app # the core framework, and only the core framework
0 modules/ # the directory does not exist
100 module-libs/ # third-party shelf: Azure.*, Microsoft.Extensions.AI, Radzen.Blazor…
module-libs/ is a shelf of THIRD-PARTY libraries. No MeshWeaver.AI.dll, no
MeshWeaver.Markdown.Collaboration.dll, no Blazor module. So the cascade over this repo reads:
build: 48 green, 6 red, 0 blocked of 54 in 17.2s wall
Providers RED Providers/ProvidersApp: compile CS0246 'ModelProviderConfiguration'
RolePlay RED RolePlay/Story: compile CS0103 'ThreadNodeType'
MyAi RED MyAi/Panel: compile
Collaboration RED Collaboration/Review: compile CS0103 'CommentsExtensions'
Edu RED Edu/PromptCell: compile
Hosting RED Hosting/Backup … +5: compile CS1061 IMessageHub.StartThread
🚨 Every one of those errors names the CONTENT. ModelProviderConfiguration lives in
src/MeshWeaver.AI; CommentsExtensions lives in src/MeshWeaver.Markdown.Collaboration. The
plugin source is correct and the reference set is short — but the compiler can only report the
symbol, so the message reads as this plugin is broken. That misdirection is what
MeshWeaver#2563 cost a day to, in the bake.
What fills it
The runner has accepted --module <dll> since MeshWeaver#2541 — the seam exists. Nothing
populated it per package. scripts/mesh-test.py now does, from a map the packages already
publish and nobody has to maintain:
content.moduleon a package'sindex.jsonnames that package's assembly. 33 of this repo's 54 packages declare one.content.requiressays which packages it may bind.
So the modules composed for a run are the declared modules of the selected packages' requires
closure — derived, per package, never a hand-kept list.
python3 scripts/mesh-test.py --modules <dir> # bundles, or an image's extracted /app
python3 scripts/mesh-test.py --print-modules # resolve and report; run nothing
Measured, same tree, same image, with the two declared modules composed:
build: 54 green, 0 red, 0 blocked of 54 in 19.1s wall; critical path (3): Store → AI → Hosting
built 54/54 package(s) green
compiled 80 node type(s)
CI runs it in the compile-check job, against the refs/ directory those steps already assemble
(the image's /app plus this run's module bundles), so there is no second download to drift.
What that is worth, in cases
Compiling is the gate; executing is the point. With the modules composed — and with the three
unrelated Store failures below scaffolded out so the cascade stops blocking — the whole repo runs
its in-mesh suites:
build: 54 green, 0 red, 0 blocked of 54 in 18.9s wall
tested 2035 passed, 0 failed, 134 need a mesh
Of those, 630 cases belong to the six packages that could not compile at all before, and every one of them is a case that had no way to run in the product's own lane:
| package | cases now executing | needs a mesh |
|---|---|---|
Hosting |
297 | 16 |
Edu |
209 | 22 |
RolePlay |
64 | 4 |
Collaboration |
26 | 1 |
Providers |
24 | 1 |
MyAi |
10 | 1 |
needs a mesh is not a failure: StaticTestRunner executes the parameterless cases off the mesh
and COUNTS the hosted ones rather than dropping them, so the report says exactly how much of a
type's suite the mesh lane still owns.
Three things that are load-bearing and look like details
A module is referenced WHERE IT WAS FOUND, never copied out on its own. The runner uses
Assembly.LoadFrom, so the module's dependency closure has to sit beside it. A lone
MeshWeaver.AI.dll copied into an empty directory fails with
FATAL: bake: module '/mods/MeshWeaver.AI/MeshWeaver.AI.dll' could not be loaded —
Could not load file or assembly … The system cannot find the file specified.
— naming a file that IS there, which reads as a bad path and is not one. Bundles are therefore
unpacked whole and referenced inside their own meshweaver/modules/.
The staging directory must be one the container engine shares. Docker Desktop on macOS shares
$HOME and does NOT share /private/tmp: a stage under the system temp directory mounts as an
EMPTY directory, every module resolves on the host, none exists in the container, and the runner
blames --module for a path that is perfectly correct. The default stage is therefore under
~/.cache/meshweaver; --stage / MESH_TEST_STAGE override it.
An unresolved module is reported BEFORE the run. Left silent it arrives as a CS0246 on the plugin's own source and the reader spends their first hour on content that is not broken.
What the derivation surfaced: two undeclared dependencies
Deriving the module set from requires immediately found two packages that bind MeshWeaver.AI
types while declaring only Store:
| package | binds | declared |
|---|---|---|
Providers |
ModelProviderConfiguration, ModelProviderNodeType, LanguageModelNodeType |
Store only |
Hosting |
IMessageHub.StartThread (Hosting/Backup) |
Store only |
Both now declare AI@^1.0.0. This is not cosmetic: a missing dependency HANGS rather than
errors — a node whose module is absent cannot activate, so every read costs the caller the full
60 s SubscribeRequest timeout and the page dies with nothing saying "missing dependency". The
in-mesh cascade's critical path moved from Store → Hosting to Store → AI → Hosting, which is
what a correctly declared graph looks like.
What is still in the way
The lane runs --no-tests today, and that is a statement about Store, not about the facility.
Three in-mesh cases fail on main:
Store/Maintenance—RefreshModules_AllRegistriesMissed_NamesEachRegistrysAnswerandRefreshModules_SummaryFailsOnlyWhenNothingLanded: aModuleRefreshsummary over a package no registry offered is not classified as a failure summary, so a refresh that landed nothing reports success.Store/Installer—EnsureHomeRootPlan_RetriesASlowActivation_InsteadOfFailingTheSetup: a correct existing home root is written where the test asserts zero writes.
Store is the root of the graph, so a red Store blocks its 53 dependents — with cases on, the
cascade reports 0 green, 1 red, 53 blocked and says nothing about anyone else's content. Fix those
three and the --no-tests comes off; that is one line.
The other half of the estate — which xunit suites can move at all, and what each one still needs — is sized separately, in the classification titled How much of the xunit mesh-test estate can move in-mesh. That page is not committed yet, so this one deliberately does not link to it: a link to a node that does not exist reads as a broken page rather than as work in flight.
A composed module must have exactly ONE producer (#1262 / Systemorph/MeshWeaver#3175)
Composition answers "can this content bind the module?". It does not answer "is there only one build of that module in this compile?" — and on 2026-09-03 the second question stopped core CD.
MeshWeaver.Markdown.Collaboration (the Essentials module) was composed with --module AND
present in the portal image's /app, because MeshWeaver.Blazor.Views — which ships in the image —
took a ProjectReference to it for three views. Since MeshWeaver#3022 both the bake and the gate
compile against the PORTAL image's /app, so both saw two builds of one assembly name, and the bake
refused to seal:
compile: FATAL — module(s) MeshWeaver.Markdown.Collaboration are composed with --module AND
shipped by the platform host at '/app' — two builds of one assembly name in one bake.
The maintainer's ruling is the rule to hold on to: a module lives in exactly one place. Code that binds a module belongs IN that module — which is why the three collaboration views moved there rather than the guard being softened.
The failure is not symmetric, and the harsher half is the one to design against
| the module is missing from… | what you see |
|---|---|
| the portal's view registry | the control renders through the escaped-HTML fallback slot — the page still renders |
| an in-mesh COMPILE reference set | CS0246/CS0103 naming the content, the prebuilt assembly DECLINED ("live is ABSENT — compiling instead"), and the NodeType PARKED |
MeshWeaver.Education's main was red on exactly the second shape for MeshWeaver.Maps before
Plugins#1242 sealed it: "Prebuilt assembly for Cornerstone/Pricing DECLINED … 'MeshWeaver.Maps'
built against ref:9AC0489F…, live is ABSENT", then CS0234, then PARKED. The error names the
CONTENT, so it reads as the consuming repo breaking — the same misdirection this page opens with.
Who composes the collaboration module, per lane
Removing it from /app is safe only because every lane that compiles already composes it
explicitly — none of these was added by #1262; the change removed the duplicate, not a source:
| lane | how it arrives |
|---|---|
this repo's Compile every NodeType (vs core) |
always-modules: …,MeshWeaver.Markdown.Collaboration,… + the module-bundle-MeshWeaver.{AI,Markdown.Collaboration,Maps} download |
core CD publish-bake |
module-artifacts: module-bundle-MeshWeaver.{AI,Markdown.Collaboration} |
| satellite node repos | registry-modules: AI Essentials + upstream-seed: plugins — Essentials.content.module IS this assembly |
| portal runtime | the registry-served Essentials package (preInstalled: true), plus a modules/ SEED in the image for a self-registry install |
🚨 The image seed is NOT one of these. It is a portal-image publish artifact and reaches no
tester or gate mesh; a lane that needs the module must compose it. And because /app no longer
backstops a missing bundle, compile-check now asserts every expected bundle by name rather
than only MeshWeaver.AI.dll — its comment had claimed that for a while before it was true.
The binders are easy to miss: .AddComments() lives inside a JSON string
grep --include='*.cs' does not find them. A NodeType's configuration is C# inside its
index.json, and that is where most collaboration binding actually happens:
Collaboration/Review/Source/*—ChangeRendering,CommentsExtensions,AnnotationExtensions,CommentLayoutAreas(andCollaboration/index.jsoncorrectly declaresrequires: [Store, Essentials]);- MeshWeaver.SocialMedia — six NodeTypes:
Marketing/{Campaign,Event,EventHub},SocialMedia/{Post,PostsHub,Profile}; - MeshWeaver.Reinsurance —
UWDeepfield/TreatySubmission.
Open follow-up, filed rather than fixed here: those satellite packages declare requires: [Store]
or nothing at all — never Essentials — although their content binds it. Their CI is green because
registry-modules: AI Essentials composes it regardless, so the gap is invisible to every gate; it
bites a runtime mesh that installs the package without Essentials. The remedy is the one
Plugins#1242 applied to the five map packages: one Essentials@^1.0.0 line in each package's
requires.