Nebula

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.ps1

To run only the pure C# part, which takes seconds and works while the Editor is open, run:

powershell -File Tools/conformance.ps1 -DotnetOnly

The 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 -UnityVersion or -UnityPath for 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/UnityLockfile exists 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:

TierWhere the tests areHow the script runs them
Pure C#Packages/com.1by3.nebula/Services~/Nebula.Services.Testsdotnet test --filter "TestCategory=Conformance"
Unity, EditModePackages/com.1by3.nebula/Tests/EditModethe Editor in batch mode with -testCategory Conformance
Unity, PlayModePackages/com.1by3.nebula/Tests/PlayModethe 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.

#GuaranteeStatus
1The 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
2Activating a scope is idempotent when two requesters ask at the same time, and a private key produces a distinct scope. See Simulation scopes.Available
3An idle scope retires, restores with the same persisted entities, and refuses admission until the restore completes. See Simulation scopes.Available
4Two 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
5A 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
6StateAt(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
7Entities 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
8A 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
9Nebula 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
10Loss after a worker is killed with several entities dirty never exceeds the documented durability window.Available
11The 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
12A 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
13A 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
14One 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
15Two 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
16A 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
17A 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
18A 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
19A 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
20Containers 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
21A 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
22A 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
23A 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
24Loose 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
25An 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
26A 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
27A 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
28A 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.

#GuaranteeStatus
50A 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
51A 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
52Any 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
53What 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
54A 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
55A 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
56A crate thrown through an open portal stays with its thrower and is lost by an onlooker who may not look through.Available
57A client holds an update that names a container it cannot resolve, whatever the container's kind, until the container's row arrives.Available
58Physics 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
59Proxies 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
60A client predicts its pawn against the same proxies, throat and aperture collider that the worker builds.Available
61A 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
62The 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
63A side in a physics frame works across workers and on a client.Available
64A 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
65A query through a portal can be lag-compensated: the far scope is posed at the tick the shooter rendered, and put back afterward.Available
66A 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
67A query from a planet's ground, and from a client, goes through a door in a frame.Available
68Scopes joined by a link that is not closed are one planner item and stay on one worker.Available
69When 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
70A link readies the whole far window, and a crossing lands in the deepest container that holds the arrival point.Available
71A planned reassignment of the container a portal stands in keeps its links.Available
72Every wait reason is reported by the link and in telemetry, and each one can be reached.Available
73A crossing between workers with real physics moves a pawn and a thrown crate each tick and never puts them back.Available
74A client draws several scopes at once, each at its pose against the anchor scope.Available
75Each drawn scope holds a rendering-layer slot, and Nebula stamps only the reserved bits.Available
76A 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.

On this page