mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
fix(ci): stabilize and speed up CI (#5145)
This commit is contained in:
parent
9070d7489a
commit
393d429e0a
26
.github/workflows/ci.yml
vendored
26
.github/workflows/ci.yml
vendored
@ -5,10 +5,28 @@ on:
|
|||||||
branches: [main]
|
branches: [main]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- docs/**
|
- docs/**
|
||||||
|
- .agent/**
|
||||||
|
- .github/ISSUE_TEMPLATE/**
|
||||||
|
- AGENTS.md
|
||||||
|
- CLAUDE.md
|
||||||
|
- COMMUNICATION.md
|
||||||
|
- CONTRIBUTING.md
|
||||||
|
- README.md
|
||||||
|
- SECURITY.md
|
||||||
|
- webui/README.md
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- docs/**
|
- docs/**
|
||||||
|
- .agent/**
|
||||||
|
- .github/ISSUE_TEMPLATE/**
|
||||||
|
- AGENTS.md
|
||||||
|
- CLAUDE.md
|
||||||
|
- COMMUNICATION.md
|
||||||
|
- CONTRIBUTING.md
|
||||||
|
- README.md
|
||||||
|
- SECURITY.md
|
||||||
|
- webui/README.md
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@ -68,14 +86,18 @@ jobs:
|
|||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
coverage: false
|
coverage: false
|
||||||
|
pytest_args: ""
|
||||||
- name: latest, 3.14 + coverage
|
- name: latest, 3.14 + coverage
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
python-version: "3.14"
|
python-version: "3.14"
|
||||||
coverage: true
|
coverage: true
|
||||||
|
pytest_args: ""
|
||||||
- name: Windows, 3.14
|
- name: Windows, 3.14
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
python-version: "3.14"
|
python-version: "3.14"
|
||||||
coverage: false
|
coverage: false
|
||||||
|
# Keep each test file in one worker while using both hosted-runner cores.
|
||||||
|
pytest_args: "-n 2 --dist loadfile"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -98,6 +120,9 @@ jobs:
|
|||||||
- name: Install channel dependencies
|
- name: Install channel dependencies
|
||||||
run: uv run --no-sync python -m scripts.install_channel_dependencies --all-channels
|
run: uv run --no-sync python -m scripts.install_channel_dependencies --all-channels
|
||||||
|
|
||||||
|
- name: Verify dependency consistency
|
||||||
|
run: uv pip check
|
||||||
|
|
||||||
# Channel requirements live in manifests rather than uv.lock. Avoid a
|
# Channel requirements live in manifests rather than uv.lock. Avoid a
|
||||||
# later uv run sync pruning the packages installed by the previous step.
|
# later uv run sync pruning the packages installed by the previous step.
|
||||||
- name: Lint with ruff
|
- name: Lint with ruff
|
||||||
@ -115,6 +140,7 @@ jobs:
|
|||||||
if: ${{ !matrix.coverage }}
|
if: ${{ !matrix.coverage }}
|
||||||
run: >-
|
run: >-
|
||||||
uv run --no-sync python -m pytest
|
uv run --no-sync python -m pytest
|
||||||
|
${{ matrix.pytest_args }}
|
||||||
--durations=25 --durations-min=1.0
|
--durations=25 --durations-min=1.0
|
||||||
|
|
||||||
webui:
|
webui:
|
||||||
|
|||||||
11
conftest.py
11
conftest.py
@ -9,6 +9,17 @@ from collections.abc import Iterator
|
|||||||
|
|
||||||
import certifi
|
import certifi
|
||||||
import pytest
|
import pytest
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _isolate_nanobot_log_activation() -> Iterator[None]:
|
||||||
|
"""Keep CLI log settings from leaking into later tests in the same process."""
|
||||||
|
logger.enable("nanobot")
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
logger.enable("nanobot")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
|||||||
@ -27,7 +27,8 @@ dependencies = [
|
|||||||
"anthropic>=0.45.0,<1.0.0",
|
"anthropic>=0.45.0,<1.0.0",
|
||||||
"pydantic>=2.12.0,<3.0.0",
|
"pydantic>=2.12.0,<3.0.0",
|
||||||
"pydantic-settings>=2.12.0,<3.0.0",
|
"pydantic-settings>=2.12.0,<3.0.0",
|
||||||
"websockets>=16.0,<17.0",
|
# Feishu's lark-oapi currently requires websockets<16; core supports 15 and 16.
|
||||||
|
"websockets>=15.0,<17.0",
|
||||||
"websocket-client>=1.9.0,<2.0.0",
|
"websocket-client>=1.9.0,<2.0.0",
|
||||||
"httpx>=0.28.0,<1.0.0",
|
"httpx>=0.28.0,<1.0.0",
|
||||||
"ddgs>=9.5.5,<10.0.0",
|
"ddgs>=9.5.5,<10.0.0",
|
||||||
@ -84,13 +85,14 @@ pdf = [
|
|||||||
"pypdf>=5.0.0,<6.0.0",
|
"pypdf>=5.0.0,<6.0.0",
|
||||||
]
|
]
|
||||||
olostep = [
|
olostep = [
|
||||||
"olostep>=0.1.0",
|
"olostep>=0.1.0; python_version < '3.14'",
|
||||||
]
|
]
|
||||||
dev = [
|
dev = [
|
||||||
"pytest>=9.0.0,<10.0.0",
|
"pytest>=9.0.0,<10.0.0",
|
||||||
"pytest-asyncio>=1.3.0,<2.0.0",
|
"pytest-asyncio>=1.3.0,<2.0.0",
|
||||||
"aiohttp>=3.9.0,<4.0.0",
|
"aiohttp>=3.9.0,<4.0.0",
|
||||||
"pytest-cov>=6.0.0,<7.0.0",
|
"pytest-cov>=6.0.0,<7.0.0",
|
||||||
|
"pytest-xdist>=3.8.0,<4.0.0",
|
||||||
"ruff>=0.1.0",
|
"ruff>=0.1.0",
|
||||||
"pymupdf>=1.25.0",
|
"pymupdf>=1.25.0",
|
||||||
"pypdf>=5.0.0,<6.0.0",
|
"pypdf>=5.0.0,<6.0.0",
|
||||||
|
|||||||
@ -5,8 +5,69 @@ from __future__ import annotations
|
|||||||
import sys
|
import sys
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from nanobot.channels.plugin import ChannelPlugin
|
||||||
from nanobot.channels.registry import discover_plugins
|
from nanobot.channels.registry import discover_plugins
|
||||||
from nanobot.optional_features import ensure_enabled_channel_dependencies
|
from nanobot.optional_features import (
|
||||||
|
ensure_enabled_channel_dependencies,
|
||||||
|
extra_installed,
|
||||||
|
install_args_for_extra,
|
||||||
|
install_extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
_DEPENDENCY_FAILURE = "Channel dependencies could not be installed. Check gateway logs."
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_repository_channel_dependencies(
|
||||||
|
names: set[str],
|
||||||
|
plugins: dict[str, ChannelPlugin],
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Batch repository dependency installs, then verify every channel independently."""
|
||||||
|
requirements_by_name: dict[str, list[str]] = {}
|
||||||
|
pending: dict[str, list[str]] = {}
|
||||||
|
install_args: list[str] = []
|
||||||
|
seen_args: set[str] = set()
|
||||||
|
|
||||||
|
for name in sorted(names):
|
||||||
|
plugin = plugins.get(name)
|
||||||
|
if plugin is None:
|
||||||
|
continue
|
||||||
|
dependencies = list(plugin.dependencies)
|
||||||
|
if not dependencies:
|
||||||
|
continue
|
||||||
|
requirements_by_name[name] = dependencies
|
||||||
|
if extra_installed(name, dependencies):
|
||||||
|
continue
|
||||||
|
pending[name] = dependencies
|
||||||
|
channel_args, _label = install_args_for_extra(name, dependencies)
|
||||||
|
for requirement in channel_args:
|
||||||
|
if requirement not in seen_args:
|
||||||
|
seen_args.add(requirement)
|
||||||
|
install_args.append(requirement)
|
||||||
|
|
||||||
|
if not pending:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if install_args:
|
||||||
|
result = install_extra("channel-dependencies", install_args)
|
||||||
|
if result.ok:
|
||||||
|
unresolved = {
|
||||||
|
name
|
||||||
|
for name, dependencies in requirements_by_name.items()
|
||||||
|
if not extra_installed(name, dependencies)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
unresolved = set(requirements_by_name)
|
||||||
|
else:
|
||||||
|
unresolved = set(requirements_by_name)
|
||||||
|
|
||||||
|
if not unresolved:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
failures = ensure_enabled_channel_dependencies(unresolved, plugins)
|
||||||
|
for name, dependencies in requirements_by_name.items():
|
||||||
|
if name not in failures and not extra_installed(name, dependencies):
|
||||||
|
failures[name] = _DEPENDENCY_FAILURE
|
||||||
|
return failures
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Sequence[str] | None = None) -> int:
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
@ -26,7 +87,7 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|||||||
print(f"Unknown channels: {', '.join(unknown)}", file=sys.stderr)
|
print(f"Unknown channels: {', '.join(unknown)}", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
failures = ensure_enabled_channel_dependencies(names, plugins)
|
failures = ensure_repository_channel_dependencies(names, plugins)
|
||||||
for name, message in sorted(failures.items()):
|
for name, message in sorted(failures.items()):
|
||||||
print(f"{name}: {message}", file=sys.stderr)
|
print(f"{name}: {message}", file=sys.stderr)
|
||||||
return 1 if failures else 0
|
return 1 if failures else 0
|
||||||
|
|||||||
@ -308,19 +308,17 @@ class TestAppendHistoryHardCap:
|
|||||||
entry = store.read_unprocessed_history(since_cursor=0)[0]
|
entry = store.read_unprocessed_history(since_cursor=0)[0]
|
||||||
assert len(entry["content"]) <= _HISTORY_ENTRY_HARD_CAP + 50
|
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."""
|
"""Repeated oversized writes should warn only on the first occurrence."""
|
||||||
from loguru import logger as loguru_logger
|
|
||||||
|
|
||||||
records: list[str] = []
|
records: list[str] = []
|
||||||
handler_id = loguru_logger.add(lambda m: records.append(m), level="WARNING")
|
monkeypatch.setattr(
|
||||||
try:
|
"nanobot.agent.memory.logger.warning",
|
||||||
huge = "x" * (_HISTORY_ENTRY_HARD_CAP + 1)
|
lambda message, *args: records.append(message.format(*args)),
|
||||||
store.append_history(huge)
|
)
|
||||||
store.append_history(huge)
|
huge = "x" * (_HISTORY_ENTRY_HARD_CAP + 1)
|
||||||
store.append_history(huge)
|
store.append_history(huge)
|
||||||
finally:
|
store.append_history(huge)
|
||||||
loguru_logger.remove(handler_id)
|
store.append_history(huge)
|
||||||
|
|
||||||
oversize_warnings = [r for r in records if "exceeds" in r and "chars" in r]
|
oversize_warnings = [r for r in records if "exceeds" in r and "chars" in r]
|
||||||
assert len(oversize_warnings) == 1
|
assert len(oversize_warnings) == 1
|
||||||
|
|||||||
@ -1492,7 +1492,7 @@ def test_repository_dependency_installer_selects_all_channel_manifests(monkeypat
|
|||||||
monkeypatch.setattr(dependencies, "discover_plugins", lambda: plugins)
|
monkeypatch.setattr(dependencies, "discover_plugins", lambda: plugins)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
dependencies,
|
dependencies,
|
||||||
"ensure_enabled_channel_dependencies",
|
"ensure_repository_channel_dependencies",
|
||||||
lambda names, discovered: prepared.append((names, discovered)) or {},
|
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)]
|
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):
|
def test_repository_dependency_installer_rejects_unknown_channel(monkeypatch, capsys):
|
||||||
from scripts import install_channel_dependencies as dependencies
|
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, "discover_plugins", lambda: {"demo": plugin})
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
dependencies,
|
dependencies,
|
||||||
"ensure_enabled_channel_dependencies",
|
"ensure_repository_channel_dependencies",
|
||||||
lambda _names, _plugins: {"demo": "dependency install failed"},
|
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["pdf"] == ["pypdf>=5.0.0,<6.0.0"]
|
||||||
assert deps["langfuse"] == ["langfuse>=3.0.0,<4.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 = {
|
channel_names = {
|
||||||
"dingtalk",
|
"dingtalk",
|
||||||
"discord",
|
"discord",
|
||||||
|
|||||||
@ -35,6 +35,10 @@ from nanobot.webui.metadata import (
|
|||||||
runner = CliRunner()
|
runner = CliRunner()
|
||||||
|
|
||||||
|
|
||||||
|
def _without_rendered_line_breaks(output: str) -> str:
|
||||||
|
return "".join(output.splitlines())
|
||||||
|
|
||||||
|
|
||||||
def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None:
|
def test_proactive_websocket_delivery_gets_fresh_turn_id() -> None:
|
||||||
metadata = {
|
metadata = {
|
||||||
"webui": True,
|
"webui": True,
|
||||||
@ -2150,7 +2154,7 @@ def test_webui_yes_still_refuses_invalid_custom_model_setup(
|
|||||||
|
|
||||||
assert result.exit_code == 1
|
assert result.exit_code == 1
|
||||||
assert "provider/model setup is incomplete" in result.stdout
|
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 onboard --wizard" in result.stdout
|
||||||
assert "nanobot status --config" in result.stdout
|
assert "nanobot status --config" in result.stdout
|
||||||
assert config_file.name in result.stdout
|
assert config_file.name in result.stdout
|
||||||
|
|||||||
@ -174,7 +174,7 @@ def test_status_reports_missing_provider_with_shortest_setup_routes(tmp_path) ->
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Agent: ✗" in result.stdout
|
assert "Agent: ✗" in result.stdout
|
||||||
assert "No provider is configured for model" 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 onboard --wizard" in result.stdout
|
||||||
assert "nanobot status --config" in result.stdout
|
assert "nanobot status --config" in result.stdout
|
||||||
|
|
||||||
|
|||||||
@ -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):
|
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()
|
manager = ExecSessionManager()
|
||||||
exec_tool = ExecTool(working_dir=str(tmp_path), timeout=5, session_manager=manager)
|
exec_tool = ExecTool(working_dir=str(tmp_path), timeout=5, session_manager=manager)
|
||||||
stdin_tool = WriteStdinTool(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)
|
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(
|
waited = await stdin_tool.execute(
|
||||||
session_id=sid,
|
session_id=sid,
|
||||||
wait_for="never-ready",
|
wait_for="never-ready",
|
||||||
wait_timeout_ms=200,
|
wait_timeout_ms=0,
|
||||||
yield_time_ms=0,
|
yield_time_ms=0,
|
||||||
)
|
)
|
||||||
cleanup = await stdin_tool.execute(session_id=sid, terminate=True, 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 "Process running" in initial
|
||||||
assert "booting" in initial + waited
|
assert "booting" in initial + ready
|
||||||
|
assert "ready" in ready
|
||||||
assert "Process running" in waited
|
assert "Process running" in waited
|
||||||
assert "Wait target not observed: 'never-ready'" in waited
|
assert "Wait target not observed: 'never-ready'" in waited
|
||||||
assert "Session terminated." in cleanup
|
assert "Session terminated." in cleanup
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user