Skip to content

Commit 4e8db60

Browse files
Brian KrafftCopilot
andcommitted
fix(5.3): preserve MRO for helper calls, add happy-path tests
Review fixes from /8eyes + /collab gate: - Route helper calls through evolver._method() to preserve MRO (GPT-5.4 MEDIUM) - Add 2 happy-path tests: confirm->execute for triggers 2 & 3 (Opus M-2) - Remove unused import logging (Opus L-1) - Move _ANALYSIS_NOTE_MAX_CHARS to evolver.py where used (Opus L-2) - Restore diagnose_skill_health return type annotation (Opus L-3) 24 trigger tests pass; 1,470 total pass, 127 skipped. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c49cca5 commit 4e8db60

3 files changed

Lines changed: 61 additions & 19 deletions

File tree

openspace/skill_engine/evolution/triggers.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from __future__ import annotations
1313

14-
import logging
1514
from pathlib import Path
1615
from typing import TYPE_CHECKING, List, Optional
1716

@@ -35,7 +34,6 @@
3534
# ---------------------------------------------------------------------------
3635

3736
_ANALYSIS_CONTEXT_MAX = 5 # Max recent analyses to include in prompt
38-
_ANALYSIS_NOTE_MAX_CHARS = 500 # Per-analysis note truncation
3937

