Headless Test Matrix: Two Client Roles Against One Zone Server
- Status: deferred (focus is on VR (20260425-godot-player.md) first; matrix resumes after the observer (20260425-godot-observer.md) lands)
- Deciders: V-Sekai, fire
- Tags: V-Sekai, Testing, Headless, Godot, ZoneServer, Deferred, 20260425-headless-test-matrix
The Context
The Godot native client has two roles (observer, player). The Three.js client is no longer being built (see 20260425-threejs-webgpu-zone-client.md), so the matrix is now Godot-only. Both phases below depend on the observer (20260425-godot-observer.md), which is deferred while we ship the VR client first, so this matrix is also deferred. Design content is preserved for when work resumes. Testing them headless first means no VR hardware and no display required. Tests follow a three-stage promotion path:
local Docker → CI headless → VR hardware
Local Docker confirms the test works on a real network stack before committing a CI job. CI headless runs on every push. VR hardware is a later milestone.
The zone server is always the Godot native binary run with --headless. headless_log_observer.gd already exists for the Godot observer path.
The Problem Statement
Two client roles (observer, player) on the Godot native client. No matrix exists that verifies both connect to one server and that player actions are reflected in observer state. Tests must pass locally in Docker before being wired to CI, and must pass in CI before any VR hardware is involved.
Design
Two roles
| ID | Client | Role | How to run headless |
|---|---|---|---|
| GO | Godot native | Observer | --headless --path demo observer.tscn via headless_log_observer.gd |
| GP | Godot native | Player | --headless --path demo observer.tscn -- --send-player (new flag, writes CH_PLAYER) |
Phase 1 — single-client tests (two tests, run serially)
Each test: start zone server → start one client → wait for entities → assert count > 0 → stop.
server + GO → entity count > 0 (headless_log_observer.gd, --dump-json)
server + GP → entity count > 0, CH_PLAYER sent, server ACKs
Phase 2 — dual-client test (one pair)
Start zone server → start GO and GP simultaneously → assert GP’s input is reflected in GO’s next observed frame.
GO + GP → GP action reflected in GO's next frame
Local Docker run (required before CI)
The existing multiplayer-fabric-hosting Compose stack already runs a zone server (zone-server service). Add a test service that runs the Godot binary headlessly inside the same Docker network (no Playwright, no browser, no Node runtime):
# docker-compose.test.yml
services:
test-runner:
image: ubuntu:24.04
depends_on:
zone-server:
condition: service_healthy
volumes:
- ./multiplayer-fabric-godot/bin:/godot
- ./tests:/tests
working_dir: /tests
environment:
ZONE_HOST: zone-server
ZONE_PORT: "17500"
GODOT_BIN: /godot/godot.linuxbsd.editor.dev.x86_64
command: bash run_matrix.sh
network_mode: host # shares zone-server portRun locally:
docker compose -f multiplayer-fabric-hosting/docker-compose.yml \
-f multiplayer-fabric-hosting/docker-compose.test.yml \
run --rm test-runnerA green local Docker run is the gate before the CI job is added.
Shell orchestration
run_matrix.sh starts the Godot binaries, polls for the zone-server port, diffs the JSON dumps, and exits non-zero on mismatch. No browser engine, no JavaScript test runner; just GDScript flags and jq.
"$GODOT_BIN" --headless --path "$DEMO_PROJECT" \
--script scripts/headless_log_observer.gd \
-- --dump-json=/tmp/go.json --frames=600Same shape for the GP role with --send-player instead of --dump-json.
Missing pieces before Phase 1
Two items need implementing before the matrix runs:
headless_log_observer.gd— add--dump-json=<path>flag that writes the final entity list as JSON on exit (currently only prints to stdout).Godot player headless — a
--send-playerflag infabric_client.gdor a new script that sends one CH_PLAYER datagram after connecting, then exits.
Phase 1 GO can run today with headless_log_observer.gd stdout parsing.
The Downsides
One pair × server startup (~3 s) for Phase 2. The server must bind a fresh port for each test to avoid inter-test interference; or tests run serially with a single shared server.
The Docker gate adds one manual step before CI promotion. A developer who skips the Docker run and pushes directly can still break CI; the gate is enforced by convention, not by tooling.
The Road Not Taken
Mocking the server: rejected; a mock cannot reproduce AOI band behaviour, HLC timestamps, or the actual CH_INTEREST encoding. All matrix tests use the real server binary.
Status
Status: Deferred. Not building yet; focus is on the VR client (20260425-godot-player.md) first; this matrix resumes after the observer (20260425-godot-observer.md) lands. Design (shell-based, no Playwright) preserved for that resumption.
Decision Makers
- iFire
Further Reading
(godot_observer?): 20260425-godot-observer.md — Godot --headless observer (GO role).
(godot_player?): 20260425-godot-player.md — Godot native PCVR player (GP role).
(dual_client?): 20260425-dual-client-test.md — superseded; coverage merged into this matrix as the GO+GP pair.