Run the conformance suite
Run the deterministic test suite that checks Nebula's handover, scope, and interaction guarantees, and read what each scenario proves.
The conformance suite is a set of deterministic tests that check Nebula's cross-worker guarantees on a small mesh built inside one process. Run it after you change Nebula itself, or when you want to confirm that a package version keeps the guarantees this page lists. It needs no build, no running mesh, and no game content.
To run the whole suite from the repository root, close the Unity Editor for this project and run:
powershell -File Tools/conformance.ps1To run only the pure C# part, which takes seconds and works while the Editor is open, run:
powershell -File Tools/conformance.ps1 -DotnetOnlyThe script prints one row per test tier with the number of passed, failed, and skipped tests, the names of any failed tests, and a final PASS or FAIL. It exits with code 0 only when every tier that ran passed. Result files are written to Logs/conformance/.
Prerequisites
- The .NET SDK, for the pure C# tier (
dotnet test). - Unity 6000.6.0f1 installed through Unity Hub, for the Unity tier. Pass
-UnityVersionor-UnityPathfor another install. - The Unity Editor must be closed for this project when you run the Unity tier. The script refuses to start the Editor while
Temp/UnityLockfileexists and tells you to close it or use-DotnetOnly.
What the suite runs
Every conformance test is an NUnit test tagged with the category Conformance. The script runs that category in two places:
| Tier | Where the tests are | How the script runs them |
|---|---|---|
| Pure C# | Packages/com.1by3.nebula/Services~/Nebula.Services.Tests | dotnet test --filter "TestCategory=Conformance" |
| Unity, EditMode | Packages/com.1by3.nebula/Tests/EditMode | the Editor in batch mode with -testCategory Conformance |
| Unity, PlayMode | Packages/com.1by3.nebula/Tests/PlayMode | the same, with -testPlatform PlayMode: the scenarios that need physics scenes, which only exist in play mode |
The Unity tier stands up two real workers (the NebulaWorker component) in one Editor process. Each worker writes to a recording transport instead of a socket, and the test delivers the recorded bytes into the other worker's message handler. A handover in this tier is built, serialized, parsed, and applied by the same code a live mesh runs. What this tier does not include: a gateway, the orchestrator, leases, and the fixed-tick loop. A test either triggers a handover directly or runs one whole worker tick when it needs one. In that tick, the worker places each entity in a container itself and hands it over when the container belongs to another worker.
The pure C# tier runs the parts of a scenario that need no Unity types, such as the wire format of a message. The standalone gateway is built from the same sources, so this tier is also what confirms that the gateway and a worker agree on those bytes.
You can also run the tests from the Unity Test Runner window (Window > General > Test Runner), filtered by the Conformance category, or with dotnet test directly using the filter above.
Scenarios
Each scenario checks one guarantee. A scenario marked available has a test in the suite today. A scenario marked not yet available describes a guarantee that Nebula does not provide yet, or that has no test yet; the row is listed so that you can see what the suite will cover, not what you can rely on now.
| # | Guarantee | Status |
|---|---|---|
| 1 | The same entity location (scope key, container ID, container-local pose) resolves to the same container in every process that holds the container, and still does after a handover, a restart, and a round trip through the persistence store. | Available |
| 2 | Activating a scope is idempotent when two requesters ask at the same time, and a private key produces a distinct scope. See Simulation scopes. | Available |
| 3 | An idle scope retires, restores with the same persisted entities, and refuses admission until the restore completes. See Simulation scopes. | Available |
| 4 | Two scopes whose chunks share coordinates get distinct leases and distinct records, and neither sees the other's ghosts or replicated entities. See scoped chunk grids. | Available |
| 5 | A cross-worker call is applied at most once when its target changes worker between send and apply, and a call carrying an outdated epoch is rejected with a reason. See the cross-worker call contract. | Available |
| 6 | StateAt(tick) returns a pose that matches the recorded history on the authoritative worker and on a ghost, within a documented bound: a ghost is exactly one tick behind the owner, and a tick outside the recorded window is reported as unavailable rather than extrapolated. See Lag compensation. | Available |
| 7 | Entities in one cohesion group change worker together, the planner never assigns their containers to different workers, and a group too large for one worker is reported instead of being split. A held container is not rebalanced until its hold expires. | Available |
| 8 | A server-driven entity that carries custom handover state keeps every field when it changes worker: position, rotation, velocity, NetworkVariable values, the bytes written by WriteHandoverState, the server-driven flag, and an epoch increased by exactly one. OnLostAuthority runs once on the old worker and OnGainedAuthority runs once on the new one, after the handover state is applied. A duplicate of the transfer message is ignored. | Available |
| 9 | Nebula reports a physics joint that connects entities in different containers when those entities are not in a cohesion group, and stays silent when they are. See Distributed physics. | Available |
| 10 | Loss after a worker is killed with several entities dirty never exceeds the documented durability window. | Available |
| 11 | The lifecycle hooks fire in the documented order: the game is told a scope is coming to life, and whether anything is saved under its key, before any of the scope's containers restores; OnContainerRestored only after the restore; the before-retire window completes before anything is checkpointed; OnRetired only after the leases are released. See Lifecycle hooks. | Available |
| 12 | A join or a transfer into a target that is at capacity is refused with a typed reason, and only the game's admission hook can let one in anyway. Nothing is split or copied to make room. | Available |
| 13 | A container that has grown hot is re-dealt to another worker along a boundary the game authored, without separating the entities of a cohesion group and without moving a container whose hold has not expired. When no such cut exists, the planner reports the container as saturated with the reason instead of shuffling containers pointlessly. | Available |
| 14 | One worker holds two scopes whose local coordinates overlap. Each shifts its floating origin on its own, moving only its own containers, entities and history; entities in each keep their precision; neither scope's container queries or physics answer for the other's; and the autoscaler's dry run puts both on one worker at low load. A gateway in the same process as the worker, as in the Multiplayer Play Mode loop, still asks for the entities around a player where the worker keeps them. | Available |
| 15 | Two carriers whose interiors overlap never ride inside each other. The worker keeps ticking, at most one rides inside the other, every container's scope resolves, and a placement that would make an entity ride inside itself is refused. This also holds after the carriers hand over to another worker. | Available |
| 16 | A crewed ship flies from one scope into a chunk of another. A crossing prepared for the ship and its crew becomes ready and commits with the crew still in their seats; every rider's client keeps the ship and its own pawn, now in the destination, with no reconnect, whether or not the riders were part of the crossing; a client on the ship's former world loses it and is never told where it went; and no client is sent an update naming a container before that container's row. See Move a crewed carrier into another scope. | Available |
| 17 | A worker that gains hundreds of containers at once restores every one of them with a few store requests instead of one per container, and its persistence client never has more than a fixed number of requests in flight. A container dealt to another worker while its records are being read is not restored from that answer. Carriers that spawn together have their cargo read the same way, and a carrier that despawns while its read is out gets nothing from that answer. | Available |
| 18 | A runtime chunk is never retired while a player is in it, including a player riding in a vehicle there at any depth, or while an entity assigned to it has flown into a cell a worker has asked for. A chunk that does retire takes each vehicle's riders with the vehicle and saves the persistent ones aboard it. See When a chunk stays leased. | Available |
| 19 | A worker that loses contact with the control plane while it is still running leaves exactly one live copy of each persistent entity. It stops saving, restoring and handing entities over until its heartbeat lands again, a save it had queued is refused once another worker has restored the entity, and no client is shown the old copy beside the restored one. See Persistence. | Available |
| 20 | Containers form a tree: baked boxes take their parent from nesting, a runtime container named inside a parent is placed in the parent's space and waits for it, the deepest box wins, an inherited container follows its parent's worker, a leased container under a moving parent without a physics frame is simulated as inherited, and nothing is placed inside its own subtree. Lease rows and ownership messages carry the placement. See Containers and handover. | Available |
| 21 | A container with a physics frame of its own simulates what is inside in its local coordinates with its own down as gravity; entities cross into and out of it only through the frame's pose owner, converted at that tick's pose; another worker hands the entity to the pose owner unconverted; and a crossing policy can veto or defer a crossing. | Available |
| 22 | A root container 10,000 km from the origin is placed to better than a centimetre on every process. See Place a container inside another. | Available |
| 23 | A planet with its own frame and eight octants leased to two workers holds a separately leased base; a ship flies in from space and lands in the other worker's octant with no scope transfer and no position error; the planet's rotation changes neither the ship's local position nor its region; the ship leaves through the planet's pose owner; a frame's origin follows what its worker simulates; and a ship at 1 km/s with an interior leased to another worker keeps its crew exact while they walk between the two workers and leave through an airlock. | Available |
| 24 | Loose rigidbodies in a moving physics frame: a stack of crates survives a whole flight in order and asleep after landing, slides and stays in the hold with FrameInertia, crosses out and back in with its velocity converted, keeps its pose and sleep through a handover of the ship, rides a moving lift, and comes back with a ship stowed with CargoPolicy.Stow. | Available |
| 25 | An entity attached to a container with FrameAttachment keeps its pose through a flight and holds loose crates up, detaches gently, stays in its chunk across a seam and follows the chunk's re-deal, stays attached through a handover and a restore, detaches when its carrier sets it down, and a client that joins later sees it attached. | Available |
| 26 | A player's pawn that changes worker while the player is disconnected is still removed when SessionReclaimSeconds runs out, counting the time already spent on the old worker. A player who reconnects in that window keeps the pawn on its new worker. | Available |
| 27 | A behavior's sync audience holds on every path to a client. A client outside an Owner audience never receives the state, not in a spawn, an update or a late joiner's cached keyframe; the owner keeps receiving it across a handover and a reconnect; WorkersOnly state reaches every worker holding a ghost and no client; a Custom audience gives a client that joins a keyframe at once and tells a client that leaves to drop its copy. | Available |
| 28 | A large entity is ghosted by its extent. A structure rooted 50 m from a seam whose 100 m extent reaches it is ghosted to the neighboring worker, and a player simulated there stops at its wall; without an extent, it is not ghosted and the player walks through. A structure attached to a chunk is ghosted to every worker whose chunk its extent comes within GhostBandMargin of, and to no other. An extent set at runtime travels with a handover. The same holds inside a planet's physics frame. | Available |
Scenario 15 is covered by ConformanceCarrierCycleTests (Unity tier). The tests spawn two and then twenty overlapping carriers on one worker and run whole worker ticks. They also refuse cycles of every length, heal a corrupt chain on the next tick, and hand a carrier with an overlapping carrier aboard to a second worker.
Scenario 16 is covered by ConformanceCrossScopeCarrierTests (.NET tier: a real gateway between two grid scopes, checking the preparation, a ship crossing alone, cargo aboard a crossing ship, a rider reconnecting after the crossing, a chunk whose lease reaches the gateway late, and variables a new owner changes while the gateway waits for that chunk's lease) and ConformanceCrossScopeCrewTests (Unity tier: one real worker committing a ship and its crew as one group).
Scenario 17 is covered by ConformanceRestoreBurstTests, in both tiers. The Unity tier deals 600 containers to one worker in a single control-plane change and checks that the worker reads them in three requests, never with more than two waiting, and restores each container from its own records. The Unity tier also spawns 600 carriers in one frame and checks that the worker reads their cargo in three requests, never with more than two waiting, and brings each crate back aboard its own carrier. The .NET tier checks the stores and the orchestrator endpoints for containers and carriers: the SQL store, the batched HTTP requests, and a limit of two requests in flight however many reads the worker asks for.
Scenario 18 is covered by ConformanceCrewedCarrierRetireTests (Unity tier). One real worker runs the chunk allocator against a control plane whose clock the test advances. A ship 17 km past its only chunk keeps that chunk while its pilot is aboard and loses it when empty. In a scope that is retiring, a player's pawn one or two vehicles deep keeps its chunk leased. An empty ship assigned to an old chunk keeps it while it is in a cell a worker has asked for, and not otherwise. Releasing a chunk despawns a ship with a shuttle, a crate and a drone aboard, and saves the shuttle and the crate aboard their carriers. ConformanceGhostCarrierRidersTests (Unity tier) repeats the release with two workers, where the second worker owns the crate and the drone aboard its copy of the ship. When the ship's worker empties the chunk, both riders leave with the ship, and the crate is saved aboard it. When the ship is destroyed instead, the second worker puts both riders down in the chunk.
Scenario 13 is covered by ConformanceRebalanceTests (both tiers). See Cohesion hints and the Assignment plan card.
Scenario 4 is covered by ConformanceScopedGridTests, in both tiers: the .NET tier stands up two chunk grids at the same coordinates behind a real gateway and checks that a client in one is told about neither the other's entities nor its chunk rows, and the Unity tier checks that an entity ghosts across its own world's seam but never into a world standing on exactly the same ground.
Scenario 14 is covered by ConformanceScopeFrameTests (both tiers: the autoscaler's dry run consolidating two low-load worlds onto one worker, and the per-scope region identifiers) and ConformanceScopeFrameWorkerTests (Unity tier: one real worker holding two worlds on the same ground, each shifting its own origin). The Unity tier drives real origin shifts, but it cannot create a scope's physics scene, which only exists in play mode — the live two-scene half is covered by the play-mode instance physics tests. See Each world keeps its own floating origin.
Scenario 7 is covered by ConformanceCohesionTests (both tiers: the assignment policy and the telemetry the orchestrator reads) and ConformanceCohesionHandoverTests (Unity tier: two and three real workers handing a group over). See Cohesion hints.
Scenario 8 is covered by ConformanceHandoverStateTests (Unity tier) and ConformanceHandoverWireTests (both tiers), plus two older tests that check the handover-state format and a persistent entity's key crossing a handover. See Physics and handover state for how to write handover state in your own behaviors.
Portal scenarios
These scenarios check the portals that join two scopes. This page lists scenarios 1 to 28 and 50 to 76. Scenarios 29 to 49, which cover frames, far relevance and the dev loop, are listed in docs/conformance-suite.md in the Nebula repository.
| # | Guarantee | Status |
|---|---|---|
| 50 | A portal's record and each party's link state reach the clients that hold the container row of either face, and only those. A client that is not a member gets no observer permission it was not given. | Available |
| 51 | A link walks its state machine (Closed, Preparing, Open, Closing) through every wait reason in order, and a player's pawn crosses an open link. | Available |
| 52 | Any entity crosses an open link on its own: a server-owned crate, a vehicle with its riders and a projectile. An entity beside the opening, or one that jumped across without being near, does not cross. | Available |
| 53 | What an entity carries in time goes through the link with it at a door that is turned and tilted: its history, its body's velocities, and the contents it carries. | Available |
| 54 | A link is applied about the faces in double precision. A door on a planet 50,000 km out, turned about two axes, lands a crossing to the millimeter in both directions. | Available |
| 55 | A client that may look through an open link sees the other side's window, both ways, decided from link records alone. A client the link does not list is sent nothing. | Available |
| 56 | A crate thrown through an open portal stays with its thrower and is lost by an onlooker who may not look through. | Available |
| 57 | A client holds an update that names a container it cannot resolve, whatever the container's kind, until the container's row arrives. | Available |
| 58 | Physics across an open face on one worker: a crate resting across the face rests on its own side, has one proxy in the other scene and sleeps; a crate pushed through keeps its speed; a refused body bounces off the door. | Available |
| 59 | Proxies across workers: a body in the band is held on the other worker as a proxy only, and nothing outside the band is ghosted across a portal. | Available |
| 60 | A client predicts its pawn against the same proxies, throat and aperture collider that the worker builds. | Available |
| 61 | A body is in a band by its extent, by its colliders' shapes when it has no extent, and by its root when it has neither. | Available |
| 62 | The same physics across the face of a door on a planet that is a carrier, wherever the planet is in its scope and however it is turned. | Available |
| 63 | A side in a physics frame works across workers and on a client. | Available |
| 64 | A raycast, sweep or overlap continues through an open aperture and reports the far scope's hit; through a closed door it hits the door. | Available |
| 65 | A query through a portal can be lag-compensated: the far scope is posed at the tick the shooter rendered, and put back afterward. | Available |
| 66 | A query whose far side is on another worker, or is a second portal, ends at the edge of what the worker knows and says so. | Available |
| 67 | A query from a planet's ground, and from a client, goes through a door in a frame. | Available |
| 68 | Scopes joined by a link that is not closed are one planner item and stay on one worker. | Available |
| 69 | When the far scope is on another worker, a crossing is one handover on the tick it commits, and nothing is held at the face. | Available |
| 70 | A link readies the whole far window, and a crossing lands in the deepest container that holds the arrival point. | Available |
| 71 | A planned reassignment of the container a portal stands in keeps its links. | Available |
| 72 | Every wait reason is reported by the link and in telemetry, and each one can be reached. | Available |
| 73 | A crossing between workers with real physics moves a pawn and a thrown crate each tick and never puts them back. | Available |
| 74 | A client draws several scopes at once, each at its pose against the anchor scope. | Available |
| 75 | Each drawn scope holds a rendering-layer slot, and Nebula stamps only the reserved bits. | Available |
| 76 | A client that rejoins in the same process gets a physics scene for each scope again, and holds a scope's empty root container in that scope's scene as soon as it exists. | Available |
Read a failure
A failed row names the test. Open the test file under Packages/com.1by3.nebula/Tests/EditMode/ to read the assertion; each assertion message says which field or step it checks. For the Unity tier, Logs/conformance/unity.log holds the Editor log and Logs/conformance/editmode.xml the full result (playmode.log and playmode.xml for the PlayMode run), including the messages Nebula logged during the test. For the pure C# tier, the TRX file under Logs/conformance/dotnet/ holds the same for dotnet test.
If the Unity tier reports not run, the summary says why: the Editor was not found, the project is open in an Editor, or the Editor produced no result file. The last case usually means a compile error; the Editor log names it.
What this suite does not measure
The conformance suite is not a load test, a soak or a benchmark: every scenario runs in one process in
milliseconds, and a slow machine never changes its answer. What a busy mesh does when a worker dies, a gateway is
lost, the control plane restarts, or everything comes back at once — and how long each of those takes — is the
scale and failure suite, which runs under its own category and is never picked up by
Tools/conformance.ps1.
Load testing a gateway fleet
Run synthetic clients against one gateway or a load-balanced fleet with nebula-loadgen, and read its report.
Run the scale and failure suite
Measure what a Nebula mesh does under sustained load, a burst, a worker kill, a lost gateway, a control-plane restart and a whole-mesh restart, against explicit thresholds.