@@ -93,6 +93,20 @@ def test_case_sensitivity(self):
9393 # Uppercase does NOT match \byes\b — by design, caller lowercases
9494 assert parse_confirmation ("YES" ) is False
9595
96+ def test_conflicting_keywords_first_wins (self ):
97+ """When both yes and no keywords appear, positive match wins
98+ because the positive branch is checked first."""
99+ assert parse_confirmation ("yes but also no" ) is True
100+
101+ def test_json_array_falls_through_to_keywords (self ):
102+ """JSON array is not a dict — falls through to keyword matching.
103+ The string '"proceed": true' is found as a substring, so returns True."""
104+ assert parse_confirmation ('[{"proceed": true}]' ) is True
105+
106+ def test_json_with_extra_fields (self ):
107+ """Extra JSON fields are ignored; only 'proceed' matters."""
108+ assert parse_confirmation ('{"proceed": true, "reason": "looks good"}' ) is True
109+
96110
97111# ---------------------------------------------------------------------------
98112# llm_confirm_evolution — async integration
@@ -238,6 +252,61 @@ async def test_mro_parse_confirmation_called(self):
238252
239253 evolver ._parse_confirmation .assert_called_once ()
240254
255+ @pytest .mark .asyncio
256+ async def test_model_fallback_to_client_model (self ):
257+ """When evolver._model is None, falls back to evolver._llm_client.model."""
258+ evolver = self ._make_evolver (llm_response = '{"proceed": true}' )
259+ evolver ._model = None
260+ evolver ._llm_client .model = "fallback-model"
261+
262+ with patch (self ._RM_PATCH ) as mock_rm :
263+ mock_rm .record_conversation_setup = AsyncMock ()
264+ mock_rm .record_iteration_context = AsyncMock ()
265+
266+ result = await llm_confirm_evolution (
267+ evolver ,
268+ skill_record = MagicMock (skill_id = "s1" ),
269+ skill_content = "# Skill" ,
270+ proposed_type = MagicMock (value = "fix" ),
271+ proposed_direction = "fix" ,
272+ trigger_context = "test" ,
273+ recent_analyses = [],
274+ )
275+
276+ assert result is True
277+ # Verify fallback model was used
278+ call_kwargs = evolver ._llm_client .complete .call_args
279+ assert call_kwargs [1 ]["model" ] == "fallback-model"
280+
281+ @pytest .mark .asyncio
282+ async def test_recording_truncates_content (self ):
283+ """Recorded messages are truncated to _RECORDING_MAX_CHARS."""
284+ from openspace .skill_engine .evolution .confirmation import _RECORDING_MAX_CHARS
285+
286+ # Create skill content larger than recording limit
287+ big_content = "x" * (_RECORDING_MAX_CHARS + 5000 )
288+ evolver = self ._make_evolver (llm_response = '{"proceed": true}' )
289+
290+ with patch (self ._RM_PATCH ) as mock_rm :
291+ mock_rm .record_conversation_setup = AsyncMock ()
292+ mock_rm .record_iteration_context = AsyncMock ()
293+
294+ await llm_confirm_evolution (
295+ evolver ,
296+ skill_record = MagicMock (skill_id = "s1" ),
297+ skill_content = big_content ,
298+ proposed_type = MagicMock (value = "fix" ),
299+ proposed_direction = "fix" ,
300+ trigger_context = "test" ,
301+ recent_analyses = [],
302+ )
303+
304+ # Verify recorded setup messages are truncated
305+ setup_call = mock_rm .record_conversation_setup .call_args
306+ recorded_msgs = setup_call [1 ]["setup_messages" ]
307+ for msg in recorded_msgs :
308+ assert len (msg ["content" ]) <= _RECORDING_MAX_CHARS + 50 # small margin for truncation suffix
309+
241310
242311# ---------------------------------------------------------------------------
243312# Constants
@@ -248,6 +317,11 @@ def test_skill_content_max_chars(self):
248317 assert isinstance (_SKILL_CONTENT_MAX_CHARS , int )
249318 assert _SKILL_CONTENT_MAX_CHARS == 12_000
250319
320+ def test_logger_uses_evolver_namespace (self ):
321+ """Logger preserves evolver namespace for log filter compatibility."""
322+ from openspace .skill_engine .evolution import confirmation
323+ assert confirmation .logger .name == "openspace.skill_engine.evolver"
324+
251325
252326# ---------------------------------------------------------------------------
253327# Backward compat
@@ -274,6 +348,50 @@ def test_parse_is_staticmethod(self):
274348 assert result is True
275349
276350
351+ # ---------------------------------------------------------------------------
352+ # Delegation seam tests (real SkillEvolver → confirmation module)
353+ # ---------------------------------------------------------------------------
354+
355+ @pytest .mark .skipif (not _HAS_EVOLVER , reason = "SkillEvolver not importable" )
356+ class TestDelegationSeam :
357+ """Verify real SkillEvolver delegates to confirmation.py functions."""
358+
359+ def test_parse_confirmation_delegates (self ):
360+ """Real SkillEvolver._parse_confirmation routes to confirmation module."""
361+ evolver = object .__new__ (SkillEvolver )
362+ assert evolver ._parse_confirmation ('{"proceed": true}' ) is True
363+ assert evolver ._parse_confirmation ('{"proceed": false}' ) is False
364+ assert evolver ._parse_confirmation ("yes do it" ) is True
365+ assert evolver ._parse_confirmation ("no skip" ) is False
366+
367+ @pytest .mark .asyncio
368+ async def test_llm_confirm_delegates_to_module (self ):
369+ """Real SkillEvolver._llm_confirm_evolution calls confirmation module."""
370+ evolver = object .__new__ (SkillEvolver )
371+ # Wire up required attributes
372+ evolver ._model = "test-model"
373+ evolver ._llm_client = MagicMock ()
374+ evolver ._llm_client .complete = AsyncMock (
375+ return_value = {"message" : {"content" : '{"proceed": true}' }}
376+ )
377+
378+ with patch ("openspace.recording.RecordingManager" ) as mock_rm :
379+ mock_rm .record_conversation_setup = AsyncMock ()
380+ mock_rm .record_iteration_context = AsyncMock ()
381+
382+ result = await evolver ._llm_confirm_evolution (
383+ skill_record = MagicMock (skill_id = "s1" ),
384+ skill_content = "# Skill" ,
385+ proposed_type = MagicMock (value = "fix" ),
386+ proposed_direction = "fix it" ,
387+ trigger_context = "test" ,
388+ recent_analyses = [],
389+ )
390+
391+ assert result is True
392+ evolver ._llm_client .complete .assert_awaited_once ()
393+
394+
277395# ---------------------------------------------------------------------------
278396# Size guard
279397# ---------------------------------------------------------------------------
0 commit comments