132 lines
4.9 KiB
Python
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"
|