0c7a12a3fa
Phase 5 of any_type_componentization_20260621 changed
WebSocketServer.broadcast(channel, payload) -> broadcast(message: WebSocketMessage)
but did not update internal callers in src/app_controller.py + src/events.py.
This adds 4 tests that pin the contract:
- test_websocket_server_broadcast_signature: asserts (self, message) signature
- test_websocket_server_broadcast_rejects_legacy_2arg_call: asserts legacy raises TypeError
- test_websocket_server_broadcast_accepts_websocket_message_instance: smoke test
- test_internal_callers_use_websocket_message_signature: structural grep over src/
The 4th test currently FAILS (red phase), identifying 2 legacy sites:
- src/app_controller.py:1849: self.event_queue.websocket_server.broadcast('telemetry', metrics)
- src/events.py:115: self.websocket_server.broadcast('events', {...})
The structural assertion is reused by code_path_audit_20260607.
70 lines
2.5 KiB
Python
70 lines
2.5 KiB
Python
"""Regression test for the WebSocketServer.broadcast() runtime TypeError bug.
|
|
|
|
Phase 5 of any_type_componentization_20260621 changed
|
|
WebSocketServer.broadcast(channel, payload) -> broadcast(message: WebSocketMessage)
|
|
but did not update internal callers in src/app_controller.py + src/events.py.
|
|
This produced worker[queue_fallback] TypeError spam on the GUI thread.
|
|
|
|
This test catches the regression and is reused by code_path_audit_20260607
|
|
as a structural assertion.
|
|
|
|
CONVENTION: 1-space indentation. NO COMMENTS.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from src.api_hooks import WebSocketMessage, WebSocketServer
|
|
|
|
|
|
class _MockApp:
|
|
test_hooks_enabled: bool = True
|
|
|
|
|
|
def _make_server() -> WebSocketServer:
|
|
return WebSocketServer(_MockApp(), port=9001)
|
|
|
|
|
|
def test_websocket_server_broadcast_signature() -> None:
|
|
"""WebSocketServer.broadcast must accept a single WebSocketMessage argument (self + message)."""
|
|
sig = inspect.signature(WebSocketServer.broadcast)
|
|
params = list(sig.parameters.keys())
|
|
assert len(params) == 2, f"expected 2 params (self + message), got {len(params)}: {params}"
|
|
|
|
|
|
def test_websocket_server_broadcast_rejects_legacy_2arg_call() -> None:
|
|
"""Calling broadcast with 2 positional args (legacy signature) must raise TypeError."""
|
|
server = _make_server()
|
|
raised = False
|
|
try:
|
|
server.broadcast("channel", {"key": "value"})
|
|
except TypeError:
|
|
raised = True
|
|
assert raised, "broadcast should reject legacy 2-arg call"
|
|
|
|
|
|
def test_websocket_server_broadcast_accepts_websocket_message_instance() -> None:
|
|
"""The new signature accepts a WebSocketMessage instance (no-op when not started)."""
|
|
server = _make_server()
|
|
msg = WebSocketMessage(channel="test", payload={"key": "value"})
|
|
server.broadcast(msg)
|
|
|
|
|
|
def test_internal_callers_use_websocket_message_signature() -> None:
|
|
"""Grep all internal callers of broadcast() in src/ and assert they use the new signature."""
|
|
src_root = Path(__file__).resolve().parents[1] / "src"
|
|
legacy_sites: list[str] = []
|
|
for py_file in src_root.rglob("*.py"):
|
|
text = py_file.read_text(encoding="utf-8")
|
|
for lineno, line in enumerate(text.splitlines(), start=1):
|
|
if ".broadcast(" not in line:
|
|
continue
|
|
if "WebSocketMessage(" in line:
|
|
continue
|
|
if 'broadcast("' not in line and "broadcast('" not in line:
|
|
continue
|
|
rel = py_file.relative_to(src_root.parent)
|
|
legacy_sites.append(f"{rel}:{lineno}: {line.strip()}")
|
|
assert not legacy_sites, "legacy broadcast() callers found:\n" + "\n".join(legacy_sites) |