Gate Content Assets

A package ships more than nodes. Its binaries — course videos and their posters, og cards, fonts — are ordinary files committed under {package}/content/**, and installing the package is only half-done until they are being served. The other half is PackageInstaller.SyncPackageContent: it classifies those files with ContentAssetMapper, and posts one SyncContentFilesRequest to the package's partition ROOT, where a portal mounts the content collection that children inherit.

This page is about the host shape that request needs, and about what the mw-plugin-test gate now asserts because of it (issue #3424).


The handler comes from exactly one place

SyncContentFilesRequest is handled by ContentImportExtensions.AddContentImportHandler, which is reached only through AddContentCollections() / AddContentCollectionsInfrastructure(). So a per-node hub either called one of those or it answers "No handler found for message type SyncContentFilesRequest" — a DeliveryFailure, not a content verdict.

Three hosts, three different answers, and it is worth being precise about which is which:

Host Where the handler comes from Where the content collection comes from
Portal (memex, memex-cloud) MemexConfiguration's ConfigureDefaultNodeHub maps attachments on every per-node hub, which calls AddContentCollections() the same lambda mounts a writable content collection on the partition ROOT only (!nodePath.Contains('/')), ExposeInChildren = true
A Space-typed root anywhere SpaceNodeType's own HubConfiguration calls AddContentCollections() nothing — the handler answers, then fails "Target content collection 'content' not found"
The gate mesh, before #3424 nothing: AddGraph()'s default node chain does not call it, and neither does the Store/Plugin NodeType most package roots declare nothing

The distinction between the last two matters when reading a log. "No handler found" means the publish was refused before the collection question was ever asked; "collection not found" means the host heard the request and has nowhere to put the bytes. Both end as the same installer warning — "the package's nodes are installed but its binaries are not being served" — because the publish logs and continues rather than throwing (a package whose nodes landed and whose binaries lag is strictly better than a half-written package).

What that cost

Measured on two MeshWeaver.Education bakes on 2026-09-06, and independently on MeshWeaver.Reinsurance's test-repos job the same day: 15 packages, 30 refusals per run, exit 0. Every package that ships content/** — AppleMusic, BusinessRules, Chess, Collaboration, DataModelling, DoublePendulum, Edu, Essentials, Feedback, FractalStars, Google, Publish, RolePlay, ThreeBody, Training — installed its nodes and then had its binaries refused, twice (the installer re-asks once after the root recycles). Four of them additionally surfaced as ContentDeliveryRefusedException, because their largest asset exceeds the inline per-delivery budget and the out-of-band transfer has to ask the owning node for its collection config first.

Two separate problems, and only one of them is about noise:

  1. The gate never exercised the path that serves a package's binaries. A package whose content/** is broken — a wrong path, a video its course's <video src> points at that never lands — passed. The bake verified 101/101 assemblies and zero bytes of content.
  2. Thirty warnings a run that read as a production incident, on every satellite, unchanged from run to run, saying nothing about the change under test.

The fix is (1). (2) follows from it, and only from it: the condition stops occurring because the publish now succeeds. Lowering the log level would have been the forbidden move — the levels reflect the production cost model, and on a real portal that warning is exactly right.

The gate now hosts content the way a portal does

PluginGateRunner's mesh gained one ConfigureDefaultNodeHub lambda that mirrors the portal shape, both halves:

Nothing here is gate-specific behaviour: it is the production mount, pointed at a throwaway directory.

…and holds a verdict on it

Exercising the path is not enough on its own. The publish logs-and-continues, so a gate that merely ran it would go green on a host where nothing landed — which is precisely how #3424 survived. The accounting is therefore structural, not a log line:

🚨 The counts are the point, not the absent error. A run in which the content check looked at nothing reports the same "no error" as a run that verified two files; only 0 versus 2 tells them apart. This is the same rule PackageResult.CountsMeasured encodes one column over — a green check has to say what it verified.

What it does not cover

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.