feat: add Zenith strategy and move latency tracing
Build and Push Docker Container / build-and-push (push) Successful in 2m18s

- Register Zenith 1.1 with balanced duel verdicts and forced-outcome overrides.
- Prioritize permanent-trail space in Constrictor and health-aware hazard routes.
- Deduct executor queue delays and return fallback directions on move timeouts.
- Correlate response headers, replay timings, and sampled structured trace logs.
- Move shared metrics publication off the response path without losing local counts.
- Allow configurable tournament opponents and add a psql helper recipe.
- Preserve empty latency values through compact replay encoding and migration.
- Update Arena game links and use default assets for missing customizations.
- Cover strategy, HTTP lifecycle, tracing, replay codecs, and dashboard behavior.
- Validate with 260 passing Python tests, 4 skips, and 2 passing JavaScript tests.
This commit is contained in:
2026-11-05 01:11:31 +01:00
parent 39a7a53873
commit a2414f0b3d
26 changed files with 758 additions and 45 deletions
+52
View File
@@ -135,6 +135,58 @@ class TestBattlesnakeLifecycle(unittest.IsolatedAsyncioTestCase):
self.assertNotEqual(replay['game']['status'], 'running')
print(f'Real SQLite HTTP timings: {timings}')
async def test_timed_out_move_returns_valid_fallback(self):
server = self.server
server.logger = Mock()
server.logger.warning.return_value = None
server.game_lifecycle.turn = AsyncMock()
server.metrics_collector.record_move = AsyncMock()
state = deepcopy(self.state)
state['game']['timeout'] = 100
async def stalled_board(*args):
await asyncio.sleep(0.2)
server.game_runtime.get_game_board = stalled_board
response = await server.app.test_client().post('/move', json=state, headers=self.headers)
self.assertEqual(response.status_code, 200)
self.assertIn((await response.get_json())['move'], ('up', 'down', 'left', 'right'))
await server.game_lifecycle.drain()
async def test_slow_metrics_publish_is_off_response_path_and_traced(self):
server = self.server
gate = asyncio.Event()
snapshots = []
async def slow_publish():
await gate.wait()
await asyncio.sleep(0.1)
async def capture(state, move, thinking):
snapshots.append(thinking)
server.metrics_collector.publish_move_metrics = slow_publish
server.gameplay_tracking.record_gameplay_turn_snapshot = capture
server.dashboard_query.push_dashboard_game_replay_update = AsyncMock()
client = server.app.test_client()
started = perf_counter()
response = await asyncio.wait_for(client.post('/move', json=self.state, headers=self.headers), 0.5)
elapsed = (perf_counter() - started) * 1000
self.assertEqual(response.status_code, 200)
self.assertLess(elapsed, 500)
trace_id = response.headers['X-Move-Trace-ID']
self.assertEqual(len(trace_id), 32)
self.assertEqual(snapshots, [])
gate.set()
await server.game_lifecycle.drain()
trace = snapshots[0]['move_trace']
self.assertEqual(trace['trace_id'], trace_id)
self.assertGreaterEqual(trace['metrics_publish_ms'], 90)
self.assertIn('executor_queue_ms', trace)
self.assertIn('strategy_ms', trace)
self.assertIn('response_encode_ms', trace)
self.assertFalse(trace['timed_out'])
async def test_invalid_agent_does_not_schedule_work(self):
response = await self.server.app.test_client().post('/start', json=self.state)
self.assertEqual(response.status_code, 404)