# -*- coding: utf-8 -*- """Tests for BootstrapHook. Covers: - Initialization - __call__ early-exit branches (flag, missing file, not first interaction) - Happy path: guidance prepended, flag created - System message skipping - Exception handling """ # pylint: disable=redefined-outer-name from pathlib import Path from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch import pytest # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def working_dir(tmp_path: Path) -> Path: """Provide an isolated working directory.""" return tmp_path @pytest.fixture def hook(working_dir): """Create a BootstrapHook with default (zh) language.""" from pineagents.agents.hooks.bootstrap import BootstrapHook return BootstrapHook(working_dir=working_dir) @pytest.fixture def mock_agent(): """Create a mock agent with state.context.""" agent = MagicMock() agent.state = SimpleNamespace(context=[]) return agent # --------------------------------------------------------------------------- # Initialization # --------------------------------------------------------------------------- class TestBootstrapHookInit: """P0: __init__ tests.""" def test_stores_working_dir(self, working_dir): from pineagents.agents.hooks.bootstrap import BootstrapHook hook = BootstrapHook(working_dir=working_dir) assert hook.working_dir == working_dir def test_default_language_is_zh(self, working_dir): from pineagents.agents.hooks.bootstrap import BootstrapHook hook = BootstrapHook(working_dir=working_dir) assert hook.language == "zh" def test_custom_language_stored(self, working_dir): from pineagents.agents.hooks.bootstrap import BootstrapHook hook = BootstrapHook(working_dir=working_dir, language="en") assert hook.language == "en" # --------------------------------------------------------------------------- # __call__ — early-exit branches # --------------------------------------------------------------------------- class TestBootstrapHookCallEarlyExit: """P1: __call__ returns None without side-effects.""" async def test_returns_none_when_completed_flag_exists( self, hook, mock_agent, ): """Already bootstrapped: flag present → skip entirely.""" (hook.working_dir / ".bootstrap_completed").touch() result = await hook(mock_agent, {}) assert result is None assert mock_agent.state.context == [] async def test_returns_none_when_bootstrap_md_missing( self, hook, mock_agent, ): """No BOOTSTRAP.md → skip entirely.""" result = await hook(mock_agent, {}) assert result is None async def test_returns_none_when_not_first_interaction( self, hook, mock_agent, ): """Not first interaction → skip guidance.""" (hook.working_dir / "BOOTSTRAP.md").write_text("# Bootstrap") with patch( "pineagents.agents.hooks.bootstrap.is_first_user_interaction", return_value=False, ): result = await hook(mock_agent, {}) assert result is None # --------------------------------------------------------------------------- # __call__ — happy path # --------------------------------------------------------------------------- class TestBootstrapHookCallHappyPath: """P1: __call__ applies guidance and creates flag.""" def _user_msg(self): msg = MagicMock() msg.role = "user" return msg async def test_prepends_guidance_to_first_user_message( self, hook, mock_agent, ): (hook.working_dir / "BOOTSTRAP.md").write_text("# Bootstrap") user_msg = self._user_msg() mock_agent.state.context = [user_msg] with patch( "pineagents.agents.hooks.bootstrap.is_first_user_interaction", return_value=True, ), patch( "pineagents.agents.hooks.bootstrap.build_bootstrap_guidance", return_value="guidance text", ) as mock_build, patch( "pineagents.agents.hooks.bootstrap.prepend_to_message_content", ) as mock_prepend: result = await hook(mock_agent, {}) assert result is None mock_build.assert_called_once_with("zh") mock_prepend.assert_called_once_with(user_msg, "guidance text") async def test_creates_completed_flag_after_success( self, hook, mock_agent, ): (hook.working_dir / "BOOTSTRAP.md").write_text("# Bootstrap") user_msg = self._user_msg() mock_agent.state.context = [user_msg] with patch( "pineagents.agents.hooks.bootstrap.is_first_user_interaction", return_value=True, ), patch( "pineagents.agents.hooks.bootstrap.build_bootstrap_guidance", return_value="guidance", ), patch( "pineagents.agents.hooks.bootstrap.prepend_to_message_content", ): await hook(mock_agent, {}) flag = hook.working_dir / ".bootstrap_completed" assert flag.exists() async def test_skips_system_messages_prepends_first_user( self, hook, mock_agent, ): """System messages before first user msg are skipped.""" (hook.working_dir / "BOOTSTRAP.md").write_text("# Bootstrap") sys_msg = MagicMock() sys_msg.role = "system" user_msg = self._user_msg() mock_agent.state.context = [sys_msg, user_msg] with patch( "pineagents.agents.hooks.bootstrap.is_first_user_interaction", return_value=True, ), patch( "pineagents.agents.hooks.bootstrap.build_bootstrap_guidance", return_value="guidance", ), patch( "pineagents.agents.hooks.bootstrap.prepend_to_message_content", ) as mock_prepend: await hook(mock_agent, {}) mock_prepend.assert_called_once_with(user_msg, "guidance") async def test_uses_hook_language_for_guidance(self, working_dir): from pineagents.agents.hooks.bootstrap import BootstrapHook hook_en = BootstrapHook(working_dir=working_dir, language="en") (working_dir / "BOOTSTRAP.md").write_text("# Bootstrap") user_msg = MagicMock() user_msg.role = "user" agent = MagicMock() agent.memory.get_memory = AsyncMock(return_value=[user_msg]) with patch( "pineagents.agents.hooks.bootstrap.is_first_user_interaction", return_value=True, ), patch( "pineagents.agents.hooks.bootstrap.build_bootstrap_guidance", return_value="en guidance", ) as mock_build, patch( "pineagents.agents.hooks.bootstrap.prepend_to_message_content", ): await hook_en(agent, {}) mock_build.assert_called_once_with("en") # --------------------------------------------------------------------------- # __call__ — exception handling # --------------------------------------------------------------------------- class TestBootstrapHookCallException: """P2: exceptions are swallowed, None is returned.""" async def test_handles_memory_error_gracefully( self, hook, mock_agent, ): (hook.working_dir / "BOOTSTRAP.md").write_text("# Bootstrap") mock_agent.state = property( lambda self: (_ for _ in ()).throw(RuntimeError("state error")), ) result = await hook(mock_agent, {}) assert result is None async def test_always_returns_none(self, hook, mock_agent): """Return value is always None regardless of kwargs.""" result = await hook(mock_agent, {"irrelevant": "kwargs"}) assert result is None