Actors and pins
How a test talks to a game process through GameActor, what the strict pins are, and how each pin is computed and enforced. Waits, startup and stopping are on Waiting and owned processes; the install checks before a launch are on Owned-run preflight.
Actors, fixtures and reports
| Type or member | Does | Fails or refuses when |
|---|---|---|
GameActor(name, transport) |
Wraps an existing transport; never owns or stops the process. Verify pins first, issue a mutation once, then poll its read-only observation. | An unknown outcome is a failure, never permission to resend. |
GameActor.CommandTimeout |
Bounds one command. Keep it below the scenario allowance: a synchronous read cannot be interrupted by an observation deadline. | |
WorldFixture.Copy(source, outputParent, expectedHashes) |
Copies a fixture into a new directory of its own and verifies the copied bytes against the manifest. | A link, an unexpected file or a hash mismatch. |
WorldFixture.CopyAsFound(source, outputParent) |
Records and verifies the hashes it finds; a standalone runner binds them as pins. | |
WorldFixture.Preserve |
true keeps the copy for debugging; otherwise disposing deletes only that copy. |
|
ScenarioReport |
Records every step, teardown failure and attached evidence in result.json and junit.xml (evidence). Put fixture, build and config hashes in Provenance. Use a new output directory for each run; Write overwrites what is there. |
Stop the owned game before disposing its world fixture. The fixture owns no accounts, characters, ports or machine reservations. Output may contain player and world data; inspect it before publishing, and keep credentials out of fixture configuration and logs.
Strict pins from tests
Every way in reaches the same game command, cli_expect --strict key=value ...:
| From | Use |
|---|---|
The valheim-cli executable |
valheim-cli --expect-strict /private/pins.txt ... (the flag is --expect-strict, not a global --strict) |
| A YAML plan | game.expect with game.expectStrict: true |
| A test | GameActor.VerifyEnvironment(expectationCommand) |
VerifyEnvironment normalizes the expectations to strict mode even if you omit the switch. Every later command rechecks the same pins before dispatch; drift invalidates the actor and blocks the action. Strict mode rejects unlisted loaded plugins and worlds; it does not make any an exact build pin, so supply reviewed full hashes and the intended world identity. The preflight is a round trip, not a lock on game state. For dispatch-time enforcement as well, configure ValheimCLI's [Expectations] guard; a test never disables it.
Execute(command) returns a GameReply and requires it accepted by default: the transport completed and no line is a known refusal (That command is a cheat, Error executing command, Unknown command, or a ValheimCLI ERROR: line), because a refused console command still completes on the transport.
GameReply member |
Does |
|---|---|
Line(prefix), Lines(prefix) |
Read the reply by prefix |
RequireLine(prefix, failure) |
Fails with failure when no line has the prefix |
Execute(..., requireAccepted: false) |
Lets a test inspect an expected refusal; it cannot bypass a pin failure |
Acceptance supplements a step's semantic check; it never replaces one.
A reload test supplies a pins file, advances the changed plugin's expected hash from the artifact it installs, and requires absent after removal. While the transition settles only cli_expect --strict is rechecked. Pass WaitForEnvironment the reload's own log line through a LogWait (waiting), so it rechecks when the plugin has loaded rather than on a timer:
await actor.WaitForEnvironment(expectations, timeout, (left, token) => log.WaitAsync(loadLine, left, cancellation: token));
Repin after a world change. The owned server's startup identity probe is the one read-only bootstrap exception: it checks token, PID and save root while the world loads, then verifies the strict pins before returning an actor.
Capabilities and extension reload
| Member | Does | Fails when |
|---|---|---|
GameActor.RequireCapability(path) |
Discovers a command through cli_extensions and returns its Capability |
The command is missing or its instance token is stale |
Invoke(command, arguments), Observe(command, arguments) |
Run a discovered command; Observe returns an Observation |
A stale instance token |
ObserveComplete(command, source, arguments) |
Returns a complete observation | complete is false: incomplete is never zero. valheim.world/terrain x z generator|loaded-ground reports complete: false and a null height where no heightmap is loaded |
ExtensionReload holds the steps of the ReloadCheck tool, which checks a replacement end to end: new instance, removed command refused, old resources cleaned up, core and connection unchanged.
| Member | Does |
|---|---|
Install(artifact, deployed) |
Copies beside the deployed DLL and renames over it |
WaitForReplacement(actor, id, version, replacing, timeout, interval) |
Waits for a new live instance at that version |
WaitForRemoval(actor, id, timeout, interval) |
Waits until no instance is live |
Both waits re-read cli_extensions, which is read-only; a registration announces itself nowhere else.
Pins
Every run uses strict pins. Each pin protects against a run that passes on something other than what you meant to test.
| Pin | Where | Protects against |
|---|---|---|
worlduid |
plan pins, checked in game by cli_expect --strict before every command |
another world, or an unexpected world transition |
plugin MD5s, absent |
plan and client pins, same check |
a stale or different mod build, an extra plugin, a mod that should not be there |
| runtime and world SHA256 manifests | plan runtime and world |
a changed fixture: any file in the runtime or save |
game |
runtimePins, client installPins |
another game build: a hotfix, or the default_old / default_pre1_0 branches |
loader |
same | another BepInEx, BepInExPack or Doorstop build, or a proxy or configuration that starts the game without BepInEx |
patchers |
same | a preloader patcher a removed mod left behind, or another build of one the run needs |
cli_expect knows plugins and the world only, so InstallPins pins the game build, loader and patchers on disk before launch. Compute them once with InstallPins.Of(<runtime>), review them, and add them to the plan:
"runtimePins": { "game": "...", "loader": "...", "patchers": "..." }
An owned client section takes the same object as "installPins". InstallPins.Check(root, kind) names what differs and the value found.
Each value is the SHA256 of a listing:
- One line per file,
<sha256> <relative path>,/separators, ordered by path, assha256sumprints it..DS_Storeand AppleDouble._*files are left out. An empty or absent folder hashes the empty listing. gamelists everyassembly_*.dllin the Managed folder (<executable>_Data/Managed,Valheim.app/Contents/Resources/Data/Managedfor the macOS client, orvalheim_server/Data/Managedfor the macOS dedicated server). A build that changes only assets or Unity's own assemblies keeps the same value.loaderlists, relative to the install's root,winhttp.dll,doorstop_config.ini,libdoorstop.dyliband everything underBepInEx/coreanddoorstop_libs.BepInEx/config/BepInEx.cfgis configuration and not part of it.patcherslists the whole folder, subfolders included.
Without the library, in the folder concerned:
find . -type f ! -name .DS_Store ! -name '._*' | sed 's|^\./||' | LC_ALL=C sort | while IFS= read -r f; do printf '%s %s\n' "$(sha256sum "$f" | cut -d' ' -f1)" "$f"; done | sha256sum
For game, list ls assembly_*.dll in the Managed folder instead of find. For loader, at the install's root, give find the paths winhttp.dll doorstop_config.ini libdoorstop.dylib BepInEx/core doorstop_libs instead of ., with 2>/dev/null for those absent.
FileHash.Sha256 computes these pins, manifests and evidence hashes. FileHash.Md5 computes plugin pins, which stay MD5 because cli_manifest and cli_expect report and compare MD5. The runtime manifest also covers the loader files, but a manifest regenerated after an update silently accepts the new build; runtimePins makes that a deliberate change.
A link in a pinned folder or loader file is refused, and so is a linked directory on the way to a pinned path (a linked valheim_Data or BepInEx). The install folder itself may be reached through a link (/tmp, ~/.steam/steam).
Expected and observed pins
A pin the plan or a campaign dependency lock supplies is expected: a mismatch is refused. When neither supplies a value, the profile owner binds what it staged as an observed pin. Both are enforced alike, before each boot and by cli_expect --strict for the rest of the run. A mixed plan checks its supplied values and observes the rest.
A standalone PinnedServerRun stages the server profile and binds:
- the profile's complete runtime manifest,
runtimePinsand startup plugin MD5 set: plugins inBepInEx/plugins, plus plugins inBepInEx/scriptsonly when the selected ScriptEngine loads them at startup; - an omitted world identity, read from the copied fixture, and an omitted world manifest from
WorldFixture.CopyAsFound. A plan that saysworld=anyorworld=<name>keeps that expectation and needs no existing world UID, so the game may create a new world in an empty fixture.
It refuses a plan that pins a script ScriptEngine does not load at startup, naming the ScriptEngine or LoadOnStart setting. After binding, every plugin the runner declares required must be in the staged set: The staged server profile is missing required plugin .... ValidateServerPlan accepts omitted standalone pins but validates every supplied manifest, runtime pin and plugin MD5.
The profile replaces the install's own ValheimCLI with the toolkit's set. A supplied pin for that source build is checked before the replacement; a different staged ValheimCLI MD5 is recorded as observed.
| Client | Needs | Observed |
|---|---|---|
| Owned, disposable profile (default) | May omit installPins and plugin pins |
The source install; the staged profile is the enforced set, every value recorded with its origin |
Owned, inPlace |
installPins and an exact ValheimCLI MD5 in pins |
Nothing |
| Owned, placed on another host by an inventory | installPins |
|
| Attached | Exact plugin expectations checked in game; no installPins, its operator's install is not read |
Nothing; the game build is not pinned, since the game refuses a join only across network versions |
Use an owned client when the client's build matters. The client fields are on Game clients in system tests.
Pin origins in the report
result.json (schema 5) records PinOrigins for standalone runs and owned client profiles. A campaign has already been rewritten to its prepared profiles by ApplyTo, so its map is empty.
| Key | For | Value |
|---|---|---|
runtimeManifest, runtimePins, worldManifest, worldUid, plugins.<guid> |
a standalone run | plan or observed |
<client>.installPins, <client>.plugins.<guid> |
an owned client profile | plan or observed |
A supplied group is observed when profile staging replaces it and the enforced value differs, the loader within runtimePins included; this is the same rule as a replaced ValheimCLI build.
| Evidence | Records |
|---|---|
input-hashes.json |
The standalone map beside the exact runtime pins, plugin pins, world identity and manifests |
replacedValheimCliPaths |
The install-relative paths of ValheimCLI files the profile replaced |
sourceValheimCliPins |
The checked GUID and MD5, when the plan supplied the replaced source build's pin |
provenance pins = pins observed, not reviewed |
Added when standalone values were observed: an evidence qualification, while the pins remain enforced |