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
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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user