This commit is contained in:
2025-10-22 13:40:26 +03:00
parent f38da15bdb
commit 4f5481fa70
8 changed files with 386 additions and 120 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ arg_parser: ArgParser = ArgParser(
) )
app: App = App( app: App = App(
dividing_line=DynamicDividingLine(), dividing_line=DynamicDividingLine(),
autocompleter=AutoCompleter(), autocompleter=AutoCompleter(history_filename="history.txt")
) )
orchestrator: Orchestrator = Orchestrator(arg_parser) orchestrator: Orchestrator = Orchestrator(arg_parser)
+1 -1
View File
@@ -6,7 +6,7 @@ from argenta.router.defaults import system_router
from dishka import FromDishka from dishka import FromDishka
work_router: Router = Router(title="Work points:", disable_redirect_stdout=True) work_router: Router = Router(title="Work points:")
flag = Flag("csdv", possible_values=PossibleValues.NEITHER) flag = Flag("csdv", possible_values=PossibleValues.NEITHER)
+14 -3
View File
@@ -7,7 +7,8 @@ from typing import Never
class AutoCompleter: class AutoCompleter:
def __init__( def __init__(
self, history_filename: str | None = None, autocomplete_button: str = "tab" self, history_filename: str | None = None,
autocomplete_button: str = "tab"
) -> None: ) -> None:
""" """
Public. Configures and implements auto-completion of input command Public. Configures and implements auto-completion of input command
@@ -60,12 +61,16 @@ class AutoCompleter:
else: else:
for line in all_commands: for line in all_commands:
readline.add_history(line) readline.add_history(line)
if not self.history_filename:
for line in all_commands:
readline.add_history(line)
readline.set_completer(self._complete) readline.set_completer(self._complete)
readline.set_completer_delims(readline.get_completer_delims().replace(" ", "")) readline.set_completer_delims(readline.get_completer_delims().replace(" ", ""))
readline.parse_and_bind(f"{self.autocomplete_button}: complete") readline.parse_and_bind(f"{self.autocomplete_button}: complete")
def exit_setup(self, all_commands: list[str]) -> None: def exit_setup(self, all_commands: list[str], ignore_command_register: bool) -> None:
""" """
Private. Exit setup function Private. Exit setup function
:return: None :return: None
@@ -76,10 +81,16 @@ class AutoCompleter:
raw_history = history_file.read() raw_history = history_file.read()
pretty_history: list[str] = [] pretty_history: list[str] = []
for line in set(raw_history.strip().split("\n")): for line in set(raw_history.strip().split("\n")):
if line.split()[0] in all_commands: if _is_command_exist(line.split()[0], all_commands, ignore_command_register):
pretty_history.append(line) pretty_history.append(line)
with open(self.history_filename, "w") as history_file: with open(self.history_filename, "w") as history_file:
_ = history_file.write("\n".join(pretty_history)) _ = history_file.write("\n".join(pretty_history))
def _is_command_exist(command: str, existing_commands: list[str], ignore_command_register: bool) -> bool:
if ignore_command_register:
return command.lower() in existing_commands
return command in existing_commands
def _get_history_items() -> list[str] | list[Never]: def _get_history_items() -> list[str] | list[Never]:
""" """
+2 -1
View File
@@ -465,7 +465,8 @@ class App(BaseApp):
if self._is_exit_command(input_command): if self._is_exit_command(input_command):
system_router.finds_appropriate_handler(input_command) system_router.finds_appropriate_handler(input_command)
self._autocompleter.exit_setup( self._autocompleter.exit_setup(
list(self._current_matching_triggers_with_routers.keys()) list(self._current_matching_triggers_with_routers.keys()),
self._ignore_command_register
) )
return return
@@ -75,10 +75,10 @@ class BooleanArgument(BaseArgument):
class InputArgument: class InputArgument:
def __init__(self, name: str, def __init__(self, name: str,
value: str | None, value: str | Literal[True],
founder_class: type[BaseArgument]) -> None: founder_class: type[BaseArgument]) -> None:
self.name: str = name self.name: str = name
self.value: str | None = value self.value: str | Literal[True] = value
self.founder_class: type[BaseArgument] = founder_class self.founder_class: type[BaseArgument] = founder_class
def __str__(self) -> str: def __str__(self) -> str:
+1 -1
View File
@@ -65,7 +65,7 @@ class ArgParser:
def _parse_args(self) -> None: def _parse_args(self) -> None:
self.parsed_argspace = ArgSpace.from_namespace(namespace=self._core.parse_args(), self.parsed_argspace = ArgSpace.from_namespace(namespace=self._core.parse_args(),
processed_args=self.processed_args) processed_args=self.processed_args)
def _register_args(self, processed_args: list[ValueArgument | BooleanArgument]) -> None: def _register_args(self, processed_args: list[ValueArgument | BooleanArgument]) -> None:
for arg in processed_args: for arg in processed_args:
+169 -111
View File
@@ -1,18 +1,21 @@
import unittest import pytest
from unittest.mock import MagicMock, patch
from argparse import Namespace from argparse import Namespace
from unittest.mock import MagicMock, call
from argenta.orchestrator.argparser.entity import ArgParser, ArgSpace from argenta.orchestrator.argparser.entity import ArgParser, ArgSpace
from argenta.orchestrator.argparser.arguments.models import ( from argenta.orchestrator.argparser.arguments.models import (
ValueArgument, ValueArgument,
BooleanArgument, BooleanArgument,
InputArgument, InputArgument,
BaseArgument BaseArgument,
) )
class TestArgumentClasses(unittest.TestCase): class TestArgumentCreation:
"""Tests for the creation and attribute validation of argument model classes."""
def test_value_argument_creation(self): def test_value_argument_creation(self):
"""Ensures ValueArgument instances are created with correct attributes."""
arg = ValueArgument( arg = ValueArgument(
name="test_arg", name="test_arg",
prefix="--", prefix="--",
@@ -20,145 +23,200 @@ class TestArgumentClasses(unittest.TestCase):
possible_values=["one", "two"], possible_values=["one", "two"],
default="one", default="one",
is_required=True, is_required=True,
is_deprecated=False is_deprecated=False,
) )
self.assertEqual(arg.name, "test_arg") assert arg.name == "test_arg"
self.assertEqual(arg.prefix, "--") assert arg.prefix == "--"
self.assertEqual(arg.help, "A test argument.") assert arg.help == "A test argument."
self.assertEqual(arg.possible_values, ["one", "two"]) assert arg.possible_values == ["one", "two"]
self.assertEqual(arg.default, "one") assert arg.default == "one"
self.assertTrue(arg.is_required) assert arg.is_required is True
self.assertFalse(arg.is_deprecated) assert arg.is_deprecated is False
self.assertEqual(arg.action, "store") assert arg.action == "store"
self.assertEqual(arg.string_entity, "--test_arg") assert arg.string_entity == "--test_arg"
def test_boolean_argument_creation(self): def test_boolean_argument_creation(self):
"""Ensures BooleanArgument instances are created with correct attributes."""
arg = BooleanArgument( arg = BooleanArgument(
name="verbose", name="verbose", prefix="-", help="Enable verbose mode.", is_deprecated=True
prefix="-",
help="Enable verbose mode.",
is_deprecated=True
) )
self.assertEqual(arg.name, "verbose") assert arg.name == "verbose"
self.assertEqual(arg.prefix, "-") assert arg.prefix == "-"
self.assertEqual(arg.help, "Enable verbose mode.") assert arg.help == "Enable verbose mode."
self.assertTrue(arg.is_deprecated) assert arg.is_deprecated is True
self.assertEqual(arg.action, "store_true") assert arg.action == "store_true"
self.assertEqual(arg.string_entity, "-verbose") assert arg.string_entity == "-verbose"
def test_input_argument_creation(self): def test_input_argument_creation(self):
"""Ensures InputArgument instances are created with correct attributes."""
arg = InputArgument( arg = InputArgument(
name="file", name="file", value="/path/to/file", founder_class=ValueArgument
value="/path/to/file",
founder_class=ValueArgument
) )
self.assertEqual(arg.name, "file") assert arg.name == "file"
self.assertEqual(arg.value, "/path/to/file") assert arg.value == "/path/to/file"
self.assertEqual(arg.founder_class, ValueArgument) assert arg.founder_class is ValueArgument
class TestArgParser(unittest.TestCase): class TestArgSpace:
def setUp(self): """Tests for the ArgSpace class, which holds parsed argument values."""
self.value_arg = ValueArgument(name="config", help="Path to config file")
self.bool_arg = BooleanArgument(name="debug", help="Enable debug mode")
self.processed_args = [self.value_arg, self.bool_arg]
def test_argparser_initialization(self): @pytest.fixture
parser = ArgParser( def mock_arguments(self) -> list[InputArgument]:
processed_args=self.processed_args, """Provides a list of mock InputArgument objects for testing."""
name="TestApp", return [
description="A test application.", InputArgument(name="arg1", value="val1", founder_class=ValueArgument),
epilog="Test epilog." InputArgument(name="arg2", value=True, founder_class=BooleanArgument),
) InputArgument(name="arg3", value="val3", founder_class=ValueArgument),
self.assertEqual(parser.name, "TestApp") ]
self.assertEqual(parser.description, "A test application.")
self.assertEqual(parser.epilog, "Test epilog.")
self.assertEqual(parser.processed_args, self.processed_args)
@patch('argenta.orchestrator.argparser.entity.ArgumentParser.parse_args') @pytest.fixture
def test_parse_args(self, mock_parse_args: MagicMock): def arg_space(self, mock_arguments: list[InputArgument]) -> ArgSpace:
mock_namespace = Namespace(config='config.json', debug=True) """Provides a pre-populated ArgSpace instance."""
mock_parse_args.return_value = mock_namespace return ArgSpace(all_arguments=mock_arguments)
parser = ArgParser(processed_args=self.processed_args) def test_initialization(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]):
arg_space = parser.parse_args() """Tests if ArgSpace is initialized correctly with a list of arguments."""
assert len(arg_space.all_arguments) == 3
assert arg_space.all_arguments == mock_arguments
self.assertIsInstance(arg_space, ArgSpace) def test_get_by_name(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]):
self.assertEqual(len(arg_space.all_arguments), 2) """Tests retrieving an argument by its name."""
found_arg = arg_space.get_by_name("arg1")
assert found_arg is not None
assert found_arg == mock_arguments[0]
def test_get_by_name_not_found(self, arg_space: ArgSpace):
"""Tests that get_by_name returns None for a non-existent argument."""
found_arg = arg_space.get_by_name("non_existent_arg")
assert found_arg is None
def test_get_by_type(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]):
"""Tests retrieving arguments based on their founder class type."""
value_args = arg_space.get_by_type(ValueArgument)
assert len(value_args) == 2
assert mock_arguments[0] in value_args
assert mock_arguments[2] in value_args
bool_args = arg_space.get_by_type(BooleanArgument)
assert len(bool_args) == 1
assert mock_arguments[1] in bool_args
def test_get_by_type_not_found(self, arg_space: ArgSpace):
"""Tests that get_by_type returns an empty list for an unused argument type."""
class OtherArgument(BaseArgument):
pass
other_args = arg_space.get_by_type(OtherArgument)
assert other_args == []
def test_from_namespace(self):
"""Tests the class method for creating an ArgSpace from an argparse.Namespace."""
namespace = Namespace(config="config.json", debug=True, verbose=False)
processed_args = [
ValueArgument(name="config", prefix="--"),
BooleanArgument(name="debug", prefix="-"),
BooleanArgument(name="verbose", prefix="-"),
]
arg_space = ArgSpace.from_namespace(namespace, processed_args)
assert len(arg_space.all_arguments) == 3
config_arg = arg_space.get_by_name('config') config_arg = arg_space.get_by_name('config')
debug_arg = arg_space.get_by_name('debug') debug_arg = arg_space.get_by_name('debug')
self.assertIsNotNone(config_arg) assert config_arg is not None
if config_arg: assert config_arg.value == "config.json"
self.assertEqual(config_arg.value, 'config.json') assert config_arg.founder_class is ValueArgument
self.assertEqual(config_arg.founder_class, ValueArgument)
self.assertIsNotNone(debug_arg) assert debug_arg is not None
if debug_arg: assert debug_arg.value is True
self.assertTrue(debug_arg.value) assert debug_arg.founder_class is BooleanArgument
self.assertEqual(debug_arg.founder_class, BooleanArgument)
class TestArgSpace(unittest.TestCase): class TestArgParser:
def setUp(self): """Tests for the ArgParser class, which orchestrates argument parsing."""
self.input_arg1 = InputArgument(name="arg1", value="val1", founder_class=ValueArgument)
self.input_arg2 = InputArgument(name="arg2", value="val2", founder_class=BooleanArgument)
self.input_arg3 = InputArgument(name="arg3", value="val3", founder_class=ValueArgument)
self.arg_space = ArgSpace(all_arguments=[self.input_arg1, self.input_arg2, self.input_arg3])
def test_argspace_initialization(self): @pytest.fixture
self.assertEqual(len(self.arg_space.all_arguments), 3) def value_arg(self) -> ValueArgument:
self.assertIn(self.input_arg1, self.arg_space.all_arguments) """Provides a sample ValueArgument."""
self.assertIn(self.input_arg2, self.arg_space.all_arguments) return ValueArgument(name="config", help="Path to config file", default="dev.json", is_required=False, possible_values=["dev.json", "prod.json"])
self.assertIn(self.input_arg3, self.arg_space.all_arguments)
def test_get_by_name(self): @pytest.fixture
found_arg = self.arg_space.get_by_name("arg1") def bool_arg(self) -> BooleanArgument:
self.assertIsNotNone(found_arg) """Provides a sample BooleanArgument."""
if found_arg: return BooleanArgument(name="debug", help="Enable debug mode")
self.assertEqual(found_arg, self.input_arg1)
def test_get_by_name_not_found(self): @pytest.fixture
found_arg = self.arg_space.get_by_name("non_existent_arg") def processed_args(self, value_arg: ValueArgument, bool_arg: BooleanArgument) -> list:
self.assertIsNone(found_arg) """Provides a list of processed arguments."""
return [value_arg, bool_arg]
def test_get_by_type(self): def test_initialization(self, processed_args: list):
value_args = self.arg_space.get_by_type(ValueArgument) """Tests that the ArgParser constructor correctly assigns attributes."""
self.assertEqual(len(value_args), 2) parser = ArgParser(
self.assertIn(self.input_arg1, value_args) processed_args=processed_args,
self.assertIn(self.input_arg3, value_args) name="TestApp",
description="A test application.",
epilog="Test epilog.",
)
assert parser.name == "TestApp"
assert parser.description == "A test application."
assert parser.epilog == "Test epilog."
assert parser.processed_args == processed_args
assert isinstance(parser.parsed_argspace, ArgSpace)
assert parser.parsed_argspace.all_arguments == []
bool_args = self.arg_space.get_by_type(BooleanArgument) def test_register_args(self, mocker, value_arg: ValueArgument, bool_arg: BooleanArgument):
self.assertEqual(len(bool_args), 1) """Tests that arguments are correctly registered with the underlying ArgumentParser."""
self.assertIn(self.input_arg2, bool_args) mock_add_argument = mocker.patch("argparse.ArgumentParser.add_argument")
def test_get_by_type_not_found(self): parser = ArgParser(processed_args=[value_arg, bool_arg])
class OtherArgument(BaseArgument):
pass
other_args = self.arg_space.get_by_type(OtherArgument) expected_calls = [
self.assertEqual(len(other_args), 0) # Call for the ValueArgument
call(
def test_from_namespace(self): value_arg.string_entity,
namespace = Namespace(arg1="val1", debug=True) action=value_arg.action,
processed_args = [ help=value_arg.help,
ValueArgument(name="arg1", prefix="--"), default=value_arg.default,
BooleanArgument(name="debug", prefix="-") choices=value_arg.possible_values,
required=value_arg.is_required,
deprecated=value_arg.is_deprecated
),
# Call for the BooleanArgument
call(
bool_arg.string_entity,
action=bool_arg.action,
help=bool_arg.help,
deprecated=bool_arg.is_deprecated
)
] ]
mock_add_argument.assert_has_calls(expected_calls, any_order=True)
arg_space = ArgSpace.from_namespace(namespace, processed_args) def test_parse_args_populates_argspace(self, mocker, processed_args: list):
self.assertEqual(len(arg_space.all_arguments), 2) """Tests that _parse_args correctly calls the parser and populates the ArgSpace."""
# 1. Mock the return value of the internal argparse instance
mock_namespace = Namespace(config='config.json', debug=True)
mocker.patch(
'argparse.ArgumentParser.parse_args',
return_value=mock_namespace
)
arg1 = arg_space.get_by_name('arg1') # 2. Initialize the parser and call the method under test
parser = ArgParser(processed_args=processed_args)
parser._parse_args() # Test the private method that contains the logic
# 3. Assert the results
arg_space = parser.parsed_argspace
assert isinstance(arg_space, ArgSpace)
assert len(arg_space.all_arguments) == 2
config_arg = arg_space.get_by_name('config')
debug_arg = arg_space.get_by_name('debug') debug_arg = arg_space.get_by_name('debug')
self.assertIsNotNone(arg1) assert config_arg is not None
if arg1: assert config_arg.value == 'config.json'
self.assertEqual(arg1.value, "val1") assert config_arg.founder_class is ValueArgument
self.assertEqual(arg1.founder_class, ValueArgument)
self.assertIsNotNone(debug_arg) assert debug_arg is not None
if debug_arg: assert debug_arg.value is True
self.assertTrue(debug_arg.value) assert debug_arg.founder_class is BooleanArgument
self.assertEqual(debug_arg.founder_class, BooleanArgument)
+196
View File
@@ -0,0 +1,196 @@
import os
from unittest.mock import MagicMock, patch, call
import pytest
# Since readline is not available on all platforms (e.g., Windows) for testing,
# it is mocked for all tests.
readline_mock = MagicMock()
# We patch the module where it's imported, not where it's defined.
@pytest.fixture
def mock_readline():
"""Fixture to provide a mock of the `readline` module."""
with patch('argenta.app.autocompleter.entity.readline', readline_mock) as mock:
# This nested state simulates readline's internal history list.
_history = []
def add_history(item: str) -> None:
_history.append(item)
def get_history_item(index: int) -> str | None:
# readline history is 1-based.
if 1 <= index <= len(_history):
return _history[index - 1]
return None
def get_current_history_length() -> int:
return len(_history)
def clear_history() -> None:
_history.clear()
# Reset all mocks and the internal history before each test.
mock.reset_mock()
clear_history()
# Apply side effects to mock functions to simulate real behavior.
mock.add_history.side_effect = add_history
mock.get_history_item.side_effect = get_history_item
mock.get_current_history_length.side_effect = get_current_history_length
# Provide a default return value for functions that are read from.
mock.get_completer_delims.return_value = " "
yield mock
# We import the class under test after setting up the patch context if needed,
# or ensure patches target the correct import location.
from argenta.app.autocompleter.entity import AutoCompleter, _get_history_items, _is_command_exist
class TestAutoCompleter:
"""Test suite for the AutoCompleter class."""
HISTORY_FILE = "test_history.txt"
COMMANDS = ["start", "stop", "status"]
def test_initialization(self):
"""Tests that the constructor correctly assigns attributes."""
completer = AutoCompleter(history_filename=self.HISTORY_FILE, autocomplete_button="tab")
assert completer.history_filename == self.HISTORY_FILE
assert completer.autocomplete_button == "tab"
def test_initial_setup_if_history_file_does_not_exist(self, fs, mock_readline):
"""Tests initial setup creates history from commands when the history file is absent."""
# Ensure the file does not exist in the fake filesystem.
if os.path.exists(self.HISTORY_FILE):
os.remove(self.HISTORY_FILE)
completer = AutoCompleter(history_filename=self.HISTORY_FILE)
completer.initial_setup(self.COMMANDS)
mock_readline.read_history_file.assert_not_called()
expected_calls = [call(cmd) for cmd in self.COMMANDS]
mock_readline.add_history.assert_has_calls(expected_calls, any_order=True)
assert mock_readline.add_history.call_count == len(self.COMMANDS)
mock_readline.set_completer.assert_called_with(completer._complete)
mock_readline.parse_and_bind.assert_called_with("tab: complete")
def test_initial_setup_if_history_file_exists(self, fs, mock_readline):
"""Tests initial setup reads from an existing history file."""
fs.create_file(self.HISTORY_FILE, contents="previous_command\n")
completer = AutoCompleter(history_filename=self.HISTORY_FILE)
completer.initial_setup(self.COMMANDS)
mock_readline.read_history_file.assert_called_once_with(self.HISTORY_FILE)
mock_readline.add_history.assert_not_called()
mock_readline.set_completer.assert_called_once()
mock_readline.parse_and_bind.assert_called_once()
def test_initial_setup_with_no_history_filename(self, mock_readline):
"""Tests initial setup when no history filename is provided."""
completer = AutoCompleter(history_filename=None)
completer.initial_setup(self.COMMANDS)
mock_readline.read_history_file.assert_not_called()
expected_calls = [call(cmd) for cmd in self.COMMANDS]
mock_readline.add_history.assert_has_calls(expected_calls, any_order=True)
def test_exit_setup_writes_and_filters_history(self, fs, mock_readline):
"""Tests that exit_setup writes a filtered and unique history to the file."""
# 1. Populate the mock readline history.
mock_readline.add_history.side_effect(None) # Temporarily disable side effect to just record calls
mock_readline.add_history("start server")
mock_readline.add_history("stop client")
mock_readline.add_history("invalid command")
mock_readline.add_history("start server") # Add a duplicate.
# 2. Simulate the state of the history file after readline.write_history_file would have run.
raw_history_content = "\n".join(["start server", "stop client", "invalid command", "start server"])
fs.create_file(self.HISTORY_FILE, contents=raw_history_content)
# 3. Call the method under test.
completer = AutoCompleter(history_filename=self.HISTORY_FILE)
completer.exit_setup(all_commands=["start", "stop"], ignore_command_register=False)
# 4. Assert that readline's write function was called.
mock_readline.write_history_file.assert_called_once_with(self.HISTORY_FILE)
# 5. Assert the file was correctly re-written with filtered and unique content.
with open(self.HISTORY_FILE, "r") as f:
content = f.read()
lines = sorted(content.strip().split("\n"))
assert lines == ["start server", "stop client"]
def test_exit_setup_with_no_history_filename(self, mock_readline):
"""Tests that exit_setup does nothing if no filename is provided."""
completer = AutoCompleter(history_filename=None)
completer.exit_setup(all_commands=self.COMMANDS, ignore_command_register=False)
mock_readline.write_history_file.assert_not_called()
def test_complete_with_no_matches(self, mock_readline):
"""Tests the _complete method when there are no matching history items."""
for cmd in ["start", "stop"]:
mock_readline.add_history(cmd)
completer = AutoCompleter()
assert completer._complete("run", 0) is None
assert completer._complete("run", 1) is None
def test_complete_with_one_match(self, mock_readline):
"""Tests the _complete method when there is exactly one match."""
mock_readline.add_history("start server")
mock_readline.add_history("stop server")
completer = AutoCompleter()
assert completer._complete("start", 0) == "start server"
assert completer._complete("start", 1) is None # Subsequent states yield no matches
def test_complete_with_multiple_matches(self, mock_readline):
"""Tests _complete with multiple matches that share a common prefix."""
mock_readline.add_history("status client")
mock_readline.add_history("status server")
mock_readline.add_history("stop")
completer = AutoCompleter()
# On state 0, it should insert the common prefix via readline and return None.
result = completer._complete("stat", 0)
assert result is None
mock_readline.insert_text.assert_called_once_with("us ") # Completes "stat" to "status "
mock_readline.redisplay.assert_called_once()
# On subsequent states, it should do nothing.
mock_readline.reset_mock()
result_state_1 = completer._complete("stat", 1)
assert result_state_1 is None
mock_readline.insert_text.assert_not_called()
class TestHelperFunctions:
"""Test suite for helper functions in the autocompleter module."""
def test_is_command_exist(self):
"""Tests the _is_command_exist helper function."""
existing = ["start", "stop", "status"]
# Case-sensitive check
assert _is_command_exist("start", existing, ignore_command_register=False) is True
assert _is_command_exist("START", existing, ignore_command_register=False) is False
assert _is_command_exist("unknown", existing, ignore_command_register=False) is False
# Case-insensitive check
assert _is_command_exist("start", existing, ignore_command_register=True) is True
assert _is_command_exist("START", existing, ignore_command_register=True) is True
assert _is_command_exist("unknown", existing, ignore_command_register=True) is False
def test_get_history_items(self, mock_readline):
"""Tests the _get_history_items helper function."""
assert _get_history_items() == []
mock_readline.add_history("first item")
mock_readline.add_history("second item")
assert _get_history_items() == ["first item", "second item"]