diff --git a/.gitea/workflows/cd.yaml b/.gitea/workflows/cd.yaml index d0fbfb035..729c0c53d 100644 --- a/.gitea/workflows/cd.yaml +++ b/.gitea/workflows/cd.yaml @@ -3264,7 +3264,7 @@ jobs: } echo "executor_boundary_stage=live_socket_boundary_verified" - read_workload_db_identity() { + read_workload_db_identity_once() { workload="$1" container="$2" representative_probe_count="$3" @@ -3412,6 +3412,23 @@ jobs: asyncio.run(main()) PY } + read_workload_db_identity() { + workload="$1" + attempt=1 + while [ "$attempt" -le 3 ]; do + if identity_output=$(read_workload_db_identity_once "$@"); then + printf '%s\n' "$identity_output" + return 0 + fi + echo "workload_db_identity_exec_retry=$attempt/3;workload=$workload" >&2 + if [ "$attempt" -lt 3 ]; then + sleep $((attempt * 3)) + fi + attempt=$((attempt + 1)) + done + echo "workload_db_identity_exec_exhausted=3;workload=$workload" >&2 + return 1 + } json_field() { field="$1" python3 -c \ diff --git a/apps/api/src/repositories/knowledge_repository.py b/apps/api/src/repositories/knowledge_repository.py index f76a497cf..b8cac6c34 100644 --- a/apps/api/src/repositories/knowledge_repository.py +++ b/apps/api/src/repositories/knowledge_repository.py @@ -153,6 +153,16 @@ _ENUM_FALLBACKS = { } +def _knowledge_enum_db_label(value): + """Match the mixed-case labels in the deployed PostgreSQL enums.""" + + if isinstance(value, EntryStatus) and value is EntryStatus.PUBLISHED: + return value.value + if isinstance(value, EntryType | EntrySource | EntryStatus): + return value.name + return value + + def _enum_member(value, enum_cls): if value is None: return _ENUM_FALLBACKS.get(enum_cls) @@ -287,11 +297,11 @@ class KnowledgeDBRepository: values = { "title": data.title, "content": data.content, - "entry_type": data.entry_type.value if hasattr(data.entry_type, "value") else data.entry_type, + "entry_type": _knowledge_enum_db_label(data.entry_type), "category": data.category, "tags": data.tags, - "source": data.source.value if hasattr(data.source, "value") else data.source, - "status": data.status.value if hasattr(data.status, "value") else data.status, + "source": _knowledge_enum_db_label(data.source), + "status": _knowledge_enum_db_label(data.status), "related_incident_id": data.related_incident_id, "related_playbook_id": data.related_playbook_id, "related_approval_id": data.related_approval_id, @@ -313,7 +323,7 @@ class KnowledgeDBRepository: "title": data.title, "content": data.content, "tags": data.tags, - "status": data.status.value if hasattr(data.status, "value") else data.status, + "status": _knowledge_enum_db_label(data.status), "related_approval_id": data.related_approval_id, }, ) @@ -339,12 +349,12 @@ class KnowledgeDBRepository: record = KnowledgeEntryRecord( title=data.title, content=data.content, - entry_type=data.entry_type, + entry_type=_knowledge_enum_db_label(data.entry_type), category=data.category, tags=data.tags, - source=data.source, + source=_knowledge_enum_db_label(data.source), # 2026-04-04 ogt: Phase 25 P1 — 支援指定 status(ANTI_PATTERN 直接 PUBLISHED) - status=data.status, + status=_knowledge_enum_db_label(data.status), related_incident_id=data.related_incident_id, related_playbook_id=data.related_playbook_id, related_approval_id=data.related_approval_id, diff --git a/apps/api/tests/test_knowledge_repository_read_model.py b/apps/api/tests/test_knowledge_repository_read_model.py index a2e68a5f6..632e43633 100644 --- a/apps/api/tests/test_knowledge_repository_read_model.py +++ b/apps/api/tests/test_knowledge_repository_read_model.py @@ -12,6 +12,7 @@ from src.repositories.knowledge_repository import ( _ASSET_TAXONOMY_KEYS, KnowledgeDBRepository, _enum_text, + _knowledge_enum_db_label, ) @@ -263,3 +264,12 @@ def test_enum_text_helper_casts_status_to_lowercase_text() -> None: assert "lower(cast(knowledge_entries.status as varchar))" in compiled assert "archived" in compiled + + +def test_knowledge_enum_db_labels_match_deployed_postgresql_contract() -> None: + assert _knowledge_enum_db_label(EntryType.INCIDENT_CASE) == "INCIDENT_CASE" + assert _knowledge_enum_db_label(EntryType.ANTI_PATTERN) == "ANTI_PATTERN" + assert _knowledge_enum_db_label(EntrySource.AI_EXTRACTED) == "AI_EXTRACTED" + assert _knowledge_enum_db_label(EntrySource.HUMAN) == "HUMAN" + assert _knowledge_enum_db_label(EntryStatus.DRAFT) == "DRAFT" + assert _knowledge_enum_db_label(EntryStatus.PUBLISHED) == "published" diff --git a/apps/api/tests/test_signal_worker_ansible_executor_binding.py b/apps/api/tests/test_signal_worker_ansible_executor_binding.py index 6fd5df564..cd7df1b99 100644 --- a/apps/api/tests/test_signal_worker_ansible_executor_binding.py +++ b/apps/api/tests/test_signal_worker_ansible_executor_binding.py @@ -264,6 +264,10 @@ def test_cd_prunes_and_verifies_api_executor_boundary_in_production() -> None: assert "read_workload_db_identity awoooi-api api 1 8" in workflow assert "read_workload_db_identity awoooi-worker worker 1 5" in workflow assert "awoooi-ansible-executor-broker broker 1 2" in workflow + assert "read_workload_db_identity_once" in workflow + assert 'while [ "$attempt" -le 3 ]' in workflow + assert "workload_db_identity_exec_retry=$attempt/3" in workflow + assert "workload_db_identity_exec_exhausted=3" in workflow assert "AWOOOI_DB_REPRESENTATIVE_PROBE_COUNT" in workflow assert "AWOOOI_DB_REQUIRED_CONNECTION_BUDGET" in workflow assert "api_http_concurrency_probe_passed" in workflow