feat(governance): process hermes km healthchecks
This commit is contained in:
120
apps/api/tests/test_hermes_kb_growth_worker.py
Normal file
120
apps/api/tests/test_hermes_kb_growth_worker.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _make_dispatch(**overrides):
|
||||
defaults = {
|
||||
"id": "dispatch-001",
|
||||
"governance_event_id": "event-001",
|
||||
"event_type": "knowledge_degradation",
|
||||
"executor_type": "hermes_kb_growth_healthcheck",
|
||||
"decision_context": {
|
||||
"workflow": {
|
||||
"impact": {
|
||||
"stale_count": 1450,
|
||||
"total_count": 1867,
|
||||
"stale_ratio": 0.777,
|
||||
"threshold": 0.2,
|
||||
"stale_days": 7,
|
||||
},
|
||||
"stage_by_dispatch_status": {
|
||||
"pending": "queued_kb_healthcheck",
|
||||
"executing": "draft_km_updates",
|
||||
"succeeded": "stale_ratio_recheck",
|
||||
},
|
||||
},
|
||||
"ownership": {
|
||||
"lead_agent": "Hermes",
|
||||
"human_owner": "KM owner / SRE owner",
|
||||
},
|
||||
},
|
||||
}
|
||||
defaults.update(overrides)
|
||||
return SimpleNamespace(**defaults)
|
||||
|
||||
|
||||
def test_km_review_payload_is_review_only():
|
||||
"""Hermes worker 只能產生 REVIEW 草稿,不可直接 approve/publish KM。"""
|
||||
from src.jobs.hermes_kb_growth_worker import _build_km_review_entry_payload
|
||||
from src.models.knowledge import EntrySource, EntryStatus, EntryType
|
||||
|
||||
payload = _build_km_review_entry_payload(_make_dispatch())
|
||||
|
||||
assert payload.entry_type == EntryType.AUTO_RUNBOOK
|
||||
assert payload.source == EntrySource.AI_EXTRACTED
|
||||
assert payload.status == EntryStatus.REVIEW
|
||||
assert "agent:Hermes" in payload.tags
|
||||
assert "needs_owner_review" in payload.tags
|
||||
assert "dispatch:dispatch-001" in payload.tags
|
||||
assert "writes_km_without_approval=false" in payload.content
|
||||
|
||||
|
||||
def test_review_context_keeps_succeeded_at_owner_review_stage():
|
||||
"""dispatch succeeded 代表 worker 完成草稿,不代表 KM 劣化已解決。"""
|
||||
from src.jobs.hermes_kb_growth_worker import _build_review_context
|
||||
|
||||
context = _make_dispatch().decision_context
|
||||
updated = _build_review_context(
|
||||
context,
|
||||
dispatch_id="dispatch-001",
|
||||
governance_event_id="event-001",
|
||||
km_entry_id="km-001",
|
||||
)
|
||||
|
||||
workflow = updated["workflow"]
|
||||
assert workflow["current_stage"] == "waiting_owner_review"
|
||||
assert workflow["stage_by_dispatch_status"]["succeeded"] == "waiting_owner_review"
|
||||
assert workflow["kb_draft_entry_id"] == "km-001"
|
||||
assert workflow["writes_km_without_approval"] is False
|
||||
assert updated["next_action"] == "owner_review_km_draft"
|
||||
assert updated["worker_result"]["status"] == "draft_created"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_once_advances_pending_dispatch_to_review_draft():
|
||||
"""pending dispatch 應推進到 succeeded + waiting_owner_review read model。"""
|
||||
from src.jobs.hermes_kb_growth_worker import run_hermes_kb_growth_once
|
||||
|
||||
pending = _make_dispatch()
|
||||
dispatched = _make_dispatch()
|
||||
executing = _make_dispatch()
|
||||
km_entry = SimpleNamespace(id="km-001")
|
||||
|
||||
transition_mock = AsyncMock(side_effect=[dispatched, executing, _make_dispatch()])
|
||||
|
||||
with (
|
||||
patch(
|
||||
"src.jobs.hermes_kb_growth_worker.list_pending_by_executor",
|
||||
new=AsyncMock(return_value=[pending]),
|
||||
) as list_pending,
|
||||
patch(
|
||||
"src.jobs.hermes_kb_growth_worker.transition_status",
|
||||
new=transition_mock,
|
||||
),
|
||||
patch(
|
||||
"src.jobs.hermes_kb_growth_worker._create_or_get_km_review_draft",
|
||||
new=AsyncMock(return_value=km_entry),
|
||||
) as create_draft,
|
||||
patch(
|
||||
"src.jobs.hermes_kb_growth_worker.update_decision_context",
|
||||
new=AsyncMock(),
|
||||
) as update_context,
|
||||
):
|
||||
result = await run_hermes_kb_growth_once(limit=5)
|
||||
|
||||
assert result == {"scanned": 1, "processed": 1, "skipped": 0, "failed": 0}
|
||||
list_pending.assert_awaited_once_with("hermes_kb_growth_healthcheck", limit=5)
|
||||
create_draft.assert_awaited_once_with(executing)
|
||||
transition_mock.assert_has_awaits([
|
||||
call("dispatch-001", "pending", "dispatched"),
|
||||
call("dispatch-001", "dispatched", "executing"),
|
||||
call("dispatch-001", "executing", "succeeded"),
|
||||
])
|
||||
update_context.assert_awaited_once()
|
||||
updated_context = update_context.call_args.args[1]
|
||||
assert updated_context["workflow"]["current_stage"] == "waiting_owner_review"
|
||||
assert updated_context["worker_result"]["km_draft_entry_id"] == "km-001"
|
||||
Reference in New Issue
Block a user