fix(km): recover primary knowledge readback via direct fallback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Failing after 1m36s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped

This commit is contained in:
ogt
2026-07-10 01:49:47 +08:00
parent 2752b9f554
commit a63e645513
5 changed files with 704 additions and 16 deletions

View File

@@ -3,9 +3,11 @@ import asyncio
import pytest
from src.models.knowledge import (
CategoryCount,
EntrySource,
EntryStatus,
EntryType,
KnowledgeAssetTaxonomyCount,
KnowledgeEntry,
KnowledgeListResponse,
)
@@ -148,6 +150,71 @@ async def test_knowledge_list_entries_bounds_primary_timeout_to_source_backed(mo
assert response.total == 13
@pytest.mark.asyncio
async def test_knowledge_list_entries_uses_direct_readback_after_session_pool_timeout(monkeypatch) -> None:
service = KnowledgeService.__new__(KnowledgeService)
primary_calls = 0
direct_calls = 0
primary_entry = KnowledgeEntry(
id="km-primary-live-1",
title="Primary KM live row",
content="Persistent KM row recovered through direct readback",
entry_type=EntryType.RUNBOOK,
category="alert_handling",
tags=["telegram", "ai_agent", "log"],
source=EntrySource.AI_EXTRACTED,
status=EntryStatus.APPROVED,
)
async def pool_exhausted_primary(**_kwargs):
nonlocal primary_calls
primary_calls += 1
raise RuntimeError("db pool exhausted")
async def direct_readback(**kwargs):
nonlocal direct_calls
direct_calls += 1
assert kwargs["limit"] == 50
return KnowledgeListResponse(
items=[primary_entry],
total=727,
categories=[
CategoryCount(category="alert_handling", count=113),
CategoryCount(category="AI自動化/Ansible受控修復", count=87),
],
asset_taxonomy=[
KnowledgeAssetTaxonomyCount(key="log", count=727),
KnowledgeAssetTaxonomyCount(key="alert", count=443),
KnowledgeAssetTaxonomyCount(key="mcp", count=36),
],
readback_status="ready_direct_connection_after_session_timeout",
primary_readback_ready=True,
operator_stage="knowledge_readback_direct_connection_recovered",
next_step="repair_session_pool_readback_without_hiding_primary_km",
writes_on_read=False,
manual_review_required=False,
)
monkeypatch.setattr(service, "_list_entries_from_primary", pool_exhausted_primary)
monkeypatch.setattr(service, "_list_entries_from_direct", direct_readback)
monkeypatch.setattr(knowledge_service_module, "_PRIMARY_KM_RETRY_DELAY_SECONDS", 0)
response = await service.list_entries(limit=50)
assert primary_calls == 2
assert direct_calls == 1
assert response.total == 727
assert response.items == [primary_entry]
assert response.readback_status == "ready_direct_connection_after_session_timeout"
assert response.primary_readback_ready is True
assert response.operator_stage == "knowledge_readback_direct_connection_recovered"
assert response.next_step == "repair_session_pool_readback_without_hiding_primary_km"
assert response.categories[0].category == "alert_handling"
assert response.asset_taxonomy[0].key == "log"
assert response.writes_on_read is False
assert response.manual_review_required is False
@pytest.mark.asyncio
async def test_knowledge_list_entries_keeps_primary_items_when_taxonomy_degrades(monkeypatch) -> None:
primary_entry = KnowledgeEntry(
@@ -247,6 +314,30 @@ async def test_knowledge_categories_fails_soft_when_readback_breaks(monkeypatch)
assert all(row.count == 1 for row in categories)
@pytest.mark.asyncio
async def test_knowledge_categories_use_direct_readback_when_pool_is_busy(monkeypatch) -> None:
service = KnowledgeService.__new__(KnowledgeService)
async def pool_exhausted_categories():
raise RuntimeError("db pool exhausted")
async def direct_categories():
return [
CategoryCount(category="alert_handling", count=113),
CategoryCount(category="AI自動化/Ansible受控修復", count=87),
]
monkeypatch.setattr(service, "_read_primary_categories", pool_exhausted_categories)
monkeypatch.setattr(service, "_read_categories_direct", direct_categories)
categories = await service.get_categories()
assert [(row.category, row.count) for row in categories] == [
("alert_handling", 113),
("AI自動化/Ansible受控修復", 87),
]
@pytest.mark.asyncio
async def test_knowledge_asset_taxonomy_fails_soft_when_readback_breaks(monkeypatch) -> None:
monkeypatch.setattr(
@@ -275,6 +366,32 @@ async def test_knowledge_asset_taxonomy_fails_soft_when_readback_breaks(monkeypa
assert all(row.count >= 1 for row in taxonomy)
@pytest.mark.asyncio
async def test_knowledge_asset_taxonomy_uses_direct_readback_when_pool_is_busy(monkeypatch) -> None:
service = KnowledgeService.__new__(KnowledgeService)
async def pool_exhausted_taxonomy():
raise RuntimeError("db pool exhausted")
async def direct_taxonomy():
return [
KnowledgeAssetTaxonomyCount(key="log", count=727),
KnowledgeAssetTaxonomyCount(key="alert", count=443),
KnowledgeAssetTaxonomyCount(key="mcp", count=36),
]
monkeypatch.setattr(service, "_read_primary_asset_taxonomy", pool_exhausted_taxonomy)
monkeypatch.setattr(service, "_read_asset_taxonomy_direct", direct_taxonomy)
taxonomy = await service.get_asset_taxonomy()
assert [(row.key, row.count) for row in taxonomy] == [
("log", 727),
("alert", 443),
("mcp", 36),
]
@pytest.mark.asyncio
async def test_knowledge_side_readbacks_bound_timeouts_to_source_backed(monkeypatch) -> None:
service = KnowledgeService.__new__(KnowledgeService)