diff --git a/mock/mock_app/main.py b/mock/mock_app/main.py index 6e5aafe..056dfc1 100644 --- a/mock/mock_app/main.py +++ b/mock/mock_app/main.py @@ -14,7 +14,7 @@ arg_parser: ArgParser = ArgParser( ) app: App = App( dividing_line=DynamicDividingLine(), - autocompleter=AutoCompleter(), + autocompleter=AutoCompleter(history_filename="history.txt") ) orchestrator: Orchestrator = Orchestrator(arg_parser) diff --git a/mock/mock_app/routers.py b/mock/mock_app/routers.py index dc2a1c3..e6c2d65 100644 --- a/mock/mock_app/routers.py +++ b/mock/mock_app/routers.py @@ -6,7 +6,7 @@ from argenta.router.defaults import system_router 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) diff --git a/src/argenta/app/autocompleter/entity.py b/src/argenta/app/autocompleter/entity.py index fb659c4..83e68c8 100644 --- a/src/argenta/app/autocompleter/entity.py +++ b/src/argenta/app/autocompleter/entity.py @@ -7,7 +7,8 @@ from typing import Never class AutoCompleter: def __init__( - self, history_filename: str | None = None, autocomplete_button: str = "tab" + self, history_filename: str | None = None, + autocomplete_button: str = "tab" ) -> None: """ Public. Configures and implements auto-completion of input command @@ -60,12 +61,16 @@ class AutoCompleter: else: for line in all_commands: 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_delims(readline.get_completer_delims().replace(" ", "")) 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 :return: None @@ -76,10 +81,16 @@ class AutoCompleter: raw_history = history_file.read() pretty_history: list[str] = [] 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) with open(self.history_filename, "w") as history_file: _ = 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]: """ diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index 83784e8..e8712b8 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -465,7 +465,8 @@ class App(BaseApp): if self._is_exit_command(input_command): system_router.finds_appropriate_handler(input_command) 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 diff --git a/src/argenta/orchestrator/argparser/arguments/models.py b/src/argenta/orchestrator/argparser/arguments/models.py index df686a9..421e891 100644 --- a/src/argenta/orchestrator/argparser/arguments/models.py +++ b/src/argenta/orchestrator/argparser/arguments/models.py @@ -75,10 +75,10 @@ class BooleanArgument(BaseArgument): class InputArgument: def __init__(self, name: str, - value: str | None, + value: str | Literal[True], founder_class: type[BaseArgument]) -> None: self.name: str = name - self.value: str | None = value + self.value: str | Literal[True] = value self.founder_class: type[BaseArgument] = founder_class def __str__(self) -> str: diff --git a/src/argenta/orchestrator/argparser/entity.py b/src/argenta/orchestrator/argparser/entity.py index 28ef573..aeab40b 100644 --- a/src/argenta/orchestrator/argparser/entity.py +++ b/src/argenta/orchestrator/argparser/entity.py @@ -65,7 +65,7 @@ class ArgParser: def _parse_args(self) -> None: 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: for arg in processed_args: diff --git a/tests/unit_tests/test_argparser.py b/tests/unit_tests/test_argparser.py index da86049..2830cd7 100644 --- a/tests/unit_tests/test_argparser.py +++ b/tests/unit_tests/test_argparser.py @@ -1,18 +1,21 @@ -import unittest -from unittest.mock import MagicMock, patch +import pytest from argparse import Namespace +from unittest.mock import MagicMock, call from argenta.orchestrator.argparser.entity import ArgParser, ArgSpace from argenta.orchestrator.argparser.arguments.models import ( ValueArgument, BooleanArgument, 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): + """Ensures ValueArgument instances are created with correct attributes.""" arg = ValueArgument( name="test_arg", prefix="--", @@ -20,145 +23,200 @@ class TestArgumentClasses(unittest.TestCase): possible_values=["one", "two"], default="one", is_required=True, - is_deprecated=False + is_deprecated=False, ) - self.assertEqual(arg.name, "test_arg") - self.assertEqual(arg.prefix, "--") - self.assertEqual(arg.help, "A test argument.") - self.assertEqual(arg.possible_values, ["one", "two"]) - self.assertEqual(arg.default, "one") - self.assertTrue(arg.is_required) - self.assertFalse(arg.is_deprecated) - self.assertEqual(arg.action, "store") - self.assertEqual(arg.string_entity, "--test_arg") + assert arg.name == "test_arg" + assert arg.prefix == "--" + assert arg.help == "A test argument." + assert arg.possible_values == ["one", "two"] + assert arg.default == "one" + assert arg.is_required is True + assert arg.is_deprecated is False + assert arg.action == "store" + assert arg.string_entity == "--test_arg" def test_boolean_argument_creation(self): + """Ensures BooleanArgument instances are created with correct attributes.""" arg = BooleanArgument( - name="verbose", - prefix="-", - help="Enable verbose mode.", - is_deprecated=True + name="verbose", prefix="-", help="Enable verbose mode.", is_deprecated=True ) - self.assertEqual(arg.name, "verbose") - self.assertEqual(arg.prefix, "-") - self.assertEqual(arg.help, "Enable verbose mode.") - self.assertTrue(arg.is_deprecated) - self.assertEqual(arg.action, "store_true") - self.assertEqual(arg.string_entity, "-verbose") + assert arg.name == "verbose" + assert arg.prefix == "-" + assert arg.help == "Enable verbose mode." + assert arg.is_deprecated is True + assert arg.action == "store_true" + assert arg.string_entity == "-verbose" def test_input_argument_creation(self): + """Ensures InputArgument instances are created with correct attributes.""" arg = InputArgument( - name="file", - value="/path/to/file", - founder_class=ValueArgument + name="file", value="/path/to/file", founder_class=ValueArgument ) - self.assertEqual(arg.name, "file") - self.assertEqual(arg.value, "/path/to/file") - self.assertEqual(arg.founder_class, ValueArgument) + assert arg.name == "file" + assert arg.value == "/path/to/file" + assert arg.founder_class is ValueArgument -class TestArgParser(unittest.TestCase): - def setUp(self): - 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] +class TestArgSpace: + """Tests for the ArgSpace class, which holds parsed argument values.""" - def test_argparser_initialization(self): - parser = ArgParser( - processed_args=self.processed_args, - name="TestApp", - description="A test application.", - epilog="Test epilog." - ) - 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) + @pytest.fixture + def mock_arguments(self) -> list[InputArgument]: + """Provides a list of mock InputArgument objects for testing.""" + return [ + InputArgument(name="arg1", value="val1", founder_class=ValueArgument), + InputArgument(name="arg2", value=True, founder_class=BooleanArgument), + InputArgument(name="arg3", value="val3", founder_class=ValueArgument), + ] - @patch('argenta.orchestrator.argparser.entity.ArgumentParser.parse_args') - def test_parse_args(self, mock_parse_args: MagicMock): - mock_namespace = Namespace(config='config.json', debug=True) - mock_parse_args.return_value = mock_namespace + @pytest.fixture + def arg_space(self, mock_arguments: list[InputArgument]) -> ArgSpace: + """Provides a pre-populated ArgSpace instance.""" + return ArgSpace(all_arguments=mock_arguments) - parser = ArgParser(processed_args=self.processed_args) - arg_space = parser.parse_args() + def test_initialization(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]): + """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) - self.assertEqual(len(arg_space.all_arguments), 2) + def test_get_by_name(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]): + """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') debug_arg = arg_space.get_by_name('debug') - self.assertIsNotNone(config_arg) - if config_arg: - self.assertEqual(config_arg.value, 'config.json') - self.assertEqual(config_arg.founder_class, ValueArgument) + assert config_arg is not None + assert config_arg.value == "config.json" + assert config_arg.founder_class is ValueArgument - self.assertIsNotNone(debug_arg) - if debug_arg: - self.assertTrue(debug_arg.value) - self.assertEqual(debug_arg.founder_class, BooleanArgument) + assert debug_arg is not None + assert debug_arg.value is True + assert debug_arg.founder_class is BooleanArgument -class TestArgSpace(unittest.TestCase): - def setUp(self): - 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]) +class TestArgParser: + """Tests for the ArgParser class, which orchestrates argument parsing.""" - def test_argspace_initialization(self): - self.assertEqual(len(self.arg_space.all_arguments), 3) - self.assertIn(self.input_arg1, self.arg_space.all_arguments) - self.assertIn(self.input_arg2, self.arg_space.all_arguments) - self.assertIn(self.input_arg3, self.arg_space.all_arguments) + @pytest.fixture + def value_arg(self) -> ValueArgument: + """Provides a sample ValueArgument.""" + return ValueArgument(name="config", help="Path to config file", default="dev.json", is_required=False, possible_values=["dev.json", "prod.json"]) - def test_get_by_name(self): - found_arg = self.arg_space.get_by_name("arg1") - self.assertIsNotNone(found_arg) - if found_arg: - self.assertEqual(found_arg, self.input_arg1) + @pytest.fixture + def bool_arg(self) -> BooleanArgument: + """Provides a sample BooleanArgument.""" + return BooleanArgument(name="debug", help="Enable debug mode") - def test_get_by_name_not_found(self): - found_arg = self.arg_space.get_by_name("non_existent_arg") - self.assertIsNone(found_arg) + @pytest.fixture + def processed_args(self, value_arg: ValueArgument, bool_arg: BooleanArgument) -> list: + """Provides a list of processed arguments.""" + return [value_arg, bool_arg] - def test_get_by_type(self): - value_args = self.arg_space.get_by_type(ValueArgument) - self.assertEqual(len(value_args), 2) - self.assertIn(self.input_arg1, value_args) - self.assertIn(self.input_arg3, value_args) + def test_initialization(self, processed_args: list): + """Tests that the ArgParser constructor correctly assigns attributes.""" + parser = ArgParser( + processed_args=processed_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) - self.assertEqual(len(bool_args), 1) - self.assertIn(self.input_arg2, bool_args) + def test_register_args(self, mocker, value_arg: ValueArgument, bool_arg: BooleanArgument): + """Tests that arguments are correctly registered with the underlying ArgumentParser.""" + mock_add_argument = mocker.patch("argparse.ArgumentParser.add_argument") - def test_get_by_type_not_found(self): - class OtherArgument(BaseArgument): - pass + parser = ArgParser(processed_args=[value_arg, bool_arg]) - other_args = self.arg_space.get_by_type(OtherArgument) - self.assertEqual(len(other_args), 0) - - def test_from_namespace(self): - namespace = Namespace(arg1="val1", debug=True) - processed_args = [ - ValueArgument(name="arg1", prefix="--"), - BooleanArgument(name="debug", prefix="-") + expected_calls = [ + # Call for the ValueArgument + call( + value_arg.string_entity, + action=value_arg.action, + help=value_arg.help, + default=value_arg.default, + 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) - self.assertEqual(len(arg_space.all_arguments), 2) + def test_parse_args_populates_argspace(self, mocker, processed_args: list): + """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') - self.assertIsNotNone(arg1) - if arg1: - self.assertEqual(arg1.value, "val1") - self.assertEqual(arg1.founder_class, ValueArgument) + assert config_arg is not None + assert config_arg.value == 'config.json' + assert config_arg.founder_class is ValueArgument - self.assertIsNotNone(debug_arg) - if debug_arg: - self.assertTrue(debug_arg.value) - self.assertEqual(debug_arg.founder_class, BooleanArgument) + assert debug_arg is not None + assert debug_arg.value is True + assert debug_arg.founder_class is BooleanArgument diff --git a/tests/unit_tests/test_autocompleter.py b/tests/unit_tests/test_autocompleter.py new file mode 100644 index 0000000..952f3b1 --- /dev/null +++ b/tests/unit_tests/test_autocompleter.py @@ -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"]