From bf0ab93b06c395dec1b155ba46dd8e80352a19df Mon Sep 17 00:00:00 2001 From: Re-bin Date: Sun, 8 Mar 2026 03:24:15 +0000 Subject: [PATCH] Merge branch 'main' into pr-1635 --- README.md | 13 ++++++++--- nanobot/cli/commands.py | 22 ++++++++--------- tests/test_commands.py | 52 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index bc11cc8..13971e2 100644 --- a/README.md +++ b/README.md @@ -724,7 +724,10 @@ nanobot provider login openai-codex nanobot agent -m "Hello!" # Target a specific workspace/config locally -nanobot agent -w ~/.nanobot/botA -c ~/.nanobot/botA/config.json -m "Hello!" +nanobot agent -c ~/.nanobot-telegram/config.json -m "Hello!" + +# One-off workspace override on top of that config +nanobot agent -c ~/.nanobot-telegram/config.json -w /tmp/nanobot-telegram-test -m "Hello!" ``` > Docker users: use `docker run -it` for interactive OAuth login. @@ -930,11 +933,15 @@ When using `--config`, nanobot derives its runtime data directory from the confi To open a CLI session against one of these instances locally: ```bash -nanobot agent -w ~/.nanobot/botA -m "Hello from botA" -nanobot agent -w ~/.nanobot/botC -c ~/.nanobot/botC/config.json +nanobot agent -c ~/.nanobot-telegram/config.json -m "Hello from Telegram instance" +nanobot agent -c ~/.nanobot-discord/config.json -m "Hello from Discord instance" + +# Optional one-off workspace override +nanobot agent -c ~/.nanobot-telegram/config.json -w /tmp/nanobot-telegram-test ``` > `nanobot agent` starts a local CLI agent using the selected workspace/config. It does not attach to or proxy through an already running `nanobot gateway` process. + | Component | Resolved From | Example | |-----------|---------------|---------| | **Config** | `--config` path | `~/.nanobot-A/config.json` | diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index d03ef93..2c8d6d3 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -266,9 +266,17 @@ def _make_provider(config: Config): def _load_runtime_config(config: str | None = None, workspace: str | None = None) -> Config: """Load config and optionally override the active workspace.""" - from nanobot.config.loader import load_config + from nanobot.config.loader import load_config, set_config_path + + config_path = None + if config: + config_path = Path(config).expanduser().resolve() + if not config_path.exists(): + console.print(f"[red]Error: Config file not found: {config_path}[/red]") + raise typer.Exit(1) + set_config_path(config_path) + console.print(f"[dim]Using config: {config_path}[/dim]") - config_path = Path(config) if config else None loaded = load_config(config_path) if workspace: loaded.agents.defaults.workspace = workspace @@ -288,16 +296,6 @@ def gateway( config: str | None = typer.Option(None, "--config", "-c", help="Path to config file"), ): """Start the nanobot gateway.""" - # Set config path if provided (must be done before any imports that use get_data_dir) - if config: - from nanobot.config.loader import set_config_path - config_path = Path(config).expanduser().resolve() - if not config_path.exists(): - console.print(f"[red]Error: Config file not found: {config_path}[/red]") - raise typer.Exit(1) - set_config_path(config_path) - console.print(f"[dim]Using config: {config_path}[/dim]") - from nanobot.agent.loop import AgentLoop from nanobot.bus.queue import MessageBus from nanobot.channels.manager import ChannelManager diff --git a/tests/test_commands.py b/tests/test_commands.py index e3709da..19c1998 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -191,13 +191,52 @@ def test_agent_uses_default_config_when_no_workspace_or_config_flags(mock_agent_ mock_agent_runtime["print_response"].assert_called_once_with("mock-response", render_markdown=True) -def test_agent_uses_explicit_config_path(mock_agent_runtime): - config_path = Path("/tmp/agent-config.json") +def test_agent_uses_explicit_config_path(mock_agent_runtime, tmp_path: Path): + config_path = tmp_path / "agent-config.json" + config_path.write_text("{}") result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_path)]) assert result.exit_code == 0 - assert mock_agent_runtime["load_config"].call_args.args == (config_path,) + assert mock_agent_runtime["load_config"].call_args.args == (config_path.resolve(),) + + +def test_agent_config_sets_active_path(monkeypatch, tmp_path: Path) -> None: + config_file = tmp_path / "instance" / "config.json" + config_file.parent.mkdir(parents=True) + config_file.write_text("{}") + + config = Config() + seen: dict[str, Path] = {} + + monkeypatch.setattr( + "nanobot.config.loader.set_config_path", + lambda path: seen.__setitem__("config_path", path), + ) + monkeypatch.setattr("nanobot.config.loader.load_config", lambda _path=None: config) + monkeypatch.setattr("nanobot.config.paths.get_cron_dir", lambda: config_file.parent / "cron") + monkeypatch.setattr("nanobot.cli.commands.sync_workspace_templates", lambda _path: None) + monkeypatch.setattr("nanobot.cli.commands._make_provider", lambda _config: object()) + monkeypatch.setattr("nanobot.bus.queue.MessageBus", lambda: object()) + monkeypatch.setattr("nanobot.cron.service.CronService", lambda _store: object()) + + class _FakeAgentLoop: + def __init__(self, *args, **kwargs) -> None: + pass + + async def process_direct(self, *_args, **_kwargs) -> str: + return "ok" + + async def close_mcp(self) -> None: + return None + + monkeypatch.setattr("nanobot.agent.loop.AgentLoop", _FakeAgentLoop) + monkeypatch.setattr("nanobot.cli.commands._print_agent_response", lambda *_args, **_kwargs: None) + + result = runner.invoke(app, ["agent", "-m", "hello", "-c", str(config_file)]) + + assert result.exit_code == 0 + assert seen["config_path"] == config_file.resolve() def test_agent_overrides_workspace_path(mock_agent_runtime): @@ -211,8 +250,9 @@ def test_agent_overrides_workspace_path(mock_agent_runtime): assert mock_agent_runtime["agent_loop_cls"].call_args.kwargs["workspace"] == workspace_path -def test_agent_workspace_override_wins_over_config_workspace(mock_agent_runtime): - config_path = Path("/tmp/agent-config.json") +def test_agent_workspace_override_wins_over_config_workspace(mock_agent_runtime, tmp_path: Path): + config_path = tmp_path / "agent-config.json" + config_path.write_text("{}") workspace_path = Path("/tmp/agent-workspace") result = runner.invoke( @@ -221,7 +261,7 @@ def test_agent_workspace_override_wins_over_config_workspace(mock_agent_runtime) ) assert result.exit_code == 0 - assert mock_agent_runtime["load_config"].call_args.args == (config_path,) + assert mock_agent_runtime["load_config"].call_args.args == (config_path.resolve(),) assert mock_agent_runtime["config"].agents.defaults.workspace == str(workspace_path) assert mock_agent_runtime["sync_templates"].call_args.args == (workspace_path,) assert mock_agent_runtime["agent_loop_cls"].call_args.kwargs["workspace"] == workspace_path