fix(onboard): allow empty strings and falsy values in input fields

Fixes two related input-handling bugs in the onboard wizard:

1. _input_text treated "" as None, preventing users from clearing
   optional string fields or entering empty strings intentionally.

2. _input_model_with_autocomplete used `if value else None`, which
   discarded falsy values such as empty strings or 0.

To support clearing optional string fields, add _is_str_or_none() and
normalize empty strings to None inside _configure_pydantic_model only
when the field annotation is `str | None`. Required str fields keep
"" as a valid value.

Also included:
- Remember last selected item in provider/channel/model menus for
  better UX when configuring multiple items.
- Rename _SIMPLE_TYPES and _MENU_DISPATCH to lowercase to follow
  Python naming conventions (they are local variables, not constants).
- Remove unused imports in test file.

Extracted from PR #3358.
This commit is contained in:
chengyongru
2026-05-08 13:21:51 +08:00
committed by Xubin Ren
parent 6a3069514c
commit 3a2f47d720
2 changed files with 171 additions and 14 deletions
+119 -5
View File
@@ -4,7 +4,6 @@ These tests focus on the business logic behind the onboard wizard,
without testing the interactive UI components.
"""
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any, cast
@@ -13,18 +12,15 @@ import pytest
from pydantic import BaseModel, Field
from nanobot.cli import onboard as onboard_wizard
# Import functions to test
from nanobot.cli.commands import _merge_missing_defaults
from nanobot.cli.onboard import (
_BACK_PRESSED,
_configure_pydantic_model,
_format_value,
_get_constraint_hint,
_get_field_display_name,
_get_field_type_info,
_get_constraint_hint,
_input_text,
_validate_field_constraint,
run_onboard,
)
from nanobot.config.schema import Config
@@ -960,3 +956,121 @@ class TestMainMenuUpdate:
assert result.should_save is True
assert pause_called["n"] == 1
class TestInputTextEmptyString:
"""Tests for _input_text empty-string handling bug fix."""
def test_empty_string_returned_not_none(self, monkeypatch):
"""_input_text should return empty string, not None, when user enters ''."""
monkeypatch.setattr(
onboard_wizard,
"_get_questionary",
lambda: SimpleNamespace(text=lambda *a, **kw: SimpleNamespace(ask=lambda: "")),
)
result = _input_text("Name", "old", "str")
assert result == ""
def test_none_still_returns_none(self, monkeypatch):
"""_input_text should return None when questionary returns None."""
monkeypatch.setattr(
onboard_wizard,
"_get_questionary",
lambda: SimpleNamespace(text=lambda *a, **kw: SimpleNamespace(ask=lambda: None)),
)
result = _input_text("Name", "old", "str")
assert result is None
class TestIsStrOrNone:
"""Tests for _is_str_or_none helper."""
def test_str_or_none_true(self):
from nanobot.cli.onboard import _is_str_or_none
assert _is_str_or_none(str | None) is True
def test_optional_str_true(self):
from typing import Optional
from nanobot.cli.onboard import _is_str_or_none
assert _is_str_or_none(Optional[str]) is True
def test_str_only_false(self):
from nanobot.cli.onboard import _is_str_or_none
assert _is_str_or_none(str) is False
def test_int_or_none_false(self):
from nanobot.cli.onboard import _is_str_or_none
assert _is_str_or_none(int | None) is False
class TestConfigurePydanticModelEmptyString:
"""Tests that optional string fields are cleared when empty string is entered."""
def test_optional_str_empty_string_becomes_none(self, monkeypatch):
"""Entering '' for an optional str field should set it to None."""
from pydantic import BaseModel
from nanobot.cli.onboard import _is_str_or_none
class M(BaseModel):
api_key: str | None = None
model = M(api_key="secret")
call_count = {"select": 0}
def fake_select(_prompt, choices, default=None):
call_count["select"] += 1
# First call: select the api_key field, then Done
if call_count["select"] == 1:
for c in choices:
if "Api Key" in c:
return c
return choices[0]
return "[Done]"
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select)
monkeypatch.setattr(onboard_wizard, "_show_config_panel", lambda *a, **kw: None)
# Simulate user entering empty string
monkeypatch.setattr(
onboard_wizard, "_input_with_existing", lambda *a, **kw: ""
)
result = _configure_pydantic_model(model, "Test")
assert result is not None
assert result.api_key is None
def test_required_str_empty_string_kept(self, monkeypatch):
"""Entering '' for a required str field should keep the empty string."""
from pydantic import BaseModel
class M(BaseModel):
api_key: str = ""
model = M(api_key="secret")
call_count = {"select": 0}
def fake_select(_prompt, choices, default=None):
call_count["select"] += 1
if call_count["select"] == 1:
for c in choices:
if "Api Key" in c:
return c
return choices[0]
return "[Done]"
monkeypatch.setattr(onboard_wizard, "_select_with_back", fake_select)
monkeypatch.setattr(onboard_wizard, "_show_config_panel", lambda *a, **kw: None)
monkeypatch.setattr(
onboard_wizard, "_input_with_existing", lambda *a, **kw: ""
)
result = _configure_pydantic_model(model, "Test")
assert result is not None
assert result.api_key == ""