diff --git a/apps/api/src/services/adr100_remediation_service.py b/apps/api/src/services/adr100_remediation_service.py index 39c0a213..82cf8833 100644 --- a/apps/api/src/services/adr100_remediation_service.py +++ b/apps/api/src/services/adr100_remediation_service.py @@ -11,6 +11,7 @@ T25: remediation queue items are now actionable without mutating incident state: from __future__ import annotations import asyncio +import hashlib from datetime import datetime, timedelta, timezone from typing import Any, Literal, Protocol @@ -770,7 +771,7 @@ def _approval_fingerprint(item: dict[str, Any]) -> str: playbook_id = str(item.get("playbook_id") or "") incident_id = str(item.get("incident_id") or "") basis = work_item_id or f"{incident_id}:{playbook_id}:{item.get('remediation_action') or ''}" - return f"adr100_playbook_authoring:{basis}"[:240] + return hashlib.sha256(f"adr100_playbook_authoring:{basis}".encode("utf-8")).hexdigest() def _approval_result_payload( diff --git a/apps/api/tests/test_adr100_remediation_service.py b/apps/api/tests/test_adr100_remediation_service.py index 9bbdae1f..7741cdb2 100644 --- a/apps/api/tests/test_adr100_remediation_service.py +++ b/apps/api/tests/test_adr100_remediation_service.py @@ -320,7 +320,7 @@ async def test_create_approval_request_is_record_only_and_does_not_authorize_rep assert result["mode"] == "approval" assert result["writes_approval_record"] is True assert result["deduplicated"] is False - assert result["fingerprint"].startswith("adr100_playbook_authoring:") + assert len(result["fingerprint"]) == 64 assert result["writes_incident_state"] is False assert result["writes_auto_repair_result"] is False assert result["approval_id"] == "00000000-0000-0000-0000-00000000a100" @@ -332,8 +332,9 @@ async def test_create_approval_request_is_record_only_and_does_not_authorize_rep assert approval_service.requests[0].metadata["approval_kind"] == "adr100_playbook_authoring" assert approval_service.requests[0].metadata["execution_authorized"] is False assert approval_service.requests[0].metadata["repair_executed"] is False - assert approval_service.fingerprints[0].startswith("lookup:adr100_playbook_authoring:") - assert approval_service.fingerprints[1].startswith("adr100_playbook_authoring:") + assert approval_service.fingerprints[0].startswith("lookup:") + assert len(approval_service.fingerprints[0].removeprefix("lookup:")) == 64 + assert len(approval_service.fingerprints[1]) == 64 assert alert_repo.calls[0]["event_type"] == "APPROVAL_ESCALATED" assert alert_repo.calls[0]["approval_id"] == "00000000-0000-0000-0000-00000000a100" assert alert_repo.calls[0]["context"]["writes_approval_record"] is True diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 198f3fc9..5a30c279 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -69,7 +69,7 @@ - `apps/api/src/services/adr100_remediation_service.py`: - 新增 `create_approval_request()`,可從 remediation work item 建立 `adr100_playbook_authoring` approval。 - approval scope 明確為 `playbook_authoring_record_only`:`execution_authorized=false`、`repair_attempted=false`、`repair_executed=false`。 - - 以 `adr100_playbook_authoring:{work_item}` fingerprint 收斂重複送審,避免同一個告警項目一直產生重複 approval。 + - 以 `adr100_playbook_authoring:{work_item}` 的 SHA-256 fingerprint 收斂重複送審,符合 production `approval_records.fingerprint VARCHAR(64)` 限制。 - 寫入 `approval_records`、`alert_operation_log(APPROVAL_ESCALATED)` 與 timeline;不寫 incident state、不寫 auto-repair result、不建立外部 ticket。 - `apps/api/src/services/approval_execution.py`: - 新增 `playbook_authoring_record_only` 執行分支。