Files
awoooi/apps/api/tests/test_hermes_kb_growth_worker.py
Your Name 8342cfa460
All checks were successful
Code Review / ai-code-review (push) Successful in 11s
CD Pipeline / tests (push) Successful in 2m1s
CD Pipeline / build-and-deploy (push) Successful in 4m45s
CD Pipeline / post-deploy-checks (push) Successful in 1m46s
fix(governance): stop km healthcheck requeue
2026-05-19 23:01:03 +08:00

132 lines
4.9 KiB
Python

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 "governance_event:event-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"
def test_knowledge_tag_filter_is_json_column_compatible():
"""knowledge_entries.tags 是 JSON 欄位時不可使用 json @> text。"""
from src.repositories.knowledge_repository import _json_string_array_has_tag
compiled = str(_json_string_array_has_tag("dispatch:abc-123"))
assert "CAST(knowledge_entries.tags AS VARCHAR)" in compiled
assert "@>" not in compiled
@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"