fix(ci): stabilize and speed up CI (#5145)

This commit is contained in:
chengyongru
2026-07-28 22:55:59 +08:00
committed by GitHub
parent 9070d7489a
commit 393d429e0a
9 changed files with 328 additions and 26 deletions
+9 -11
View File
@@ -308,19 +308,17 @@ class TestAppendHistoryHardCap:
entry = store.read_unprocessed_history(since_cursor=0)[0]
assert len(entry["content"]) <= _HISTORY_ENTRY_HARD_CAP + 50
def test_oversize_warning_is_emitted_once(self, store, caplog):
def test_oversize_warning_is_emitted_once(self, store, monkeypatch):
"""Repeated oversized writes should warn only on the first occurrence."""
from loguru import logger as loguru_logger
records: list[str] = []
handler_id = loguru_logger.add(lambda m: records.append(m), level="WARNING")
try:
huge = "x" * (_HISTORY_ENTRY_HARD_CAP + 1)
store.append_history(huge)
store.append_history(huge)
store.append_history(huge)
finally:
loguru_logger.remove(handler_id)
monkeypatch.setattr(
"nanobot.agent.memory.logger.warning",
lambda message, *args: records.append(message.format(*args)),
)
huge = "x" * (_HISTORY_ENTRY_HARD_CAP + 1)
store.append_history(huge)
store.append_history(huge)
store.append_history(huge)
oversize_warnings = [r for r in records if "exceeds" in r and "chars" in r]
assert len(oversize_warnings) == 1
+193 -2
View File
@@ -1492,7 +1492,7 @@ def test_repository_dependency_installer_selects_all_channel_manifests(monkeypat
monkeypatch.setattr(dependencies, "discover_plugins", lambda: plugins)
monkeypatch.setattr(
dependencies,
"ensure_enabled_channel_dependencies",
"ensure_repository_channel_dependencies",
lambda names, discovered: prepared.append((names, discovered)) or {},
)
@@ -1500,6 +1500,191 @@ def test_repository_dependency_installer_selects_all_channel_manifests(monkeypat
assert prepared == [(set(plugins), plugins)]
def test_repository_dependency_installer_batches_missing_manifests(monkeypatch):
from nanobot.optional_features import InstallResult
from scripts import install_channel_dependencies as dependencies
plugins = {
"second": ChannelPlugin(
name="second",
display_name="Second",
runtime="missing.second.runtime:SecondChannel",
dependencies=("shared-sdk>=1", "second-sdk>=2"),
),
"first": ChannelPlugin(
name="first",
display_name="First",
runtime="missing.first.runtime:FirstChannel",
dependencies=("first-sdk>=1", "shared-sdk>=1"),
),
"ready": ChannelPlugin(
name="ready",
display_name="Ready",
runtime="missing.ready.runtime:ReadyChannel",
dependencies=("ready-sdk>=1",),
),
}
batch_installed = False
installs: list[tuple[str, list[str]]] = []
def extra_installed(name: str, _requirements: list[str]) -> bool:
return name == "ready" or batch_installed
def install_extra(name: str, requirements: list[str]) -> InstallResult:
nonlocal batch_installed
installs.append((name, requirements))
batch_installed = True
return InstallResult(True, name, ["pip"])
monkeypatch.setattr(dependencies, "extra_installed", extra_installed)
monkeypatch.setattr(dependencies, "install_extra", install_extra)
monkeypatch.setattr(
dependencies,
"ensure_enabled_channel_dependencies",
lambda _names, _plugins: pytest.fail("verified batch must not use the fallback"),
)
failures = dependencies.ensure_repository_channel_dependencies(set(plugins), plugins)
assert failures == {}
assert installs == [
(
"channel-dependencies",
["first-sdk>=1", "shared-sdk>=1", "second-sdk>=2"],
)
]
def test_repository_dependency_installer_falls_back_after_batch_failure(monkeypatch):
from nanobot.optional_features import InstallResult
from scripts import install_channel_dependencies as dependencies
plugins = {
name: ChannelPlugin(
name=name,
display_name=name.title(),
runtime=f"missing.{name}.runtime:Channel",
dependencies=(f"{name}-sdk>=1",),
)
for name in ("first", "second")
}
fallbacks: list[set[str]] = []
fallback_finished = False
def extra_installed(name: str, _requirements: list[str]) -> bool:
return fallback_finished and name == "first"
def fallback(names: set[str], _plugins: dict[str, ChannelPlugin]) -> dict[str, str]:
nonlocal fallback_finished
fallbacks.append(names)
fallback_finished = True
return {"second": "install failed"}
monkeypatch.setattr(dependencies, "extra_installed", extra_installed)
monkeypatch.setattr(
dependencies,
"install_extra",
lambda name, _requirements: InstallResult(False, name, ["pip"]),
)
monkeypatch.setattr(
dependencies,
"ensure_enabled_channel_dependencies",
fallback,
)
failures = dependencies.ensure_repository_channel_dependencies(set(plugins), plugins)
assert failures == {"second": "install failed"}
assert fallbacks == [set(plugins)]
def test_repository_dependency_installer_rechecks_each_channel_after_batch(monkeypatch):
from nanobot.optional_features import InstallResult
from scripts import install_channel_dependencies as dependencies
plugins = {
name: ChannelPlugin(
name=name,
display_name=name.title(),
runtime=f"missing.{name}.runtime:Channel",
dependencies=(f"{name}-sdk>=1",),
)
for name in ("first", "second")
}
batch_finished = False
fallback_finished = False
fallbacks: list[set[str]] = []
def extra_installed(name: str, _requirements: list[str]) -> bool:
if fallback_finished:
return True
if batch_finished:
return name == "second"
return name == "first"
def install_extra(name: str, _requirements: list[str]) -> InstallResult:
nonlocal batch_finished
batch_finished = True
return InstallResult(True, name, ["pip"])
def fallback(names: set[str], _plugins: dict[str, ChannelPlugin]) -> dict[str, str]:
nonlocal fallback_finished
fallbacks.append(names)
fallback_finished = True
return {}
monkeypatch.setattr(dependencies, "extra_installed", extra_installed)
monkeypatch.setattr(dependencies, "install_extra", install_extra)
monkeypatch.setattr(
dependencies,
"ensure_enabled_channel_dependencies",
fallback,
)
failures = dependencies.ensure_repository_channel_dependencies(set(plugins), plugins)
assert failures == {}
assert fallbacks == [{"first"}]
def test_repository_dependency_installer_reports_conflict_after_fallback(monkeypatch):
from nanobot.optional_features import InstallResult
from scripts import install_channel_dependencies as dependencies
plugins = {
name: ChannelPlugin(
name=name,
display_name=name.title(),
runtime=f"missing.{name}.runtime:Channel",
dependencies=(f"{name}-sdk>=1",),
)
for name in ("first", "second")
}
fallback_finished = False
def extra_installed(name: str, _requirements: list[str]) -> bool:
return fallback_finished and name == "second"
def fallback(_names: set[str], _plugins: dict[str, ChannelPlugin]) -> dict[str, str]:
nonlocal fallback_finished
fallback_finished = True
return {}
monkeypatch.setattr(dependencies, "extra_installed", extra_installed)
monkeypatch.setattr(
dependencies,
"install_extra",
lambda name, _requirements: InstallResult(False, name, ["pip"]),
)
monkeypatch.setattr(dependencies, "ensure_enabled_channel_dependencies", fallback)
failures = dependencies.ensure_repository_channel_dependencies(set(plugins), plugins)
assert failures == {
"first": "Channel dependencies could not be installed. Check gateway logs."
}
def test_repository_dependency_installer_rejects_unknown_channel(monkeypatch, capsys):
from scripts import install_channel_dependencies as dependencies
@@ -1520,7 +1705,7 @@ def test_repository_dependency_installer_propagates_install_failure(monkeypatch,
monkeypatch.setattr(dependencies, "discover_plugins", lambda: {"demo": plugin})
monkeypatch.setattr(
dependencies,
"ensure_enabled_channel_dependencies",
"ensure_repository_channel_dependencies",
lambda _names, _plugins: {"demo": "dependency install failed"},
)
@@ -2388,6 +2573,12 @@ def test_optional_dependency_metadata_for_enable():
]
assert deps["pdf"] == ["pypdf>=5.0.0,<6.0.0"]
assert deps["langfuse"] == ["langfuse>=3.0.0,<4.0.0"]
assert deps["olostep"] == ["olostep>=0.1.0; python_version < '3.14'"]
expected_olostep_args = [] if sys.version_info >= (3, 14) else ["olostep>=0.1.0"]
assert optional_features.install_args_for_extra("olostep", deps["olostep"]) == (
expected_olostep_args,
"olostep support",
)
channel_names = {
"dingtalk",
"discord",
+5 -1
View File
@@ -35,6 +35,10 @@ from nanobot.webui.metadata import (
runner = CliRunner()
def _without_rendered_line_breaks(output: str) -> str:
return "".join(output.splitlines())
def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None:
metadata = {
"webui": True,
@@ -2150,7 +2154,7 @@ def test_webui_yes_still_refuses_invalid_custom_model_setup(
assert result.exit_code == 1
assert "provider/model setup is incomplete" in result.stdout
assert "Settings → Models" in result.stdout
assert "Settings → Models" in _without_rendered_line_breaks(result.stdout)
assert "nanobot onboard --wizard" in result.stdout
assert "nanobot status --config" in result.stdout
assert config_file.name in result.stdout
+1 -1
View File
@@ -174,7 +174,7 @@ def test_status_reports_missing_provider_with_shortest_setup_routes(tmp_path) ->
assert result.exit_code == 0
assert "Agent: ✗" in result.stdout
assert "No provider is configured for model" in result.stdout
assert "Settings → Models" in result.stdout
assert "Settings → Models" in _without_rendered_line_breaks(result.stdout)
assert "nanobot onboard --wizard" in result.stdout
assert "nanobot status --config" in result.stdout
+16 -7
View File
@@ -334,27 +334,36 @@ def test_write_stdin_can_wait_for_expected_output(tmp_path):
def test_write_stdin_wait_for_reports_timeout_without_killing_session(tmp_path):
async def run() -> tuple[str, str, str]:
async def run() -> tuple[str, str, str, str]:
manager = ExecSessionManager()
exec_tool = ExecTool(working_dir=str(tmp_path), timeout=5, session_manager=manager)
stdin_tool = WriteStdinTool(manager=manager)
command = _waiting_shell_command("booting")
command = _waiting_shell_command("booting", delayed="ready")
initial = await exec_tool.execute(command=command, yield_time_ms=100)
initial = await exec_tool.execute(command=command, yield_time_ms=0)
sid = _session_id(initial)
# Synchronize on an stdin-gated marker before exercising the immediate timeout below.
ready = await stdin_tool.execute(
session_id=sid,
chars="\n",
wait_for="ready",
wait_timeout_ms=10000,
yield_time_ms=0,
)
waited = await stdin_tool.execute(
session_id=sid,
wait_for="never-ready",
wait_timeout_ms=200,
wait_timeout_ms=0,
yield_time_ms=0,
)
cleanup = await stdin_tool.execute(session_id=sid, terminate=True, yield_time_ms=0)
return initial, waited, cleanup
return initial, ready, waited, cleanup
initial, waited, cleanup = asyncio.run(run())
initial, ready, waited, cleanup = asyncio.run(run())
assert "Process running" in initial
assert "booting" in initial + waited
assert "booting" in initial + ready
assert "ready" in ready
assert "Process running" in waited
assert "Wait target not observed: 'never-ready'" in waited
assert "Session terminated." in cleanup