refactor: enforce BasedPyright strict type checking (#5158)

This commit is contained in:
chengyongru
2026-07-29 21:37:11 +08:00
committed by GitHub
parent e703481755
commit 757ad9c764
166 changed files with 4728 additions and 2621 deletions
+3 -2
View File
@@ -891,7 +891,8 @@ def test_drop_malformed_tool_calls_trims_response():
tool_calls=[
ToolCallRequest(id="1", name=None, arguments={}),
ToolCallRequest(id="2", name="", arguments={}),
ToolCallRequest(id="3", name="read_file", arguments={}),
ToolCallRequest(id="3", name={"unexpected": "object"}, arguments={}),
ToolCallRequest(id="4", name="read_file", arguments={}),
],
finish_reason="tool_calls",
)
@@ -899,7 +900,7 @@ def test_drop_malformed_tool_calls_trims_response():
assert [tc.name for tc in response.tool_calls] == ["read_file"]
assert response.finish_reason == "tool_calls"
assert response.should_execute_tools is True
assert dropped == 2
assert dropped == 3
assert all_dropped is False
assert orig == "tool_calls"
+2
View File
@@ -1217,6 +1217,7 @@ def test_channels_login_uses_discovered_plugin_class(monkeypatch):
async def login(self, force: bool = False) -> bool:
seen["force"] = force
seen["config"] = self.config
seen["bus"] = self.bus
return True
monkeypatch.setattr("nanobot.config.loader.load_config", lambda config_path=None: Config())
@@ -1229,6 +1230,7 @@ def test_channels_login_uses_discovered_plugin_class(monkeypatch):
assert result.exit_code == 0
assert seen["force"] is True
assert isinstance(seen["bus"], MessageBus)
def test_channels_login_sets_custom_config_path(monkeypatch, tmp_path):
+28 -7
View File
@@ -13,6 +13,7 @@ import pytest
from typer.testing import CliRunner
from nanobot.agent.memory import MemoryStore
from nanobot.agent.tools.registry import ToolRegistry
from nanobot.agent.turn_delivery import TurnDeliveryFactory
from nanobot.bus.events import InboundMessage, OutboundMessage
from nanobot.cli import commands as cli_commands
@@ -71,6 +72,26 @@ class _StopGatewayError(RuntimeError):
pass
class _GatewayAgentContractStub:
"""Minimal stable AgentLoop surface required by gateway assembly tests."""
tools = ToolRegistry()
@staticmethod
def pending_cron_job_ids_for_session(_session_key: str) -> set[str]:
return set()
@staticmethod
def pending_local_trigger_ids_for_session(_session_key: str) -> set[str]:
return set()
async def submit_local_trigger_turn(
self,
_msg: InboundMessage,
) -> OutboundMessage | None:
return None
def test_gateway_signal_handler_first_signal_stops_and_second_forces() -> None:
class _FakeLoop:
def __init__(self) -> None:
@@ -1949,7 +1970,7 @@ def test_heartbeat_empty_response_still_retains_recent_messages(
def register_system_job(self, _job: CronJob) -> None:
raise _StopGatewayError("stop")
class _FakeAgentLoop:
class _FakeAgentLoop(_GatewayAgentContractStub):
@classmethod
def from_config(cls, config, bus=None, **extra):
return cls(**extra)
@@ -2615,7 +2636,7 @@ def test_gateway_unbound_agent_cron_is_skipped(
self.on_job = None
seen["cron"] = self
class _FakeAgentLoop:
class _FakeAgentLoop(_GatewayAgentContractStub):
@classmethod
def from_config(cls, config, bus=None, **extra):
return cls(**extra)
@@ -2731,7 +2752,7 @@ def test_gateway_bound_cron_runs_as_session_turn(
def write_run_record(self, run_id: str, record: dict[str, object]) -> None:
seen["run_records"].append((run_id, record))
class _FakeAgentLoop:
class _FakeAgentLoop(_GatewayAgentContractStub):
@classmethod
def from_config(cls, config, bus=None, **extra):
return cls(**extra)
@@ -2947,7 +2968,7 @@ def test_gateway_local_trigger_queue_submits_agent_turns(
def register_system_job(self, _job) -> None:
return None
class _FakeAgentLoop:
class _FakeAgentLoop(_GatewayAgentContractStub):
@classmethod
def from_config(cls, config, bus=None, **extra):
seen["agent_from_config_kwargs"] = extra
@@ -3200,7 +3221,7 @@ def test_gateway_health_endpoint_binds_and_serves_expected_responses(
def flush_all(self) -> int:
return 0
class _FakeAgentLoop:
class _FakeAgentLoop(_GatewayAgentContractStub):
@classmethod
def from_config(cls, config, bus=None, **extra):
return cls(**extra)
@@ -3393,7 +3414,7 @@ def test_gateway_shutdown_lets_agent_task_own_mcp_cleanup(
def flush_all(self) -> int:
return 0
class _FakeAgentLoop:
class _FakeAgentLoop(_GatewayAgentContractStub):
@classmethod
def from_config(cls, config, bus=None, **extra):
return cls(**extra)
@@ -3492,7 +3513,7 @@ def test_gateway_shutdown_event_exits_forever_runtime_tasks(
def flush_all(self) -> int:
return 0
class _FakeAgentLoop:
class _FakeAgentLoop(_GatewayAgentContractStub):
@classmethod
def from_config(cls, config, bus=None, **extra):
return cls(**extra)
+2
View File
@@ -231,6 +231,7 @@ def _build_runnable_dream(
context=SimpleNamespace(memory=store, timezone="UTC"),
sessions=SimpleNamespace(sessions_dir=sessions_dir),
process_direct=process_direct,
dream_runtime=lambda: None,
)
ctx = CommandContext(msg=msg, session=None, key=msg.session_key, raw="/dream", args="", loop=loop)
return ctx, store
@@ -317,6 +318,7 @@ async def test_dream_noop_batch_unlocks_following_history(tmp_path) -> None:
context=SimpleNamespace(memory=store, timezone="UTC"),
sessions=SimpleNamespace(sessions_dir=sessions_dir),
process_direct=process_direct,
dream_runtime=lambda: None,
)
ctx = CommandContext(msg=msg, session=None, key=msg.session_key, raw="/dream", args="", loop=loop)
@@ -2,6 +2,7 @@
from __future__ import annotations
from inspect import Parameter, signature
from unittest.mock import AsyncMock, MagicMock
import pytest
@@ -13,6 +14,13 @@ from nanobot.command.builtin import (
from nanobot.command.router import CommandContext, CommandRouter
def test_command_context_requires_loop_as_keyword_dependency() -> None:
loop_parameter = signature(CommandContext).parameters["loop"]
assert loop_parameter.kind is Parameter.KEYWORD_ONLY
assert loop_parameter.default is Parameter.empty
class TestIsDispatchableCommand:
"""Unit tests for the is_dispatchable_command() predicate."""
+7
View File
@@ -7,6 +7,7 @@ from nanobot.config.loader import (
_resolve_env_vars,
load_config,
resolve_config_env_vars,
resolve_env_refs,
save_config,
)
from nanobot.config.schema import Config
@@ -50,6 +51,12 @@ class TestResolveEnvVars:
_resolve_env_vars("${DOES_NOT_EXIST}")
class TestResolveSingleEnvRefs:
@pytest.mark.parametrize("value", [None, 42, True, {"key": "value"}])
def test_non_string_values_pass_through_unchanged(self, value):
assert resolve_env_refs(value) is value
class TestResolveConfig:
def test_resolves_env_vars_in_config(self, tmp_path, monkeypatch):
monkeypatch.setenv("TEST_API_KEY", "resolved-key")
+11
View File
@@ -65,6 +65,17 @@ def test_load_jobs_accepts_snake_case_schedule_and_run_history(tmp_path) -> None
assert jobs[0].state.run_history[0].duration_ms == 12
def test_cron_job_from_dict_rejects_malformed_run_history() -> None:
with pytest.raises(TypeError):
CronJob.from_dict(
{
"id": "j1",
"name": "t",
"state": {"run_history": [None]},
}
)
def test_load_jobs_coerces_string_schedule_and_state_ms(tmp_path) -> None:
store_path = tmp_path / "cron" / "jobs.json"
store_path.parent.mkdir(parents=True)
+19
View File
@@ -1,3 +1,5 @@
import json
import pytest
from nanobot.pairing import __all__ as pairing_all
@@ -272,6 +274,23 @@ def test_load_treats_null_approved_and_pending_maps_as_empty(tmp_path, monkeypat
assert store.get_approved("telegram") == []
@pytest.mark.parametrize(
("field", "value"),
[("approved", "corrupt"), ("pending", ["corrupt"])],
)
def test_load_treats_non_object_approved_and_pending_maps_as_empty(
tmp_path, monkeypatch, field, value
):
path = tmp_path / "pairing.json"
payload = {"approved": {}, "pending": {}}
payload[field] = value
path.write_text(json.dumps(payload), encoding="utf-8")
monkeypatch.setattr(store, "_store_path", lambda: path)
assert store.is_approved("telegram", "123") is False
assert store.list_pending() == []
@pytest.mark.parametrize("payload", ["null", "[]", "true"])
def test_load_treats_non_object_store_as_empty(tmp_path, monkeypatch, payload):
path = tmp_path / "pairing.json"
+1 -1
View File
@@ -181,7 +181,7 @@ def test_resolver_env_ref_missing_var_degrades_to_not_configured() -> None:
# Unresolved reference degrades to a falsy key rather than the literal
# "${...}" string, so the config reports itself as not configured.
assert not resolved.api_key
assert resolved.api_key == ""
assert resolved.configured is False
+22
View File
@@ -156,6 +156,28 @@ def test_parse_json_content_validates_user_role() -> None:
_parse_json_content(body)
@pytest.mark.parametrize(
("part", "field"),
[
({"type": "text", "text": 1}, r"content\[\]\.text"),
(
{"type": "image_url", "image_url": "not-an-object"},
r"content\[\]\.image_url",
),
(
{"type": "image_url", "image_url": {"url": 1}},
r"image_url\.url",
),
],
)
def test_parse_json_content_validates_typed_block_fields(part, field) -> None:
"""Dynamic content blocks are checked before their values reach typed code."""
body = {"messages": [{"role": "user", "content": [part]}]}
with pytest.raises(TypeError, match=field):
_parse_json_content(body)
def test_parse_json_content_rejects_oversized_base64_file(tmp_path) -> None:
"""Oversized JSON data URLs should fail before writing to disk."""
large_payload = base64.b64encode(b"x" * (11 * 1024 * 1024)).decode()
+7
View File
@@ -67,6 +67,13 @@ class TestExtractText:
result = extract_text(txt_file)
assert result == content
def test_extract_text_accepts_string_path(self, tmp_path: Path):
"""String paths retain the compatibility behavior of Path inputs."""
txt_file = tmp_path / "string-path.txt"
txt_file.write_text("string path", encoding="utf-8")
assert extract_text(str(txt_file)) == "string path"
def test_extract_text_txt_file_with_truncation(self, tmp_path: Path):
"""Test that large text files are truncated."""
txt_file = tmp_path / "large.txt"
+8
View File
@@ -26,6 +26,14 @@ from nanobot.config.schema import MCPServerConfig
_PROXY_ENV_VARS = ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy")
def test_type_checking_only_mcp_annotations_are_deferred() -> None:
assert mcp_mod._MCPWrapperBase.__annotations__["_session"] == "ClientSession"
assert MCPToolWrapper.__init__.__annotations__["session"] == "ClientSession"
assert MCPResourceWrapper.__init__.__annotations__["resource_def"] == "Resource"
assert MCPPromptWrapper.__init__.__annotations__["prompt_def"] == "Prompt"
assert connect_mcp_servers.__annotations__["mcp_servers"] == "dict[str, MCPServerConfig]"
class _FakeTextContent:
def __init__(self, text: str) -> None:
self.text = text