Merge remote-tracking branch 'origin/main' into codex/p0-006-ai-log-triage-20260709
# Conflicts: # .gitea/workflows/cd.yaml
This commit is contained in:
@@ -406,6 +406,10 @@ jobs:
|
||||
;;
|
||||
apps/api/src/services/gitea_workflow_runner_owner_attestation_request.py)
|
||||
;;
|
||||
apps/api/src/services/gitea_cicd_alert_receipt_chain_readback.py)
|
||||
;;
|
||||
apps/api/src/services/gitea_capability_ci_cd_optimization_readback.py)
|
||||
;;
|
||||
apps/api/src/services/reboot_auto_recovery_slo_scorecard.py)
|
||||
;;
|
||||
apps/api/src/services/windows99_vmx_source_locator_readback.py)
|
||||
@@ -542,6 +546,10 @@ jobs:
|
||||
;;
|
||||
apps/api/tests/test_gitea_repo_bundle_backup_readback_api.py)
|
||||
;;
|
||||
apps/api/tests/test_gitea_cicd_alert_receipt_chain_readback_api.py)
|
||||
;;
|
||||
apps/api/tests/test_gitea_capability_ci_cd_optimization_readback_api.py)
|
||||
;;
|
||||
apps/api/tests/test_knowledge_repository_read_model.py)
|
||||
;;
|
||||
apps/api/tests/test_credential_escrow_evidence_intake_readiness_api.py)
|
||||
@@ -918,6 +926,8 @@ jobs:
|
||||
src/services/gitea_private_inventory_closeout_validation.py \
|
||||
src/services/gitea_private_inventory_p0_scorecard.py \
|
||||
src/services/gitea_workflow_runner_owner_attestation_request.py \
|
||||
src/services/gitea_cicd_alert_receipt_chain_readback.py \
|
||||
src/services/gitea_capability_ci_cd_optimization_readback.py \
|
||||
src/services/reboot_auto_recovery_slo_scorecard.py \
|
||||
src/services/reboot_auto_recovery_drill_preflight.py \
|
||||
src/services/stockplatform_public_api_runtime_readback.py \
|
||||
@@ -1039,6 +1049,8 @@ jobs:
|
||||
tests/test_backup_dr_readiness_matrix.py \
|
||||
tests/test_backup_dr_readiness_matrix_api.py \
|
||||
tests/test_gitea_repo_bundle_backup_readback_api.py \
|
||||
tests/test_gitea_cicd_alert_receipt_chain_readback_api.py \
|
||||
tests/test_gitea_capability_ci_cd_optimization_readback_api.py \
|
||||
tests/test_credential_escrow_evidence_intake_readiness_api.py \
|
||||
tests/test_gitea_private_inventory_p0_scorecard_api.py \
|
||||
tests/test_gitea_workflow_runner_owner_attestation_request_api.py \
|
||||
@@ -1091,6 +1103,16 @@ jobs:
|
||||
2>&1 | tee /tmp/pytest-output.txt; PYTEST_EXIT=${PIPESTATUS[0]}
|
||||
else
|
||||
python3.11 ../../scripts/ci/generate-cicd-artifact-receipts.py --repo-root ../.. --out-dir /tmp/awoooi-cicd-artifacts
|
||||
echo "✅ full profile: running learning-chain E2E in isolated pytest process"
|
||||
DATABASE_URL="${DATABASE_URL:-postgresql+asyncpg://ci:ci@localhost/ci}" \
|
||||
PYTHONFAULTHANDLER=1 python3.11 -m pytest tests/test_learning_chain_e2e.py -v --tb=short -p no:cacheprovider \
|
||||
2>&1 | tee /tmp/pytest-learning-chain-output.txt; LEARNING_CHAIN_EXIT=${PIPESTATUS[0]}
|
||||
cat /tmp/pytest-learning-chain-output.txt > /tmp/pytest-output.txt
|
||||
if [ "$LEARNING_CHAIN_EXIT" -ne 0 ]; then
|
||||
tail -60 /tmp/pytest-output.txt
|
||||
cleanup_pytest_workspace_cache
|
||||
exit "$LEARNING_CHAIN_EXIT"
|
||||
fi
|
||||
DATABASE_URL="${DATABASE_URL:-postgresql+asyncpg://ci:ci@localhost/ci}" \
|
||||
PYTHONFAULTHANDLER=1 python3.11 -m pytest tests/ -v --tb=short -x -p no:cacheprovider \
|
||||
--ignore=tests/integration \
|
||||
@@ -1099,8 +1121,9 @@ jobs:
|
||||
--ignore=tests/test_redis_multisig.py \
|
||||
--ignore=tests/test_model_regression.py \
|
||||
--ignore=tests/test_prompt_validation.py \
|
||||
--ignore=tests/test_learning_chain_e2e.py \
|
||||
--ignore=tests/e2e_network_test.py \
|
||||
2>&1 | tee /tmp/pytest-output.txt; PYTEST_EXIT=${PIPESTATUS[0]}
|
||||
2>&1 | tee -a /tmp/pytest-output.txt; PYTEST_EXIT=${PIPESTATUS[0]}
|
||||
fi
|
||||
tail -60 /tmp/pytest-output.txt
|
||||
cleanup_pytest_workspace_cache
|
||||
|
||||
@@ -32,7 +32,6 @@ from src.models.playbook import (
|
||||
from src.services.auto_repair_service import AutoRepairService
|
||||
from src.utils.timezone import now_taipei
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Stubs
|
||||
# =============================================================================
|
||||
@@ -54,7 +53,11 @@ class StubVerifier:
|
||||
warmup_sec: float = 0.0,
|
||||
) -> str:
|
||||
self.calls.append(
|
||||
{"incident_id": incident.incident_id, "snapshot": snapshot, "action_taken": action_taken}
|
||||
{
|
||||
"incident_id": incident.incident_id,
|
||||
"snapshot": snapshot,
|
||||
"action_taken": action_taken,
|
||||
}
|
||||
)
|
||||
if self.raise_exc is not None:
|
||||
raise self.raise_exc
|
||||
@@ -113,6 +116,26 @@ class StubPlaybookService:
|
||||
return playbook is not None
|
||||
|
||||
|
||||
class StubRunbookGenerator:
|
||||
"""No-network/no-DB runbook generator for auto-repair fire-and-forget tasks."""
|
||||
|
||||
async def generate_runbook(self, *args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
async def generate_anti_pattern(self, *args, **kwargs) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class StubAutoRepairExecutionRepository:
|
||||
async def create(self, *args, **kwargs) -> dict:
|
||||
return {"stored": False}
|
||||
|
||||
|
||||
class StubAnomalyCounter:
|
||||
async def record_disposition(self, *args, **kwargs) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class StubRecommendation:
|
||||
def __init__(self, playbook: Playbook, similarity_score: float = 0.9) -> None:
|
||||
self.playbook = playbook
|
||||
@@ -179,6 +202,46 @@ async def _no_cooldown(*args, **kwargs) -> tuple[bool, str]:
|
||||
return True, "允許修復 (test bypass)"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolate_auto_repair_side_effects(monkeypatch):
|
||||
"""Keep this E2E chain in-process only: no DB, network, KM, or runbook writes."""
|
||||
|
||||
async def _record_global_repair_action() -> None:
|
||||
return None
|
||||
|
||||
import src.services.auto_repair_service as _ars_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
_ars_mod, "record_global_repair_action", _record_global_repair_action
|
||||
)
|
||||
|
||||
import src.services.runbook_generator as _rg_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
_rg_mod, "get_runbook_generator", lambda: StubRunbookGenerator()
|
||||
)
|
||||
|
||||
import src.repositories.audit_log_repository as _audit_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
_audit_mod,
|
||||
"get_auto_repair_execution_repository",
|
||||
lambda: StubAutoRepairExecutionRepository(),
|
||||
)
|
||||
|
||||
import src.services.anomaly_counter as _counter_mod
|
||||
|
||||
monkeypatch.setattr(
|
||||
_counter_mod, "get_anomaly_counter", lambda: StubAnomalyCounter()
|
||||
)
|
||||
|
||||
|
||||
async def _drain_service_tasks(service: AutoRepairService) -> None:
|
||||
drain = await service.drain_pending_tasks(timeout=1.0)
|
||||
assert drain.get("timeout") is False, drain
|
||||
assert not service._pending_tasks
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Tests
|
||||
# =============================================================================
|
||||
@@ -195,12 +258,15 @@ async def test_auto_repair_success_triggers_verify_and_learn(monkeypatch):
|
||||
|
||||
# 替換 module-level getters(pure Python, no MagicMock)
|
||||
import src.services.auto_repair_service as _ars_mod
|
||||
|
||||
monkeypatch.setattr(_ars_mod, "_verifier_getter", None, raising=False)
|
||||
|
||||
import src.services.post_execution_verifier as _pev_mod
|
||||
|
||||
monkeypatch.setattr(_pev_mod, "_verifier", stub_verifier)
|
||||
|
||||
import src.services.learning_service as _ls_mod
|
||||
|
||||
monkeypatch.setattr(_ls_mod, "_learning_service", stub_learning)
|
||||
|
||||
playbook = _make_playbook()
|
||||
@@ -225,15 +291,20 @@ async def test_auto_repair_success_triggers_verify_and_learn(monkeypatch):
|
||||
assert len(stub_verifier.calls) == 1, "verifier.verify() 應被呼叫一次"
|
||||
assert stub_verifier.calls[0]["incident_id"] == incident.incident_id
|
||||
assert stub_verifier.calls[0]["snapshot"] is None
|
||||
assert stub_verifier.calls[0]["action_taken"].startswith(f"auto_repair:{playbook.playbook_id}")
|
||||
assert stub_verifier.calls[0]["action_taken"].startswith(
|
||||
f"auto_repair:{playbook.playbook_id}"
|
||||
)
|
||||
assert "steps=Step 1:" in stub_verifier.calls[0]["action_taken"]
|
||||
|
||||
assert len(stub_learning.verification_calls) == 1, "record_verification_result 應被呼叫一次"
|
||||
assert (
|
||||
len(stub_learning.verification_calls) == 1
|
||||
), "record_verification_result 應被呼叫一次"
|
||||
call = stub_learning.verification_calls[0]
|
||||
assert call["incident_id"] == incident.incident_id
|
||||
assert call["action_taken"] == stub_verifier.calls[0]["action_taken"]
|
||||
assert call["verification_result"] == "success"
|
||||
assert call["matched_playbook_id"] == playbook.playbook_id
|
||||
await _drain_service_tasks(service)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -246,9 +317,11 @@ async def test_auto_repair_can_delegate_post_verification(monkeypatch):
|
||||
stub_learning = StubLearningService()
|
||||
|
||||
import src.services.post_execution_verifier as _pev_mod
|
||||
|
||||
monkeypatch.setattr(_pev_mod, "_verifier", stub_verifier)
|
||||
|
||||
import src.services.learning_service as _ls_mod
|
||||
|
||||
monkeypatch.setattr(_ls_mod, "_learning_service", stub_learning)
|
||||
|
||||
playbook = _make_playbook()
|
||||
@@ -273,6 +346,7 @@ async def test_auto_repair_can_delegate_post_verification(monkeypatch):
|
||||
|
||||
assert stub_verifier.calls == []
|
||||
assert stub_learning.verification_calls == []
|
||||
await _drain_service_tasks(service)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -283,9 +357,11 @@ async def test_auto_repair_failure_does_not_call_verifier(monkeypatch):
|
||||
stub_verifier = StubVerifier(result="success")
|
||||
|
||||
import src.services.post_execution_verifier as _pev_mod
|
||||
|
||||
monkeypatch.setattr(_pev_mod, "_verifier", stub_verifier)
|
||||
|
||||
import src.services.learning_service as _ls_mod
|
||||
|
||||
stub_learning = StubLearningService()
|
||||
monkeypatch.setattr(_ls_mod, "_learning_service", stub_learning)
|
||||
|
||||
@@ -320,7 +396,10 @@ async def test_auto_repair_failure_does_not_call_verifier(monkeypatch):
|
||||
|
||||
# 失敗路徑不進入 verify-and-learn 塊
|
||||
assert len(stub_verifier.calls) == 0, "執行失敗時不應呼叫 verifier"
|
||||
assert len(stub_learning.verification_calls) == 0, "執行失敗時不應呼叫 record_verification_result"
|
||||
assert (
|
||||
len(stub_learning.verification_calls) == 0
|
||||
), "執行失敗時不應呼叫 record_verification_result"
|
||||
await _drain_service_tasks(service)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -330,9 +409,11 @@ async def test_auto_repair_failed_step_string_marks_execution_failure(monkeypatc
|
||||
stub_learning = StubLearningService()
|
||||
|
||||
import src.services.post_execution_verifier as _pev_mod
|
||||
|
||||
monkeypatch.setattr(_pev_mod, "_verifier", stub_verifier)
|
||||
|
||||
import src.services.learning_service as _ls_mod
|
||||
|
||||
monkeypatch.setattr(_ls_mod, "_learning_service", stub_learning)
|
||||
|
||||
playbook = _make_playbook()
|
||||
@@ -355,6 +436,7 @@ async def test_auto_repair_failed_step_string_marks_execution_failure(monkeypatc
|
||||
assert "simulated executor failure" in (result.error or "")
|
||||
assert len(stub_verifier.calls) == 0
|
||||
assert len(stub_learning.verification_calls) == 0
|
||||
await _drain_service_tasks(service)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -421,9 +503,11 @@ async def test_verifier_exception_does_not_block_repair(monkeypatch):
|
||||
stub_learning = StubLearningService()
|
||||
|
||||
import src.services.post_execution_verifier as _pev_mod
|
||||
|
||||
monkeypatch.setattr(_pev_mod, "_verifier", stub_verifier)
|
||||
|
||||
import src.services.learning_service as _ls_mod
|
||||
|
||||
monkeypatch.setattr(_ls_mod, "_learning_service", stub_learning)
|
||||
|
||||
playbook = _make_playbook()
|
||||
@@ -446,4 +530,7 @@ async def test_verifier_exception_does_not_block_repair(monkeypatch):
|
||||
# verifier 被呼叫(但拋了例外)
|
||||
assert len(stub_verifier.calls) == 1
|
||||
# learning 不應被呼叫(因為 verifier raise 中斷了 _verify_and_learn)
|
||||
assert len(stub_learning.verification_calls) == 0, "verifier 拋例外後 learning 不應被呼叫"
|
||||
assert (
|
||||
len(stub_learning.verification_calls) == 0
|
||||
), "verifier 拋例外後 learning 不應被呼叫"
|
||||
await _drain_service_tasks(service)
|
||||
|
||||
Reference in New Issue
Block a user