# -*- coding: utf-8 -*- # pylint: disable=protected-access """Tests for DefaultMode gate ownership and lifecycle.""" from __future__ import annotations from types import SimpleNamespace from unittest.mock import patch import pytest from pineagents.loop.gates.doom_loop import DoomLoopGate from pineagents.loop.gates.iteration import IterationGate from pineagents.modes.default import DefaultMode @pytest.fixture(autouse=True) def _force_session_id(): with patch( "pineagents.loop.gates.loop_gate._session_id", return_value="test-session", ): yield def _running_config( max_iters=100, doom_enabled=True, rubric_enabled=True, ): doom_stage = SimpleNamespace( after=3, action="stop", prompt="doom", ) doom = SimpleNamespace( enabled=doom_enabled, window_size=3, similarity_threshold=1.0, stages=[doom_stage], ) iteration = SimpleNamespace( enabled=True, max_iterations=max_iters, ) rubric = SimpleNamespace( enabled=rubric_enabled, prompt="continue working", max_interventions=1, ) loop = SimpleNamespace( iteration=iteration, doom_loop=doom, rubric=rubric, ) return SimpleNamespace( max_iters=max_iters, loop=loop, ) def _workspace(): plugins = SimpleNamespace(stop_handlers=[]) return SimpleNamespace(plugins=plugins) def _context(config, agent=None): return SimpleNamespace( agent_config=SimpleNamespace(running=config), agent=agent, ) def _find_gate(mode, gate_type): return next( gate for gate in mode.handler.gates if isinstance(gate, gate_type) ) def test_setup_registers_one_default_handler(): """Mode setup registers one default-scoped handler.""" workspace = _workspace() mode = DefaultMode() mode.setup(workspace) assert len(workspace.plugins.stop_handlers) == 1 registration = workspace.plugins.stop_handlers[0] assert registration.handler is mode.handler assert registration.scope == "default" @pytest.mark.asyncio async def test_turn_start_builds_configured_gates(): """First turn builds gates from the current running config.""" mode = DefaultMode() await mode.on_turn_start(_context(_running_config())) assert len(mode.handler.gates) == 3 assert _find_gate(mode, IterationGate)._state() is not None @pytest.mark.asyncio async def test_next_turn_resets_iteration_and_doom_state(): """A new turn resets current-session state without rebuilding gates.""" mode = DefaultMode() ctx = _context(_running_config(max_iters=10)) await mode.on_turn_start(ctx) iteration = _find_gate(mode, IterationGate) doom = _find_gate(mode, DoomLoopGate) for _ in range(5): await iteration.check({}) doom.record("tool_a", "hash") assert iteration._state().iteration == 5 assert len(doom._state().history) == 1 await mode.on_turn_start(ctx) assert iteration._state().iteration == 0 assert len(doom._state().history) == 0 @pytest.mark.asyncio async def test_next_turn_reactivates_exhausted_iteration_gate(): """A terminated iteration gate is active again on the next turn.""" mode = DefaultMode() ctx = _context(_running_config(max_iters=1)) await mode.on_turn_start(ctx) iteration = _find_gate(mode, IterationGate) await iteration.check({}) assert iteration._state() is None await mode.on_turn_start(ctx) assert iteration._state() is not None assert iteration._state().iteration == 0 @pytest.mark.asyncio async def test_config_change_rebuilds_without_duplicate_registration(): """Hot-loaded config replaces gates on the existing handler.""" workspace = _workspace() mode = DefaultMode() mode.setup(workspace) await mode.on_turn_start(_context(_running_config())) await mode.on_turn_start( _context( _running_config( doom_enabled=False, rubric_enabled=False, ), ), ) assert len(mode.handler.gates) == 1 assert len(workspace.plugins.stop_handlers) == 1 @pytest.mark.asyncio async def test_conversation_reset_clears_session_and_pending_state(): """Full reset tears down current gates and deferred decisions.""" agent = SimpleNamespace(_gate_pending_stop=object()) mode = DefaultMode() await mode.on_turn_start(_context(_running_config(), agent)) iteration = _find_gate(mode, IterationGate) await mode.on_conversation_reset(_context(_running_config(), agent)) assert iteration._state() is None assert agent._gate_pending_stop is None