Skip to content

test: add MTE network and multiplayer integration tests - #5325

Merged
Cervator merged 10 commits into
MovingBlocks:developfrom
SiliconSaga:test/salvage-mte-network-tests
Jul 26, 2026
Merged

test: add MTE network and multiplayer integration tests#5325
Cervator merged 10 commits into
MovingBlocks:developfrom
SiliconSaga:test/salvage-mte-network-tests

Conversation

@agent-refr

Copy link
Copy Markdown
Collaborator

AI-assisted change proposal. Filed by agent driven by @Cervator via GDD.

Summary

  • Add 3 MTE integration tests salvaged from PR Antigravity take on the Gestalt DI PR - fixes multiplayer! #5304 (Antigravity DI exploration): client network state verification, client system update ticks, and two-client chat propagation
  • Add docs/Engine-Testing-Patterns.md documenting MTE testing patterns, including the network event registration problem (inner-class @BroadcastEvent/@OwnerEvent/@ServerEvent events don't replicate) and context isolation

Test plan

  • ClientNetworkStateTest — verifies client NetworkSystem mode (CLIENT) and NetworkSubsystem presence
  • ClientSystemUpdateTest — registers UpdateSubscriberSystem on client, verifies it receives ticks
  • TwoClientChatTest — two clients connect, chat message broadcast via ChatMessageEvent propagates between them
  • All 3 tests pass locally on Windows (JDK 17, Gradle 8.10.2)

Related

@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a testing guide and three ModuleTestingEnvironment integration tests that exercise client creation, client-side subsystem updates, and multi-client chat propagation under a listen-server network environment.

Changes

Cohort / File(s) Summary
Testing Documentation
docs/Engine-Testing-Patterns.md
New documentation describing Terasology engine test tiers, MTE field injection (@In), single- and multiplayer @IntegrationEnvironment usage, network event registration/replication rules, singleton save/restore patterns, verification patterns for network behavior, and Gradle commands for running tests.
Integration Tests — Network & Client State
engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientNetworkStateTest.java
New @IntegrationEnvironment(networkMode = LISTEN_SERVER) test that creates a client via ModuleTestingHelper and asserts the client Context has a NetworkSystem in CLIENT mode and that the GameEngine includes a NetworkSubsystem.
Integration Tests — Client System Updates
engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientSystemUpdateTest.java
New @IntegrationEnvironment(networkMode = LISTEN_SERVER) test registering a client-side TickCountingSystem with the client ComponentSystemManager, advancing the environment until the system records a target number of update ticks, and asserting update delivery.
Integration Tests — Multi-client Chat
engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java
New @IntegrationEnvironment(networkMode = LISTEN_SERVER) test that creates two clients, waits for host registration, grants chat permission to clients, installs a ChatProbe on client 2, sends a ChatMessageEvent from the host, waits for replication, and asserts the probe observed the expected message.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~45 minutes

Poem

🐰 I nibble notes and hop through tests,
Clients boot up, then chat and rest.
Ticks count down in gentle stride,
Packets scamper from host to side.
A tiny cheer for each green test.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the primary change: adding MTE network and multiplayer integration tests. It clearly reflects the main additions in the PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the salvaged tests, documentation additions, test plan verification, and related context from PR #5304.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/Engine-Testing-Patterns.md`:
- Around line 79-85: The docs incorrectly state that ChatMessageEvent is a
broadcast event; update the wording to reflect that the ChatMessageEvent class
is annotated with `@OwnerEvent` (not `@BroadcastEvent`) so tests shouldn’t assume
broadcast semantics — locate references to ChatMessageEvent in the docs (e.g.,
the example using clientEntity.send(new ChatMessageEvent(...))) and change the
description to say it is owner-only/owner-annotated (`@OwnerEvent`) and not
replicated as a broadcast, and suggest using an engine-registered
`@BroadcastEvent` if true broadcast replication is required.
- Around line 98-107: The docs currently show registering a probe twice which
causes duplicate event delivery; update the example to only register the probe
via ComponentSystemManager.register (e.g., remove the
EventSystem.registerEventHandler call), and note that
ComponentSystemManager.register(probe) already registers
BaseComponentSystem-derived systems as event handlers so calling
EventSystem.registerEventHandler(probe) (as shown for MyProbe) is unnecessary
and should be omitted.

In
`@engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java`:
- Around line 40-79: The test currently picks an arbitrary sender (senderClient)
from hostNetwork.getPlayers(), so client1Ctx is unused; change the sender
selection to specifically use client1Ctx: obtain the clientInfo for the local
client created in client1Ctx (via its ClientComponent.clientInfo) and then find
the matching Client in hostNetwork.getPlayers() whose
client.getEntity().getComponent(ClientComponent.class).clientInfo equals that
client1 clientInfo, assign that matching Client to senderClient, and use it when
building senderInfo and sending ChatMessageEvent so the test actually verifies
client1→client2 chat propagation.
- Around line 64-69: The ChatProbe is being registered twice which can cause
duplicate event delivery; remove the redundant direct EventSystem registration
by deleting the call to
client2Ctx.get(EventSystem.class).registerEventHandler(probe) and rely solely on
client2Csm.register(probe) (where ChatProbe is the class with `@ReceiveEvent`
handlers) so the probe is only registered once.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0953e51e-430e-4ff6-ab84-82b40ed75685

📥 Commits

Reviewing files that changed from the base of the PR and between 9621e21 and 2b136f8.

📒 Files selected for processing (4)
  • docs/Engine-Testing-Patterns.md
  • engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientNetworkStateTest.java
  • engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientSystemUpdateTest.java
  • engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java

Comment thread docs/Engine-Testing-Patterns.md
Comment thread docs/Engine-Testing-Patterns.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java (2)

65-70: ⚠️ Potential issue | 🟡 Minor

Remove the duplicate event-handler registration.

client2Csm.register(probe) should be the single registration path for the component system; registering the same handler directly with EventSystem can double-deliver ChatMessageEvents.

🧹 Proposed fix
         ChatProbe probe = new ChatProbe();
         ComponentSystemManager client2Csm = client2Ctx.get(ComponentSystemManager.class);
         client2Csm.register(probe);
-        // Also register with EventSystem for post-init registration
-        client2Ctx.get(EventSystem.class).registerEventHandler(probe);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java`
around lines 65 - 70, The ChatProbe is being registered twice which causes
duplicate event delivery; remove the direct EventSystem registration and rely
solely on the component system registration. Specifically, keep the call
client2Csm.register(probe) (ComponentSystemManager.register) and delete the line
that calls client2Ctx.get(EventSystem.class).registerEventHandler(probe) so
ChatProbe is only registered once.

72-80: ⚠️ Potential issue | 🟠 Major

Bind the sender to client 1 before sending the message.

Line 74 picks an arbitrary connected host client, so this may validate host-to-client delivery from either client rather than the intended client1-to-client2 path.

🎯 Proposed direction
-        Client senderClient = hostNetwork.getPlayers().iterator().next();
+        EntityRef client1Info = findLocalClientInfo(client1Ctx);
+        Client senderClient = null;
+        for (Client client : hostNetwork.getPlayers()) {
+            EntityRef candidateInfo = client.getEntity().getComponent(ClientComponent.class).clientInfo;
+            if (candidateInfo.equals(client1Info)) {
+                senderClient = client;
+                break;
+            }
+        }
+        assertNotNull(senderClient, "Client 1 should be registered on the host");
         EntityRef senderInfo = senderClient.getEntity().getComponent(ClientComponent.class).clientInfo;

Add a small helper in the test class:

private static EntityRef findLocalClientInfo(Context clientContext) {
    NetworkSystem network = clientContext.get(NetworkSystem.class);
    for (Client client : network.getPlayers()) {
        ClientComponent component = client.getEntity().getComponent(ClientComponent.class);
        if (component != null && component.local) {
            return component.clientInfo;
        }
    }
    throw new AssertionError("Local client info should exist");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java`
around lines 72 - 80, The test currently picks an arbitrary sender from
hostNetwork.getPlayers(), which can pick the wrong client; add and use a helper
to find the local client's ClientComponent.clientInfo and bind the sender to
client1 explicitly. Implement a private helper like findLocalClientInfo(Context
clientContext) that iterates network.getPlayers(), checks
ClientComponent.component.local, and returns component.clientInfo (throw an
assertion if missing), then in TwoClientChatTest replace the arbitrary
senderClient/senderInfo selection with a call that obtains the client1 context
and uses findLocalClientInfo(client1Context) as the senderInfo when constructing
ChatMessageEvent and sending to other clients.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java`:
- Around line 65-70: The ChatProbe is being registered twice which causes
duplicate event delivery; remove the direct EventSystem registration and rely
solely on the component system registration. Specifically, keep the call
client2Csm.register(probe) (ComponentSystemManager.register) and delete the line
that calls client2Ctx.get(EventSystem.class).registerEventHandler(probe) so
ChatProbe is only registered once.
- Around line 72-80: The test currently picks an arbitrary sender from
hostNetwork.getPlayers(), which can pick the wrong client; add and use a helper
to find the local client's ClientComponent.clientInfo and bind the sender to
client1 explicitly. Implement a private helper like findLocalClientInfo(Context
clientContext) that iterates network.getPlayers(), checks
ClientComponent.component.local, and returns component.clientInfo (throw an
assertion if missing), then in TwoClientChatTest replace the arbitrary
senderClient/senderInfo selection with a call that obtains the client1 context
and uses findLocalClientInfo(client1Context) as the senderInfo when constructing
ChatMessageEvent and sending to other clients.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0f5b89a-67a5-46b6-9fac-e9a1d66b87a2

📥 Commits

Reviewing files that changed from the base of the PR and between e5e096e and 3ed1b86.

📒 Files selected for processing (3)
  • engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientNetworkStateTest.java
  • engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientSystemUpdateTest.java
  • engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientSystemUpdateTest.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java (1)

25-30: ⚠️ Potential issue | 🟡 Minor

Bind the sender, or narrow the test name/scope.

Line 70 still picks an arbitrary host-side client, so the sender can be the same client as the probed receiver. Either derive the sender from the first created client context, or rename the test/Javadoc to describe host-to-client delivery rather than “between clients.”

Also applies to: 40-40, 70-75

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java`
around lines 25 - 30, The test currently chooses an arbitrary host-side client
as the sender, which allows the sender and probed receiver to be the same;
update TwoClientChatTest so the host-side sender is explicitly derived from the
first created client context (instead of the arbitrary pick used when creating
the ChatMessageEvent) — locate the code that selects the host-side client entity
for sending the ChatMessageEvent and bind it to the first client context created
in the test setup (or alternatively change the test/Javadoc/test name to
indicate this is host-to-client delivery rather than “between clients”); ensure
references to ChatMessageEvent and the LISTEN_SERVER verification use the newly
bound sender.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java`:
- Around line 78-80: The test sets a too-short real-time safety cap which can
trigger MainLoop.runWhile() safety timeout before the intended game-time wait;
update the test around helper.setSafetyTimeoutMs(...) and helper.runUntil(...)
so the safety timeout is not shorter than the normal propagation wait (e.g.,
remove the 10000ms override or increase it to a larger value such as 60000ms),
ensuring probe.received has time to become true without hitting
MainLoop.runWhile()'s safety timeout.

---

Duplicate comments:
In
`@engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java`:
- Around line 25-30: The test currently chooses an arbitrary host-side client as
the sender, which allows the sender and probed receiver to be the same; update
TwoClientChatTest so the host-side sender is explicitly derived from the first
created client context (instead of the arbitrary pick used when creating the
ChatMessageEvent) — locate the code that selects the host-side client entity for
sending the ChatMessageEvent and bind it to the first client context created in
the test setup (or alternatively change the test/Javadoc/test name to indicate
this is host-to-client delivery rather than “between clients”); ensure
references to ChatMessageEvent and the LISTEN_SERVER verification use the newly
bound sender.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7201417b-5195-42ac-844b-5cd689e34d8c

📥 Commits

Reviewing files that changed from the base of the PR and between 3ed1b86 and 5841573.

📒 Files selected for processing (2)
  • docs/Engine-Testing-Patterns.md
  • engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java
✅ Files skipped from review due to trivial changes (1)
  • docs/Engine-Testing-Patterns.md

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new ModuleTestingEnvironment (MTE) multiplayer/network integration tests and accompanying documentation to help validate and guide engine-level network behavior under headless listen-server scenarios.

Changes:

  • Add 3 MTE integration tests covering client network state, client update ticks, and two-client chat event propagation.
  • Add new documentation page describing MTE testing patterns and common pitfalls for network event replication.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.

File Description
engine-tests/src/test/java/org/terasology/engine/integrationenvironment/TwoClientChatTest.java Adds a two-client listen-server integration test for chat event replication using ChatMessageEvent.
engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientSystemUpdateTest.java Adds a client-side integration test ensuring UpdateSubscriberSystem receives ticks in the MTE loop.
engine-tests/src/test/java/org/terasology/engine/integrationenvironment/ClientNetworkStateTest.java Adds client integration tests validating NetworkSystem mode and presence of NetworkSubsystem.
docs/Engine-Testing-Patterns.md Documents engine/MTE testing patterns, network event registration gotchas, and isolation guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/Engine-Testing-Patterns.md Outdated
@agent-refr
agent-refr force-pushed the test/salvage-mte-network-tests branch from 1692754 to 75574a7 Compare July 26, 2026 14:47
@Cervator
Cervator requested a review from Copilot July 26, 2026 14:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

engine-tests/src/main/java/org/terasology/engine/integrationenvironment/ModuleTestingEnvironment.java:64

  • Same as the overload above: this overload can also throw an UncheckedTimeoutException if the safety timeout is exceeded while waiting. Documenting that here makes the failure modes clearer.
     * @param gameTimeTimeoutMs how long to wait, in game time
     * @param description what is being waited for, phrased to read after "timed out waiting for"
     * @throws AssertionError if the condition does not hold within {@code gameTimeTimeoutMs} of game time
     */
    void awaitUntil(long gameTimeTimeoutMs, String description, Supplier<Boolean> condition);

Comment on lines +170 to +171
* @param description what is being waited for, phrased to read after "timed out waiting for"
* @throws AssertionError if the condition does not hold within DEFAULT_GAME_TIME_TIMEOUT of game time
Comment on lines +52 to +55
* @param description what is being waited for, phrased to read after "timed out waiting for"
* @throws AssertionError if the condition does not hold within DEFAULT_GAME_TIME_TIMEOUT of game time
*/
void awaitUntil(String description, Supplier<Boolean> condition);
Cervator and others added 10 commits July 26, 2026 22:39
Salvaged and cleaned up from PR MovingBlocks#5304 (Antigravity DI exploration).
All debug logging removed, imports cleaned, tests restructured.

- ClientNetworkStateTest: verifies client NetworkSystem mode and
  NetworkSubsystem presence
- ClientSystemUpdateTest: registers a system on a client and verifies
  it receives update ticks through the MTE game loop
- TwoClientChatTest: two-client chat propagation via ChatMessageEvent
  broadcast through the LISTEN_SERVER network stack

Also adds Engine-Testing-Patterns.md documenting MTE patterns, the
network event registration problem (inner-class events don't replicate),
context isolation, and Gradle execution tips.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the PathManager/CoreRegistry singleton save/restore pattern,
the @tempdir cleanup ordering gotcha, and why missing tearDown causes
non-deterministic cross-class test failures in CI.

Prompted by a real CI failure: PathManagerTest mutated PathManager
without restoring, causing ServerConnectListManagerTest to fail with
NoSuchFileException on Jenkins but not locally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
All tests that call helper.createClient() are subject to connection
timeouts in resource-constrained CI environments. This matches the
existing pattern in ExampleTest where all client-creating methods
are tagged @tag("flaky").

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… registration

- ChatMessageEvent is @OwnerEvent, not @BroadcastEvent — fix docs and
  test Javadoc
- Remove redundant EventSystem.registerEventHandler() call in test and
  docs example — ComponentSystemManager.register() already handles it
- Remove unused client1Ctx variable and unused EventSystem import
- Fix test message to reflect what's actually tested (host-to-client,
  not client1-to-client2)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ng diagnostics

- Match client 1 to its host-side representation via ClientComponent
  entity ID so the sender identity is explicit, not arbitrary
- Bump safety timeout from 10s to 45s — test takes ~27s locally, 10s
  was too low for CI
- Add SLF4J timing logs at each milestone (client connect, registration,
  send, receive) to help diagnose flaky CI failures
- Fix Javadoc to describe @OwnerEvent semantics correctly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tics

TwoClientChatTest: remove fragile cross-engine entity ID matching (IDs
aren't stable across engines), dead permission-grant block, and the
lowered safety timeout — use the 60s default instead.

ClientNetworkStateTest: use assertInstanceOf instead of bare cast.

All three tests: add JUnit 5 TestReporter.publishEntry() for structured
timing data (client_connect_ms, total_ms, etc.) that appears in JUnit
XML reports for Jenkins API access.

Engine-Testing-Patterns.md: add TestReporter diagnostics section, fix
NetworkMode qualifier in table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…erns

Two sections written alongside this branch's earlier work but never committed, recovered from a stash.

**Test Context Pattern** documents the arrangement that keeps `@BeforeAll`-built environments from leaking across test methods: hold a stable `baseContext`, build each test's context as a child of *that* rather than of the previous test's context, point `CoreRegistry` at the per-test context, and register loaded instances rather than lazy constructors. Includes the chaining anti-pattern it replaces, which is worth showing because the broken version looks reasonable.

This is the same pattern MovingBlocks#5323 applies to `TerasologyTestingEnvironment`. Documenting it here rather than there because this branch already owns the file, and the pattern generalises past the one class that motivated it.

**DI Patterns** is a living reference harvested during the Gestalt DI migration: constructor injection over field injection, `@In` as legacy *except* in MTE test classes where the harness requires it, never injecting `Context` itself, `ServiceRegistry` during registration and `ImmutableContextImpl` afterwards, and the duplicate-bean-key trap when explicitly mapping an interface that Gestalt already auto-maps. Points at reference implementations rather than restating them, so it ages by going stale rather than by going wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`runUntil` reports a timeout by returning `true`. That is easy to read backwards — `if (runUntil(cond))` looks like it means the condition was met — and easier still to drop, because nothing forces you to look at it. A dropped timeout is silent: the test carries on and fails at a later assertion with nothing indicating the wait was the problem. Only the *safety* timeout throws; the game-time timeout does not. `MainLoop` already carries a TODO calling this "a weird interface due for a revision".

Every wait in the tests added on this branch had exactly that shape:

```java
helper.runUntil(() -> probe.received);   // return value discarded
assertThat(probe.received).isTrue();     // where it actually fails
```

Adds `awaitUntil(description, condition)` and a timeout-taking overload, which throw an `AssertionError` naming what was awaited, the game time and real time elapsed, and the limit that was hit. Converted the waits in `TwoClientChatTest`, `ClientSystemUpdateTest` and `ExampleTest` to use it.

`ExampleTest` is not new on this branch, but it is the canonical MTE demo — the testing docs point newcomers at it — so leaving the ignorable pattern there guarantees it keeps being copied.

`runUntil` and `runWhile` are unchanged and stay public; a caller that genuinely wants to branch on the outcome still can. This is additive, so nothing outside the engine breaks: modules implementing `ModuleTestingEnvironment` would need the new methods, but the interface is implemented in-tree by `ModuleTestingHelper` and consumed elsewhere, not implemented.

Two tests cover the new method — that it returns quietly when the condition already holds, and that a timeout names what it was waiting for. The second uses a short game-time timeout deliberately, to exercise the game-time path rather than the safety timeout, since that is the one that previously failed silently.

Doc section added to `Engine-Testing-Patterns.md` covering both helpers, why to prefer `awaitUntil`, and the distinction between the game-time and safety timeouts, which is not obvious from the method names.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ng out

`createClient` is the most common source of intermittent MTE failures, and it was reporting them in the least useful way available:

```java
if (!connectToHost(client, mainLoop)) {
    throw new RuntimeException(String.format("Could not connect client %s to local host - timeout.", client));
}
```

Two problems. `connectToHost` ended in `return !mainLoop.runUntil(...)` — the same inverted-boolean shape `awaitUntil` was added to remove, where a timeout is a return value rather than a failure. And the `JoinStatus` returned by `NetworkSystem.join` was assigned, handed to `StateLoading`, and then dropped, so everything that could explain the failure was thrown away before the exception was built.

The result was that a join the host actively **refused** and a handshake that was merely **slow** produced byte-identical messages, with no indication of where it stalled. That is bearable locally, where you can re-run and watch. In CI it means a flaky failure is undiagnosable after the fact — which is exactly where it happens.

`connectToHost` now waits via `awaitUntil` and, on timeout, throws with the client's state at the point of abandonment plus the `JoinStatus`: its status, current activity, progress, and error message when present.

```
Could not connect client TerasologyEngine@1d3aefd to local host. Client state
when we gave up: StateLoading; join status IN_PROGRESS during 'Awaiting
Connection...' (0%)
```

That distinction is actionable rather than decorative: `FAILED` with an error means the host refused and waiting longer would never have helped, while `IN_PROGRESS` points at timing or a stalled loading step and names which one. The `awaitUntil` `AssertionError` is kept as the cause, so the elapsed game and real time are still in the trace.

`connectToHost` returns void now instead of a boolean nobody could use correctly; it has one caller. The TODO about making this asynchronous is left alone — that is a real redesign, and this is about making the current behaviour legible.

Prompted by a flaky failure of `ExampleTest.testSendEvent` on MovingBlocks#5323's Jenkins run, whose entire report was "Could not connect client ... - timeout."

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@soloturn
soloturn force-pushed the test/salvage-mte-network-tests branch from 5f9d680 to e69b070 Compare July 26, 2026 20:39
@Cervator
Cervator requested a review from Copilot July 26, 2026 21:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Comment on lines 294 to +298
* @param mainLoop
*
* return true if the connection to the host was successful and the client is in state <i>in-game</i>.
*/
boolean connectToHost(TerasologyEngine client, MainLoop mainLoop) {
void connectToHost(TerasologyEngine client, MainLoop mainLoop) {
@Cervator
Cervator merged commit 962b199 into MovingBlocks:develop Jul 26, 2026
12 checks passed
@Cervator
Cervator deleted the test/salvage-mte-network-tests branch July 26, 2026 22:34
Copilot AI pushed a commit that referenced this pull request Jul 27, 2026
…tine filesystem side effects

Leftovers from the reviews of #5323 and #5325 that were not worth another cycle on those PRs.

**`connectToHost` javadoc (Copilot, #5325).** The doc still promised a boolean after the method became void. It was already stale before that - a bare `@param mainLoop` with no text, and a `return` line that was never a `@return` tag, so javadoc was not rendering it anyway. Rewritten to describe what the method does and what it throws, including that the message distinguishes a refused join from a slow one.

**PMD `GuardLogStatement` in `TwoClientChatTest`.** Two `logger.info` calls computed `System.currentTimeMillis() - startTime` inside the argument list, which is the work the rule exists to stop. Hoisted both into locals, matching what the rest of the method already does for `client1Ms`, `registeredMs` and `totalMs`.

That also fixes a small inconsistency: `messages_sent_ms` was published from one `System.currentTimeMillis()` call and logged from a second, so the reported metric and the log line could disagree by a millisecond. They now come from the same value.

**Quarantined the one test that writes outside a `@TempDir`.** `useDefaultHomePathWindows` creates a real `Terasology` folder in the developer's own Saved Games directory, because the Windows home lookup goes through the OS rather than `user.home` and cannot be redirected the way the Mac and Linux cases are. Copilot flagged it on #5323 and it was already noted as wanting a tag group.

Tagged `filesystem-side-effects` and excluded from `test` and `unitTest`, with a new opt-in `filesystemSideEffectTest` task for running it deliberately. Worth noting the effect is the opposite of what a tag usually buys: the test is `@EnabledOnOs(WINDOWS)`, so it is skipped wherever the CI agent is not Windows and in practice only ever ran on developer machines - the people it inconveniences. Opt-in is the right default here rather than a CI-only group.

Verified both directions on Windows, where it actually matters: `unitTest` no longer lists the test at all (the tag filter drops it before `@EnabledOnOs` is evaluated), and `filesystemSideEffectTest` runs exactly that one test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI pushed a commit that referenced this pull request Jul 27, 2026
…y MTE tests

Two small leftovers that were being tracked separately and are cheaper together than as their own PRs.

**Deprecated `addEntityManagementRelatedClasses(Context, ServiceRegistry)`.** The overload ignores the context it is handed and delegates to the single-argument form, so passing a context implies a relationship that does not exist and makes it harder to reason about which context the registrations apply to. Copilot raised this on #5323 against the one call site that PR touched; the same call shape was left in three other places. All three now call the non-deprecated overload.

The deprecated method itself stays. It is public on an engine class and modules may call it, so removing it is a separate decision from stopping our own use of it. With these three gone it has no remaining in-tree callers, which is the state it should be in before anyone proposes deleting it.

**Timing diagnostics on `ExampleTest`.** `TestReporter.publishEntry` was added to the integration tests introduced in #5325 so CI timing is queryable through the Jenkins API rather than only readable in logs. The pre-existing `@Tag("flaky")` tests never got the same treatment, which is backwards - those are the ones whose timing anyone actually wants when they fail.

`testClientCreation`, `testClientConnection` and `testSendEvent` now publish how long client connection took. All three are flaky-tagged, and all three flake in the same place: waiting for a client to reach in-game. `testSendEvent` in particular failed this way on #5323's Jenkins run with no timing recorded, which is what prompted this.

Deliberately not touching `DelayManagerTest`, the other test named alongside `ExampleTest` for this retrofit: it lives in the separate ModuleTestingEnvironment module repository and rides that repo's own change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants