Testing

Test strategy and coverage requirements for Galaxy.

Coverage Goals

Category Target Rationale
Physics/simulation 95% Critical correctness, bugs cause cascading errors
API endpoints 90% Core functionality, user-facing
Data persistence 90% Data integrity critical
Web client logic 80% UI can tolerate minor bugs
Overall 90% High confidence before deployment

Coverage Tooling

Python Services

All Python services use pytest-cov (already a dev dependency) with branch coverage enabled.

Setting Value Notes
Tool pytest-cov >= 4.1.0 Installed in dev dependencies
Source src Measures coverage of src/ package only
Branch coverage Enabled Detects untested conditional paths
Report format term-missing Shows uncovered line numbers in terminal
fail_under Not set initially Baselines measured first, thresholds added in follow-up

Configuration lives in each service’s pyproject.toml:

[tool.pytest.ini_options]
addopts = "--cov=src --cov-report=term-missing"

[tool.coverage.run]
source = ["src"]
branch = true

[tool.coverage.report]
show_missing = true
exclude_lines = [
    "pragma: no cover",
    "if __name__ == .__main__.",
    "pass",
]

Coverage runs automatically via addopts — no extra flags needed on pytest invocations.

CI installs pytest-cov in the test image and copies pyproject.toml so the config takes effect. The CI test command (python -m pytest tests/ -v --tb=short) is unchanged.

Web Client

The web-client enforces 88% line coverage via vitest (npx vitest run --coverage).

Test Framework

Component Framework
Python services pytest
Web client Jest + Testing Library
E2E Playwright
API pytest + httpx

Protobuf Testing Pattern

gRPC service tests use real compiled protobuf modules from the Docker image — proto modules are NOT mocked. This avoids the anti-pattern where PhysicsServicer(physics_pb2_grpc.PhysicsServicer) inherits from a MagicMock base class, which causes all method access to return MagicMock instead of real async methods.

  • Dependencies (redis_state, simulation, service) are still mocked
  • Request objects use MagicMock() with explicit attribute values (e.g., request.dt = 1.0)
  • Assertions check return value fields directly (e.g., response.success, response.bodies)
  • Tests run in Docker where proto compilation happens during image build
  • Never use importlib.reload with proto mocking — it pollutes sys.modules for subsequent tests

Per-Service Unit Test Cases

Physics Service

Models (test_models.py)

Test Case Expected Behavior
Vec3 creation Construct from (x, y, z); default to zero vector
Vec3 magnitude (3, 4, 0) → 5.0
Vec3 normalization Result has magnitude 1.0; zero vector returns zero
Vec3 dot product (1,0,0) · (0,1,0) → 0; (1,0,0) · (1,0,0) → 1
Vec3 cross product (1,0,0) × (0,1,0)(0,0,1)
Quaternion creation Construct from (w, x, y, z)
Quaternion identity (1, 0, 0, 0) is identity
Quaternion normalization Result has magnitude 1.0
Quaternion rotation Rotating (1,0,0) by 90° about Z → (0,1,0)
CelestialBody creation Construct with position, velocity, mass, radius
Ship creation Construct with position, velocity, attitude, fuel
Ship mass calculation Dry mass + remaining fuel mass
Ship inertia calculation Moment of inertia from mass distribution

Simulation (test_simulation.py)

Gravity:

Test Case Expected Behavior
Single body no acceleration Lone body has zero gravitational acceleration
Two bodies mutual attraction Bodies attract with F = GMm/r²
Two bodies known acceleration Acceleration matches analytical calculation
Three bodies superposition Net gravity = vector sum of pairwise attractions
Very close bodies Large acceleration, no division by zero
Coincident bodies skipped Zero-distance pairs skipped with warning logged
Empty bodies list No crash, zero acceleration
Inverse square law Doubling distance → quarter acceleration
Earth surface gravity 9.8 m/s² at Earth’s surface ± 1%
LEO orbital velocity ~7.7 km/s at 400 km altitude

Ship gravity:

Test Case Expected Behavior
No bodies Ship has zero gravitational acceleration
Single body attraction Ship attracted toward body center
Ship at body position Handled gracefully with warning
Multiple bodies superposition Net gravity on ship = vector sum

Thrust:

Test Case Expected Behavior
No thrust requested Zero thrust level → zero force, no fuel consumed
No fuel Zero fuel → zero force regardless of thrust level
Negative thrust level Clamped to zero
Full thrust Maximum force along ship’s forward axis
Partial thrust Force proportional to thrust level
Fuel limits thrust Limited fuel caps available impulse
Long time step Thrust impulse scales linearly with dt
Fuel consumption scales with dt Fuel consumed ∝ thrust × dt

Attitude control (inertia-compensated PD controller):

Test Case Expected Behavior
Zero angular velocity Zero damping torque
Positive angular velocity Damping torque opposes rotation
Negative angular velocity Damping torque opposes rotation
Torque not clamped Output in N·m, not normalized [-1,1]
All axes Torque computed independently per axis
Z-axis lower inertia Z-axis has lower moment of inertia
No torque no change Zero torque → attitude unchanged
Torque changes angular velocity Non-zero torque → angular velocity changes
Angular velocity rotates attitude Non-zero ω → quaternion evolves
Attitude remains normalized Quaternion stays unit length after integration
Inertia affects angular acceleration α = τ/I
No input no torque No rotation input and no attitude hold → zero torque
Rotation input applies torque Manual rotation input → torque on corresponding axis
Wheel momentum accumulates Continuous torque → wheel momentum increases
Saturated wheels use RCS Wheels at capacity → RCS thrusters fire
No fuel no RCS RCS requires fuel
Desaturation when idle Wheels desaturate when no torque demanded
Desaturation during attitude hold Desaturation runs concurrently with attitude hold
No desaturation below threshold Small wheel momentum not desaturated
Concurrent torque and desaturation Both operate simultaneously

