mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-07 18:08:34 +00:00
fix: modernize dependency recovery guidance (#5282)
This commit is contained in:
parent
02a002a0e6
commit
ff6deda178
@ -27,7 +27,7 @@ nanobot agent -m "Hello!"
|
|||||||
Install Langfuse:
|
Install Langfuse:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pip install langfuse
|
nanobot plugins enable langfuse
|
||||||
```
|
```
|
||||||
|
|
||||||
## Minimal working example
|
## Minimal working example
|
||||||
|
|||||||
@ -549,7 +549,7 @@ This recipe applies after the agent works and you want observability for OpenAI-
|
|||||||
Install the optional package in the same Python environment that runs nanobot:
|
Install the optional package in the same Python environment that runs nanobot:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pip install langfuse
|
nanobot plugins enable langfuse
|
||||||
```
|
```
|
||||||
|
|
||||||
Set the environment variables before starting nanobot:
|
Set the environment variables before starting nanobot:
|
||||||
|
|||||||
@ -458,7 +458,10 @@ class WebSearchTool(Tool):
|
|||||||
Olostep_BaseError, # pyright: ignore[reportUnknownVariableType]
|
Olostep_BaseError, # pyright: ignore[reportUnknownVariableType]
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return ToolResult.error("Error: olostep package not installed. Run: pip install olostep")
|
return ToolResult.error(
|
||||||
|
"Error: Olostep support is not installed. "
|
||||||
|
"Run `nanobot plugins enable olostep`."
|
||||||
|
)
|
||||||
async_olostep = cast(Any, AsyncOlostep)
|
async_olostep = cast(Any, AsyncOlostep)
|
||||||
olostep_base_error = cast(type[Exception], Olostep_BaseError)
|
olostep_base_error = cast(type[Exception], Olostep_BaseError)
|
||||||
api_key = self.config.api_key or os.environ.get("OLOSTEP_API_KEY", "")
|
api_key = self.config.api_key or os.environ.get("OLOSTEP_API_KEY", "")
|
||||||
|
|||||||
@ -1683,7 +1683,10 @@ def _encrypt_aes_ecb(data: bytes, aes_key_b64: str) -> bytes:
|
|||||||
encryptor = cipher_obj.encryptor()
|
encryptor = cipher_obj.encryptor()
|
||||||
return encryptor.update(padded) + encryptor.finalize()
|
return encryptor.update(padded) + encryptor.finalize()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning("Cannot encrypt media: install 'pycryptodome' or 'cryptography'")
|
logger.warning(
|
||||||
|
"Cannot encrypt media. Run `nanobot plugins enable weixin` "
|
||||||
|
"to install WeChat support."
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
@ -1715,7 +1718,10 @@ def _decrypt_aes_ecb(data: bytes, aes_key_b64: str) -> bytes:
|
|||||||
decryptor = cipher_obj.decryptor()
|
decryptor = cipher_obj.decryptor()
|
||||||
decrypted = decryptor.update(data) + decryptor.finalize()
|
decrypted = decryptor.update(data) + decryptor.finalize()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
logger.warning("Cannot decrypt media: install 'pycryptodome' or 'cryptography'")
|
logger.warning(
|
||||||
|
"Cannot decrypt media. Run `nanobot plugins enable weixin` "
|
||||||
|
"to install WeChat support."
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
return _pkcs7_unpad_safe(decrypted)
|
return _pkcs7_unpad_safe(decrypted)
|
||||||
|
|||||||
@ -1080,6 +1080,32 @@ def test_decrypt_aes_ecb_strips_valid_pkcs7_padding() -> None:
|
|||||||
assert decrypted == plaintext
|
assert decrypted == plaintext
|
||||||
|
|
||||||
|
|
||||||
|
def test_missing_aes_dependency_recommends_weixin_plugin(monkeypatch) -> None:
|
||||||
|
real_import = __import__
|
||||||
|
|
||||||
|
def fake_import(name, *args, **kwargs):
|
||||||
|
if name.startswith(("Crypto", "cryptography")):
|
||||||
|
raise ImportError("missing AES dependency")
|
||||||
|
return real_import(name, *args, **kwargs)
|
||||||
|
|
||||||
|
warnings: list[str] = []
|
||||||
|
monkeypatch.setattr("builtins.__import__", fake_import)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
weixin_mod.logger,
|
||||||
|
"warning",
|
||||||
|
lambda message, *args: warnings.append(message.format(*args)),
|
||||||
|
)
|
||||||
|
key_b64 = "MDEyMzQ1Njc4OWFiY2RlZg=="
|
||||||
|
data = b"unencrypted media"
|
||||||
|
|
||||||
|
assert _encrypt_aes_ecb(data, key_b64) == data
|
||||||
|
assert _decrypt_aes_ecb(data, key_b64) == data
|
||||||
|
assert warnings == [
|
||||||
|
"Cannot encrypt media. Run `nanobot plugins enable weixin` to install WeChat support.",
|
||||||
|
"Cannot decrypt media. Run `nanobot plugins enable weixin` to install WeChat support.",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class _DummyDownloadResponse:
|
class _DummyDownloadResponse:
|
||||||
def __init__(self, content: bytes, status_code: int = 200) -> None:
|
def __init__(self, content: bytes, status_code: int = 200) -> None:
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|||||||
@ -32,6 +32,7 @@ from nanobot.cli.models import (
|
|||||||
)
|
)
|
||||||
from nanobot.config.loader import get_config_path, load_config, resolve_config_env_vars
|
from nanobot.config.loader import get_config_path, load_config, resolve_config_env_vars
|
||||||
from nanobot.config.schema import Config, ModelPresetConfig
|
from nanobot.config.schema import Config, ModelPresetConfig
|
||||||
|
from nanobot.providers.oauth_guidance import OAUTH_CLI_KIT_MISSING_MESSAGE
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
@ -1674,7 +1675,7 @@ def _quick_start_oauth_login(config: Config, provider_name: str) -> bool:
|
|||||||
login_oauth_interactive,
|
login_oauth_interactive,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
console.print("[red]oauth_cli_kit not installed. Run: pip install oauth-cli-kit[/red]")
|
console.print(f"[red]{OAUTH_CLI_KIT_MISSING_MESSAGE}[/red]")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import typer
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from nanobot import __logo__
|
from nanobot import __logo__
|
||||||
|
from nanobot.providers.oauth_guidance import OAUTH_CLI_KIT_MISSING_MESSAGE
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from nanobot.providers.registry import ProviderSpec
|
from nanobot.providers.registry import ProviderSpec
|
||||||
@ -74,7 +75,7 @@ def _required_module_attribute(module_name: str, attribute: str) -> object:
|
|||||||
|
|
||||||
|
|
||||||
def _load_openai_oauth_client() -> tuple[_GetOAuthToken, _LoginOAuthInteractive]:
|
def _load_openai_oauth_client() -> tuple[_GetOAuthToken, _LoginOAuthInteractive]:
|
||||||
"""Load the optional untyped OAuth client behind a typed boundary."""
|
"""Load the untyped OAuth client behind a typed boundary."""
|
||||||
return (
|
return (
|
||||||
cast(_GetOAuthToken, _required_module_attribute("oauth_cli_kit", "get_token")),
|
cast(_GetOAuthToken, _required_module_attribute("oauth_cli_kit", "get_token")),
|
||||||
cast(
|
cast(
|
||||||
@ -85,7 +86,7 @@ def _load_openai_oauth_client() -> tuple[_GetOAuthToken, _LoginOAuthInteractive]
|
|||||||
|
|
||||||
|
|
||||||
def _load_openai_oauth_storage() -> tuple[_OAuthProviderConfig, _FileTokenStorageFactory]:
|
def _load_openai_oauth_storage() -> tuple[_OAuthProviderConfig, _FileTokenStorageFactory]:
|
||||||
"""Load the optional untyped OAuth storage API behind a typed boundary."""
|
"""Load the untyped OAuth storage API behind a typed boundary."""
|
||||||
return (
|
return (
|
||||||
cast(
|
cast(
|
||||||
_OAuthProviderConfig,
|
_OAuthProviderConfig,
|
||||||
@ -241,7 +242,7 @@ def _login_openai_codex() -> None:
|
|||||||
f"[green]✓ Authenticated with OpenAI Codex[/green] [dim]{token.account_id}[/dim]"
|
f"[green]✓ Authenticated with OpenAI Codex[/green] [dim]{token.account_id}[/dim]"
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
console.print("[red]oauth_cli_kit not installed. Run: pip install oauth-cli-kit[/red]")
|
console.print(f"[red]{OAUTH_CLI_KIT_MISSING_MESSAGE}[/red]")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
|
|
||||||
@ -250,7 +251,7 @@ def _logout_openai_codex() -> None:
|
|||||||
try:
|
try:
|
||||||
provider_config, storage_factory = _load_openai_oauth_storage()
|
provider_config, storage_factory = _load_openai_oauth_storage()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
console.print("[red]oauth_cli_kit not installed. Run: pip install oauth-cli-kit[/red]")
|
console.print(f"[red]{OAUTH_CLI_KIT_MISSING_MESSAGE}[/red]")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
storage = storage_factory(token_filename=provider_config.token_filename)
|
storage = storage_factory(token_filename=provider_config.token_filename)
|
||||||
@ -309,7 +310,7 @@ def _logout_github_copilot() -> None:
|
|||||||
try:
|
try:
|
||||||
from nanobot.providers.github_copilot_provider import get_storage
|
from nanobot.providers.github_copilot_provider import get_storage
|
||||||
except ImportError:
|
except ImportError:
|
||||||
console.print("[red]oauth_cli_kit not installed. Run: pip install oauth-cli-kit[/red]")
|
console.print(f"[red]{OAUTH_CLI_KIT_MISSING_MESSAGE}[/red]")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
storage = get_storage()
|
storage = get_storage()
|
||||||
|
|||||||
6
nanobot/providers/oauth_guidance.py
Normal file
6
nanobot/providers/oauth_guidance.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"""Shared recovery guidance for OAuth dependency failures."""
|
||||||
|
|
||||||
|
OAUTH_CLI_KIT_MISSING_MESSAGE = (
|
||||||
|
"This nanobot installation is missing the required oauth-cli-kit package. "
|
||||||
|
"Reinstall or upgrade nanobot-ai using the same installation method."
|
||||||
|
)
|
||||||
@ -586,7 +586,7 @@ class OpenAICompatProvider(LLMProvider):
|
|||||||
if os.environ.get("LANGFUSE_SECRET_KEY"):
|
if os.environ.get("LANGFUSE_SECRET_KEY"):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"LANGFUSE_SECRET_KEY is set but langfuse is not installed; "
|
"LANGFUSE_SECRET_KEY is set but langfuse is not installed; "
|
||||||
"install with `pip install langfuse` to enable tracing"
|
"run `nanobot plugins enable langfuse` to enable tracing"
|
||||||
)
|
)
|
||||||
from openai import AsyncOpenAI as _AsyncOpenAI
|
from openai import AsyncOpenAI as _AsyncOpenAI
|
||||||
AsyncOpenAI = _AsyncOpenAI
|
AsyncOpenAI = _AsyncOpenAI
|
||||||
|
|||||||
@ -4,7 +4,7 @@ The WebSocket channel owns transport/authentication. This module owns the
|
|||||||
settings payload shape and the allowlisted config mutations exposed to WebUI.
|
settings payload shape and the allowlisted config mutations exposed to WebUI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# oauth-cli-kit is an optional dependency and does not publish type stubs.
|
# oauth-cli-kit does not publish type stubs.
|
||||||
# pyright: reportMissingTypeStubs=false
|
# pyright: reportMissingTypeStubs=false
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@ -36,6 +36,7 @@ from nanobot.providers.image_generation import (
|
|||||||
get_image_gen_provider,
|
get_image_gen_provider,
|
||||||
image_gen_provider_names,
|
image_gen_provider_names,
|
||||||
)
|
)
|
||||||
|
from nanobot.providers.oauth_guidance import OAUTH_CLI_KIT_MISSING_MESSAGE
|
||||||
from nanobot.providers.registry import PROVIDERS, create_dynamic_spec, find_by_name
|
from nanobot.providers.registry import PROVIDERS, create_dynamic_spec, find_by_name
|
||||||
from nanobot.security.network import is_loopback_host
|
from nanobot.security.network import is_loopback_host
|
||||||
from nanobot.security.workspace_access import workspace_sandbox_status
|
from nanobot.security.workspace_access import workspace_sandbox_status
|
||||||
@ -1794,9 +1795,7 @@ def login_oauth_provider(query: QueryParams) -> dict[str, Any]:
|
|||||||
try:
|
try:
|
||||||
from nanobot.providers.openai_codex_oauth import start_openai_codex_oauth_login
|
from nanobot.providers.openai_codex_oauth import start_openai_codex_oauth_login
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise WebUISettingsError(
|
raise WebUISettingsError(OAUTH_CLI_KIT_MISSING_MESSAGE, status=500) from None
|
||||||
"oauth_cli_kit not installed. Run: pip install oauth-cli-kit", status=500
|
|
||||||
) from None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
proxy = resolve_config_env_vars(load_config()).providers.openai_codex.proxy or None
|
proxy = resolve_config_env_vars(load_config()).providers.openai_codex.proxy or None
|
||||||
@ -1834,9 +1833,7 @@ def login_oauth_provider(query: QueryParams) -> dict[str, Any]:
|
|||||||
login_github_copilot,
|
login_github_copilot,
|
||||||
)
|
)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise WebUISettingsError(
|
raise WebUISettingsError(OAUTH_CLI_KIT_MISSING_MESSAGE, status=500) from None
|
||||||
"oauth_cli_kit not installed. Run: pip install oauth-cli-kit", status=500
|
|
||||||
) from None
|
|
||||||
|
|
||||||
token = get_github_copilot_login_status()
|
token = get_github_copilot_login_status()
|
||||||
if not token:
|
if not token:
|
||||||
@ -1934,18 +1931,14 @@ def logout_oauth_provider(query: QueryParams) -> dict[str, Any]:
|
|||||||
from oauth_cli_kit.providers import OPENAI_CODEX_PROVIDER
|
from oauth_cli_kit.providers import OPENAI_CODEX_PROVIDER
|
||||||
from oauth_cli_kit.storage import FileTokenStorage
|
from oauth_cli_kit.storage import FileTokenStorage
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise WebUISettingsError(
|
raise WebUISettingsError(OAUTH_CLI_KIT_MISSING_MESSAGE, status=500) from None
|
||||||
"oauth_cli_kit not installed. Run: pip install oauth-cli-kit", status=500
|
|
||||||
) from None
|
|
||||||
_clear_webui_oauth_flows(spec.name)
|
_clear_webui_oauth_flows(spec.name)
|
||||||
token_path = FileTokenStorage(token_filename=OPENAI_CODEX_PROVIDER.token_filename).get_token_path()
|
token_path = FileTokenStorage(token_filename=OPENAI_CODEX_PROVIDER.token_filename).get_token_path()
|
||||||
elif spec.name == "github_copilot":
|
elif spec.name == "github_copilot":
|
||||||
try:
|
try:
|
||||||
from nanobot.providers.github_copilot_provider import get_storage
|
from nanobot.providers.github_copilot_provider import get_storage
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise WebUISettingsError(
|
raise WebUISettingsError(OAUTH_CLI_KIT_MISSING_MESSAGE, status=500) from None
|
||||||
"oauth_cli_kit not installed. Run: pip install oauth-cli-kit", status=500
|
|
||||||
) from None
|
|
||||||
token_path = get_storage().get_token_path()
|
token_path = get_storage().get_token_path()
|
||||||
elif spec.name == "xai_grok":
|
elif spec.name == "xai_grok":
|
||||||
from nanobot.providers.xai_oauth import logout_xai_oauth
|
from nanobot.providers.xai_oauth import logout_xai_oauth
|
||||||
|
|||||||
@ -1092,6 +1092,23 @@ class TestMainMenuUpdate:
|
|||||||
assert config.providers.openai.api_key == "${UNRELATED_MISSING_KEY}"
|
assert config.providers.openai.api_key == "${UNRELATED_MISSING_KEY}"
|
||||||
assert config.providers.openai_codex.proxy == "${CODEX_PROXY}"
|
assert config.providers.openai_codex.proxy == "${CODEX_PROXY}"
|
||||||
|
|
||||||
|
def test_quick_start_openai_codex_reports_incomplete_installation(self, monkeypatch):
|
||||||
|
import oauth_cli_kit
|
||||||
|
|
||||||
|
messages: list[str] = []
|
||||||
|
monkeypatch.delattr(oauth_cli_kit, "get_token")
|
||||||
|
monkeypatch.setattr(
|
||||||
|
onboard_wizard.console,
|
||||||
|
"print",
|
||||||
|
lambda message, *args, **kwargs: messages.append(str(message)),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert onboard_wizard._quick_start_oauth_login(Config(), "openai_codex") is False
|
||||||
|
assert messages == [
|
||||||
|
"[red]This nanobot installation is missing the required oauth-cli-kit package. "
|
||||||
|
"Reinstall or upgrade nanobot-ai using the same installation method.[/red]"
|
||||||
|
]
|
||||||
|
|
||||||
def test_quick_start_openai_codex_runs_interactive_login_for_bad_cached_token(
|
def test_quick_start_openai_codex_runs_interactive_login_for_bad_cached_token(
|
||||||
self, monkeypatch
|
self, monkeypatch
|
||||||
):
|
):
|
||||||
|
|||||||
@ -686,7 +686,10 @@ def test_provider_login_openai_codex_handles_missing_oauth_symbol(monkeypatch):
|
|||||||
result = runner.invoke(app, ["provider", "login", "openai-codex"])
|
result = runner.invoke(app, ["provider", "login", "openai-codex"])
|
||||||
|
|
||||||
assert result.exit_code == 1
|
assert result.exit_code == 1
|
||||||
assert "oauth_cli_kit not installed" in result.stdout
|
assert (
|
||||||
|
"This nanobot installation is missing the required oauth-cli-kit package. "
|
||||||
|
"Reinstall or upgrade nanobot-ai using the same installation method."
|
||||||
|
) in re.sub(r"\s+", " ", result.stdout)
|
||||||
assert result.exception is not None
|
assert result.exception is not None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from unittest.mock import patch, sentinel
|
from unittest.mock import patch, sentinel
|
||||||
|
|
||||||
|
from nanobot.providers import openai_compat_provider
|
||||||
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
||||||
from nanobot.providers.registry import ProviderSpec
|
from nanobot.providers.registry import ProviderSpec
|
||||||
|
|
||||||
@ -59,3 +60,22 @@ async def test_openai_compat_provider_timeout_can_be_overridden_by_env(monkeypat
|
|||||||
await provider._ensure_client()
|
await provider._ensure_client()
|
||||||
|
|
||||||
assert mock_async_openai.call_args.kwargs["timeout"] == 45.0
|
assert mock_async_openai.call_args.kwargs["timeout"] == 45.0
|
||||||
|
|
||||||
|
|
||||||
|
async def test_missing_langfuse_warning_recommends_plugin_command(monkeypatch) -> None:
|
||||||
|
monkeypatch.setenv("LANGFUSE_SECRET_KEY", "secret")
|
||||||
|
monkeypatch.setattr(openai_compat_provider, "AsyncOpenAI", None)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("importlib.util.find_spec", return_value=None),
|
||||||
|
patch("openai.AsyncOpenAI") as mock_async_openai,
|
||||||
|
patch("nanobot.providers.openai_compat_provider.logger.warning") as mock_warning,
|
||||||
|
):
|
||||||
|
provider = OpenAICompatProvider(api_key="test-key", api_base="https://example.com/v1")
|
||||||
|
await provider._ensure_client()
|
||||||
|
|
||||||
|
mock_warning.assert_called_once_with(
|
||||||
|
"LANGFUSE_SECRET_KEY is set but langfuse is not installed; "
|
||||||
|
"run `nanobot plugins enable langfuse` to enable tracing"
|
||||||
|
)
|
||||||
|
mock_async_openai.assert_called_once()
|
||||||
|
|||||||
@ -820,4 +820,6 @@ async def test_olostep_package_missing_returns_install_hint(monkeypatch):
|
|||||||
tool = _tool(provider="olostep", api_key="olostep-key")
|
tool = _tool(provider="olostep", api_key="olostep-key")
|
||||||
result = await tool.execute(query="test query")
|
result = await tool.execute(query="test query")
|
||||||
|
|
||||||
assert result == "Error: olostep package not installed. Run: pip install olostep"
|
assert result == (
|
||||||
|
"Error: Olostep support is not installed. Run `nanobot plugins enable olostep`."
|
||||||
|
)
|
||||||
|
|||||||
@ -1624,7 +1624,10 @@ def test_openai_codex_oauth_login_reports_missing_oauth_cli_kit(
|
|||||||
with pytest.raises(WebUISettingsError) as exc:
|
with pytest.raises(WebUISettingsError) as exc:
|
||||||
login_oauth_provider({"provider": ["openai-codex"]})
|
login_oauth_provider({"provider": ["openai-codex"]})
|
||||||
|
|
||||||
assert "oauth_cli_kit not installed. Run: pip install oauth-cli-kit" in str(exc.value)
|
assert str(exc.value) == (
|
||||||
|
"This nanobot installation is missing the required oauth-cli-kit package. "
|
||||||
|
"Reinstall or upgrade nanobot-ai using the same installation method."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_github_copilot_oauth_login_reports_missing_oauth_cli_kit(
|
def test_github_copilot_oauth_login_reports_missing_oauth_cli_kit(
|
||||||
@ -1642,7 +1645,10 @@ def test_github_copilot_oauth_login_reports_missing_oauth_cli_kit(
|
|||||||
with pytest.raises(WebUISettingsError) as exc:
|
with pytest.raises(WebUISettingsError) as exc:
|
||||||
login_oauth_provider({"provider": ["github-copilot"]})
|
login_oauth_provider({"provider": ["github-copilot"]})
|
||||||
|
|
||||||
assert "oauth_cli_kit not installed. Run: pip install oauth-cli-kit" in str(exc.value)
|
assert str(exc.value) == (
|
||||||
|
"This nanobot installation is missing the required oauth-cli-kit package. "
|
||||||
|
"Reinstall or upgrade nanobot-ai using the same installation method."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_xai_grok_login_starts_fresh_browser_flow_with_proxy(
|
def test_xai_grok_login_starts_fresh_browser_flow_with_proxy(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user