Task Summary
ClusterListener (34 lines) is the last file in the engine at 0% coverage, and it has no spec.
It is worth covering because the count it maintains is what the frontend's cluster badge renders: updateClusterStatus recomputes numWorkerNodesInCluster on every membership event and pushes a ClusterStatusUpdateEvent to every open session. A listener that stops subscribing, or stops fanning out, leaves every client showing a stale node count with nothing failing.
It is testable without a second JVM. AmberRuntime.pekkoConfig selects the cluster provider with artery bound on port 0, so joining the node to itself makes it the leader and produces genuine MemberUp events. That is not a convenience — Member is private[cluster] and cannot be synthesized, so a real join is the only way to reach the event path at all.
Three things to be careful about, each of which produced a failing draft before it was understood:
- The catch-all arm needs
TestActorRef.receive, not !. With a normal send, an exception from receive is taken by the supervisor and the actor is restarted — and a restarted listener answers the next request exactly like one that never failed. A draft written with ! stayed green with the catch-all replaced by a throw.
- Listeners must be stopped at the end of each case. One left running stays subscribed and keeps iterating
SessionState.getAllSessionStates on every membership event.
- A mock session must be removed inside the case that created it. ScalaMock scopes expectations per test while the
SessionState registry is JVM-global, so a leftover session gets called by a later test's listener against an expired mock.
Points 2 and 3 both stem from a real production race worth fixing separately: SessionState's registry is a plain unsynchronized mutable.HashMap, and ClusterListener.updateClusterStatus iterates it from the cluster-event thread while websocket open/close mutate it from container threads. A node joining or leaving while a user opens a tab can throw ConcurrentModificationException inside the listener.
Out of scope: the MemberRemoved recovery arm, which walks every live execution and calls notifyNodeFailure or forcefullyStop, so it needs real Amber clients.
Task Type
Task Summary
ClusterListener(34 lines) is the last file in the engine at 0% coverage, and it has no spec.It is worth covering because the count it maintains is what the frontend's cluster badge renders:
updateClusterStatusrecomputesnumWorkerNodesInClusteron every membership event and pushes aClusterStatusUpdateEventto every open session. A listener that stops subscribing, or stops fanning out, leaves every client showing a stale node count with nothing failing.It is testable without a second JVM.
AmberRuntime.pekkoConfigselects the cluster provider with artery bound on port 0, so joining the node to itself makes it the leader and produces genuineMemberUpevents. That is not a convenience —Memberisprivate[cluster]and cannot be synthesized, so a real join is the only way to reach the event path at all.Three things to be careful about, each of which produced a failing draft before it was understood:
TestActorRef.receive, not!. With a normal send, an exception fromreceiveis taken by the supervisor and the actor is restarted — and a restarted listener answers the next request exactly like one that never failed. A draft written with!stayed green with the catch-all replaced by athrow.SessionState.getAllSessionStateson every membership event.SessionStateregistry is JVM-global, so a leftover session gets called by a later test's listener against an expired mock.Points 2 and 3 both stem from a real production race worth fixing separately:
SessionState's registry is a plain unsynchronizedmutable.HashMap, andClusterListener.updateClusterStatusiterates it from the cluster-event thread while websocket open/close mutate it from container threads. A node joining or leaving while a user opens a tab can throwConcurrentModificationExceptioninside the listener.Out of scope: the
MemberRemovedrecovery arm, which walks every live execution and callsnotifyNodeFailureorforcefullyStop, so it needs real Amber clients.Task Type