189 lines
8.1 KiB
Python
189 lines
8.1 KiB
Python
"""ORCH-071 — Phase C finalizer x merge-verify under-gate (integration).
|
|
|
|
Covers TC-05 (FR-3/G2/AC-1: deploy SUCCESS but PR open -> NOT done + alert),
|
|
TC-06 (AC-4: deploy SUCCESS + merge confirmed -> done) and TC-14 (AC-11: Phase B
|
|
runs only on confirm_deploy; merge/verify never introduce an auto-deploy).
|
|
|
|
Mirrors tests/test_deploy_terminal_sync.py: the finalizer drives advance_stage,
|
|
the deploy gate is forced green, and the merge-actor/verifier are mocked so the
|
|
test stays deterministic (no real Gitea/git).
|
|
"""
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
_test_db = os.path.join(tempfile.gettempdir(), "test_orch_merge_verify.db")
|
|
os.environ["ORCH_DB_PATH"] = _test_db
|
|
os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir()
|
|
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
|
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
|
|
|
from unittest.mock import MagicMock # noqa: E402
|
|
|
|
import src.db as _db # noqa: E402
|
|
from src.db import init_db, get_db # noqa: E402
|
|
from src import stage_engine # noqa: E402
|
|
from src import self_deploy # noqa: E402
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def fresh_db(monkeypatch, tmp_path):
|
|
monkeypatch.setattr(_db.settings, "db_path", _test_db)
|
|
if os.path.exists(_test_db):
|
|
os.unlink(_test_db)
|
|
init_db()
|
|
monkeypatch.setattr(self_deploy.settings, "repos_dir", str(tmp_path))
|
|
monkeypatch.setattr(self_deploy.settings, "host_repos_dir", str(tmp_path))
|
|
monkeypatch.setattr(stage_engine.self_deploy, "write_deploy_log", MagicMock(return_value=True))
|
|
# The under-gate is disabled by conftest default; these tests target it.
|
|
monkeypatch.setattr(stage_engine.merge_gate.settings, "merge_verify_enabled", True)
|
|
monkeypatch.setattr(stage_engine.merge_gate.settings, "merge_verify_repos", "")
|
|
# The merged_to_main stamp is an observability side effect (no log file here).
|
|
monkeypatch.setattr(
|
|
stage_engine.self_deploy, "record_merged_to_main", MagicMock(return_value=True)
|
|
)
|
|
# ORCH-021 post-deploy monitor is orthogonal; keep it off for these tests.
|
|
monkeypatch.setattr(stage_engine.post_deploy.settings, "post_deploy_monitor_enabled", False)
|
|
yield
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def silence_side_effects(monkeypatch):
|
|
for name in (
|
|
"notify_stage_change", "notify_qg_failure", "notify_approve_requested",
|
|
"send_telegram", "plane_notify_stage", "plane_notify_qg", "plane_add_comment",
|
|
"set_issue_in_review", "set_issue_needs_input", "set_issue_in_progress",
|
|
"set_issue_blocked", "set_issue_done", "set_issue_analysis",
|
|
"set_issue_awaiting_deploy", "set_issue_deploying", "set_issue_monitoring",
|
|
):
|
|
monkeypatch.setattr(stage_engine, name, MagicMock())
|
|
monkeypatch.setattr(stage_engine.merge_gate, "release_merge_lease", MagicMock())
|
|
|
|
|
|
def _pass(*a, **k):
|
|
return (True, "ok")
|
|
|
|
|
|
def _make_task(stage, repo="orchestrator", branch="feature/ORCH-071-x", wi="ORCH-071"):
|
|
conn = get_db()
|
|
cur = conn.execute(
|
|
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage) VALUES (?, ?, ?, ?, ?)",
|
|
(f"plane-{wi}", wi, repo, branch, stage),
|
|
)
|
|
task_id = cur.lastrowid
|
|
conn.commit()
|
|
conn.close()
|
|
return task_id
|
|
|
|
|
|
def _stage(task_id):
|
|
conn = get_db()
|
|
row = conn.execute("SELECT stage FROM tasks WHERE id=?", (task_id,)).fetchone()
|
|
conn.close()
|
|
return row[0]
|
|
|
|
|
|
def _force_deploy_gate_green(monkeypatch):
|
|
monkeypatch.setattr(
|
|
stage_engine, "QG_CHECKS",
|
|
{**stage_engine.QG_CHECKS, "check_deploy_status": _pass},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-05 (AC-1): deploy_status=SUCCESS but PR open -> task is HELD (not done) + alert.
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc05_success_but_not_merged_holds_and_alerts(monkeypatch):
|
|
self_deploy.write_marker("orchestrator", "ORCH-071", self_deploy.RESULT, "0")
|
|
_force_deploy_gate_green(monkeypatch)
|
|
# The merge-actor finds no merge and the verifier confirms NOT merged.
|
|
monkeypatch.setattr(stage_engine.merge_gate, "merge_pr", MagicMock(return_value=(False, "no open PR")))
|
|
monkeypatch.setattr(stage_engine.merge_gate, "verify_merged_to_main", MagicMock(return_value=False))
|
|
|
|
task_id = _make_task("deploy")
|
|
stage_engine.run_deploy_finalizer(
|
|
{"task_id": task_id, "repo": "orchestrator", "id": 1, "agent": "deploy-finalizer"}
|
|
)
|
|
|
|
# AC-1 PASS: the task did NOT reach done and was Blocked for manual handling.
|
|
assert _stage(task_id) == "deploy"
|
|
assert stage_engine.set_issue_blocked.called
|
|
assert not stage_engine.set_issue_done.called
|
|
assert not stage_engine.set_issue_monitoring.called
|
|
# An alert was sent ("deploy succeeded but not merged").
|
|
assert stage_engine.send_telegram.called
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-06 (AC-4): deploy_status=SUCCESS + merge confirmed -> done (happy-path).
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc06_success_and_merged_reaches_done(monkeypatch):
|
|
self_deploy.write_marker("orchestrator", "ORCH-071", self_deploy.RESULT, "0")
|
|
_force_deploy_gate_green(monkeypatch)
|
|
merge_pr = MagicMock(return_value=(True, "merged PR #1"))
|
|
verify = MagicMock(return_value=True)
|
|
monkeypatch.setattr(stage_engine.merge_gate, "merge_pr", merge_pr)
|
|
monkeypatch.setattr(stage_engine.merge_gate, "verify_merged_to_main", verify)
|
|
|
|
task_id = _make_task("deploy")
|
|
stage_engine.run_deploy_finalizer(
|
|
{"task_id": task_id, "repo": "orchestrator", "id": 1, "agent": "deploy-finalizer"}
|
|
)
|
|
|
|
assert _stage(task_id) == "done"
|
|
# The deterministic merge-actor + verifier both ran on the deploy->done edge.
|
|
assert merge_pr.called
|
|
assert verify.called
|
|
# Self-hosting: terminal status -> Monitoring (post_deploy off here -> Done set).
|
|
assert not stage_engine.set_issue_blocked.called
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-14 (AC-11): a plain Approved on `deploy` (confirm_deploy=False) is a no-op —
|
|
# Phase B (prod deploy) requires "Confirm Deploy", and merge/verify do NOT run
|
|
# (the under-gate never introduces an auto-deploy).
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc14_plain_approved_on_deploy_is_noop_no_merge(monkeypatch):
|
|
monkeypatch.setattr(stage_engine.self_deploy.settings, "self_deploy_enabled", True)
|
|
monkeypatch.setattr(stage_engine.self_deploy.settings, "self_deploy_repos", "")
|
|
monkeypatch.setattr(stage_engine.settings, "deploy_require_manual_approve", True)
|
|
|
|
merge_pr = MagicMock()
|
|
verify = MagicMock()
|
|
initiate = MagicMock(return_value=(True, "ok"))
|
|
monkeypatch.setattr(stage_engine.merge_gate, "merge_pr", merge_pr)
|
|
monkeypatch.setattr(stage_engine.merge_gate, "verify_merged_to_main", verify)
|
|
monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", initiate)
|
|
|
|
task_id = _make_task("deploy")
|
|
# finished_agent=None + confirm_deploy=False == a plain Approved on `deploy`.
|
|
result = stage_engine.advance_stage(
|
|
task_id, "deploy", "orchestrator", "ORCH-071", "feature/ORCH-071-x",
|
|
finished_agent=None, confirm_deploy=False,
|
|
)
|
|
|
|
assert result.note == "approved-on-deploy-noop"
|
|
assert _stage(task_id) == "deploy"
|
|
# No prod deploy initiated and the merge-verify under-gate never fired.
|
|
assert not initiate.called
|
|
assert not merge_pr.called
|
|
assert not verify.called
|
|
|
|
|
|
def test_tc14_confirm_deploy_initiates_phase_b(monkeypatch):
|
|
monkeypatch.setattr(stage_engine.self_deploy.settings, "self_deploy_enabled", True)
|
|
monkeypatch.setattr(stage_engine.self_deploy.settings, "self_deploy_repos", "")
|
|
monkeypatch.setattr(stage_engine.settings, "deploy_require_manual_approve", True)
|
|
initiate = MagicMock(return_value=(True, "ok"))
|
|
monkeypatch.setattr(stage_engine.self_deploy, "initiate_deploy", initiate)
|
|
|
|
task_id = _make_task("deploy")
|
|
stage_engine.advance_stage(
|
|
task_id, "deploy", "orchestrator", "ORCH-071", "feature/ORCH-071-x",
|
|
finished_agent=None, confirm_deploy=True,
|
|
)
|
|
# Only the dedicated "Confirm Deploy" signal initiates the prod deploy.
|
|
assert initiate.called
|