"""ORCH-036 TC-01/02/03: deterministic exit-code -> deploy_status mapping. The finalizer (Phase C) maps the host-hook exit-code to the machine verdict via a PURE function (no LLM, no I/O), so it is unit-testable in isolation. Contract (hook exit-code 0/1/2, AC-1/AC-3): 0 -> SUCCESS; 1 (rolled back), 2 (rollback also failed), and anything else -> FAILED (fail-closed). """ import os os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") from src import self_deploy # noqa: E402 from src.self_deploy import map_exit_code_to_status, build_deploy_log # noqa: E402 def test_tc01_exit0_maps_to_success(): assert map_exit_code_to_status(0) == "SUCCESS" def test_tc02_exit1_rolled_back_maps_to_failed(): assert map_exit_code_to_status(1) == "FAILED" def test_tc03_exit2_rollback_also_failed_maps_to_failed(): assert map_exit_code_to_status(2) == "FAILED" def test_tc09_provenance_fail_closed_exit1_maps_to_failed(): """ORCH-058 TC-09: the Strategy-B hook fail-close uses `exit 1`; that must map to FAILED so the existing БАГ-8 rollback path triggers (prod never left stale).""" assert map_exit_code_to_status(1) == "FAILED" def test_other_exit_codes_map_to_failed(): for code in (3, 127, 255, -1): assert map_exit_code_to_status(code) == "FAILED" def test_non_int_or_none_maps_to_failed_fail_closed(): assert map_exit_code_to_status(None) == "FAILED" assert map_exit_code_to_status("garbage") == "FAILED" def test_deploy_log_frontmatter_carries_status(): """The rendered log must expose deploy_status in YAML frontmatter so the existing _parse_deploy_status contract (AC-10) reads the right verdict.""" body_ok = build_deploy_log("ORCH-036", 0, "SUCCESS") assert body_ok.startswith("---\n") assert "deploy_status: SUCCESS" in body_ok body_fail = build_deploy_log("ORCH-036", 2, "FAILED") assert "deploy_status: FAILED" in body_fail assert "hook_exit_code: 2" in body_fail def test_clear_state_removes_all_markers_and_is_idempotent(monkeypatch, tmp_path): """clear_state wipes the whole work-item state dir (all sentinels) and treats a missing dir as success, so a re-deploy after rollback starts from a clean slate.""" monkeypatch.setattr(self_deploy.settings, "repos_dir", str(tmp_path)) repo, wi = "orchestrator", "ORCH-036" self_deploy.write_marker(repo, wi, self_deploy.APPROVE_REQUESTED, "t") self_deploy.write_marker(repo, wi, self_deploy.INITIATED, "t") self_deploy.write_marker(repo, wi, self_deploy.RESULT, "1") assert self_deploy.has_marker(repo, wi, self_deploy.INITIATED) is True assert self_deploy.clear_state(repo, wi) is True assert self_deploy.has_marker(repo, wi, self_deploy.APPROVE_REQUESTED) is False assert self_deploy.has_marker(repo, wi, self_deploy.INITIATED) is False assert self_deploy.has_marker(repo, wi, self_deploy.RESULT) is False # Idempotent: clearing an already-absent dir is still success (never raises). assert self_deploy.clear_state(repo, wi) is True