Revert "merge: tier2/phase2_4_5_call_site_completion_20260621 (parent + follow-up + Phase 6e analysis)"

This reverts commit f914b2bcd4, reversing
changes made to 7fef95cc87.
This commit is contained in:
ed
2026-06-21 22:39:14 -04:00
parent f32e4fd268
commit 751b94d4e8
81 changed files with 2683 additions and 5005 deletions
+3 -3
View File
@@ -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,
))
+3 -3
View File
@@ -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,
)
+3 -3
View File
@@ -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,
)
-99
View File
@@ -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"]
-98
View File
@@ -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
+1 -3
View File
@@ -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:
-148
View File
@@ -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
-123
View File
@@ -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'
+3 -4
View File
@@ -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"
-206
View File
@@ -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"
-131
View File
@@ -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"]
+1 -33
View File
@@ -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 -2
View File
@@ -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()