4038
# Rule-based thresholds for candidate screening (relaxed — LLM confirms)
4139
_FALLBACK_THRESHOLD = 0.4
@@ -64,7 +62,7 @@ async def process_analysis(
6462

6563
contexts: List[EvolutionContext] = []
6664
for suggestion in analysis.evolution_suggestions:
67-
ctx = build_context_from_analysis(evolver, analysis, suggestion)
65+
ctx = evolver._build_context_from_analysis(analysis, suggestion)
6866
if ctx is not None:
6967
contexts.append(ctx)
7068

@@ -137,7 +135,7 @@ async def process_tool_degradation(
137135
recent = evolver._store.load_analyses(
138136
skill_id=skill_record.skill_id, limit=_ANALYSIS_CONTEXT_MAX,
139137
)
140-
content = load_skill_content(evolver, skill_record)
138+
content = evolver._load_skill_content(skill_record)
141139
if not content:
142140
continue
143141

@@ -225,11 +223,11 @@ async def process_metric_check(
225223
if record.total_selections < min_selections:
226224
continue
227225

228-
evo_type, direction = diagnose_skill_health(record)
226+
evo_type, direction = evolver._diagnose_skill_health(record)
229227
if evo_type is None:
230228
continue
231229

232-
content = load_skill_content(evolver, record)
230+
content = evolver._load_skill_content(record)
233231
if not content:
234232
continue
235233

@@ -313,7 +311,7 @@ def build_context_from_analysis(
313311
if not rec:
314312
logger.warning("Target skill not found: %s", target_id)
315313
return None
316-
content = load_skill_content(evolver, rec)
314+
content = evolver._load_skill_content(rec)
317315
if not content:
318316
logger.warning("Cannot load content for skill: %s", target_id)
319317
return None
@@ -360,7 +358,7 @@ def load_skill_content(evolver, record: "SkillRecord") -> str:
360358

361359
def diagnose_skill_health(
362360
record: "SkillRecord",
363-
) -> tuple:
361+
) -> tuple[Optional[EvolutionType], str]:
364362
"""Diagnose what type of evolution a skill needs based on metrics.
365363
366364
Returns ``(EvolutionType, direction_str)`` or ``(None, "")`` if healthy.

openspace/skill_engine/evolver.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
)
4242
from .evolution.triggers import (
4343
_ANALYSIS_CONTEXT_MAX,
44-
_ANALYSIS_NOTE_MAX_CHARS,
4544
build_context_from_analysis as _build_context_from_analysis_impl,
4645
diagnose_skill_health as _diagnose_skill_health_impl,
4746
load_skill_content as _load_skill_content_impl,
@@ -1055,6 +1054,8 @@ def _format_skill_dir_content(skill_dir: Path) -> str:
10551054
@staticmethod
10561055
def _format_analysis_context(analyses: List[ExecutionAnalysis]) -> str:
10571056
"""Format recent analyses into a concise context block for prompts."""
1057+
_ANALYSIS_NOTE_MAX_CHARS = 500 # Per-analysis note truncation
1058+
10581059
if not analyses:
10591060
return "(no execution history available)"
10601061

tests/test_evolution_triggers.py

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ class _FakeToolQualityRecord:
7979

8080
from openspace.skill_engine.evolution.triggers import (
8181
_ANALYSIS_CONTEXT_MAX,
82-
_ANALYSIS_NOTE_MAX_CHARS,
8382
_FALLBACK_THRESHOLD,
8483
_HIGH_APPLIED_FOR_FIX,
8584
_LOW_COMPLETION_THRESHOLD,
@@ -208,7 +207,7 @@ def test_fix_loads_single_target(self):
208207
evolver = MagicMock()
209208
evolver._available_tools = []
210209
evolver._store.load_record.return_value = _FakeSkillRecord(path="/skills/s1/SKILL.md")
211-
evolver._registry.load_skill_content.return_value = "# Content"
210+
evolver._load_skill_content.return_value = "# Content"
212211

213212
analysis = _FakeAnalysis()
214213
suggestion = _FakeSuggestion(
@@ -241,8 +240,8 @@ async def test_no_candidates_returns_empty(self):
241240
async def test_suggestions_dispatched(self):
242241
evolver = MagicMock()
243242
evolver._available_tools = []
244-
evolver._store.load_record.return_value = _FakeSkillRecord(path="/s/SKILL.md")
245-
evolver._registry.load_skill_content.return_value = "# Content"
243+
# After MRO fix: process_analysis calls evolver._build_context_from_analysis
244+
evolver._build_context_from_analysis.return_value = MagicMock() # truthy context
246245
evolver._execute_contexts = AsyncMock(return_value=[_FakeSkillRecord(name="evolved")])
247246

248247
analysis = _FakeAnalysis(
@@ -251,9 +250,7 @@ async def test_suggestions_dispatched(self):
251250
],
252251
)
253252

254-
with patch("openspace.skill_engine.evolution.triggers.EvolutionType", _FakeEvolutionType), \
255-
patch("openspace.skill_engine.evolution.triggers.EvolutionTrigger", _FakeTrigger):
256-
result = await process_analysis(evolver, analysis)
253+
result = await process_analysis(evolver, analysis)
257254

258255
assert len(result) == 1
259256
evolver._execute_contexts.assert_awaited_once()
@@ -300,6 +297,28 @@ async def test_prunes_recovered_tools(self):
300297
assert "old_tool" not in evolver._addressed_degradations
301298

302299

300+
@pytest.mark.asyncio
301+
async def test_confirmed_skill_triggers_execution(self):
302+
"""Happy path: LLM confirms → context built → _execute_contexts fires."""
303+
evolver = MagicMock()
304+
evolver._addressed_degradations = {}
305+
evolver._store.find_skills_by_tool.return_value = ["skill-1"]
306+
evolver._store.load_record.return_value = _FakeSkillRecord(path="/s/SKILL.md")
307+
evolver._load_skill_content.return_value = "# Content"
308+
evolver._store.load_analyses.return_value = []
309+
evolver._llm_confirm_evolution = AsyncMock(return_value=True)
310+
evolver._execute_contexts = AsyncMock(return_value=[_FakeSkillRecord(name="fixed")])
311+
evolver._available_tools = []
312+
313+
tool = _FakeToolQualityRecord(tool_key="shell_exec")
314+
result = await process_tool_degradation(evolver, [tool])
315+
316+
assert len(result) == 1
317+
assert result[0].name == "fixed"
318+
evolver._llm_confirm_evolution.assert_awaited_once()
319+
evolver._execute_contexts.assert_awaited_once()
320+
321+
303322
# ---------------------------------------------------------------------------
304323
# process_metric_check tests
305324
# ---------------------------------------------------------------------------
@@ -327,12 +346,37 @@ async def test_healthy_skills_skipped(self):
327346
),
328347
}
329348
evolver._available_tools = []
349+
# After MRO fix: process_metric_check calls evolver._diagnose_skill_health
350+
evolver._diagnose_skill_health.return_value = (None, "")
330351

331-
with patch("openspace.skill_engine.evolution.triggers.EvolutionType", _FakeEvolutionType):
332-
result = await process_metric_check(evolver, min_selections=5)
352+
result = await process_metric_check(evolver, min_selections=5)
333353

334354
assert result == []
335355

356+
@pytest.mark.asyncio
357+
async def test_confirmed_metric_triggers_execution(self):
358+
"""Happy path: diagnose → LLM confirm → _execute_contexts fires."""
359+
evolver = MagicMock()
360+
evolver._store.load_active.return_value = {
361+
"s1": _FakeSkillRecord(total_selections=10),
362+
}
363+
evolver._available_tools = []
364+
evolver._diagnose_skill_health.return_value = (_FakeEvolutionType.FIX, "needs fix")
365+
evolver._load_skill_content.return_value = "# Skill Content"
366+
evolver._store.load_analyses.return_value = []
367+
evolver._llm_confirm_evolution = AsyncMock(return_value=True)
368+
evolver._execute_contexts = AsyncMock(return_value=[_FakeSkillRecord(name="improved")])
369+
370+
with patch("openspace.skill_engine.evolution.triggers.EvolutionType", _FakeEvolutionType), \
371+
patch("openspace.skill_engine.evolution.triggers.EvolutionTrigger", _FakeTrigger), \
372+
patch("openspace.skill_engine.evolution.triggers.EvolutionSuggestion", _FakeSuggestion):
373+
result = await process_metric_check(evolver, min_selections=5)
374+
375+
assert len(result) == 1
376+
assert result[0].name == "improved"
377+
evolver._llm_confirm_evolution.assert_awaited_once()
378+
evolver._execute_contexts.assert_awaited_once()
379+
336380

337381
# ---------------------------------------------------------------------------
338382
# Constants integrity
@@ -348,7 +392,6 @@ def test_thresholds_are_numbers(self):
348392

349393
def test_analysis_constants(self):
350394
assert isinstance(_ANALYSIS_CONTEXT_MAX, int)
351-
assert isinstance(_ANALYSIS_NOTE_MAX_CHARS, int)
352395

353396
def test_constants_no_longer_in_evolver(self):
354397
"""Constants should be imported, not defined, in evolver.py."""

0 commit comments

Comments
 (0)