forked from Rockachopa/Timmy-time-dashboard
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
368 lines
13 KiB
Python
368 lines
13 KiB
Python
"""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
|