From 3d773d905433dfb36ae4d4387aedc6d46d6c17ac Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Fri, 19 Jun 2026 00:58:25 +0800 Subject: [PATCH] fix: preserve config during non-interactive defaults onboarding Maintainer edit: make explicit --defaults refresh existing configs without prompting when no TTY is available, preserving user values for CI and Docker runs. --- docs/cli-reference.md | 8 ++++--- nanobot/cli/commands.py | 6 ++++-- tests/cli/test_commands.py | 44 +++++++++++++++++++++++++++++++++++--- 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/docs/cli-reference.md b/docs/cli-reference.md index e9b5f5ea..e4312a73 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -8,7 +8,7 @@ Use this page when you know what you want to run and need the command shape. For |---|---|---| | Check the install | `nanobot --version` | If this fails, try `python -m nanobot --version` | | Use guided setup | `nanobot onboard` | Best when you prefer prompts over hand-editing JSON | -| Create or refresh defaults | `nanobot onboard --defaults` | Creates `~/.nanobot/config.json` and `~/.nanobot/workspace/` without prompts | +| Create or refresh defaults | `nanobot onboard --defaults` | Creates `~/.nanobot/config.json` and `~/.nanobot/workspace/` without the wizard | | Check config without calling a model | `nanobot status` | Reads the default config and summarizes the active model/provider | | Send one test message | `nanobot agent -m "Hello!"` | First proof that install, config, provider, model, and workspace all work | | Chat in the terminal | `nanobot agent` | Interactive local chat; exit with `exit`, `/exit`, `:q`, or `Ctrl+D` | @@ -53,11 +53,13 @@ Long-running commands keep working until you stop them. Press `Ctrl+C` in that t | Command | Description | |---|---| | `nanobot onboard` | Use the interactive setup wizard | -| `nanobot onboard --defaults` | Initialize or refresh the default config and workspace without prompts | -| `nanobot onboard --defaults --config --workspace ` | Initialize or refresh a specific instance without prompts | +| `nanobot onboard --defaults` | Initialize or refresh the default config and workspace without the wizard | +| `nanobot onboard --defaults --config --workspace ` | Initialize or refresh a specific instance without the wizard | Without `--defaults`, `nanobot onboard` opens the wizard only when stdin and stdout are attached to a terminal. In scripts, CI, and Docker non-TTY runs, it falls back to the defaults setup. +When `--defaults` refreshes an existing config in scripts, CI, or Docker non-TTY runs, it preserves existing values without prompting. In an interactive terminal, nanobot still asks before replacing an existing config with fresh defaults. + Default paths: | Path | Default | diff --git a/nanobot/cli/commands.py b/nanobot/cli/commands.py index d8b15f4d..191c80ff 100644 --- a/nanobot/cli/commands.py +++ b/nanobot/cli/commands.py @@ -452,9 +452,11 @@ def onboard( from nanobot.config.loader import get_config_path, load_config, save_config, set_config_path from nanobot.config.schema import Config + can_prompt = _onboard_can_prompt() wants_wizard = wizard or not defaults - use_wizard = wants_wizard and not defaults and _onboard_can_prompt() + use_wizard = wants_wizard and not defaults and can_prompt default_fallback = wants_wizard and not use_wizard and not defaults + default_refresh_without_prompt = defaults and not can_prompt if default_fallback: console.print( "[yellow]No interactive terminal detected; using --defaults setup instead.[/yellow]" @@ -476,7 +478,7 @@ def onboard( if config_path.exists(): if use_wizard: config = _apply_workspace_override(load_config(config_path)) - elif default_fallback: + elif default_fallback or default_refresh_without_prompt: config = _apply_workspace_override(load_config(config_path)) save_config(config, config_path) console.print( diff --git a/tests/cli/test_commands.py b/tests/cli/test_commands.py index ea86aa6b..c14a20f2 100644 --- a/tests/cli/test_commands.py +++ b/tests/cli/test_commands.py @@ -108,10 +108,11 @@ def test_onboard_fresh_install(mock_paths): assert mock_ws.call_args.args == (expected_workspace,) -def test_onboard_existing_config_refresh(mock_paths): +def test_onboard_existing_config_refresh(mock_paths, monkeypatch): """Config exists, user declines overwrite — should refresh (load-merge-save).""" config_file, workspace_dir, _ = mock_paths config_file.write_text('{"existing": true}') + monkeypatch.setattr("nanobot.cli.commands._onboard_can_prompt", lambda: True) result = runner.invoke(app, ["onboard", "--defaults"], input="n\n") @@ -136,10 +137,46 @@ def test_onboard_non_tty_existing_config_refreshes_without_prompt(mock_paths): assert workspace_dir.exists() -def test_onboard_existing_config_overwrite(mock_paths): +def test_onboard_defaults_non_tty_existing_config_preserves_values_without_prompt( + tmp_path, monkeypatch +): + """Explicit --defaults should refresh existing configs without prompts outside a TTY.""" + config_path = tmp_path / "config.json" + workspace_path = tmp_path / "workspace" + config_path.write_text( + json.dumps({"agents": {"defaults": {"model": "custom/keep"}}}), + encoding="utf-8", + ) + + monkeypatch.setattr("nanobot.cli.commands._onboard_can_prompt", lambda: False) + monkeypatch.setattr("nanobot.channels.registry.discover_all", lambda: {}) + + result = runner.invoke( + app, + [ + "onboard", + "--defaults", + "--config", + str(config_path), + "--workspace", + str(workspace_path), + ], + ) + + assert result.exit_code == 0 + assert "Config already exists" not in result.stdout + assert "Overwrite?" not in result.stdout + assert "existing values preserved" in result.stdout + saved = Config.model_validate(json.loads(config_path.read_text(encoding="utf-8"))) + assert saved.agents.defaults.model == "custom/keep" + assert saved.workspace_path == workspace_path + + +def test_onboard_existing_config_overwrite(mock_paths, monkeypatch): """Config exists, user confirms overwrite — should reset to defaults.""" config_file, workspace_dir, _ = mock_paths config_file.write_text('{"existing": true}') + monkeypatch.setattr("nanobot.cli.commands._onboard_can_prompt", lambda: True) result = runner.invoke(app, ["onboard", "--defaults"], input="y\n") @@ -149,11 +186,12 @@ def test_onboard_existing_config_overwrite(mock_paths): assert workspace_dir.exists() -def test_onboard_existing_workspace_safe_create(mock_paths): +def test_onboard_existing_workspace_safe_create(mock_paths, monkeypatch): """Workspace exists — should not recreate, but still add missing templates.""" config_file, workspace_dir, _ = mock_paths workspace_dir.mkdir(parents=True) config_file.write_text("{}") + monkeypatch.setattr("nanobot.cli.commands._onboard_can_prompt", lambda: True) result = runner.invoke(app, ["onboard", "--defaults"], input="n\n")