import asyncio from types import SimpleNamespace from unittest.mock import AsyncMock import pytest from defense_sim.server import socketio_server as server class FakeSimulation: def __init__(self, run_id, steps=4): self.run_id = run_id self.projectiles = [SimpleNamespace(active=True)] self.events = [] self.remaining = steps self.time_s = 0. self.dt = .001 def step(self): base = len(self.events) self.events.extend({"type": "probe", "index": base+i} for i in range(100)) self.remaining -= 1 self.time_s += self.dt self.projectiles[0].active = self.remaining > 0 def snapshot(self): return {"run_id": self.run_id, "events": self.events[-250:]} def test_event_history_cursor_and_socket_isolation(monkeypatch): emit = AsyncMock() monkeypatch.setattr(server.sio, 'emit', emit) async def run(): await asyncio.gather(server._publish_loop(FakeSimulation('a'), 'client-a'), server._publish_loop(FakeSimulation('b'), 'client-b')) asyncio.run(run()) for sid in ('client-a', 'client-b'): events = [call.args[1] for call in emit.call_args_list if call.args[0] == server.EVENT and call.kwargs['to'] == sid] assert len(events) == 400 assert [event['index'] for event in events] == list(range(400)) assert all(call.kwargs['to'] in {'client-a', 'client-b'} for call in emit.call_args_list) def test_cancel_only_requested_connection(monkeypatch): async def run(): waiting = asyncio.Event() a, b = asyncio.create_task(waiting.wait()), asyncio.create_task(waiting.wait()) monkeypatch.setattr(server, 'tasks', {'a': a, 'b': b}) await server._cancel_run('a') assert a.cancelled() and not b.done() await server._cancel_run('b') asyncio.run(run()) def test_live_clock_never_runs_ahead_and_transport_cost_does_not_add_drift(monkeypatch): clock = [100.] sleeps = [] sim = FakeSimulation('paced', steps=8) sim.dt = .1 sim.time_s = 7. # restarting a nonzero engine must establish a new origin real_sleep = asyncio.sleep monkeypatch.setattr(server.time, 'perf_counter', lambda: clock[0]) async def sleep(delay): sleeps.append(delay) clock[0] += delay await real_sleep(0) async def emit(event, payload, **kwargs): assert sim.time_s-7. <= clock[0]-100.+1e-9 clock[0] += .0001 # network emission takes wall time monkeypatch.setattr(server.asyncio, 'sleep', sleep) monkeypatch.setattr(server.sio, 'emit', emit) asyncio.run(server._publish_loop(sim, 'client')) assert sim.time_s == pytest.approx(7.8) assert .8 <= clock[0]-100. < .825 assert any(delay > 0 for delay in sleeps)