Attitude reference frames:

Test Case Expected Behavior
Prograde via helper Quaternion aligns +Y with velocity direction
Retrograde via helper Quaternion aligns +Y against velocity direction
Normal (circular orbit XY plane) Normal perpendicular to orbital plane
Antinormal Opposite of normal direction
Radial (circular orbit) Points away from central body
Antiradial Points toward central body
Local horizontal (mixed velocity) Horizontal component of velocity direction
Local vertical Points radially from central body
Degenerate: normal for radial trajectory Falls back gracefully
Degenerate: horizontal for purely radial motion Falls back gracefully
Degenerate: prograde at low velocity Falls back to current orientation
Degenerate: vertical at body center Falls back gracefully
Degenerate: radial at low angular momentum Falls back gracefully
Normal with body offset Works when reference body is not at origin

Integration (Leapfrog):

Test Case Expected Behavior
Position displacement scales with dt Δx ∝ dt
Velocity change scales with dt Δv ∝ dt
Wheel momentum scales with dt ΔL ∝ dt
Angular velocity change scales with dt Δω ∝ dt
Position-velocity relationship Leapfrog half-step ordering preserved
Constant acceleration Exact solution for constant F

State restoration:

Test Case Expected Behavior
Restore bodies from Redis RestoreBodies loads evolved positions/velocities
Does not write to Redis Restore is read-only
Clears previous bodies Old in-memory state replaced
Empty Redis returns zero No bodies in Redis → zero count, no crash

RefBody snapshots:

Test Case Expected Behavior
Preserves pre-update positions Snapshot captures positions before tick update
Fields accessible All RefBody fields readable
Works with find_reference_body RefBody used in reference body lookup
Works with compute_ship_gravity RefBody used in gravity calculations

Station Physics (test_simulation.py)

Test Case Expected Behavior
Station gravity only Station updated with gravity, no thrust or attitude changes
Station Leapfrog integration Position and velocity evolve correctly under gravity
Station attitude fixed Attitude quaternion unchanged after tick
Station batch write All stations written via pipeline
Spawn station equatorial orbit Station placed at correct altitude with orbital velocity
Spawn station Lagrange L5 Station placed at −60° from secondary body
Spawn station Lagrange L4 Station placed at +60° from secondary body
Spawn station equatorial tilt Position/velocity rotated to body’s equatorial plane
Station gravity matches ship gravity Same acceleration at same position

Ship Classes (test_simulation.py)

Test Case Expected Behavior
Spawn fast_frigate default Ship created with fast_frigate parameters
Spawn cargo hauler Ship created with cargo hauler parameters
Unknown class defaults to fast_frigate Invalid class name falls back to fast_frigate
Inertia tensor at full fuel Returns inertia_full diagonal values
Inertia tensor at empty fuel Returns inertia_dry diagonal values
Inertia tensor interpolation Linear interpolation at 50% fuel
Ship class persists in Redis ship_class field roundtrips through Redis
Legacy ship without class Defaults to fast_frigate on deserialization

gRPC Server (test_grpc_server.py)

Test Case Expected Behavior
InitializeBodies Accepts body list, initializes simulation
RestoreBodies Loads body state from Redis
ProcessTick Advances simulation by one tick
GetBody Returns single body state by ID
GetBodies Returns all body states
CreateShip Creates ship at specified position/velocity
GetShip Returns single ship state by ID
GetShips Returns all ship states
SetThrust Updates ship thrust level
SetRotation Updates ship rotation input
DeleteShip Removes ship from simulation
SpawnStation Creates station in equatorial orbit
SpawnStation Lagrange Creates station at L4/L5 point
RemoveStation Deletes station, publishes event
GetAllStations Returns all station states
ClearAllStations Removes all stations, returns count

Redis State (test_redis_state.py)

Test Case Expected Behavior
Redis connection Connect, disconnect, reconnect
Get body Retrieve body by key
Set body Store body with correct key pattern
Get all bodies Retrieve all bodies
Set bodies batch Atomic batch store
Get ship Retrieve ship by ID
Set ship Store ship state
Get all ships Retrieve all ships
Delete ship Remove ship from Redis
Clear ships Remove all ships
Get station Retrieve station by ID
Set station Store station with correct key pattern
Get all stations Retrieve all stations
Set stations batch Atomic batch store via pipeline
Delete station Remove station from Redis
Clear all stations Remove all stations
Publish station spawned Event published to galaxy:stations stream
Publish station removed Event published to galaxy:stations stream

Health (test_health.py)

Test Case Expected Behavior
Readiness OK Returns 200 when initialized
Readiness not ready Returns 503 when not initialized
Readiness shutting down Returns 503 during shutdown
Liveness Always returns 200

API Gateway Service

Auth (test_auth.py)

Test Case Expected Behavior
Verify valid JWT Returns decoded payload
Verify expired JWT Raises authentication error
Verify invalid JWT Raises authentication error
Hash password Returns bcrypt hash
Verify correct password Returns true
Verify incorrect password Returns false

Admin Auth (test_admin_auth.py)

Test Case Expected Behavior
Admin required with valid token Dependency passes, returns admin identity
Admin required without token Returns 401
Admin required with invalid token Returns 401
Admin required with expired token Returns 401

Chat Rate Limiter (test_chat.py)

Test Case Expected Behavior
Allow messages within limit 5 messages in 1 second all allowed
Block excess messages 6th message in same window returns False
Window reset allows new messages Messages allowed after window expires
Per-player isolation One player’s limit doesn’t affect another
Cleanup removes player state cleanup_player() frees memory
Monotonic timing Uses time.monotonic(), not wall clock

