feat(governance): archive duplicate km review drafts
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
Type Sync Check / check-type-sync (push) Successful in 33s
CD Pipeline / tests (push) Successful in 3m31s
CD Pipeline / build-and-deploy (push) Successful in 4m41s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
Type Sync Check / check-type-sync (push) Successful in 33s
CD Pipeline / tests (push) Successful in 3m31s
CD Pipeline / build-and-deploy (push) Successful in 4m41s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s
This commit is contained in:
@@ -14,7 +14,7 @@ Unit Tests — AI Governance Endpoints (PR 1)
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -29,14 +29,20 @@ from src.models.governance import (
|
||||
GovernanceEventsResponse,
|
||||
GovernanceQueueResponse,
|
||||
GovernanceSummaryResponse,
|
||||
KnowledgeReviewDraftArchiveRequest,
|
||||
KnowledgeReviewDraftArchiveResponse,
|
||||
KnowledgeReviewDraftDedupeGroup,
|
||||
KnowledgeReviewDraftDedupeResponse,
|
||||
map_severity,
|
||||
)
|
||||
from src.services.governance_km_review_service import (
|
||||
KmReviewDraftArchiveError,
|
||||
_validate_archive_request_against_plan,
|
||||
)
|
||||
from src.services.governance_query_service import (
|
||||
_build_km_review_draft_dedupe_groups,
|
||||
_extract_kb_draft_entry_id,
|
||||
_extract_governance_event_id_from_tags,
|
||||
_extract_kb_draft_entry_id,
|
||||
_extract_remediation,
|
||||
_extract_worker_status,
|
||||
_merge_dispatch_ids,
|
||||
@@ -256,6 +262,7 @@ class TestEventsReadSideNormalization:
|
||||
def test_events_query_joins_dispatch_table_for_history_buttons(self):
|
||||
"""events endpoint 不可只讀 details.dispatch_ids,必須查 dispatch table。"""
|
||||
import inspect
|
||||
|
||||
from src.services import governance_query_service
|
||||
|
||||
source = inspect.getsource(governance_query_service._load_dispatch_ids_for_events)
|
||||
@@ -489,6 +496,98 @@ class TestKmReviewDraftDedupe:
|
||||
assert first.duplicate_entry_ids == ["km-latest"]
|
||||
assert first.writes_on_read is False
|
||||
|
||||
def test_archive_endpoint_requires_owner_shape_and_returns_audit_result(self, client):
|
||||
"""Owner 批准後的 archive endpoint 應回傳 KM write 與 audit write 結果。"""
|
||||
fake = KnowledgeReviewDraftArchiveResponse(
|
||||
governance_event_id="event-001",
|
||||
canonical_entry_id="km-canonical",
|
||||
requested_duplicate_entry_ids=["km-dup-1", "km-dup-2"],
|
||||
archived_entry_ids=["km-dup-1", "km-dup-2"],
|
||||
status="archived",
|
||||
owner="operator_console",
|
||||
owner_approved=True,
|
||||
dry_run=False,
|
||||
writes_km=True,
|
||||
writes_governance_audit=True,
|
||||
audit_dispatch_id="dispatch-audit-001",
|
||||
generated_at=NOW,
|
||||
)
|
||||
captured: dict = {}
|
||||
|
||||
async def mock_archive(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return fake
|
||||
|
||||
with patch(
|
||||
"src.api.v1.ai_governance.archive_km_review_draft_duplicates",
|
||||
new=mock_archive,
|
||||
):
|
||||
r = client.post(
|
||||
"/api/v1/ai/governance/km-review-drafts/dedupe/event-001/archive-duplicates",
|
||||
json={
|
||||
"canonical_entry_id": "km-canonical",
|
||||
"duplicate_entry_ids": ["km-dup-1", "km-dup-2"],
|
||||
"owner": "operator_console",
|
||||
"owner_approved": True,
|
||||
"dry_run": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert captured["governance_event_id"] == "event-001"
|
||||
assert captured["request"].owner_approved is True
|
||||
data = r.json()
|
||||
assert data["status"] == "archived"
|
||||
assert data["writes_km"] is True
|
||||
assert data["writes_governance_audit"] is True
|
||||
assert data["audit_dispatch_id"] == "dispatch-audit-001"
|
||||
|
||||
def test_archive_endpoint_maps_validation_error_to_http(self, client):
|
||||
async def mock_archive(**kwargs):
|
||||
raise KmReviewDraftArchiveError(409, "stale plan")
|
||||
|
||||
with patch(
|
||||
"src.api.v1.ai_governance.archive_km_review_draft_duplicates",
|
||||
new=mock_archive,
|
||||
):
|
||||
r = client.post(
|
||||
"/api/v1/ai/governance/km-review-drafts/dedupe/event-001/archive-duplicates",
|
||||
json={
|
||||
"canonical_entry_id": "km-canonical",
|
||||
"duplicate_entry_ids": ["km-dup-1"],
|
||||
"owner_approved": True,
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 409
|
||||
assert r.json()["detail"] == "stale plan"
|
||||
|
||||
def test_archive_plan_validation_rejects_stale_duplicates(self):
|
||||
group = KnowledgeReviewDraftDedupeGroup(
|
||||
governance_event_id="event-001",
|
||||
canonical_entry_id="km-canonical",
|
||||
canonical_title="canonical",
|
||||
preferred_source="dispatch_context",
|
||||
duplicate_entry_ids=["km-dup-1", "km-dup-2"],
|
||||
duplicate_count=2,
|
||||
total_entries=3,
|
||||
suggested_action="owner_review_canonical_then_archive_duplicates",
|
||||
owner_action="review_canonical_and_archive_duplicate_drafts",
|
||||
writes_on_read=False,
|
||||
can_archive_without_owner_approval=False,
|
||||
)
|
||||
request = KnowledgeReviewDraftArchiveRequest(
|
||||
canonical_entry_id="km-canonical",
|
||||
duplicate_entry_ids=["km-dup-1"],
|
||||
owner_approved=True,
|
||||
)
|
||||
|
||||
with pytest.raises(KmReviewDraftArchiveError) as exc:
|
||||
_validate_archive_request_against_plan(group, request, ["km-dup-1"])
|
||||
|
||||
assert exc.value.status_code == 409
|
||||
assert "latest dedupe plan" in exc.value.detail
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 4. summary endpoint compliance_rate
|
||||
|
||||
Reference in New Issue
Block a user