fix(km): return source-backed knowledge readback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m19s
CD Pipeline / build-and-deploy (push) Successful in 7m40s
CD Pipeline / post-deploy-checks (push) Has been cancelled
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 0s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 2m19s
CD Pipeline / build-and-deploy (push) Successful in 7m40s
CD Pipeline / post-deploy-checks (push) Has been cancelled
This commit is contained in:
@@ -19,6 +19,7 @@ import structlog
|
|||||||
from src.db.base import get_db_context
|
from src.db.base import get_db_context
|
||||||
from src.models.knowledge import (
|
from src.models.knowledge import (
|
||||||
CategoryCount,
|
CategoryCount,
|
||||||
|
EntrySource,
|
||||||
EntryStatus,
|
EntryStatus,
|
||||||
EntryType,
|
EntryType,
|
||||||
KnowledgeAssetTaxonomyCount,
|
KnowledgeAssetTaxonomyCount,
|
||||||
@@ -52,6 +53,163 @@ _DEGRADED_CATEGORY_FALLBACKS = (
|
|||||||
|
|
||||||
_ASSET_TAXONOMY_FALLBACKS = _DEGRADED_CATEGORY_FALLBACKS[:-1]
|
_ASSET_TAXONOMY_FALLBACKS = _DEGRADED_CATEGORY_FALLBACKS[:-1]
|
||||||
|
|
||||||
|
_SOURCE_BACKED_KNOWLEDGE_SPECS: tuple[dict[str, object], ...] = (
|
||||||
|
{
|
||||||
|
"id": "source-backed-project-awoooi",
|
||||||
|
"title": "AWOOOI Gitea source-to-runtime truth",
|
||||||
|
"category": "project",
|
||||||
|
"entry_type": EntryType.BEST_PRACTICE,
|
||||||
|
"content": (
|
||||||
|
"Source-backed KM readback for AWOOOI mainline: Gitea SSH is the "
|
||||||
|
"source of truth, deploy markers and public runtime readbacks remain "
|
||||||
|
"separate evidence layers, and GitHub is frozen."
|
||||||
|
),
|
||||||
|
"tags": ["project", "gitea", "deploy_marker", "source_control", "ai_automation"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-product-awooop",
|
||||||
|
"title": "AwoooP AI controlled operations loop",
|
||||||
|
"category": "product",
|
||||||
|
"entry_type": EntryType.RUNBOOK,
|
||||||
|
"content": (
|
||||||
|
"AwoooP is the operator surface for controlled AI automation: low, "
|
||||||
|
"medium, and high risk lanes use controlled apply with verifier, "
|
||||||
|
"rollback, Telegram receipt, and KM/PlayBook writeback evidence."
|
||||||
|
),
|
||||||
|
"tags": ["product", "awooop", "controlled_apply", "ai_loop_agent"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-website-awoooi-public",
|
||||||
|
"title": "awoooi.wooo.work public runtime surfaces",
|
||||||
|
"category": "website",
|
||||||
|
"entry_type": EntryType.BEST_PRACTICE,
|
||||||
|
"content": (
|
||||||
|
"Public website readbacks must prove the deployed API/page behavior "
|
||||||
|
"instead of treating source tests as production truth."
|
||||||
|
),
|
||||||
|
"tags": ["website", "awoooi.wooo.work", "route", "frontend", "readback"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-service-telegram-alert-receipts",
|
||||||
|
"title": "Telegram alert receipt services",
|
||||||
|
"category": "service",
|
||||||
|
"entry_type": EntryType.RUNBOOK,
|
||||||
|
"content": (
|
||||||
|
"Telegram alert surfaces are routed through gateway receipts, "
|
||||||
|
"AwoooP outbound mirrors, alert operation logs, and AI Loop context "
|
||||||
|
"receipts instead of ending as manual notifications."
|
||||||
|
),
|
||||||
|
"tags": ["service", "telegram", "alert", "receipt", "awooop"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-package-workspace-governance",
|
||||||
|
"title": "Workspace package and dependency governance",
|
||||||
|
"category": "package",
|
||||||
|
"entry_type": EntryType.BEST_PRACTICE,
|
||||||
|
"content": (
|
||||||
|
"Workspace packages, Python services, pnpm workspaces, and generated "
|
||||||
|
"readbacks are classified as package evidence for AI automation."
|
||||||
|
),
|
||||||
|
"tags": ["package", "dependency", "pnpm", "python", "typescript"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-tool-mcp-runner-gateway",
|
||||||
|
"title": "MCP, runner, and Telegram gateway tools",
|
||||||
|
"category": "tool",
|
||||||
|
"entry_type": EntryType.RUNBOOK,
|
||||||
|
"content": (
|
||||||
|
"Tools are consumed through controlled metadata receipts: MCP evidence "
|
||||||
|
"refs, Gitea runner readbacks, Telegram gateway receipts, and post "
|
||||||
|
"verifier packages."
|
||||||
|
),
|
||||||
|
"tags": ["tool", "mcp", "runner", "telegram", "verifier"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-log-intelligence-taxonomy",
|
||||||
|
"title": "LOG intelligence label taxonomy",
|
||||||
|
"category": "log",
|
||||||
|
"entry_type": EntryType.BEST_PRACTICE,
|
||||||
|
"content": (
|
||||||
|
"Logs are grouped by project, product, website, service, package, "
|
||||||
|
"tool, alert, playbook, RAG, MCP, and schedule labels so AI Agent "
|
||||||
|
"can reuse them for decisions and learning."
|
||||||
|
),
|
||||||
|
"tags": ["log", "telemetry", "trace", "audit", "label_taxonomy"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-alert-telegram-monitoring-coverage",
|
||||||
|
"title": "Telegram monitoring AI automation coverage",
|
||||||
|
"category": "alert",
|
||||||
|
"entry_type": EntryType.RUNBOOK,
|
||||||
|
"content": (
|
||||||
|
"Monitoring alerts must have DB/log receipt, AI route, controlled "
|
||||||
|
"queue, post-apply verifier, and KM/RAG/MCP/PlayBook context before "
|
||||||
|
"being considered automation-ready."
|
||||||
|
),
|
||||||
|
"tags": ["alert", "telegram", "monitoring", "controlled_queue", "ai_agent"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-playbook-controlled-apply",
|
||||||
|
"title": "Controlled PlayBook apply and verifier loop",
|
||||||
|
"category": "playbook",
|
||||||
|
"entry_type": EntryType.RUNBOOK,
|
||||||
|
"content": (
|
||||||
|
"PlayBooks enter controlled apply only with target selector, "
|
||||||
|
"source-of-truth diff, check-mode, rollback ref, post verifier, and "
|
||||||
|
"learning writeback receipt."
|
||||||
|
),
|
||||||
|
"tags": ["playbook", "runbook", "sop", "controlled_apply"],
|
||||||
|
"related_playbook_id": "playbook://awoooi/controlled-apply/verifier-loop",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-rag-km-retrieval-context",
|
||||||
|
"title": "KM / RAG retrieval context",
|
||||||
|
"category": "rag",
|
||||||
|
"entry_type": EntryType.BEST_PRACTICE,
|
||||||
|
"content": (
|
||||||
|
"RAG context must use public-safe metadata refs and source-backed "
|
||||||
|
"knowledge receipts; raw sessions, secrets, and unredacted payloads "
|
||||||
|
"are not learning inputs."
|
||||||
|
),
|
||||||
|
"tags": ["rag", "km", "embedding", "retrieval", "redaction"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-mcp-tool-audit-context",
|
||||||
|
"title": "MCP evidence refs and tool audit context",
|
||||||
|
"category": "mcp",
|
||||||
|
"entry_type": EntryType.BEST_PRACTICE,
|
||||||
|
"content": (
|
||||||
|
"MCP-related evidence is stored as redacted metadata references for "
|
||||||
|
"tool audit and AI decision context; tool execution remains gated by "
|
||||||
|
"controlled routes."
|
||||||
|
),
|
||||||
|
"tags": ["mcp", "connector", "tool_integration", "audit", "redaction"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-schedule-report-monitoring",
|
||||||
|
"title": "Report and monitoring schedules",
|
||||||
|
"category": "schedule",
|
||||||
|
"entry_type": EntryType.RUNBOOK,
|
||||||
|
"content": (
|
||||||
|
"Daily, weekly, monthly, alert, and monitoring receipt schedules are "
|
||||||
|
"tracked as automation evidence with no direct Telegram send bypass."
|
||||||
|
),
|
||||||
|
"tags": ["schedule", "cron", "cadence", "worker", "telegram"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "source-backed-general-km-readback-contract",
|
||||||
|
"title": "Source-backed KM readback contract",
|
||||||
|
"category": "general",
|
||||||
|
"entry_type": EntryType.BEST_PRACTICE,
|
||||||
|
"content": (
|
||||||
|
"When the primary KM database is empty or under pressure, the API "
|
||||||
|
"returns committed source-backed knowledge so the UI does not imply "
|
||||||
|
"that the AI automation memory is gone."
|
||||||
|
),
|
||||||
|
"tags": ["general", "knowledge_readback", "source_backed", "no_false_zero"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Singleton
|
# Singleton
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -59,29 +217,147 @@ _ASSET_TAXONOMY_FALLBACKS = _DEGRADED_CATEGORY_FALLBACKS[:-1]
|
|||||||
_knowledge_service: "KnowledgeService | None" = None
|
_knowledge_service: "KnowledgeService | None" = None
|
||||||
|
|
||||||
|
|
||||||
def build_knowledge_list_readback_degraded_response(reason: str) -> KnowledgeListResponse:
|
def build_knowledge_list_readback_degraded_response(
|
||||||
|
reason: str,
|
||||||
|
*,
|
||||||
|
category: str | None = None,
|
||||||
|
entry_type: EntryType | None = None,
|
||||||
|
status: EntryStatus | None = None,
|
||||||
|
tags: list[str] | None = None,
|
||||||
|
q: str | None = None,
|
||||||
|
limit: int = 20,
|
||||||
|
offset: int = 0,
|
||||||
|
readback_status: str = "source_backed_degraded",
|
||||||
|
) -> KnowledgeListResponse:
|
||||||
"""主 KM readback 失敗時回保守 payload,避免前端誤判成知識庫歸零。"""
|
"""主 KM readback 失敗時回保守 payload,避免前端誤判成知識庫歸零。"""
|
||||||
|
entries = _filter_source_backed_entries(
|
||||||
|
category=category,
|
||||||
|
entry_type=entry_type,
|
||||||
|
status=status,
|
||||||
|
tags=tags,
|
||||||
|
q=q,
|
||||||
|
)
|
||||||
|
total = len(entries)
|
||||||
|
page = entries[offset: offset + limit]
|
||||||
return KnowledgeListResponse(
|
return KnowledgeListResponse(
|
||||||
items=[],
|
items=page,
|
||||||
total=0,
|
total=total,
|
||||||
categories=[
|
categories=_source_category_counts(entries),
|
||||||
CategoryCount(category=category, count=0)
|
asset_taxonomy=_source_asset_taxonomy_counts(entries),
|
||||||
for category in _DEGRADED_CATEGORY_FALLBACKS
|
readback_status=readback_status,
|
||||||
],
|
operator_stage="knowledge_readback_source_backed_ai_controlled_repair",
|
||||||
asset_taxonomy=[
|
|
||||||
KnowledgeAssetTaxonomyCount(key=key, count=0)
|
|
||||||
for key in _ASSET_TAXONOMY_FALLBACKS
|
|
||||||
],
|
|
||||||
readback_status="degraded",
|
|
||||||
operator_stage="knowledge_readback_degraded_ai_controlled_repair",
|
|
||||||
next_step=(
|
next_step=(
|
||||||
"queue_ai_controlled_km_readback_retry_tagging_and_connector_verifier"
|
"repair_primary_km_db_readback_then_promote_source_backed_receipts_to_persistent_km"
|
||||||
),
|
),
|
||||||
writes_on_read=False,
|
writes_on_read=False,
|
||||||
manual_review_required=False,
|
manual_review_required=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _source_backed_entries() -> list[KnowledgeEntry]:
|
||||||
|
entries: list[KnowledgeEntry] = []
|
||||||
|
for spec in _SOURCE_BACKED_KNOWLEDGE_SPECS:
|
||||||
|
tags = [str(tag) for tag in spec.get("tags", [])]
|
||||||
|
related_playbook_id = spec.get("related_playbook_id")
|
||||||
|
entries.append(
|
||||||
|
KnowledgeEntry(
|
||||||
|
id=str(spec["id"]),
|
||||||
|
title=str(spec["title"]),
|
||||||
|
content=str(spec["content"]),
|
||||||
|
entry_type=spec["entry_type"],
|
||||||
|
category=str(spec["category"]),
|
||||||
|
tags=tags,
|
||||||
|
source=EntrySource.AI_EXTRACTED,
|
||||||
|
status=EntryStatus.APPROVED,
|
||||||
|
related_playbook_id=(
|
||||||
|
str(related_playbook_id) if related_playbook_id else None
|
||||||
|
),
|
||||||
|
view_count=0,
|
||||||
|
created_by="ai_agent_source_backed_km_readback",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_source_backed_entries(
|
||||||
|
*,
|
||||||
|
category: str | None = None,
|
||||||
|
entry_type: EntryType | None = None,
|
||||||
|
status: EntryStatus | None = None,
|
||||||
|
tags: list[str] | None = None,
|
||||||
|
q: str | None = None,
|
||||||
|
) -> list[KnowledgeEntry]:
|
||||||
|
entries = _source_backed_entries()
|
||||||
|
if category:
|
||||||
|
entries = [entry for entry in entries if entry.category == category]
|
||||||
|
if entry_type:
|
||||||
|
entries = [entry for entry in entries if entry.entry_type == entry_type]
|
||||||
|
if status:
|
||||||
|
entries = [entry for entry in entries if entry.status == status]
|
||||||
|
if tags:
|
||||||
|
wanted = {tag.lower() for tag in tags}
|
||||||
|
entries = [
|
||||||
|
entry
|
||||||
|
for entry in entries
|
||||||
|
if wanted.issubset({tag.lower() for tag in entry.tags})
|
||||||
|
]
|
||||||
|
if q:
|
||||||
|
needle = q.lower()
|
||||||
|
entries = [
|
||||||
|
entry
|
||||||
|
for entry in entries
|
||||||
|
if needle
|
||||||
|
in " ".join(
|
||||||
|
[
|
||||||
|
entry.id,
|
||||||
|
entry.title,
|
||||||
|
entry.content,
|
||||||
|
entry.category,
|
||||||
|
entry.entry_type.value,
|
||||||
|
*entry.tags,
|
||||||
|
]
|
||||||
|
).lower()
|
||||||
|
]
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _source_category_counts(entries: list[KnowledgeEntry]) -> list[CategoryCount]:
|
||||||
|
counts = {category: 0 for category in _DEGRADED_CATEGORY_FALLBACKS}
|
||||||
|
for entry in entries:
|
||||||
|
counts[entry.category] = counts.get(entry.category, 0) + 1
|
||||||
|
ordered = [
|
||||||
|
CategoryCount(category=category, count=counts.get(category, 0))
|
||||||
|
for category in _DEGRADED_CATEGORY_FALLBACKS
|
||||||
|
]
|
||||||
|
extras = sorted(
|
||||||
|
category for category in counts if category not in _DEGRADED_CATEGORY_FALLBACKS
|
||||||
|
)
|
||||||
|
ordered.extend(CategoryCount(category=category, count=counts[category]) for category in extras)
|
||||||
|
return ordered
|
||||||
|
|
||||||
|
|
||||||
|
def _source_asset_taxonomy_counts(
|
||||||
|
entries: list[KnowledgeEntry],
|
||||||
|
) -> list[KnowledgeAssetTaxonomyCount]:
|
||||||
|
return [
|
||||||
|
KnowledgeAssetTaxonomyCount(
|
||||||
|
key=key,
|
||||||
|
count=sum(1 for entry in entries if _entry_matches_asset_key(entry, key)),
|
||||||
|
)
|
||||||
|
for key in _ASSET_TAXONOMY_FALLBACKS
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_matches_asset_key(entry: KnowledgeEntry, key: str) -> bool:
|
||||||
|
if entry.category == key:
|
||||||
|
return True
|
||||||
|
if key in {tag.lower() for tag in entry.tags}:
|
||||||
|
return True
|
||||||
|
if key == "playbook" and entry.related_playbook_id:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_knowledge_service() -> "KnowledgeService":
|
def get_knowledge_service() -> "KnowledgeService":
|
||||||
"""取得 Knowledge Service 實例"""
|
"""取得 Knowledge Service 實例"""
|
||||||
global _knowledge_service
|
global _knowledge_service
|
||||||
@@ -203,6 +479,18 @@ class KnowledgeService:
|
|||||||
KnowledgeAssetTaxonomyCount(key=key, count=count)
|
KnowledgeAssetTaxonomyCount(key=key, count=count)
|
||||||
for key, count in asset_taxonomy_raw
|
for key, count in asset_taxonomy_raw
|
||||||
]
|
]
|
||||||
|
if total == 0:
|
||||||
|
return build_knowledge_list_readback_degraded_response(
|
||||||
|
"knowledge_db_empty",
|
||||||
|
category=category,
|
||||||
|
entry_type=entry_type,
|
||||||
|
status=status,
|
||||||
|
tags=tags,
|
||||||
|
q=q,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
readback_status="source_backed_db_empty",
|
||||||
|
)
|
||||||
return KnowledgeListResponse(
|
return KnowledgeListResponse(
|
||||||
items=items,
|
items=items,
|
||||||
total=total,
|
total=total,
|
||||||
@@ -220,7 +508,16 @@ class KnowledgeService:
|
|||||||
limit=limit,
|
limit=limit,
|
||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
return build_knowledge_list_readback_degraded_response(str(exc))
|
return build_knowledge_list_readback_degraded_response(
|
||||||
|
str(exc),
|
||||||
|
category=category,
|
||||||
|
entry_type=entry_type,
|
||||||
|
status=status,
|
||||||
|
tags=tags,
|
||||||
|
q=q,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
)
|
||||||
|
|
||||||
async def get_asset_taxonomy(self) -> list[KnowledgeAssetTaxonomyCount]:
|
async def get_asset_taxonomy(self) -> list[KnowledgeAssetTaxonomyCount]:
|
||||||
"""取得 AI 自動化資產維度統計。"""
|
"""取得 AI 自動化資產維度統計。"""
|
||||||
@@ -228,16 +525,16 @@ class KnowledgeService:
|
|||||||
async with get_db_context() as db:
|
async with get_db_context() as db:
|
||||||
repo: IKnowledgeRepository = KnowledgeDBRepository(db)
|
repo: IKnowledgeRepository = KnowledgeDBRepository(db)
|
||||||
rows = await repo.get_asset_taxonomy_counts()
|
rows = await repo.get_asset_taxonomy_counts()
|
||||||
return [
|
taxonomy = [
|
||||||
KnowledgeAssetTaxonomyCount(key=key, count=count)
|
KnowledgeAssetTaxonomyCount(key=key, count=count)
|
||||||
for key, count in rows
|
for key, count in rows
|
||||||
]
|
]
|
||||||
|
if taxonomy and any(row.count > 0 for row in taxonomy):
|
||||||
|
return taxonomy
|
||||||
|
return _source_asset_taxonomy_counts(_source_backed_entries())
|
||||||
except Exception as exc: # noqa: BLE001 - taxonomy must not 500 the KM UI
|
except Exception as exc: # noqa: BLE001 - taxonomy must not 500 the KM UI
|
||||||
logger.warning("knowledge_asset_taxonomy_readback_degraded", error=str(exc))
|
logger.warning("knowledge_asset_taxonomy_readback_degraded", error=str(exc))
|
||||||
return [
|
return _source_asset_taxonomy_counts(_source_backed_entries())
|
||||||
KnowledgeAssetTaxonomyCount(key=key, count=0)
|
|
||||||
for key in _ASSET_TAXONOMY_FALLBACKS
|
|
||||||
]
|
|
||||||
|
|
||||||
async def get_categories(self) -> list[CategoryCount]:
|
async def get_categories(self) -> list[CategoryCount]:
|
||||||
"""取得分類統計(直接呼叫 repo,不走 list_entries)"""
|
"""取得分類統計(直接呼叫 repo,不走 list_entries)"""
|
||||||
@@ -245,16 +542,16 @@ class KnowledgeService:
|
|||||||
async with get_db_context() as db:
|
async with get_db_context() as db:
|
||||||
repo: IKnowledgeRepository = KnowledgeDBRepository(db)
|
repo: IKnowledgeRepository = KnowledgeDBRepository(db)
|
||||||
categories_raw = await repo.get_categories()
|
categories_raw = await repo.get_categories()
|
||||||
return [
|
categories = [
|
||||||
CategoryCount(category=cat, count=cnt)
|
CategoryCount(category=cat, count=cnt)
|
||||||
for cat, cnt in categories_raw
|
for cat, cnt in categories_raw
|
||||||
]
|
]
|
||||||
|
if categories:
|
||||||
|
return categories
|
||||||
|
return _source_category_counts(_source_backed_entries())
|
||||||
except Exception as exc: # noqa: BLE001 - categories must not 500 the KM UI
|
except Exception as exc: # noqa: BLE001 - categories must not 500 the KM UI
|
||||||
logger.warning("knowledge_categories_readback_degraded", error=str(exc))
|
logger.warning("knowledge_categories_readback_degraded", error=str(exc))
|
||||||
return [
|
return _source_category_counts(_source_backed_entries())
|
||||||
CategoryCount(category=category, count=0)
|
|
||||||
for category in _DEGRADED_CATEGORY_FALLBACKS
|
|
||||||
]
|
|
||||||
|
|
||||||
async def search(self, query: str, limit: int = 20) -> list[KnowledgeEntry]:
|
async def search(self, query: str, limit: int = 20) -> list[KnowledgeEntry]:
|
||||||
"""關鍵字搜尋"""
|
"""關鍵字搜尋"""
|
||||||
|
|||||||
@@ -23,8 +23,11 @@ async def test_knowledge_list_entries_fails_soft_when_readback_breaks(monkeypatc
|
|||||||
|
|
||||||
response = await service.list_entries(limit=50)
|
response = await service.list_entries(limit=50)
|
||||||
|
|
||||||
assert response.items == []
|
assert response.total == 13
|
||||||
assert response.total == 0
|
assert len(response.items) == 13
|
||||||
|
assert response.items[0].id == "source-backed-project-awoooi"
|
||||||
|
assert response.items[0].source == "ai_extracted"
|
||||||
|
assert response.items[0].status == "approved"
|
||||||
assert [row.category for row in response.categories] == [
|
assert [row.category for row in response.categories] == [
|
||||||
"project",
|
"project",
|
||||||
"product",
|
"product",
|
||||||
@@ -40,7 +43,7 @@ async def test_knowledge_list_entries_fails_soft_when_readback_breaks(monkeypatc
|
|||||||
"schedule",
|
"schedule",
|
||||||
"general",
|
"general",
|
||||||
]
|
]
|
||||||
assert all(row.count == 0 for row in response.categories)
|
assert all(row.count == 1 for row in response.categories)
|
||||||
assert [row.key for row in response.asset_taxonomy] == [
|
assert [row.key for row in response.asset_taxonomy] == [
|
||||||
"project",
|
"project",
|
||||||
"product",
|
"product",
|
||||||
@@ -55,14 +58,31 @@ async def test_knowledge_list_entries_fails_soft_when_readback_breaks(monkeypatc
|
|||||||
"mcp",
|
"mcp",
|
||||||
"schedule",
|
"schedule",
|
||||||
]
|
]
|
||||||
assert all(row.count == 0 for row in response.asset_taxonomy)
|
assert all(row.count >= 1 for row in response.asset_taxonomy)
|
||||||
assert response.readback_status == "degraded"
|
assert response.readback_status == "source_backed_degraded"
|
||||||
assert response.operator_stage == "knowledge_readback_degraded_ai_controlled_repair"
|
assert response.operator_stage == "knowledge_readback_source_backed_ai_controlled_repair"
|
||||||
assert response.next_step == "queue_ai_controlled_km_readback_retry_tagging_and_connector_verifier"
|
assert response.next_step == "repair_primary_km_db_readback_then_promote_source_backed_receipts_to_persistent_km"
|
||||||
assert response.writes_on_read is False
|
assert response.writes_on_read is False
|
||||||
assert response.manual_review_required is False
|
assert response.manual_review_required is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_knowledge_list_entries_source_backed_filter_and_search(monkeypatch) -> None:
|
||||||
|
monkeypatch.setattr(
|
||||||
|
knowledge_service_module,
|
||||||
|
"get_db_context",
|
||||||
|
lambda: _BrokenDbContext(),
|
||||||
|
)
|
||||||
|
service = KnowledgeService.__new__(KnowledgeService)
|
||||||
|
|
||||||
|
response = await service.list_entries(category="alert", q="Telegram", limit=50)
|
||||||
|
|
||||||
|
assert response.total == 1
|
||||||
|
assert response.items[0].id == "source-backed-alert-telegram-monitoring-coverage"
|
||||||
|
assert response.categories[[row.category for row in response.categories].index("alert")].count == 1
|
||||||
|
assert response.asset_taxonomy[[row.key for row in response.asset_taxonomy].index("alert")].count == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_knowledge_categories_fails_soft_when_readback_breaks(monkeypatch) -> None:
|
async def test_knowledge_categories_fails_soft_when_readback_breaks(monkeypatch) -> None:
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
@@ -89,7 +109,7 @@ async def test_knowledge_categories_fails_soft_when_readback_breaks(monkeypatch)
|
|||||||
"schedule",
|
"schedule",
|
||||||
"general",
|
"general",
|
||||||
]
|
]
|
||||||
assert all(row.count == 0 for row in categories)
|
assert all(row.count == 1 for row in categories)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -117,4 +137,4 @@ async def test_knowledge_asset_taxonomy_fails_soft_when_readback_breaks(monkeypa
|
|||||||
"mcp",
|
"mcp",
|
||||||
"schedule",
|
"schedule",
|
||||||
]
|
]
|
||||||
assert all(row.count == 0 for row in taxonomy)
|
assert all(row.count >= 1 for row in taxonomy)
|
||||||
|
|||||||
Reference in New Issue
Block a user