Config (test_config.py)

Test Case Expected Behavior
JWT secret required Missing secret raises validation error
Sensitive field masking Secrets masked in log output

Request Validation (test_validation.py)

Test Case Expected Behavior
Valid thrust level 0.0–1.0 accepted
Invalid thrust level Negative or > 1.0 rejected
Valid rotation input 3D vector within bounds accepted
Invalid rotation input Out-of-range values rejected
Valid tick rate Within configured bounds accepted
Invalid tick rate Out-of-range values rejected

Rate Limiting (test_rate_limit.py)

Test Case Expected Behavior
Requests within limit Allowed through
Excess requests blocked Returns 429 after limit exceeded
Resets after window Requests allowed after window expires
Per-user isolation One user’s limit doesn’t affect another
Admin exemption Admin requests bypass rate limits

Metrics (test_metrics.py)

Test Case Expected Behavior
Middleware records requests Counter incremented per request
Metrics endpoint Returns Prometheus-formatted metrics

WebSocket Manager (test_websocket_manager.py)

Test Case Expected Behavior
Connection established Client connects, added to connection pool
Disconnection Client removed from pool on disconnect
Subscribe to events Client receives only subscribed event types
Unsubscribe from events Client stops receiving unsubscribed events
Broadcast All connected clients receive broadcast messages
User-specific message Only targeted user receives message
Authentication Unauthenticated WebSocket connections rejected

Health (test_health.py)

Test Case Expected Behavior
Readiness OK Returns 200 when ready
Readiness not ready Returns 503 when not ready
Readiness shutting down Returns 503 during shutdown
Liveness Always returns 200

Tick Engine Service

State (test_state.py)

Test Case Expected Behavior
Get tick Returns current tick number from Redis
Increment tick Atomically increments tick counter
Get game time Returns current game time
Set game time Updates game time in Redis
Get tick rate Returns configured ticks per second
Set tick rate Updates tick rate in Redis
Is paused Returns current pause state
Set paused Updates pause state in Redis

Tick Loop (test_tick_loop.py)

Initialization:

Test Case Expected Behavior
Calls galaxy service first Galaxy InitializeBodies called before physics
Passes bodies to physics Bodies from galaxy forwarded to physics InitializeBodies
Fails when galaxy init fails Returns error, does not proceed to physics
Fails when galaxy raises exception Exception caught, initialization fails
Fails when GetBodies raises exception Exception caught, initialization fails
Fails when physics init fails Returns error despite galaxy success
Sets initialized flag on success Flag set only after both services succeed
Logs fallback warning Warning logged when galaxy uses fallback ephemeris

Tick processing:

Test Case Expected Behavior
Process tick success Calls physics ProcessTick, increments counter
Retries on failure Transient failure retried up to configured limit
Retries on RPC error gRPC errors trigger retry
Unhealthy after all retries fail Service marked unhealthy when retries exhausted
Passes correct tick number Current tick number sent to physics
Uses configured timeout gRPC timeout from configuration
Fast-fails when circuit open Skips physics call, records failure
Records success closes circuit Successful tick closes open circuit
Records failure on retries exhausted Circuit breaker failure recorded

Health check loop:

Test Case Expected Behavior
Recovers physics service Health check detects physics recovery
Resets on failure Failed health check maintains unhealthy state
Auto-resumes after recovery Game unpaused after physics recovers
Skips when already healthy No action when physics is healthy
Resets circuit on recovery Circuit breaker reset on health recovery

Pause/Resume:

Test Case Expected Behavior
Pause publishes event tick.paused event broadcast via WebSocket
Resume publishes event tick.resumed event broadcast via WebSocket
Resume clears auto-paused flag Auto-pause flag cleared on manual resume
Resume resets last tick time Prevents time jump after long pause

Run loop:

Test Case Expected Behavior
Processes ticks when not paused Ticks advance at configured rate
Sets game time incrementally Game time advanced by dt each tick
Skips processing when paused No tick processing while paused
Uses effective time scale for dt Time scale applied to dt calculation
Auto-pauses when physics unhealthy Game paused when physics service unreachable

Time synchronization:

Test Case Expected Behavior
Positive drift speeds up Positive drift → time scale > 1.0
Negative drift slows down Negative drift → time scale < 1.0
Deadband no correction Drift within ±10s → no correction
Cap at ±5% Time scale clamped to [0.95, 1.05]
Disabled uses admin scale Sync disabled → admin time scale used
Non-unit admin scale bypasses sync Custom admin scale disables sync
Deadband edge at 10s Exactly 10s drift → no correction

Game reset:

Test Case Expected Behavior
Pauses first Game paused before reset operations
Clears all ships All ships removed from physics and Redis
Clears state before reinitialize State cleared before fresh initialization
Resumes after completion Game unpaused after reset
Resets last tick time Prevents time jump after reset
Handles clear ships failure Gracefully handles failure during ship clear
Returns correct values Returns new tick number and body count

Snapshots:

Test Case Expected Behavior
Acquires tick lock Snapshot waits for current tick to complete
Returns snapshot result Snapshot metadata returned
Waits for tick processing Snapshot taken between ticks, not during
Snapshot loop creates periodic snapshots Snapshots created at configured interval
Snapshot loop continues on error Failed snapshot does not stop loop
Snapshot loop stops on shutdown Loop exits on shutdown signal

Connection management:

Test Case Expected Behavior
Connect creates gRPC channels Channels created to galaxy and physics
Close closes gRPC channels Channels closed on shutdown
Close handles None channels No error if channels not initialized

Tick rate:

