Private
Public Access
Revert "merge: tier2/phase2_4_5_call_site_completion_20260621 (parent + follow-up + Phase 6e analysis)"
This reverts commitf914b2bcd4, reversing changes made to7fef95cc87.
This commit is contained in:
@@ -26,10 +26,10 @@ def caps() -> VendorCapabilities:
|
||||
return VendorCapabilities(vendor="test", model="test-model", tool_calling=True, context_window=8192)
|
||||
|
||||
def _make_normalized_response(text: str = "ok", tool_calls: list[dict[str, Any]] | None = None) -> Result[NormalizedResponse]:
|
||||
from src.openai_schemas import UsageStats
|
||||
return Result(data=NormalizedResponse(
|
||||
text=text, tool_calls=tool_calls or (),
|
||||
usage=UsageStats(input_tokens=10, output_tokens=5),
|
||||
text=text, tool_calls=tool_calls or [],
|
||||
usage_input_tokens=10, usage_output_tokens=5,
|
||||
usage_cache_read_tokens=0, usage_cache_creation_tokens=0,
|
||||
raw_response=None,
|
||||
))
|
||||
|
||||
|
||||
@@ -13,10 +13,10 @@ from src.result_types import Result
|
||||
from src.vendor_capabilities import VendorCapabilities
|
||||
|
||||
def _make_normalized_response(text: str = "ok", tool_calls: list[dict[str, Any]] | None = None) -> NormalizedResponse:
|
||||
from src.openai_schemas import UsageStats
|
||||
return NormalizedResponse(
|
||||
text=text, tool_calls=tool_calls or (),
|
||||
usage=UsageStats(input_tokens=10, output_tokens=5),
|
||||
text=text, tool_calls=tool_calls or [],
|
||||
usage_input_tokens=10, usage_output_tokens=5,
|
||||
usage_cache_read_tokens=0, usage_cache_creation_tokens=0,
|
||||
raw_response=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ from src.ai_client import run_with_tool_loop
|
||||
from src.vendor_capabilities import VendorCapabilities
|
||||
|
||||
def _make_normalized_response(text: str = "ok", tool_calls: list[dict[str, Any]] | None = None) -> NormalizedResponse:
|
||||
from src.openai_schemas import UsageStats
|
||||
return NormalizedResponse(
|
||||
text=text, tool_calls=tool_calls or (),
|
||||
usage=UsageStats(input_tokens=10, output_tokens=5),
|
||||
text=text, tool_calls=tool_calls or [],
|
||||
usage_input_tokens=10, usage_output_tokens=5,
|
||||
usage_cache_read_tokens=0, usage_cache_creation_tokens=0,
|
||||
raw_response=None,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
"""Tests for src/api_hooks.py WebSocketMessage + JsonValue usage
|
||||
|
||||
Phase 5 of any_type_componentization_20260621. Verifies:
|
||||
- WebSocketMessage dataclass (channel, payload: JsonValue)
|
||||
- WebSocketMessage is frozen=True
|
||||
- _serialize_for_api uses JsonValue type hint
|
||||
- broadcast() takes WebSocketMessage instead of (channel, payload)
|
||||
- _get_app_attr / _set_app_attr signatures UNCHANGED (Pattern 4 preserved)
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from src import api_hooks
|
||||
from src.type_aliases import JsonValue
|
||||
|
||||
|
||||
def test_websocket_message_construction() -> None:
|
||||
msg = api_hooks.WebSocketMessage(channel="status", payload={"status": "ok"})
|
||||
assert msg.channel == "status"
|
||||
assert msg.payload == {"status": "ok"}
|
||||
|
||||
|
||||
def test_websocket_message_with_list_payload() -> None:
|
||||
msg = api_hooks.WebSocketMessage(channel="events", payload=[{"type": "x"}, {"type": "y"}])
|
||||
assert msg.payload == [{"type": "x"}, {"type": "y"}]
|
||||
|
||||
|
||||
def test_websocket_message_with_nested_payload() -> None:
|
||||
msg = api_hooks.WebSocketMessage(
|
||||
channel="data",
|
||||
payload={"users": [{"name": "a", "meta": {"active": True}}], "count": 1}
|
||||
)
|
||||
assert msg.payload["count"] == 1
|
||||
assert msg.payload["users"][0]["meta"]["active"] is True
|
||||
|
||||
|
||||
def test_websocket_message_is_frozen() -> None:
|
||||
msg = api_hooks.WebSocketMessage(channel="x", payload={})
|
||||
with pytest.raises(Exception):
|
||||
msg.channel = "mutated"
|
||||
|
||||
|
||||
def test_websocket_message_to_json() -> None:
|
||||
msg = api_hooks.WebSocketMessage(channel="status", payload={"ok": True})
|
||||
j = json.dumps({"channel": msg.channel, "payload": msg.payload})
|
||||
assert json.loads(j) == {"channel": "status", "payload": {"ok": True}}
|
||||
|
||||
|
||||
def test_serialize_for_api_returns_dict_for_to_dict_object() -> None:
|
||||
class WithToDict:
|
||||
def to_dict(self) -> dict:
|
||||
return {"k": "v"}
|
||||
result = api_hooks._serialize_for_api(WithToDict())
|
||||
assert result == {"k": "v"}
|
||||
|
||||
|
||||
def test_serialize_for_api_handles_nested_lists() -> None:
|
||||
obj = {"items": [{"a": 1}, {"b": 2}]}
|
||||
result = api_hooks._serialize_for_api(obj)
|
||||
assert result == {"items": [{"a": 1}, {"b": 2}]}
|
||||
|
||||
|
||||
def test_serialize_for_api_handles_purepath() -> None:
|
||||
from pathlib import PurePath, PureWindowsPath
|
||||
p = PurePath("a/b/c") # Use a relative path to avoid Windows normalization
|
||||
result = api_hooks._serialize_for_api(p)
|
||||
assert isinstance(result, str)
|
||||
# Either forward or backslash separator; both are valid string representations
|
||||
assert result.replace("\\", "/") == "a/b/c"
|
||||
|
||||
|
||||
def test_serialize_for_api_passthrough_for_primitives() -> None:
|
||||
assert api_hooks._serialize_for_api(42) == 42
|
||||
assert api_hooks._serialize_for_api("hello") == "hello"
|
||||
assert api_hooks._serialize_for_api(None) is None
|
||||
|
||||
|
||||
def test_serialize_for_api_handles_mixed_nesting() -> None:
|
||||
obj = {"list": [1, 2, {"nested": "deep"}], "scalar": True}
|
||||
result = api_hooks._serialize_for_api(obj)
|
||||
assert result == obj
|
||||
|
||||
|
||||
def test_get_app_attr_signature_preserved() -> None:
|
||||
"""Pattern 4: _get_app_attr / _set_app_attr must NOT change signature."""
|
||||
import inspect
|
||||
sig = inspect.signature(api_hooks._get_app_attr)
|
||||
params = list(sig.parameters.keys())
|
||||
assert params == ["app", "name", "default"]
|
||||
|
||||
|
||||
def test_set_app_attr_signature_preserved() -> None:
|
||||
import inspect
|
||||
sig = inspect.signature(api_hooks._set_app_attr)
|
||||
params = list(sig.parameters.keys())
|
||||
assert params == ["app", "name", "value"]
|
||||
@@ -1,98 +0,0 @@
|
||||
"""Tests for scripts/audit_dataclass_coverage.py
|
||||
|
||||
The audit counts `dict[str, Any]` and `list[dict[...]]` annotations that
|
||||
remain outside the 5 promoted dataclass sites (mcp_tool_specs, openai_schemas,
|
||||
provider_state, log_registry.Session, api_hooks.WebSocketMessage).
|
||||
|
||||
Mirrors tests/test_audit_weak_types.py structure.
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
AUDIT_SCRIPT = REPO_ROOT / "scripts" / "audit_dataclass_coverage.py"
|
||||
BASELINE_FILE = REPO_ROOT / "scripts" / "audit_dataclass_coverage.baseline.json"
|
||||
|
||||
|
||||
def _run_audit(*args: str) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(AUDIT_SCRIPT), *args],
|
||||
cwd=str(REPO_ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
|
||||
def test_audit_script_exists() -> None:
|
||||
assert AUDIT_SCRIPT.is_file(), f"audit script missing: {AUDIT_SCRIPT}"
|
||||
|
||||
|
||||
def test_audit_help_runs() -> None:
|
||||
result = _run_audit("--help")
|
||||
assert result.returncode == 0
|
||||
assert "audit" in result.stdout.lower()
|
||||
|
||||
|
||||
def test_audit_json_mode_emits_valid_json() -> None:
|
||||
result = _run_audit("--json")
|
||||
assert result.returncode == 0, f"audit --json failed: {result.stderr}"
|
||||
payload = json.loads(result.stdout)
|
||||
assert "files_scanned" in payload
|
||||
assert "total_weak" in payload
|
||||
assert "by_category" in payload
|
||||
assert isinstance(payload["total_weak"], int)
|
||||
assert payload["total_weak"] >= 0
|
||||
|
||||
|
||||
def test_audit_default_mode_emits_human_report() -> None:
|
||||
result = _run_audit()
|
||||
assert result.returncode == 0, f"audit default mode failed: {result.stderr}"
|
||||
assert "Dataclass Coverage Audit" in result.stdout or "dataclass" in result.stdout.lower()
|
||||
|
||||
|
||||
def test_audit_strict_mode_against_existing_baseline_passes() -> None:
|
||||
if not BASELINE_FILE.is_file():
|
||||
pytest.skip("baseline not yet generated; skip --strict assertion")
|
||||
result = _run_audit("--strict", "--baseline", str(BASELINE_FILE))
|
||||
assert result.returncode == 0, (
|
||||
f"audit --strict failed (current count > baseline): {result.stderr}"
|
||||
)
|
||||
assert "STRICT OK" in result.stdout
|
||||
|
||||
|
||||
def test_audit_strict_mode_fails_when_baseline_is_zero() -> None:
|
||||
tmp_baseline = REPO_ROOT / "tests" / "artifacts" / "tier2_state" / "any_type_componentization_20260621" / "_zero_baseline.json"
|
||||
tmp_baseline.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_baseline.write_text(json.dumps({"total_weak": 0}), encoding="utf-8")
|
||||
try:
|
||||
result = _run_audit("--strict", "--baseline", str(tmp_baseline))
|
||||
assert result.returncode == 1, "audit --strict should fail when current > baseline=0"
|
||||
assert "STRICT" in result.stderr or "regression" in result.stderr.lower()
|
||||
finally:
|
||||
if tmp_baseline.exists():
|
||||
tmp_baseline.unlink()
|
||||
|
||||
|
||||
def test_audit_baseline_field_shape() -> None:
|
||||
result = _run_audit("--json")
|
||||
assert result.returncode == 0
|
||||
payload = json.loads(result.stdout)
|
||||
assert "total_weak" in payload
|
||||
assert "files_with_findings" in payload
|
||||
assert "by_category" in payload
|
||||
assert "by_file" in payload
|
||||
assert isinstance(payload["by_file"], list)
|
||||
if payload["by_file"]:
|
||||
entry = payload["by_file"][0]
|
||||
assert "filename" in entry
|
||||
assert "weak_count" in entry
|
||||
@@ -17,9 +17,7 @@ def test_auto_whitelist_keywords(registry_setup: LogRegistry) -> None:
|
||||
reg.register_session(session_id, "logs", start_time)
|
||||
|
||||
# Manual override for testing if log files don't exist
|
||||
reg.update_session_metadata(
|
||||
session_id, message_count=0, errors=0, size_kb=0, whitelisted=True, reason="manual override",
|
||||
)
|
||||
reg.data[session_id]["whitelisted"] = True
|
||||
assert reg.is_session_whitelisted(session_id) is True
|
||||
|
||||
def test_auto_whitelist_message_count(registry_setup: LogRegistry) -> None:
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
"""Tests for src/log_registry.py Session + SessionMetadata dataclasses
|
||||
|
||||
Phase 4 of any_type_componentization_20260621. Verifies:
|
||||
- Session dataclass (session_id, path, start_time, whitelisted, metadata)
|
||||
- SessionMetadata dataclass (message_count, errors, size_kb, whitelisted, reason, timestamp)
|
||||
- Session.from_dict() round-trip
|
||||
- Session.to_dict() preserves TOML-compatible shape
|
||||
- LogRegistry.data is now dict[str, Session] (typed)
|
||||
- LogRegistry.register_session() returns Session instance
|
||||
- LogRegistry.update_session_metadata() sets Session.metadata
|
||||
- LogRegistry.get_old_non_whitelisted_sessions() returns Session list
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from src.log_registry import (
|
||||
LogRegistry,
|
||||
Session,
|
||||
SessionMetadata,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tmp_registry(tmp_path) -> LogRegistry:
|
||||
path = tmp_path / "registry.toml"
|
||||
return LogRegistry(str(path))
|
||||
|
||||
|
||||
def test_session_dataclass_construction() -> None:
|
||||
s = Session(session_id="s1", path="/tmp/s1", start_time="2026-06-21T10:00:00")
|
||||
assert s.session_id == "s1"
|
||||
assert s.path == "/tmp/s1"
|
||||
assert s.start_time == "2026-06-21T10:00:00"
|
||||
assert s.whitelisted is False
|
||||
assert s.metadata is None
|
||||
|
||||
|
||||
def test_session_metadata_dataclass_construction() -> None:
|
||||
m = SessionMetadata(message_count=10, errors=2, size_kb=5)
|
||||
assert m.message_count == 10
|
||||
assert m.errors == 2
|
||||
assert m.size_kb == 5
|
||||
assert m.whitelisted is False
|
||||
assert m.reason == ""
|
||||
|
||||
|
||||
def test_session_from_dict_basic() -> None:
|
||||
d = {"path": "/x", "start_time": "2026-06-21T10:00:00", "whitelisted": False, "metadata": None}
|
||||
s = Session.from_dict("s1", d)
|
||||
assert s.session_id == "s1"
|
||||
assert s.path == "/x"
|
||||
assert s.start_time == "2026-06-21T10:00:00"
|
||||
assert s.whitelisted is False
|
||||
assert s.metadata is None
|
||||
|
||||
|
||||
def test_session_from_dict_with_metadata() -> None:
|
||||
d = {
|
||||
"path": "/x",
|
||||
"start_time": "2026-06-21T10:00:00",
|
||||
"whitelisted": True,
|
||||
"metadata": {"message_count": 100, "errors": 1, "size_kb": 20, "whitelisted": True, "reason": "high"},
|
||||
}
|
||||
s = Session.from_dict("s1", d)
|
||||
assert s.whitelisted is True
|
||||
assert s.metadata is not None
|
||||
assert s.metadata.message_count == 100
|
||||
assert s.metadata.reason == "high"
|
||||
|
||||
|
||||
def test_session_to_dict_round_trip() -> None:
|
||||
m = SessionMetadata(message_count=42, errors=0, size_kb=15, whitelisted=True, reason="high count")
|
||||
s = Session(session_id="s1", path="/x", start_time="2026-06-21T10:00:00", whitelisted=True, metadata=m)
|
||||
d = s.to_dict()
|
||||
assert d["path"] == "/x"
|
||||
assert d["start_time"] == "2026-06-21T10:00:00"
|
||||
assert d["whitelisted"] is True
|
||||
assert d["metadata"]["message_count"] == 42
|
||||
|
||||
|
||||
def test_session_metadata_to_dict() -> None:
|
||||
m = SessionMetadata(message_count=5, errors=1, size_kb=2)
|
||||
d = m.to_dict()
|
||||
assert d == {"message_count": 5, "errors": 1, "size_kb": 2, "whitelisted": False, "reason": "", "timestamp": None}
|
||||
|
||||
|
||||
def test_log_registry_data_is_typed() -> None:
|
||||
"""self.data is now dict[str, Session]."""
|
||||
registry = LogRegistry("/tmp/_test_registry_xyz.toml")
|
||||
assert isinstance(registry.data, dict)
|
||||
|
||||
|
||||
def test_log_registry_register_session_returns_session(tmp_registry: LogRegistry) -> None:
|
||||
tmp_registry.register_session("s1", "/tmp/s1", "2026-06-21T10:00:00")
|
||||
s = tmp_registry.data["s1"]
|
||||
assert isinstance(s, Session)
|
||||
assert s.session_id == "s1"
|
||||
assert s.path == "/tmp/s1"
|
||||
assert s.start_time == "2026-06-21T10:00:00"
|
||||
assert s.whitelisted is False
|
||||
|
||||
|
||||
def test_log_registry_update_session_metadata_sets_metadata(tmp_registry: LogRegistry) -> None:
|
||||
tmp_registry.register_session("s1", "/tmp/s1", "2026-06-21T10:00:00")
|
||||
tmp_registry.update_session_metadata("s1", message_count=10, errors=2, size_kb=5, whitelisted=True, reason="test")
|
||||
s = tmp_registry.data["s1"]
|
||||
assert s.metadata is not None
|
||||
assert s.metadata.message_count == 10
|
||||
assert s.metadata.errors == 2
|
||||
assert s.whitelisted is True
|
||||
|
||||
|
||||
def test_log_registry_is_session_whitelisted(tmp_registry: LogRegistry) -> None:
|
||||
tmp_registry.register_session("s1", "/tmp/s1", "2026-06-21T10:00:00")
|
||||
assert tmp_registry.is_session_whitelisted("s1") is False
|
||||
tmp_registry.update_session_metadata("s1", 10, 0, 5, True, "test")
|
||||
assert tmp_registry.is_session_whitelisted("s1") is True
|
||||
|
||||
|
||||
def test_log_registry_get_old_non_whitelisted_sessions(tmp_registry: LogRegistry) -> None:
|
||||
cutoff = datetime(2026, 6, 1)
|
||||
old_start = "2026-05-01T10:00:00"
|
||||
recent_start = "2026-06-21T10:00:00"
|
||||
tmp_registry.register_session("old", "/tmp/old", old_start)
|
||||
tmp_registry.register_session("recent", "/tmp/recent", recent_start)
|
||||
# Update metadata so neither session is "empty" (otherwise both would be flagged as old)
|
||||
tmp_registry.update_session_metadata("old", 10, 0, 5, False, "test")
|
||||
tmp_registry.update_session_metadata("recent", 10, 0, 5, False, "test")
|
||||
old_sessions = tmp_registry.get_old_non_whitelisted_sessions(cutoff)
|
||||
assert any(s["session_id"] == "old" for s in old_sessions)
|
||||
assert not any(s["session_id"] == "recent" for s in old_sessions)
|
||||
|
||||
|
||||
def test_session_is_frozen() -> None:
|
||||
s = Session(session_id="s1", path="/x", start_time="2026-06-21T10:00:00")
|
||||
with pytest.raises(Exception):
|
||||
s.path = "mutated"
|
||||
|
||||
|
||||
def test_session_metadata_is_frozen() -> None:
|
||||
m = SessionMetadata(message_count=10)
|
||||
with pytest.raises(Exception):
|
||||
m.message_count = 999
|
||||
@@ -1,123 +0,0 @@
|
||||
"""Tests for src/mcp_tool_specs.py
|
||||
|
||||
Phase 1 of any_type_componentization_20260621. Verifies:
|
||||
- 45 ToolSpec instances are registered
|
||||
- get_tool_spec(name) dispatches correctly
|
||||
- tool_names() returns the expected set
|
||||
- get_tool_schemas() returns the expected list
|
||||
- ToolParameter / ToolSpec dataclasses have correct frozen=True semantics
|
||||
- to_dict() round-trip preserves the legacy dict shape
|
||||
- Cross-module invariant: tool_names() == models.AGENT_TOOL_NAMES subset
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from src import mcp_tool_specs
|
||||
from src import models
|
||||
|
||||
|
||||
EXPECTED_TOOLS: set[str] = {
|
||||
'py_remove_def', 'py_add_def', 'py_move_def', 'py_region_wrap',
|
||||
'read_file', 'list_directory', 'search_files', 'get_file_summary',
|
||||
'py_get_skeleton', 'py_get_code_outline',
|
||||
'ts_c_get_skeleton', 'ts_cpp_get_skeleton',
|
||||
'ts_c_get_code_outline', 'ts_cpp_get_code_outline',
|
||||
'ts_c_get_definition', 'ts_cpp_get_definition',
|
||||
'ts_c_get_signature', 'ts_cpp_get_signature',
|
||||
'ts_c_update_definition', 'ts_cpp_update_definition',
|
||||
'get_file_slice', 'set_file_slice', 'edit_file',
|
||||
'py_get_definition', 'py_update_definition',
|
||||
'py_get_signature', 'py_set_signature',
|
||||
'py_get_class_summary', 'py_get_var_declaration', 'py_set_var_declaration',
|
||||
'get_git_diff', 'web_search', 'fetch_url', 'get_ui_performance',
|
||||
'py_find_usages', 'py_get_imports', 'py_check_syntax',
|
||||
'py_get_hierarchy', 'py_get_docstring', 'get_tree',
|
||||
'bd_create', 'bd_update', 'bd_list', 'bd_ready',
|
||||
'derive_code_path',
|
||||
}
|
||||
|
||||
|
||||
def test_module_loads_with_45_registrations() -> None:
|
||||
assert len(mcp_tool_specs._REGISTRY) == 45
|
||||
|
||||
|
||||
def test_tool_names_set_matches_expected_45() -> None:
|
||||
names = mcp_tool_specs.tool_names()
|
||||
assert len(names) == 45
|
||||
assert names == EXPECTED_TOOLS
|
||||
|
||||
|
||||
def test_get_tool_spec_returns_correct_instance() -> None:
|
||||
spec = mcp_tool_specs.get_tool_spec('py_remove_def')
|
||||
assert spec.name == 'py_remove_def'
|
||||
assert 'Excises' in spec.description or 'class or function' in spec.description
|
||||
assert len(spec.parameters) >= 2
|
||||
path_param = next((p for p in spec.parameters if p.name == 'path'), None)
|
||||
assert path_param is not None
|
||||
assert path_param.required is True
|
||||
assert path_param.type == 'string'
|
||||
|
||||
|
||||
def test_get_tool_spec_raises_for_unknown_name() -> None:
|
||||
with pytest.raises(KeyError):
|
||||
mcp_tool_specs.get_tool_spec('nonexistent_tool_xyz')
|
||||
|
||||
|
||||
def test_get_tool_schemas_returns_all_specs() -> None:
|
||||
schemas = mcp_tool_specs.get_tool_schemas()
|
||||
assert len(schemas) == 45
|
||||
assert all(isinstance(s, mcp_tool_specs.ToolSpec) for s in schemas)
|
||||
|
||||
|
||||
def test_tool_spec_is_frozen() -> None:
|
||||
spec = mcp_tool_specs.get_tool_spec('read_file')
|
||||
with pytest.raises(Exception):
|
||||
spec.name = 'mutated'
|
||||
|
||||
|
||||
def test_tool_parameter_is_frozen() -> None:
|
||||
spec = mcp_tool_specs.get_tool_spec('read_file')
|
||||
param = spec.parameters[0]
|
||||
with pytest.raises(Exception):
|
||||
param.name = 'mutated'
|
||||
|
||||
|
||||
def test_to_dict_round_trip_preserves_shape() -> None:
|
||||
spec = mcp_tool_specs.get_tool_spec('py_remove_def')
|
||||
d = spec.to_dict()
|
||||
assert d['name'] == 'py_remove_def'
|
||||
assert 'description' in d
|
||||
assert d['parameters']['type'] == 'object'
|
||||
assert 'path' in d['parameters']['properties']
|
||||
assert 'name' in d['parameters']['properties']
|
||||
assert 'path' in d['parameters']['required']
|
||||
assert 'name' in d['parameters']['required']
|
||||
|
||||
|
||||
def test_tool_parameter_to_dict_includes_enum() -> None:
|
||||
spec = mcp_tool_specs.get_tool_spec('py_add_def')
|
||||
anchor_param = next((p for p in spec.parameters if p.name == 'anchor_type'), None)
|
||||
assert anchor_param is not None
|
||||
assert anchor_param.enum is not None
|
||||
assert 'before' in anchor_param.enum
|
||||
d = anchor_param.to_dict()
|
||||
assert 'enum' in d
|
||||
assert 'before' in d['enum']
|
||||
|
||||
|
||||
def test_tool_names_subset_of_models_agent_tool_names() -> None:
|
||||
"""Cross-module invariant: every MCP tool is also an agent tool."""
|
||||
native_names = mcp_tool_specs.tool_names()
|
||||
agent_names = set(models.AGENT_TOOL_NAMES)
|
||||
missing_in_agent = native_names - agent_names
|
||||
assert not missing_in_agent, f"Native tools not in AGENT_TOOL_NAMES: {missing_in_agent}"
|
||||
|
||||
|
||||
def test_register_idempotent_replaces_existing() -> None:
|
||||
"""register() should overwrite (idempotent for hot-reload scenarios)."""
|
||||
from src.mcp_tool_specs import ToolSpec, ToolParameter, register
|
||||
custom = ToolSpec(name='read_file', description='custom', parameters=(ToolParameter(name='x', type='string', description='x'),))
|
||||
register(custom)
|
||||
assert mcp_tool_specs.get_tool_spec('read_file').description == 'custom'
|
||||
@@ -5,7 +5,6 @@ from src.openai_compatible import (
|
||||
OpenAICompatibleRequest,
|
||||
send_openai_compatible,
|
||||
)
|
||||
from src.openai_schemas import UsageStats
|
||||
from src.vendor_capabilities import VendorCapabilities, register
|
||||
|
||||
@pytest.fixture
|
||||
@@ -59,8 +58,8 @@ def test_tool_call_detection_in_blocking_response(caps: VendorCapabilities) -> N
|
||||
kwargs = {"model": "m", "messages": [{"role": "user", "content": "ping"}], "temperature": 0.0, "top_p": 1.0, "max_tokens": 8192, "stream": False}
|
||||
response = _send_blocking(client, kwargs)
|
||||
assert len(response.tool_calls) == 1
|
||||
assert response.tool_calls[0].function.name == "read_file"
|
||||
assert response.tool_calls[0].id == "call_1"
|
||||
assert response.tool_calls[0]["function"]["name"] == "read_file"
|
||||
assert response.tool_calls[0]["id"] == "call_1"
|
||||
|
||||
def test_vision_multimodal_message(caps: VendorCapabilities) -> None:
|
||||
client = MagicMock()
|
||||
@@ -85,6 +84,6 @@ def test_error_classification_429_to_rate_limit(caps: VendorCapabilities) -> Non
|
||||
|
||||
def test_normalized_response_is_frozen_dataclass() -> None:
|
||||
from dataclasses import FrozenInstanceError
|
||||
r = NormalizedResponse(text="x", tool_calls=(), usage=UsageStats(input_tokens=0, output_tokens=0), raw_response=None)
|
||||
r = NormalizedResponse(text="x", tool_calls=[], usage_input_tokens=0, usage_output_tokens=0, usage_cache_read_tokens=0, usage_cache_creation_tokens=0, raw_response=None)
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
r.text = "y"
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
"""Tests for src/openai_schemas.py
|
||||
|
||||
Phase 2 of any_type_componentization_20260621. Verifies:
|
||||
- ToolCall + ToolCallFunction round-trip via to_dict
|
||||
- ChatMessage round-trip for all 4 roles
|
||||
- UsageStats field access
|
||||
- NormalizedResponse legacy dict preservation
|
||||
- OpenAICompatibleRequest typed messages
|
||||
- raw_response remains Any (Pattern 3 preserved)
|
||||
- tools field stays list[dict[str, Any]] for cross-phase Phase 1 ToolSpec
|
||||
(deferred to follow-up track per spec 3.4)
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from src import openai_schemas
|
||||
|
||||
|
||||
def test_tool_call_function_construction() -> None:
|
||||
tcf = openai_schemas.ToolCallFunction(name="get_weather", arguments='{"city": "sf"}')
|
||||
assert tcf.name == "get_weather"
|
||||
assert tcf.arguments == '{"city": "sf"}'
|
||||
|
||||
|
||||
def test_tool_call_to_dict_round_trip() -> None:
|
||||
tc = openai_schemas.ToolCall(
|
||||
id="call_123",
|
||||
type="function",
|
||||
function=openai_schemas.ToolCallFunction(name="read_file", arguments='{"path": "/x.py"}'),
|
||||
)
|
||||
d = tc.to_dict()
|
||||
assert d["id"] == "call_123"
|
||||
assert d["type"] == "function"
|
||||
assert d["function"]["name"] == "read_file"
|
||||
assert d["function"]["arguments"] == '{"path": "/x.py"}'
|
||||
|
||||
|
||||
def test_tool_call_defaults() -> None:
|
||||
tc = openai_schemas.ToolCall(
|
||||
id="call_x",
|
||||
function=openai_schemas.ToolCallFunction(name="noop", arguments="{}"),
|
||||
)
|
||||
assert tc.type == "function"
|
||||
|
||||
|
||||
def test_tool_call_is_frozen() -> None:
|
||||
tc = openai_schemas.ToolCall(
|
||||
id="call_y",
|
||||
function=openai_schemas.ToolCallFunction(name="noop", arguments="{}"),
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
tc.id = "mutated"
|
||||
|
||||
|
||||
def test_chat_message_system_role() -> None:
|
||||
msg = openai_schemas.ChatMessage(role="system", content="You are a helper.")
|
||||
d = msg.to_dict()
|
||||
assert d["role"] == "system"
|
||||
assert d["content"] == "You are a helper."
|
||||
assert "tool_calls" not in d
|
||||
assert "tool_call_id" not in d
|
||||
|
||||
|
||||
def test_chat_message_user_role() -> None:
|
||||
msg = openai_schemas.ChatMessage(role="user", content="Hello")
|
||||
d = msg.to_dict()
|
||||
assert d["role"] == "user"
|
||||
assert d["content"] == "Hello"
|
||||
|
||||
|
||||
def test_chat_message_assistant_with_tool_calls() -> None:
|
||||
tc = openai_schemas.ToolCall(
|
||||
id="call_a",
|
||||
function=openai_schemas.ToolCallFunction(name="read_file", arguments='{"path": "/x"}'),
|
||||
)
|
||||
msg = openai_schemas.ChatMessage(role="assistant", content="", tool_calls=(tc,))
|
||||
d = msg.to_dict()
|
||||
assert d["role"] == "assistant"
|
||||
assert d["content"] == ""
|
||||
assert len(d["tool_calls"]) == 1
|
||||
assert d["tool_calls"][0]["function"]["name"] == "read_file"
|
||||
|
||||
|
||||
def test_chat_message_tool_role() -> None:
|
||||
msg = openai_schemas.ChatMessage(
|
||||
role="tool", content='{"result": "ok"}', tool_call_id="call_a"
|
||||
)
|
||||
d = msg.to_dict()
|
||||
assert d["role"] == "tool"
|
||||
assert d["tool_call_id"] == "call_a"
|
||||
|
||||
|
||||
def test_chat_message_is_frozen() -> None:
|
||||
msg = openai_schemas.ChatMessage(role="user", content="hi")
|
||||
with pytest.raises(Exception):
|
||||
msg.role = "mutated"
|
||||
|
||||
|
||||
def test_usage_stats_construction() -> None:
|
||||
u = openai_schemas.UsageStats(input_tokens=100, output_tokens=50)
|
||||
assert u.input_tokens == 100
|
||||
assert u.output_tokens == 50
|
||||
assert u.cache_read_tokens == 0
|
||||
assert u.cache_creation_tokens == 0
|
||||
|
||||
|
||||
def test_usage_stats_with_cache() -> None:
|
||||
u = openai_schemas.UsageStats(
|
||||
input_tokens=100,
|
||||
output_tokens=50,
|
||||
cache_read_tokens=80,
|
||||
cache_creation_tokens=20,
|
||||
)
|
||||
assert u.cache_read_tokens == 80
|
||||
assert u.cache_creation_tokens == 20
|
||||
|
||||
|
||||
def test_usage_stats_is_frozen() -> None:
|
||||
u = openai_schemas.UsageStats(input_tokens=1, output_tokens=1)
|
||||
with pytest.raises(Exception):
|
||||
u.input_tokens = 999
|
||||
|
||||
|
||||
def test_normalized_response_construction() -> None:
|
||||
tc = openai_schemas.ToolCall(
|
||||
id="call_z",
|
||||
function=openai_schemas.ToolCallFunction(name="noop", arguments="{}"),
|
||||
)
|
||||
usage = openai_schemas.UsageStats(input_tokens=10, output_tokens=20)
|
||||
resp = openai_schemas.NormalizedResponse(
|
||||
text="hello", tool_calls=(tc,), usage=usage, raw_response=None
|
||||
)
|
||||
assert resp.text == "hello"
|
||||
assert len(resp.tool_calls) == 1
|
||||
assert resp.usage.input_tokens == 10
|
||||
assert resp.raw_response is None
|
||||
|
||||
|
||||
def test_normalized_response_raw_can_be_any_type() -> None:
|
||||
"""Pattern 3: raw_response is intentionally Any (SDK-specific)."""
|
||||
usage = openai_schemas.UsageStats(input_tokens=0, output_tokens=0)
|
||||
resp = openai_schemas.NormalizedResponse(
|
||||
text="", tool_calls=(), usage=usage, raw_response={"vendor_specific": True}
|
||||
)
|
||||
assert resp.raw_response == {"vendor_specific": True}
|
||||
|
||||
|
||||
def test_normalized_response_to_legacy_dict_preserves_shape() -> None:
|
||||
tc = openai_schemas.ToolCall(
|
||||
id="call_q",
|
||||
function=openai_schemas.ToolCallFunction(name="x", arguments="{}"),
|
||||
)
|
||||
usage = openai_schemas.UsageStats(
|
||||
input_tokens=10, output_tokens=20, cache_read_tokens=5, cache_creation_tokens=3
|
||||
)
|
||||
resp = openai_schemas.NormalizedResponse(
|
||||
text="hello", tool_calls=(tc,), usage=usage, raw_response="sdk_obj"
|
||||
)
|
||||
d = resp.to_legacy_dict()
|
||||
assert d["text"] == "hello"
|
||||
assert d["tool_calls"][0]["id"] == "call_q"
|
||||
assert d["usage"]["input_tokens"] == 10
|
||||
assert d["usage"]["cache_read_tokens"] == 5
|
||||
assert d["raw_response"] == "sdk_obj"
|
||||
|
||||
|
||||
def test_openai_compatible_request_defaults() -> None:
|
||||
msg = openai_schemas.ChatMessage(role="user", content="hi")
|
||||
req = openai_schemas.OpenAICompatibleRequest(messages=[msg], model="gpt-4")
|
||||
assert req.messages == [msg]
|
||||
assert req.model == "gpt-4"
|
||||
assert req.temperature == 0.0
|
||||
assert req.top_p == 1.0
|
||||
assert req.max_tokens == 8192
|
||||
assert req.tools is None
|
||||
assert req.tool_choice == "auto"
|
||||
assert req.stream is False
|
||||
assert req.stream_callback is None
|
||||
assert req.extra_body is None
|
||||
|
||||
|
||||
def test_openai_compatible_request_tools_field_stays_dict_list() -> None:
|
||||
"""Cross-phase coupling (deferred): Phase 1 ToolSpec migration is a
|
||||
follow-up track per spec 3.4. The tools field stays list[dict[str, Any]]
|
||||
for now."""
|
||||
msg = openai_schemas.ChatMessage(role="user", content="hi")
|
||||
tools = [{"type": "function", "function": {"name": "x"}}]
|
||||
req = openai_schemas.OpenAICompatibleRequest(messages=[msg], model="gpt-4", tools=tools)
|
||||
assert req.tools == tools
|
||||
|
||||
|
||||
def test_chat_message_to_dict_handles_optional_fields() -> None:
|
||||
msg = openai_schemas.ChatMessage(role="assistant", content="", name=None, tool_call_id=None)
|
||||
d = msg.to_dict()
|
||||
assert "name" not in d
|
||||
assert "tool_call_id" not in d
|
||||
|
||||
|
||||
def test_normalized_response_is_frozen() -> None:
|
||||
usage = openai_schemas.UsageStats(input_tokens=0, output_tokens=0)
|
||||
resp = openai_schemas.NormalizedResponse(text="x", tool_calls=(), usage=usage, raw_response=None)
|
||||
with pytest.raises(Exception):
|
||||
resp.text = "mutated"
|
||||
@@ -1,131 +0,0 @@
|
||||
"""Tests for src/provider_state.py
|
||||
|
||||
Phase 3 of any_type_componentization_20260621. Verifies:
|
||||
- 6 ProviderHistory instances pre-registered
|
||||
- get_history() returns singleton instance per provider
|
||||
- ProviderHistory.append() / get_all() / replace_all() / clear() are thread-safe
|
||||
- clear_all() resets all 6
|
||||
- providers() returns the expected 6-tuple
|
||||
|
||||
CONVENTION: 1-space indentation. NO COMMENTS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
|
||||
import pytest
|
||||
from src import provider_state
|
||||
|
||||
|
||||
EXPECTED_PROVIDERS: tuple[str, ...] = ("anthropic", "deepseek", "minimax", "qwen", "grok", "llama")
|
||||
|
||||
|
||||
def test_six_providers_registered() -> None:
|
||||
assert provider_state.providers() == EXPECTED_PROVIDERS
|
||||
|
||||
|
||||
def test_get_history_returns_singleton_per_provider() -> None:
|
||||
a1 = provider_state.get_history("anthropic")
|
||||
a2 = provider_state.get_history("anthropic")
|
||||
assert a1 is a2
|
||||
g1 = provider_state.get_history("grok")
|
||||
g2 = provider_state.get_history("grok")
|
||||
assert g1 is g2
|
||||
assert a1 is not g1
|
||||
|
||||
|
||||
def test_get_history_raises_for_unknown() -> None:
|
||||
with pytest.raises(KeyError):
|
||||
provider_state.get_history("nonexistent_provider")
|
||||
|
||||
|
||||
def test_provider_history_starts_empty() -> None:
|
||||
provider_state.clear_all()
|
||||
h = provider_state.get_history("anthropic")
|
||||
assert h.get_all() == []
|
||||
|
||||
|
||||
def test_provider_history_append() -> None:
|
||||
provider_state.clear_all()
|
||||
h = provider_state.get_history("deepseek")
|
||||
h.append({"role": "user", "content": "hello"})
|
||||
h.append({"role": "assistant", "content": "world"})
|
||||
assert h.get_all() == [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "world"},
|
||||
]
|
||||
|
||||
|
||||
def test_provider_history_get_all_returns_copy() -> None:
|
||||
h = provider_state.get_history("qwen")
|
||||
h.clear()
|
||||
h.append({"role": "user", "content": "hi"})
|
||||
snapshot = h.get_all()
|
||||
snapshot.append({"role": "user", "content": "leaked"})
|
||||
assert h.get_all() == [{"role": "user", "content": "hi"}]
|
||||
|
||||
|
||||
def test_provider_history_replace_all() -> None:
|
||||
h = provider_state.get_history("minimax")
|
||||
h.clear()
|
||||
h.append({"role": "user", "content": "old"})
|
||||
h.replace_all([{"role": "user", "content": "new"}])
|
||||
assert h.get_all() == [{"role": "user", "content": "new"}]
|
||||
|
||||
|
||||
def test_provider_history_replace_all_takes_copy() -> None:
|
||||
h = provider_state.get_history("llama")
|
||||
h.clear()
|
||||
new_messages = [{"role": "user", "content": "x"}]
|
||||
h.replace_all(new_messages)
|
||||
new_messages.append({"role": "user", "content": "leaked"})
|
||||
assert h.get_all() == [{"role": "user", "content": "x"}]
|
||||
|
||||
|
||||
def test_provider_history_clear() -> None:
|
||||
h = provider_state.get_history("grok")
|
||||
h.append({"role": "user", "content": "x"})
|
||||
h.clear()
|
||||
assert h.get_all() == []
|
||||
|
||||
|
||||
def test_clear_all_resets_every_provider() -> None:
|
||||
for p in EXPECTED_PROVIDERS:
|
||||
provider_state.get_history(p).append({"role": "user", "content": f"{p}-msg"})
|
||||
provider_state.clear_all()
|
||||
for p in EXPECTED_PROVIDERS:
|
||||
assert provider_state.get_history(p).get_all() == []
|
||||
|
||||
|
||||
def test_provider_history_thread_safety() -> None:
|
||||
h = provider_state.get_history("anthropic")
|
||||
h.clear()
|
||||
num_threads = 10
|
||||
per_thread = 100
|
||||
barrier = threading.Barrier(num_threads)
|
||||
def worker() -> None:
|
||||
barrier.wait()
|
||||
for i in range(per_thread):
|
||||
h.append({"role": "user", "content": f"msg-{i}"})
|
||||
threads = [threading.Thread(target=worker) for _ in range(num_threads)]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
assert len(h.get_all()) == num_threads * per_thread
|
||||
|
||||
|
||||
def test_independent_locks_per_provider() -> None:
|
||||
h1 = provider_state.get_history("anthropic")
|
||||
h2 = provider_state.get_history("deepseek")
|
||||
assert h1.lock is not h2.lock
|
||||
acquired_both = []
|
||||
def lock_h1() -> None:
|
||||
with h1.lock:
|
||||
acquired_both.append("h1")
|
||||
lock_h2()
|
||||
def lock_h2() -> None:
|
||||
with h2.lock:
|
||||
acquired_both.append("h2")
|
||||
lock_h1()
|
||||
assert acquired_both == ["h1", "h2"]
|
||||
@@ -49,36 +49,4 @@ def test_file_items_diff_named_tuple_has_two_fields() -> None:
|
||||
def test_result_with_file_items_alias_composes() -> None:
|
||||
r: result_types.Result[type_aliases.FileItems] = result_types.Result(data=[])
|
||||
assert r.ok is True
|
||||
assert isinstance(r.data, list)
|
||||
|
||||
|
||||
def test_json_primitive_alias_resolves_to_union() -> None:
|
||||
assert hasattr(type_aliases, "JsonPrimitive")
|
||||
hints = get_type_hints(type_aliases)
|
||||
assert "JsonPrimitive" in hints
|
||||
|
||||
|
||||
def test_json_value_alias_resolves_to_recursive_union() -> None:
|
||||
assert hasattr(type_aliases, "JsonValue")
|
||||
hints = get_type_hints(type_aliases)
|
||||
assert "JsonValue" in hints
|
||||
jv = hints["JsonValue"]
|
||||
assert jv is not None
|
||||
|
||||
|
||||
def test_json_value_accepts_primitive_dict() -> None:
|
||||
payload: type_aliases.JsonValue = {"key": "value", "count": 42, "active": True, "nothing": None}
|
||||
assert payload["key"] == "value"
|
||||
assert payload["count"] == 42
|
||||
assert payload["active"] is True
|
||||
assert payload["nothing"] is None
|
||||
|
||||
|
||||
def test_json_value_accepts_nested_structures() -> None:
|
||||
payload: type_aliases.JsonValue = {
|
||||
"users": [{"name": "alice", "age": 30}, {"name": "bob", "age": 25}],
|
||||
"metadata": {"source": "test", "tags": ["a", "b", "c"]},
|
||||
}
|
||||
assert len(payload["users"]) == 2
|
||||
assert payload["users"][0]["name"] == "alice"
|
||||
assert payload["metadata"]["tags"][1] == "b"
|
||||
assert isinstance(r.data, list)
|
||||
@@ -1,70 +0,0 @@
|
||||
"""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)
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
import asyncio
|
||||
import json
|
||||
import websockets
|
||||
from src.api_hooks import WebSocketMessage, WebSocketServer
|
||||
from src.api_hooks import WebSocketServer
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_websocket_subscription_and_broadcast():
|
||||
@@ -32,7 +32,7 @@ async def test_websocket_subscription_and_broadcast():
|
||||
|
||||
# Broadcast an event from the server
|
||||
event_payload = {"event": "test_event", "data": "hello"}
|
||||
server.broadcast(WebSocketMessage(channel="events", payload=event_payload))
|
||||
server.broadcast("events", event_payload)
|
||||
|
||||
# Receive the broadcast
|
||||
broadcast_response = await websocket.recv()
|
||||
|
||||
Reference in New Issue
Block a user