feat(governance): complete stale km owner review
All checks were successful
CD Pipeline / tests (push) Successful in 5m28s
Code Review / ai-code-review (push) Successful in 12s
Type Sync Check / check-type-sync (push) Successful in 26s
CD Pipeline / build-and-deploy (push) Successful in 5m12s
CD Pipeline / post-deploy-checks (push) Successful in 1m31s

This commit is contained in:
Your Name
2026-05-24 18:28:10 +08:00
parent 00cf6f009d
commit 630cd5381c
8 changed files with 1435 additions and 2 deletions

View File

@@ -22,7 +22,7 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.ai_governance import router
from src.db.models import KnowledgeEntryRecord
from src.db.models import GovernanceRemediationDispatch, KnowledgeEntryRecord
from src.models.governance import (
DailyCount,
DispatchItem,
@@ -37,6 +37,8 @@ from src.models.governance import (
KnowledgeReviewDraftStaleRatioSnapshot,
KnowledgeStaleCandidate,
KnowledgeStaleCandidatesResponse,
KnowledgeStaleOwnerReviewCompleteRequest,
KnowledgeStaleOwnerReviewCompleteResponse,
KnowledgeStaleOwnerReviewRequest,
KnowledgeStaleOwnerReviewResponse,
map_severity,
@@ -51,7 +53,13 @@ from src.services.governance_km_review_service import (
)
from src.services.governance_km_stale_review_service import (
KmStaleOwnerReviewError,
_build_completion_plan_fingerprint,
_build_owner_review_completion_audit_context,
_build_stale_owner_review_decision_context,
_completion_stage_for_outcome,
)
from src.services.governance_km_stale_review_service import (
_build_stale_ratio_recheck_context as _build_stale_owner_review_recheck_context,
)
from src.services.governance_query_service import (
_build_km_review_draft_dedupe_groups,
@@ -791,6 +799,166 @@ class TestKmReviewDraftDedupe:
assert ctx["worker_result"]["status"] == "queued_owner_review"
assert ctx["ownership"]["lead_agent"] == "Hermes"
def test_complete_stale_candidate_endpoint_returns_writeback_audit_result(self, client):
"""Owner 審核完成後要回傳 KM write / audit / stale ratio recheck 狀態。"""
fake = KnowledgeStaleOwnerReviewCompleteResponse(
entry_id="km-001",
project_id="awoooi",
status="completed",
review_outcome="refresh_with_evidence",
governance_event_id="event-001",
dispatch_id="dispatch-001",
audit_dispatch_id="dispatch-audit-001",
stale_ratio_recheck_dispatch_id="dispatch-recheck-001",
workflow_stage="km_writeback_after_approval",
owner="operator_console",
owner_approved=True,
dry_run=False,
writes_km=True,
writes_governance_audit=True,
stale_ratio_snapshot=KnowledgeReviewDraftStaleRatioSnapshot(
stale_count=119,
total_count=200,
stale_ratio=0.595,
threshold=0.2,
stale_days=7,
),
dry_run_plan_fingerprint="sha256:" + "a" * 64,
generated_at=NOW,
)
captured: dict = {}
async def mock_complete(**kwargs):
captured.update(kwargs)
return fake
with patch(
"src.api.v1.ai_governance.complete_km_stale_owner_review",
new=mock_complete,
):
r = client.post(
"/api/v1/ai/governance/km-stale-candidates/km-001/complete-review",
json={
"dispatch_id": "dispatch-001",
"owner": "operator_console",
"owner_approved": True,
"dry_run": False,
"review_outcome": "refresh_with_evidence",
"owner_note": "Reviewed Sentry and SigNoz evidence.",
"dry_run_plan_fingerprint": "sha256:" + "a" * 64,
},
)
assert r.status_code == 200
assert captured["entry_id"] == "km-001"
assert isinstance(captured["request"], KnowledgeStaleOwnerReviewCompleteRequest)
data = r.json()
assert data["schema_version"] == "km_stale_owner_review_complete_v1"
assert data["status"] == "completed"
assert data["workflow_stage"] == "km_writeback_after_approval"
assert data["writes_km"] is True
assert data["writes_governance_audit"] is True
assert data["audit_dispatch_id"] == "dispatch-audit-001"
assert data["stale_ratio_recheck_dispatch_id"] == "dispatch-recheck-001"
assert data["stale_ratio_snapshot"]["stale_ratio"] == pytest.approx(0.595)
def test_complete_stale_candidate_endpoint_maps_validation_error(self, client):
async def mock_complete(**kwargs):
raise KmStaleOwnerReviewError(403, "owner_approved=true is required")
with patch(
"src.api.v1.ai_governance.complete_km_stale_owner_review",
new=mock_complete,
):
r = client.post(
"/api/v1/ai/governance/km-stale-candidates/km-001/complete-review",
json={
"dispatch_id": "dispatch-001",
"review_outcome": "archive",
"dry_run": False,
},
)
assert r.status_code == 403
assert r.json()["detail"] == "owner_approved=true is required"
def test_stale_owner_review_completion_fingerprint_and_audit_are_visible(self):
record = KnowledgeEntryRecord(
id="km-001",
project_id="awoooi",
title="Sentry checkout failure repair",
content="old evidence",
entry_type=EntryType.AUTO_RUNBOOK,
category="AI系統",
tags=["sentry"],
source=EntrySource.AI_EXTRACTED,
status=EntryStatus.REVIEW,
updated_at=NOW - timedelta(days=35),
)
dispatch = GovernanceRemediationDispatch(
id="dispatch-001",
governance_event_id="event-001",
event_type="knowledge_degradation",
dispatch_status="pending",
decision_context={
"workflow": {
"entry_id": "km-001",
"steps": ["detected", "waiting_owner_review"],
}
},
executor_type="hermes_km_stale_owner_review",
dispatched_at=NOW,
)
request = KnowledgeStaleOwnerReviewCompleteRequest(
dispatch_id="dispatch-001",
owner="operator_console",
owner_approved=True,
dry_run=False,
review_outcome="refresh_with_evidence",
owner_note="Reviewed Incident, Sentry, SigNoz, and PlayBook.",
)
snapshot = KnowledgeReviewDraftStaleRatioSnapshot(
stale_count=119,
total_count=200,
stale_ratio=0.595,
threshold=0.2,
stale_days=7,
)
fingerprint = _build_completion_plan_fingerprint(
record=record,
dispatch=dispatch,
request=request,
superseded_by=None,
)
audit = _build_owner_review_completion_audit_context(
governance_event_id="event-001",
source_dispatch_id="dispatch-001",
entry=record,
request=request,
stale_ratio_snapshot=snapshot,
recheck_dispatch_id="dispatch-recheck-001",
plan_fingerprint=fingerprint,
completed_at=NOW.isoformat(),
)
recheck = _build_stale_owner_review_recheck_context(
governance_event_id="event-001",
entry_id="km-001",
request=request,
stale_ratio_snapshot=snapshot,
plan_fingerprint=fingerprint,
)
assert fingerprint.startswith("sha256:")
assert _completion_stage_for_outcome("refresh_with_evidence") == "km_writeback_after_approval"
assert audit["workflow"]["current_stage"] == "km_writeback_after_approval"
assert audit["workflow"]["writes_km_without_approval"] is False
assert audit["worker_result"]["status"] == "owner_review_completed"
assert audit["worker_result"]["above_threshold"] is True
assert audit["stale_ratio_recheck"]["dispatch_id"] == "dispatch-recheck-001"
assert recheck["workflow"]["current_stage"] == "stale_ratio_recheck"
assert recheck["workflow"]["stale_ratio_snapshot"]["stale_ratio"] == pytest.approx(0.595)
def test_archive_endpoint_requires_owner_shape_and_returns_audit_result(self, client):
"""Owner 批准後的 archive endpoint 應回傳 KM write 與 audit write 結果。"""
fake = KnowledgeReviewDraftArchiveResponse(