Test Case Expected Behavior
Set valid rate Rate updated in Redis
Minimum bound Rate ≥ configured minimum
Maximum bound Rate ≤ configured maximum
Below minimum clamped Value below minimum clamped to minimum
Above maximum clamped Value above maximum clamped to maximum

Automation Engine (test_automation.py)

Rule Evaluation:

Test Case Expected Behavior
Evaluate simple condition ship.fuel < 0.5 triggers when fuel low
Evaluate orbital condition orbit.apoapsis > 500000 triggers correctly
Evaluate distance condition ship.distance_to with body arg computes distance
AND logic all conditions All conditions must be true to trigger
AND logic partial match Some false conditions prevent trigger
Immediate field immediate condition always triggers
Unknown field Unknown condition field treated as false
Once mode disables after trigger Rule set to enabled=false after firing
Continuous mode keeps firing Rule stays enabled after trigger

Actions:

Test Case Expected Behavior
Set thrust action Thrust level applied via physics gRPC
Set attitude prograde Attitude mode set to prograde
Set attitude hold Attitude hold enabled
Alert action Message logged, no gRPC call
Start circularize Maneuver hash created, prograde attitude, full thrust
Start set_inclination Maneuver hash created with target inclination
Start rendezvous Maneuver hash created with target_id, phase=transfer

Circularize Maneuver:

Test Case Expected Behavior
Prograde burn raises orbit Attitude set to prograde, thrust applied
Retrograde burn lowers orbit Attitude set to retrograde when apoapsis needs lowering
Completion at low eccentricity Maneuver completes when e < 0.005

Set Inclination Maneuver:

Test Case Expected Behavior    
Normal burn at ascending node Burns normal near ascending node    
Antinormal burn at descending node Burns antinormal near descending node    
Completion at target inclination Maneuver completes when incl − target < 0.5°

Rendezvous Maneuver:

Test Case Expected Behavior
Maneuver creation Maneuver stored with phase “plane_change” and strategy “manual”
Approach throttle-down Thrust proportional to relative velocity
Approach completion distance < 1 km AND rel_vel < 1 m/s completes maneuver
Target not found Missing target aborts maneuver
Maneuver abort abort flag detected, thrust zeroed, maneuver cleared

Brachistochrone Edge Cases:

Test Case Expected Behavior
Fuel depleted gRPC failure ApplyControl/SetAttitudeMode exceptions during fuel=0 don’t crash maneuver
Coast phase gRPC failure Physics command exceptions during coast don’t crash maneuver
Burn phase gRPC failure Physics command exceptions during burn don’t crash maneuver
Direction hold — first tick No previous direction → use computed direction, reset age
Direction hold — spin freeze High angular velocity → hold previous direction
Direction hold — not due Between updates → hold previous direction
Direction hold — due for update Past update interval → use fresh direction, reset age
Prograde escape bypass During prograde escape → dir_age forced to 0
Cross-body missing parent Common parent body not in positions → fallback to local frame
Zero command magnitude a_cmd_mag ≈ 0 → use position-based direction fallback
Periapsis safety status status_text includes “[periapsis safety]” when active
Dynamic braking trigger fires dv_remaining ≤ rel_vel × 1.15 during coast → burn2
Dynamic braking trigger holds dv_remaining > rel_vel × 1.15 during coast → stays coast
Fuel exactly 1.0 fuel=1.0 → NOT fuel-depleted, uses normal guidance
Sub-phase boundary burn1→coast elapsed == burn_time → transitions to coast
Sub-phase boundary coast→burn2 (fallback) elapsed == burn_time + coast_time → transitions to burn2
Maneuver execute gRPC error AioRpcError during maneuver tick → logged, not crash
Missing ref body Maneuver ref_body not in body_positions → early return
Non-numeric coast ratio coast_ratio=”invalid” → defaults to 0.0

Automation Maneuver Phase Handlers (test_automation_maneuvers.py):

Escape Phase:

Test Case Expected Behavior
Escape achieved — cross_soi Energy ≥ 0 and v² ≥ 1.2 × v_esc² → transition to interplanetary
Escape achieved — brachistochrone Same energy condition, non-cross_soi → transition to brachistochrone
Still burning Negative energy → prograde steering, full thrust, status contains energy

Capture Phase:

Test Case Expected Behavior
Capture achieved Energy < 0 → transition to circularize
Capture achieved sets transfer_target_r oe_target provided → transfer_target_r set from oe_target["a"]
Still braking Energy ≥ 0 → retrograde steering, full thrust, status contains v_excess

Plane Change Phase:

Test Case Expected Behavior
OOP converged, AP/PE converged Transitions to phase, removes burn_eta_seconds
OOP converged, AP/PE not converged Transitions to transfer_plan
OOP not converged — normal steering oop_steering called, steering applied, no transition

Approach Phase:

Test Case Expected Behavior
Completion distance < 500 and rel_vel < 1.0 → _complete_maneuver called
Closing mode rel_vel < 2.0 and distance > 500 → ATTITUDE_TARGET steering
Braking mode rel_vel ≥ 2.0 → ATTITUDE_TARGET_RETROGRADE steering

Station-Keep Maneuver:

Test Case Expected Behavior    
Approach within threshold   dist_error < 1000m → transitions to hold, cuts thrust
Approach too far dist_error > 0 → steers toward target    
Approach too close dist_error < 0 → steers away (ATTITUDE_ANTI_TARGET)    
Hold within deadband   dist_error < db and rel_vel < 0.5 → zero thrust (coast)
Hold drifted outside deadband   dist_error > db → RCS correction with ATTITUDE_TARGET
Target not found Missing target → early return, no commands sent    

Transfer Burn Phase:

