fix(governance): intake km healthcheck dispatches
All checks were successful
Code Review / ai-code-review (push) Successful in 22s
CD Pipeline / tests (push) Successful in 6m3s
CD Pipeline / build-and-deploy (push) Successful in 4m26s
CD Pipeline / post-deploy-checks (push) Successful in 1m21s

This commit is contained in:
Your Name
2026-05-19 21:27:25 +08:00
parent aee0a70021
commit b85ab70c45
6 changed files with 256 additions and 3 deletions

View File

@@ -235,14 +235,21 @@ class TestCheckKnowledgeDegradation:
total_mock.scalar.return_value = 10
stale_mock = MagicMock()
stale_mock.scalar.return_value = 3
insert_mock = MagicMock()
mock_db.execute = AsyncMock(side_effect=[total_mock, stale_mock])
mock_db.execute = AsyncMock(side_effect=[total_mock, stale_mock, insert_mock])
alerter = AsyncMock()
alerter.alert_governance = AsyncMock()
agent = _make_agent(alerter=alerter)
with patch("src.services.governance_agent.get_db_context") as mock_ctx:
with (
patch("src.services.governance_agent.get_db_context") as mock_ctx,
patch(
"src.services.governance_agent.create_dispatch",
new=AsyncMock(),
) as mock_create_dispatch,
):
mock_ctx.return_value.__aenter__ = AsyncMock(return_value=mock_db)
mock_ctx.return_value.__aexit__ = AsyncMock(return_value=False)
@@ -256,9 +263,49 @@ class TestCheckKnowledgeDegradation:
assert "OpenClaw" in payload["ownership"]["support_agents"][0]
assert "ElephantAlpha" in payload["ownership"]["support_agents"][1]
assert payload["ownership"]["human_owner"] == "KM owner / SRE owner"
mock_create_dispatch.assert_awaited_once()
dispatch_kwargs = mock_create_dispatch.call_args.kwargs
assert dispatch_kwargs["event_type"] == "knowledge_degradation"
assert dispatch_kwargs["executor_type"] == "hermes_kb_growth_healthcheck"
assert dispatch_kwargs["created_by"] == "governance_agent_intake"
assert dispatch_kwargs["decision_context"]["next_action"] == "run_kb_growth_healthcheck"
assert dispatch_kwargs["decision_context"]["ownership"]["lead_agent"] == "Hermes"
assert dispatch_kwargs["decision_context"]["workflow"]["writes_km_without_approval"] is False
assert result["stale"] == 3
assert result["ratio"] == 0.3
def test_knowledge_degradation_dispatch_context(self):
"""intake dispatch context 必須能被 Work Items 直接讀出 owner / stage / next_action."""
from src.services.governance_agent import _build_knowledge_degradation_dispatch_context
ctx = _build_knowledge_degradation_dispatch_context(
"evt-km-001",
{
"impact": {
"stale_count": 1451,
"total_count": 1870,
"stale_ratio": 0.776,
"threshold": 0.2,
"stale_days": 7,
},
"remediation": {"next_action": "run_kb_growth_healthcheck"},
"ownership": {
"lead_agent": "Hermes",
"support_agents": ["OpenClaw", "ElephantAlpha"],
"human_owner": "KM owner / SRE owner",
},
},
)
assert ctx["trigger_source"] == "governance_agent"
assert ctx["next_action"] == "run_kb_growth_healthcheck"
assert ctx["decision_path"] == "pending_owner_review"
assert ctx["ownership"]["lead_agent"] == "Hermes"
assert ctx["workflow"]["current_stage"] == "queued_kb_healthcheck"
assert ctx["workflow"]["stage_by_dispatch_status"]["executing"] == "draft_km_updates"
assert ctx["workflow"]["writes_km_without_approval"] is False
assert ctx["extra"]["event_id"] == "evt-km-001"
# =============================================================================
# check_llm_hallucination

View File

@@ -216,6 +216,43 @@ class TestDispatchGovernanceEvent:
assert result is None
mock_create.assert_not_awaited()
@pytest.mark.asyncio
async def test_knowledge_degradation_creates_kb_healthcheck_intake(self):
"""knowledge_degradation 應先建立 non-executing Hermes KB healthcheck work item。"""
event = _make_governance_event(event_type="knowledge_degradation")
event.details = {
"impact": {"stale_count": 1451, "total_count": 1870, "stale_ratio": 0.776},
"remediation": {"next_action": "run_kb_growth_healthcheck"},
"ownership": {"lead_agent": "Hermes"},
}
mock_dispatch_row = MagicMock()
mock_dispatch_row.id = "dispatch-kb-001"
with (
patch(
"src.services.governance_dispatcher.get_active_for_event",
new=AsyncMock(return_value=None),
),
patch(
"src.services.governance_dispatcher.get_decision_fusion_adapter",
) as mock_adapter_factory,
patch(
"src.services.governance_dispatcher.create_dispatch",
new=AsyncMock(return_value=mock_dispatch_row),
) as mock_create,
):
from src.services.governance_dispatcher import dispatch_governance_event
result = await dispatch_governance_event(event)
assert result == "dispatch-kb-001"
mock_adapter_factory.assert_not_called()
mock_create.assert_awaited_once()
dispatch_kwargs = mock_create.call_args.kwargs
assert dispatch_kwargs["executor_type"] == "hermes_kb_growth_healthcheck"
assert dispatch_kwargs["created_by"] == "governance_dispatcher_intake"
assert dispatch_kwargs["decision_context"]["next_action"] == "run_kb_growth_healthcheck"
assert dispatch_kwargs["decision_context"]["workflow"]["current_stage"] == "queued_kb_healthcheck"
@pytest.mark.asyncio
async def test_llm_failure_fallback_to_skip(self):
"""fusion adapter 拋 Exception → fallback skip不寫 dispatch返回 None。"""