fix(performance): use simple height estimation instead of PIL rendering

- Replace estimate_block_height (PIL-based) with estimate_simple_height (word wrap)
- Update viewport filter tests to match new height-based filtering (~4 items vs 24)
- Fix CI task duplication in mise.toml (remove redundant depends)

Closes #38
Closes #36
This commit is contained in:
2026-03-18 22:33:36 -07:00
parent abe49ba7d7
commit c57617bb3d
26 changed files with 3938 additions and 1956 deletions

View File

@@ -160,3 +160,236 @@ class TestWebSocketDisplayUnavailable:
"""show does nothing when websockets unavailable."""
display = WebSocketDisplay()
display.show(["line1", "line2"])
class TestWebSocketUIPanelIntegration:
"""Tests for WebSocket-UIPanel integration for remote control."""
def test_set_controller_stores_controller(self):
"""set_controller stores the controller reference."""
with patch("engine.display.backends.websocket.websockets", MagicMock()):
display = WebSocketDisplay()
mock_controller = MagicMock()
display.set_controller(mock_controller)
assert display._controller is mock_controller
def test_set_command_callback_stores_callback(self):
"""set_command_callback stores the callback."""
with patch("engine.display.backends.websocket.websockets", MagicMock()):
display = WebSocketDisplay()
callback = MagicMock()
display.set_command_callback(callback)
assert display._command_callback is callback
def test_get_state_snapshot_returns_none_without_controller(self):
"""_get_state_snapshot returns None when no controller is set."""
with patch("engine.display.backends.websocket.websockets", MagicMock()):
display = WebSocketDisplay()
assert display._get_state_snapshot() is None
def test_get_state_snapshot_returns_controller_state(self):
"""_get_state_snapshot returns state from controller."""
with patch("engine.display.backends.websocket.websockets", MagicMock()):
display = WebSocketDisplay()
# Create mock controller with expected attributes
mock_controller = MagicMock()
mock_controller.stages = {
"test_stage": MagicMock(
enabled=True, params={"intensity": 0.5}, selected=False
)
}
mock_controller._current_preset = "demo"
mock_controller._presets = ["demo", "test"]
mock_controller.selected_stage = "test_stage"
display.set_controller(mock_controller)
state = display._get_state_snapshot()
assert state is not None
assert "stages" in state
assert "test_stage" in state["stages"]
assert state["stages"]["test_stage"]["enabled"] is True
assert state["stages"]["test_stage"]["params"] == {"intensity": 0.5}
assert state["preset"] == "demo"
assert state["presets"] == ["demo", "test"]
assert state["selected_stage"] == "test_stage"
def test_get_state_snapshot_handles_missing_attributes(self):
"""_get_state_snapshot handles controller without all attributes."""
with patch("engine.display.backends.websocket.websockets", MagicMock()):
display = WebSocketDisplay()
# Create mock controller without stages attribute using spec
# This prevents MagicMock from auto-creating the attribute
mock_controller = MagicMock(spec=[]) # Empty spec means no attributes
display.set_controller(mock_controller)
state = display._get_state_snapshot()
assert state == {}
def test_broadcast_state_sends_to_clients(self):
"""broadcast_state sends state update to all connected clients."""
with patch("engine.display.backends.websocket.websockets", MagicMock()):
display = WebSocketDisplay()
# Mock client with send method
mock_client = MagicMock()
mock_client.send = MagicMock()
display._clients.add(mock_client)
test_state = {"test": "state"}
display.broadcast_state(test_state)
# Verify send was called with JSON containing state
mock_client.send.assert_called_once()
call_args = mock_client.send.call_args[0][0]
assert '"type": "state"' in call_args
assert '"test"' in call_args
def test_broadcast_state_noop_when_no_clients(self):
"""broadcast_state does nothing when no clients connected."""
with patch("engine.display.backends.websocket.websockets", MagicMock()):
display = WebSocketDisplay()
display._clients.clear()
# Should not raise error
display.broadcast_state({"test": "state"})
class TestWebSocketHTTPServerPath:
"""Tests for WebSocket HTTP server client directory path calculation."""
def test_client_dir_path_calculation(self):
"""Client directory path is correctly calculated from websocket.py location."""
import os
# Use the actual websocket.py file location, not the test file
websocket_module = __import__(
"engine.display.backends.websocket", fromlist=["WebSocketDisplay"]
)
websocket_file = websocket_module.__file__
parts = websocket_file.split(os.sep)
if "engine" in parts:
engine_idx = parts.index("engine")
project_root = os.sep.join(parts[:engine_idx])
client_dir = os.path.join(project_root, "client")
else:
# Fallback calculation (shouldn't happen in normal test runs)
client_dir = os.path.join(
os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(websocket_file)))
),
"client",
)
# Verify the client directory exists and contains expected files
assert os.path.exists(client_dir), f"Client directory not found: {client_dir}"
assert "index.html" in os.listdir(client_dir), (
"index.html not found in client directory"
)
assert "editor.html" in os.listdir(client_dir), (
"editor.html not found in client directory"
)
# Verify the path is correct (should be .../Mainline/client)
assert client_dir.endswith("client"), (
f"Client dir should end with 'client': {client_dir}"
)
assert "Mainline" in client_dir, (
f"Client dir should contain 'Mainline': {client_dir}"
)
def test_http_server_directory_serves_client_files(self):
"""HTTP server directory correctly serves client files."""
import os
# Use the actual websocket.py file location, not the test file
websocket_module = __import__(
"engine.display.backends.websocket", fromlist=["WebSocketDisplay"]
)
websocket_file = websocket_module.__file__
parts = websocket_file.split(os.sep)
if "engine" in parts:
engine_idx = parts.index("engine")
project_root = os.sep.join(parts[:engine_idx])
client_dir = os.path.join(project_root, "client")
else:
client_dir = os.path.join(
os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(websocket_file)))
),
"client",
)
# Verify the handler would be able to serve files from this directory
# We can't actually instantiate the handler without a valid request,
# but we can verify the directory is accessible
assert os.access(client_dir, os.R_OK), (
f"Client directory not readable: {client_dir}"
)
# Verify key files exist
index_path = os.path.join(client_dir, "index.html")
editor_path = os.path.join(client_dir, "editor.html")
assert os.path.exists(index_path), f"index.html not found at: {index_path}"
assert os.path.exists(editor_path), f"editor.html not found at: {editor_path}"
# Verify files are readable
assert os.access(index_path, os.R_OK), "index.html not readable"
assert os.access(editor_path, os.R_OK), "editor.html not readable"
def test_old_buggy_path_does_not_find_client_directory(self):
"""The old buggy path (3 dirname calls) should NOT find the client directory.
This test verifies that the old buggy behavior would have failed.
The old code used:
client_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "client"
)
This would resolve to: .../engine/client (which doesn't exist)
Instead of: .../Mainline/client (which does exist)
"""
import os
# Use the actual websocket.py file location
websocket_module = __import__(
"engine.display.backends.websocket", fromlist=["WebSocketDisplay"]
)
websocket_file = websocket_module.__file__
# OLD BUGGY CODE: 3 dirname calls
old_buggy_client_dir = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(websocket_file))), "client"
)
# This path should NOT exist (it's the buggy path)
assert not os.path.exists(old_buggy_client_dir), (
f"Old buggy path should not exist: {old_buggy_client_dir}\n"
f"If this assertion fails, the bug may have been fixed elsewhere or "
f"the test needs updating."
)
# The buggy path should be .../engine/client, not .../Mainline/client
assert old_buggy_client_dir.endswith("engine/client"), (
f"Old buggy path should end with 'engine/client': {old_buggy_client_dir}"
)
# Verify that going up one more level (4 dirname calls) finds the correct path
correct_client_dir = os.path.join(
os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(websocket_file)))
),
"client",
)
assert os.path.exists(correct_client_dir), (
f"Correct path should exist: {correct_client_dir}"
)
assert "index.html" in os.listdir(correct_client_dir), (
f"index.html should exist in correct path: {correct_client_dir}"
)