Test Case Expected Behavior
Orbit shape reached dv_rem < 0.5 → transitions to transfer_coast
Still burning dv_rem > threshold → prograde/retrograde steering applied

Departure Wait Phase:

Test Case Expected Behavior
Phase window detected phase_err within threshold → transitions to transfer_burn
Passive wait ETA ≤ active threshold → pre-orient prograde, zero thrust

Orbital Helpers (test_orbital.py):

Test Case Expected Behavior
Circular orbit elements e ≈ 0, periapsis ≈ apoapsis
Elliptical orbit elements Correct a, e, periapsis, apoapsis
Escape orbit is_escape = True, apoapsis = None
Inclination calculation Correct relative to body equatorial plane
Node angles Ascending/descending node angles computed
Degenerate near-zero velocity Returns None gracefully

Circuit Breaker (test_circuit_breaker.py)

Test Case Expected Behavior
Closed initially Circuit starts in closed (healthy) state
Opens after failures Consecutive failures exceed threshold → open
Half-open after timeout Open circuit transitions to half-open after timeout
Closes on success Successful call in half-open → closed
Reopens on half-open failure Failed call in half-open → open again

Config (test_config.py)

Test Case Expected Behavior
Default values All settings have sensible defaults
Validation Invalid values rejected
Tick rate bounds Bounds enforced
Invalid tick rate Out-of-range rejected

Health (test_health.py)

Test Case Expected Behavior
Readiness OK Returns 200 when initialized
Readiness not ready Returns 503 when not initialized
Readiness shutting down Returns 503 during shutdown
Liveness Always returns 200

Players Service

Auth (test_auth.py)

Test Case Expected Behavior
Hash password Returns bcrypt hash
Verify correct password Returns true
Verify incorrect password Returns false
Create access token Returns valid JWT with expected claims
Verify valid token Returns decoded payload
Verify expired token Raises error
Verify invalid token Raises error

Models (test_models.py)

Test Case Expected Behavior
Player model creation Construct with username, password hash, ship ID
Player model validation Invalid fields rejected
PlayerSummary creation Construct with subset of player fields
PlayerSummary from Player Convert full Player to summary

Config (test_config.py)

Test Case Expected Behavior
JWT secret required Missing secret raises validation error
Sensitive field masking Secrets masked in log output

Health (test_health.py)

Test Case Expected Behavior
Readiness OK Returns 200 when ready
Readiness not ready Returns 503 when not ready
Readiness shutting down Returns 503 during shutdown
Liveness Always returns 200

Galaxy Service

Models (test_models.py)

Test Case Expected Behavior
Vec3 creation Construct from (x, y, z)
Vec3 operations Add, subtract, scale, magnitude
BodyType enum STAR, PLANET, MOON, DWARF_PLANET values
CelestialBody creation Construct with position, velocity, mass, radius, type
CelestialBody validation Invalid fields rejected

Ephemeris (test_ephemeris.py)

Test Case Expected Behavior
File structure Ephemeris data files load correctly
Major bodies returned Sun, planets, and moons present
Body positions calculated Positions computed from ephemeris data
Body velocities calculated Velocities computed from ephemeris data
Rotation data loaded Rotation periods and axial tilts present
JPL Horizons response parsing API responses parsed into body data
Fallback ephemeris Hardcoded data used when JPL unavailable

gRPC Server (test_grpc_server.py)

Test Case Expected Behavior
InitializeBodies Returns body data from ephemeris
GetBodies Returns all celestial bodies
GetBody Returns single body by ID
Service health Health RPC returns serving status

Health (test_health.py)

Test Case Expected Behavior
Readiness OK Returns 200 when initialized
Readiness not ready Returns 503 when not initialized
Readiness shutting down Returns 503 during shutdown
Liveness Always returns 200

Admin CLI Service

Config (test_config.py)

Test Case Expected Behavior
API base URL default Defaults to https://localhost:30002/api
Token file path Points to ~/.galaxy/admin-token
Token validity hours Set to 24
Admin credentials default ADMIN_USER and ADMIN_PASSWORD are None by default

Auth (test_auth.py)

Test Case Expected Behavior
Ensure galaxy dir creates directory Creates ~/.galaxy with 0o700 permissions
Read cached token returns None when missing Returns None if token file does not exist
Read cached token returns data when valid Returns token data if not expired
Read cached token returns None when expired Returns None and removes expired token file
Read cached token returns None when corrupt Returns None and removes corrupt token file
Save token writes JSON Writes token + expires_at to file
Save token sets permissions File created with 0o600 permissions
Authenticate success Returns token from API response
Authenticate 401 Raises AuthError for invalid credentials
Authenticate connection error Raises AuthError on connection failure
Get admin token cached Returns cached token without API call
Get admin token env vars Authenticates with env vars, saves token
Get admin token non-interactive no creds Raises AuthError when no credentials available

API Client (test_api.py)

Test Case Expected Behavior
Headers include bearer token Authorization header set with Bearer prefix
Successful GET request Returns parsed JSON response
Successful POST request Sends JSON body, returns response
401 retry clears and retries Clears token, re-authenticates, retries once
HTTP error raises APIError Non-401 HTTP errors wrapped in APIError
Connection error raises APIError Network errors wrapped in APIError
Module wrappers call correct endpoints get_status, pause, resume, etc. use correct method and path
PUT request Sends PUT with JSON body, returns response
set_tick_rate wrapper Calls PUT /admin/game/tick-rate with rate payload
list_snapshots wrapper Calls GET /admin/snapshots
create_snapshot wrapper Calls POST /admin/snapshot
restore_snapshot wrapper Calls POST /admin/restore with snapshot_id
reset_game wrapper Calls POST /admin/game/reset
list_players wrapper Calls GET /admin/players
reset_player_password wrapper Calls POST /admin/players/{id}/reset-password

