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

This commit is contained in:
Your Name
2026-05-20 00:30:17 +08:00
parent 101cd42974
commit c8a995aff2
9 changed files with 779 additions and 46 deletions

View File

@@ -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