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 server was updated. Reloading the page to pick up the latest version.