CLI Commands (test_main.py)

Test Case Expected Behavior
Status command Displays game status table
Pause command Shows paused message with tick number
Resume command Shows resumed message with tick number
Tick rate set valid Calls API with rate, shows change
Tick rate set out of range Exits with error, no API call
Snapshot list Displays snapshot table
Snapshot create Shows created snapshot ID and tick
Snapshot restore with –yes Calls restore API without prompt
Players list Displays players table
Reset with –yes Calls reset API without prompt
Auth logout Clears cached token
Auth login error Exits with error code 1
Auth login success Shows authenticated message on success
Status command paused Shows PAUSED when game is paused
Status command ticks behind Shows non-zero ticks behind in red
Status command API error Exits with error on API failure
Tick rate get Shows configured and actual tick rate
Tick rate set API error Exits with error on API failure
Snapshot list empty Shows “No snapshots available” message
Snapshot restore without –yes Prompts for confirmation
Players list empty Shows “No players registered” message
Players reset-password Looks up player by username and resets
Players reset-password not found Shows error for unknown username
Reset without –yes Prompts for confirmation twice
Pause API error Exits with error on API failure
Resume API error Exits with error on API failure

Database

Migrations (test_migrations.py)

Test Case Expected Behavior
Upgrade creates tables Creates players, admins, snapshots, game_config
Upgrade creates indexes idx_players_username, idx_players_ship_id, idx_snapshots_tick
Downgrade drops tables All tables removed on downgrade to base
Upgrade is idempotent Running upgrade twice does not error
Upgrade preserves existing data Pre-existing rows survive migration
Alembic version tracked alembic_version table records revision “001”

Integration Test Scenarios

Auth Flow (tests/integration/test_auth.py)

Test Case Expected Behavior
Player register success New account created, credentials stored
Player register duplicate username Returns conflict error
Player register duplicate email Returns conflict error
Player login success Returns valid JWT token
Player login wrong password Returns authentication error
Player login nonexistent user Returns authentication error
Admin login success Returns valid admin JWT token
Admin login wrong password Returns authentication error
Admin login nonexistent admin Returns authentication error

Admin Controls (tests/integration/test_admin.py)

Test Case Expected Behavior
Admin status Returns system status (tick, pause state, body count)
Pause game Game paused, tick counter stops advancing
Resume game Game unpaused, tick counter resumes
Set tick rate Tick rate updated, processing frequency changes
Create snapshot Game state captured to persistent storage
Restore snapshot Game state restored from snapshot
Reset game All ships cleared, bodies re-initialized

Game State (tests/integration/test_game.py)

Test Case Expected Behavior
Get ship Returns ship state for authenticated player
Set thrust Ship thrust level updated, reflected in subsequent ticks
Set rotation Ship rotation input updated
Game state updates Player actions reflected in state after tick processing

Cross-Service Data Contracts

Scenario Services Involved Validation
Body serialization Galaxy → Physics via gRPC Protobuf schema enforced
Ship state in Redis Physics ↔ Tick Engine ↔ API Gateway JSON schema consistent across readers/writers
Tick event broadcasting Tick Engine → Redis Pub/Sub → API Gateway → WebSocket Event format matches client expectations
Auth token propagation Players → API Gateway → all authenticated endpoints JWT claims consistent

End-to-End Test Flows

New Player Journey (tests/e2e/test_full_flow.py)

  1. Register → POST /api/auth/register → 201 Created
  2. Login → POST /api/auth/login → JWT token returned
  3. Connect WebSocket → Authenticate with JWT → receive tick data
  4. Spawn ship → Ship created at default location (Earth orbit)
  5. Apply thrust → POST /api/game/thrust → ship acceleration changes
  6. Verify position change → Subsequent tick shows position delta
  7. Disconnect → WebSocket closed
  8. Reconnect → New WebSocket → ship state preserved

Admin Operations (tests/e2e/test_full_flow.py)

  1. Admin login → POST /api/admin/login → admin JWT token
  2. Pause game → POST /api/admin/game/pause → tick counter stops
  3. Verify paused → Multiple checks confirm tick unchanged
  4. Resume game → POST /api/admin/game/resume → ticks advance again
  5. Create snapshot → POST /api/admin/snapshot → snapshot metadata returned
  6. Restore snapshot → POST /api/admin/snapshot/restore → state reverted

WebSocket Event Flow (tests/e2e/test_full_flow.py)

  1. Connect → WebSocket established with auth
  2. Receive tick data → Body and ship positions each tick
  3. Receive pause/resume eventstick.paused / tick.resumed events
  4. Receive version updates → Client notified of service version changes

Orbital Mechanics (planned)

  1. Spawn → Ship in circular Earth orbit
  2. Engage prograde hold → Press P → attitude converges to prograde
  3. Verify attitude convergence → Quaternion aligns with velocity within ~8s settling time
  4. Apply thrust → Raise orbit with prograde burn
  5. Verify orbital elements change → Semi-major axis increases
  6. Engage retrograde hold → Shift+P → attitude flips to retrograde
  7. Decelerate → Lower orbit with retrograde burn

Multi-Player (planned)

  1. Player A connects → Spawns ship
  2. Player B connects → Spawns ship, sees Player A’s ship
  3. Player A sees Player B → Both ships visible via WebSocket state
  4. Player A disconnects → Ship removed
  5. Player B notified → Player A’s ship no longer in state

Physics Validation Tests

Verify simulation accuracy against known values.

