"""Tests for infrastructure.visitor — visitor state tracking.""" from unittest.mock import patch from infrastructure.visitor import VisitorRegistry, VisitorState # --------------------------------------------------------------------------- # VisitorState dataclass tests # --------------------------------------------------------------------------- class TestVisitorState: """Tests for the VisitorState dataclass.""" def test_defaults(self): """VisitorState has correct defaults when only visitor_id is provided.""" v = VisitorState(visitor_id="v1") assert v.visitor_id == "v1" assert v.display_name == "v1" # Defaults to visitor_id assert v.position == {"x": 0.0, "y": 0.0, "z": 0.0} assert v.rotation == 0.0 assert "T" in v.connected_at # ISO format check def test_custom_values(self): """VisitorState accepts custom values for all fields.""" v = VisitorState( visitor_id="v2", display_name="Alice", position={"x": 1.0, "y": 2.0, "z": 3.0}, rotation=90.0, connected_at="2026-03-21T12:00:00Z", ) assert v.visitor_id == "v2" assert v.display_name == "Alice" assert v.position == {"x": 1.0, "y": 2.0, "z": 3.0} assert v.rotation == 90.0 assert v.connected_at == "2026-03-21T12:00:00Z" def test_display_name_defaults_to_visitor_id(self): """Empty display_name falls back to visitor_id.""" v = VisitorState(visitor_id="charlie", display_name="") assert v.display_name == "charlie" def test_position_is_copied_not_shared(self): """Each VisitorState has its own position dict.""" pos = {"x": 1.0, "y": 2.0, "z": 3.0} v1 = VisitorState(visitor_id="v1", position=pos) v2 = VisitorState(visitor_id="v2", position=pos) v1.position["x"] = 99.0 assert v2.position["x"] == 1.0 # v2 unchanged # --------------------------------------------------------------------------- # VisitorRegistry singleton tests # --------------------------------------------------------------------------- class TestVisitorRegistrySingleton: """Tests for the VisitorRegistry singleton behavior.""" def setup_method(self): """Clear registry before each test.""" VisitorRegistry._instance = None def teardown_method(self): """Clean up after each test.""" VisitorRegistry._instance = None def test_singleton_returns_same_instance(self): """Multiple calls return the same registry object.""" r1 = VisitorRegistry() r2 = VisitorRegistry() assert r1 is r2 def test_singleton_shares_state(self): """State is shared across all references to the singleton.""" r1 = VisitorRegistry() r1.add("v1") r2 = VisitorRegistry() assert len(r2) == 1 assert r2.get("v1") is not None # --------------------------------------------------------------------------- # VisitorRegistry.add tests # --------------------------------------------------------------------------- class TestVisitorRegistryAdd: """Tests for VisitorRegistry.add().""" def setup_method(self): """Clear registry before each test.""" VisitorRegistry._instance = None self.registry = VisitorRegistry() def teardown_method(self): """Clean up after each test.""" VisitorRegistry._instance = None def test_add_returns_visitor_state(self): """add() returns the created VisitorState.""" result = self.registry.add("v1") assert isinstance(result, VisitorState) assert result.visitor_id == "v1" def test_add_with_display_name(self): """add() accepts a custom display name.""" result = self.registry.add("v1", display_name="Alice") assert result.display_name == "Alice" def test_add_with_position(self): """add() accepts an initial position.""" pos = {"x": 10.0, "y": 20.0, "z": 30.0} result = self.registry.add("v1", position=pos) assert result.position == pos def test_add_increases_count(self): """Each add increases the registry size.""" assert len(self.registry) == 0 self.registry.add("v1") assert len(self.registry) == 1 self.registry.add("v2") assert len(self.registry) == 2 # --------------------------------------------------------------------------- # VisitorRegistry.remove tests # --------------------------------------------------------------------------- class TestVisitorRegistryRemove: """Tests for VisitorRegistry.remove().""" def setup_method(self): """Clear registry and add test visitors.""" VisitorRegistry._instance = None self.registry = VisitorRegistry() self.registry.add("v1") self.registry.add("v2") def teardown_method(self): """Clean up after each test.""" VisitorRegistry._instance = None def test_remove_existing_returns_true(self): """Removing an existing visitor returns True.""" result = self.registry.remove("v1") assert result is True assert len(self.registry) == 1 def test_remove_nonexistent_returns_false(self): """Removing a non-existent visitor returns False.""" result = self.registry.remove("unknown") assert result is False assert len(self.registry) == 2 def test_removes_correct_visitor(self): """remove() only removes the specified visitor.""" self.registry.remove("v1") assert self.registry.get("v1") is None assert self.registry.get("v2") is not None # --------------------------------------------------------------------------- # VisitorRegistry.update_position tests # --------------------------------------------------------------------------- class TestVisitorRegistryUpdatePosition: """Tests for VisitorRegistry.update_position().""" def setup_method(self): """Clear registry and add test visitor.""" VisitorRegistry._instance = None self.registry = VisitorRegistry() self.registry.add("v1", position={"x": 0.0, "y": 0.0, "z": 0.0}) def teardown_method(self): """Clean up after each test.""" VisitorRegistry._instance = None def test_update_position_returns_true(self): """update_position returns True for existing visitor.""" result = self.registry.update_position("v1", {"x": 1.0, "y": 2.0, "z": 3.0}) assert result is True def test_update_position_returns_false_for_unknown(self): """update_position returns False for non-existent visitor.""" result = self.registry.update_position("unknown", {"x": 1.0, "y": 2.0, "z": 3.0}) assert result is False def test_update_position_changes_values(self): """update_position updates the stored position.""" new_pos = {"x": 10.0, "y": 20.0, "z": 30.0} self.registry.update_position("v1", new_pos) visitor = self.registry.get("v1") assert visitor.position == new_pos def test_update_position_with_rotation(self): """update_position can also update rotation.""" self.registry.update_position("v1", {"x": 1.0, "y": 0.0, "z": 0.0}, rotation=180.0) visitor = self.registry.get("v1") assert visitor.rotation == 180.0 def test_update_position_without_rotation_preserves_it(self): """Calling update_position without rotation preserves existing rotation.""" self.registry.update_position("v1", {"x": 1.0, "y": 0.0, "z": 0.0}, rotation=90.0) self.registry.update_position("v1", {"x": 2.0, "y": 0.0, "z": 0.0}) visitor = self.registry.get("v1") assert visitor.rotation == 90.0 # --------------------------------------------------------------------------- # VisitorRegistry.get tests # --------------------------------------------------------------------------- class TestVisitorRegistryGet: """Tests for VisitorRegistry.get().""" def setup_method(self): """Clear registry and add test visitor.""" VisitorRegistry._instance = None self.registry = VisitorRegistry() self.registry.add("v1", display_name="Alice") def teardown_method(self): """Clean up after each test.""" VisitorRegistry._instance = None def test_get_existing_returns_visitor(self): """get() returns VisitorState for existing visitor.""" result = self.registry.get("v1") assert isinstance(result, VisitorState) assert result.visitor_id == "v1" assert result.display_name == "Alice" def test_get_nonexistent_returns_none(self): """get() returns None for non-existent visitor.""" result = self.registry.get("unknown") assert result is None # --------------------------------------------------------------------------- # VisitorRegistry.get_all tests # --------------------------------------------------------------------------- class TestVisitorRegistryGetAll: """Tests for VisitorRegistry.get_all() — Matrix protocol format.""" def setup_method(self): """Clear registry and add test visitors.""" VisitorRegistry._instance = None self.registry = VisitorRegistry() self.registry.add("v1", display_name="Alice", position={"x": 1.0, "y": 2.0, "z": 3.0}) self.registry.add("v2", display_name="Bob", position={"x": 4.0, "y": 5.0, "z": 6.0}) def teardown_method(self): """Clean up after each test.""" VisitorRegistry._instance = None def test_get_all_returns_list(self): """get_all() returns a list.""" result = self.registry.get_all() assert isinstance(result, list) assert len(result) == 2 def test_get_all_format_has_required_fields(self): """Each entry has type, visitor_id, data, and ts.""" result = self.registry.get_all() for entry in result: assert "type" in entry assert "visitor_id" in entry assert "data" in entry assert "ts" in entry def test_get_all_type_is_visitor_state(self): """The type field is 'visitor_state'.""" result = self.registry.get_all() assert all(entry["type"] == "visitor_state" for entry in result) def test_get_all_data_has_required_fields(self): """data dict contains display_name, position, rotation, connected_at.""" result = self.registry.get_all() for entry in result: data = entry["data"] assert "display_name" in data assert "position" in data assert "rotation" in data assert "connected_at" in data def test_get_all_position_is_dict(self): """position within data is a dict with x, y, z.""" result = self.registry.get_all() for entry in result: pos = entry["data"]["position"] assert isinstance(pos, dict) assert "x" in pos assert "y" in pos assert "z" in pos def test_get_all_ts_is_unix_timestamp(self): """ts is an integer Unix timestamp.""" result = self.registry.get_all() for entry in result: assert isinstance(entry["ts"], int) assert entry["ts"] > 0 @patch("infrastructure.visitor.time") def test_get_all_uses_current_time(self, mock_time): """ts is set from time.time().""" mock_time.time.return_value = 1742529600 result = self.registry.get_all() assert all(entry["ts"] == 1742529600 for entry in result) def test_get_all_empty_registry(self): """get_all() returns empty list when no visitors.""" self.registry.clear() result = self.registry.get_all() assert result == [] # --------------------------------------------------------------------------- # VisitorRegistry.clear tests # --------------------------------------------------------------------------- class TestVisitorRegistryClear: """Tests for VisitorRegistry.clear().""" def setup_method(self): """Clear registry and add test visitors.""" VisitorRegistry._instance = None self.registry = VisitorRegistry() self.registry.add("v1") self.registry.add("v2") self.registry.add("v3") def teardown_method(self): """Clean up after each test.""" VisitorRegistry._instance = None def test_clear_removes_all_visitors(self): """clear() removes all visitors from the registry.""" assert len(self.registry) == 3 self.registry.clear() assert len(self.registry) == 0 def test_clear_allows_readding(self): """Visitors can be re-added after clear().""" self.registry.clear() self.registry.add("v1") assert len(self.registry) == 1