Test Validation
Orbital period Earth completes orbit in 365.25 days ± 0.1%
Moon orbit Luna completes orbit in 27.3 days ± 0.1%
Gravity acceleration 9.8 m/s² at Earth surface ± 1%
Kepler’s laws Verify period² ∝ semi-major axis³
Energy conservation (short) Total system energy stable over 1,000 ticks ± 0.01%
Energy conservation (long) Total system energy stable over 1,000,000 ticks ± 0.1%
Momentum conservation Total momentum stable over 1,000 ticks ± 0.01%

Energy Conservation Notes

The Leapfrog (Störmer-Verlet) integrator is symplectic, meaning:

  • Energy error oscillates rather than drifting monotonically
  • Error magnitude is O(Δt²) where Δt = tick duration
  • Long-term stability is excellent despite short-term oscillation

Test thresholds account for oscillating behavior:

  • Short-term (1,000 ticks = ~17 minutes): Tight threshold catches integration bugs
  • Long-term (1,000,000 ticks = ~11.5 days): Looser threshold allows for oscillation amplitude

Ship Mechanics Tests

Test Validation
Thrust acceleration F=ma verified for various masses
Fuel consumption Matches spec rate at full thrust
Delta-v Matches Tsiolkovsky equation
Rotation Torque produces correct angular acceleration
Reaction wheel saturation Wheels saturate at documented capacity

Performance Benchmarks

Metric Target Rationale
Tick processing time (50 ships) < 100ms Must complete within 1s tick interval with headroom
WebSocket broadcast latency < 50ms Smooth client updates at tick rate
Ship spawn time < 500ms Responsive player experience
Physics step (N-body, 30 bodies) < 10ms Leaves headroom for ship calculations
Memory per ship (Redis) < 1 MB Supports hundreds of concurrent ships
gRPC round-trip (ProcessTick) < 50ms Tick engine ↔ physics overhead budget
JWT verification < 5ms Per-request overhead for authenticated endpoints
WebSocket connection setup < 200ms Fast reconnection after disconnect

Failure and Resilience Test Scenarios

Redis Disconnection

Scenario Expected Behavior
Physics loses Redis connection State updates fail gracefully; simulation continues in-memory
Tick engine loses Redis connection Tick processing paused; auto-resume on reconnection
API gateway loses Redis connection Returns 503 on state-dependent endpoints
Redis reconnects after outage Services resume normal operation without restart

Service Restart

Scenario Expected Behavior
Physics restart Ships survive via RestoreBodies loading evolved state from Redis
Tick engine restart Reconnects to physics; calls RestoreBodies; resumes tick processing
API gateway restart WebSocket clients reconnect; no state loss (state in Redis)
Players service restart No impact on active sessions (JWT stateless)
Galaxy service restart Ephemeris reloaded; no runtime impact (data is static)

Circuit Breaker

Scenario Expected Behavior
Repeated physics failures Circuit opens after threshold; tick processing fast-fails
Physics recovers while circuit open Half-open probe succeeds; circuit closes; ticks resume
Physics fails during half-open Circuit reopens; wait for next timeout

Graceful Shutdown

Scenario Expected Behavior
Shutdown signal received Health probes return 503 immediately
In-flight requests Complete before process exits
WebSocket connections Clients notified before disconnect
gRPC streams Streams closed cleanly

Clock and Timing

Scenario Expected Behavior
Tick processing exceeds tick duration Next tick starts immediately; drift tracked
Time synchronization drift > 10s Time scale adjusted (capped at ±5%)
Time synchronization drift ≤ 10s No correction (deadband)
Long pause then resume No time jump; last tick time reset

Redis Integration Tests

Integration tests that run against a real Redis instance to verify serialization roundtrips, pipeline behavior, stream consumer groups, and cross-service data contracts.

Infrastructure

Tests run via Docker Compose (tests/redis/docker-compose.yml):

  • redis: Ephemeral Redis 7.x (no AOF, no RDB saves)
  • test-runner: Python 3.12 container with service source code and test dependencies

Resource limits: --cpus=2 --memory=512m per container.

Run command:

sudo docker compose -f tests/redis/docker-compose.yml up --build \
  --abort-on-container-exit --exit-code-from test-runner

Test Isolation

  • Each test gets a clean database via flushdb() before and after
  • Stream consumer groups use UUID-based names per test to avoid collisions
  • Service state objects (RedisState, TickEngineState) injected with real Redis client, bypassing connect()
  • TickEngineState._db_pool mocked (PostgreSQL not needed for Redis-only tests)

Physics RedisState Roundtrips (test_physics_state.py)

Test Case Expected Behavior
Body set/get roundtrip All fields preserved including rotation_period, axial_tilt, prime_meridian_at_epoch
Ship set/get roundtrip All 25+ fields preserved including attitude, angular_velocity, fuel, wheel_momentum
Batch pipeline write bodies Multiple bodies stored atomically via pipeline
Batch pipeline read bodies All bodies retrieved via pipeline scan
Ship delete Ship removed, get returns None
Clear all ships All ships removed, spawn counter reset to 0
Spawn counter increment Counter increments correctly, concurrent increments safe
Control update (thrust) Thrust level updated on existing ship
Control update (rotation) Rotation input vector updated on existing ship
Control update nonexistent ship Returns False for missing ship
Attitude hold enable Sets attitude_hold=true, mode=hold, clears rotation input
Attitude hold disable Sets attitude_hold=false, mode=none
Attitude mode with target Mode and target quaternion updated correctly
Float precision at astronomical scale 1.496e11 survives roundtrip within float64 precision
Float precision at small scale 0.001 survives roundtrip exactly
Scan isolation body:, ship:, and station:* keys don’t cross-contaminate
Station set/get roundtrip All 14 fields preserved including attitude and parent_body
Station batch pipeline write Multiple stations stored atomically via pipeline
Station batch pipeline read All stations retrieved via pipeline scan
Station delete Station removed, get returns None
Clear all stations All stations removed, count returned

Tick Engine State Roundtrips (test_tick_engine_state.py)

Test Case Expected Behavior
Tick get/set roundtrip Integer tick value preserved
Tick increment Atomically increments and returns new value
Game time get/set roundtrip ISO datetime preserved with timezone
Tick rate get/set roundtrip Float rate preserved
Time scale get/set roundtrip Float scale preserved
Paused get/set roundtrip Boolean serialized as “true”/”false”
Time sync get/set roundtrip Boolean serialized as “true”/”false”
Time drift get/set roundtrip Float drift preserved
Initialize game state (fresh) All keys created, returns True
Initialize game state (existing) No overwrite, returns False
Reset game state All game keys deleted
Conserved quantities set/get Energy and momentum values preserved
Clear conserved quantities All conservation keys deleted
Default values when empty Returns defaults (0, current time, settings defaults)

Stream Publish/Consume (test_streams.py)

Test Case Expected Behavior
Publish ship.spawned Message appears in galaxy:ships stream with correct fields
Publish ship.removed Message appears in galaxy:ships stream with correct fields
Publish tick.completed Message appears in galaxy:tick stream with all fields
Publish tick.paused Message appears in galaxy:tick stream
Publish tick.resumed Message appears in galaxy:tick stream
Publish station.spawned Message appears in galaxy:stations stream with correct fields
Publish station.removed Message appears in galaxy:stations stream with correct fields
Publish automation.triggered Message appears in galaxy:automations stream with correct fields
Consumer group creation Group created successfully, BUSYGROUP handled idempotently
Consumer group reads new messages Reading with “>” returns only new messages
Acknowledge and pending recovery Unacked messages appear in pending; acked messages don’t
Message ordering Messages read in publication order
Stream field types All field values are strings (Redis stream requirement)

Cross-Service Data Contracts (test_cross_service.py)

Test Case Expected Behavior
Physics body → tick-engine snapshot read Body written by physics RedisState readable by pipeline hgetall
Physics ship → tick-engine snapshot read Ship written by physics RedisState readable by pipeline hgetall
Physics ship event → gateway consumer ship.spawned fields match gateway expectations
Tick-engine tick event → gateway consumer tick.completed fields match gateway expectations
Tick-engine game config → gateway read Game config keys readable by any service
Key naming: body:{name} Bodies stored at body:{name} keys
Key naming: ship:{id} Ships stored at ship:{id} keys
Key naming: game:* Game state stored at game:* keys
Key naming: station:{id} Stations stored at station:{id} keys
Key naming: maneuver:{id} Active maneuvers stored at maneuver:{ship_id} keys
Key naming: automation:{id}:* Automation rules at automation:{ship_id}:{rule_id} keys
Physics station → gateway broadcast Station written by physics readable by gateway
Physics station event → gateway consumer station.spawned fields match gateway expectations
Tick-engine automation event → gateway automation.triggered fields match gateway expectations

Pipeline Behavior (test_pipeline.py)

Test Case Expected Behavior
Batch write atomicity 10 bodies in pipeline all exist after execute
Batch read via pipeline Write individually, read all via pipeline
Large pipeline (100 items) 100 items written and read back correctly
Concurrent coroutine access Two async writers don’t interfere
Empty pipeline No error on empty pipeline execute

Rendezvous Verification Test Suite

Live integration tests that run rendezvous maneuvers inside the running game cluster. Located at scripts/run-test.py, executed inside the tick-engine pod.

Ship Pool

Each test gets a dedicated chaser + target ship pair with deterministic UUIDs (uuid5(NAMESPACE_DNS, "galaxy-test.{test_name}.{role}")). Ship class properties (dry_mass, fuel_capacity) are written to Redis so the automation engine reads correct values.

Test Chaser Class Target Class Scenario
t1_hohmann fast_frigate fast_frigate Earth coplanar 400km, Hohmann
t1_fast fast_frigate fast_frigate Earth coplanar 400km, fast
t1_express fast_frigate fast_frigate Earth coplanar 400km, express
t2_cargo cargo_hauler fast_frigate Earth cargo hauler 400km
t3_altitude fast_frigate fast_frigate Earth 400km to 35000km
t5_inclined fast_frigate fast_frigate Earth 15deg plane change
t6_luna fast_frigate fast_frigate Luna 100km to 500km
t7_mars cargo_hauler fast_frigate Mars cargo hauler descent
t8_jupiter fast_frigate fast_frigate Jupiter 100Mm to 120Mm

Commands

Command Description
run-test.py list List all tests with ship IDs
run-test.py setup <name> Set up single test (game must be paused)
run-test.py monitor <name> Monitor single test until complete/timeout
run-test.py run <name> Setup + monitor single test
run-test.py run-all Setup all tests, one RestoreBodies call, monitor all in parallel with threaded monitoring, print summary table

Parallel Execution (run-all)

  1. Sets up all 9 tests with skip_restore=True (no per-test RestoreBodies)
  2. Single RestoreBodies call syncs all 18 ships at once
  3. Spawns one monitoring thread per test (each with its own Redis connection)
  4. Prints interleaved progress with [test_name] prefix
  5. Prints summary table with PASS/FAIL, elapsed time, fuel remaining, and phases

Continuous Integration

  • All tests run on every pull request
  • Coverage report generated and must meet targets
  • Physics validation tests run nightly (longer duration)
  • Performance benchmarks run weekly with regression alerts

Back to top

Galaxy — Kubernetes-based multiplayer space game

This site uses Just the Docs, a documentation theme for Jekyll.