fix(pipeline): make deploy-staging gate conditional on self-hosting repo (ORCH-35)
This commit is contained in:
119
tests/test_qg.py
119
tests/test_qg.py
@@ -452,58 +452,75 @@ class TestCheckTestsLocal:
|
||||
|
||||
|
||||
class TestCheckStagingStatus:
|
||||
"""ORCH-35: deploy-staging -> deploy gate reads machine-readable staging_status:
|
||||
from 15-staging-log.md frontmatter. Mirrors check_deploy_status pattern."""
|
||||
"""ORCH-35 conditional gate (Variant A): deploy-staging gate is active ONLY for
|
||||
the self-hosting orchestrator repo (has staging infra on localhost:8501). All
|
||||
other repos pass immediately with "Staging gate N/A for <repo>".
|
||||
|
||||
def _write_log(self, repo_dir, content):
|
||||
wi_dir = repo_dir / "docs" / "work-items" / "ET-035"
|
||||
Self-hosting path: reads machine-readable staging_status: from 15-staging-log.md
|
||||
frontmatter. Mirrors check_deploy_status pattern.
|
||||
"""
|
||||
|
||||
@pytest.fixture()
|
||||
def orch_dir(self, tmp_path, monkeypatch):
|
||||
"""Temp orchestrator repo dir (self-hosting)."""
|
||||
monkeypatch.setattr("src.qg.checks.settings.repos_dir", str(tmp_path))
|
||||
d = tmp_path / "orchestrator"
|
||||
d.mkdir(exist_ok=True)
|
||||
return d
|
||||
|
||||
def _write_log(self, repo_dir, content, wi="ORCH-035"):
|
||||
wi_dir = repo_dir / "docs" / "work-items" / wi
|
||||
wi_dir.mkdir(parents=True, exist_ok=True)
|
||||
(wi_dir / "15-staging-log.md").write_text(content)
|
||||
|
||||
def test_success_verdict_passes(self, setup_work_item_dir):
|
||||
# ------------------------------------------------------------------
|
||||
# Self-hosting (orchestrator) path -- real file check
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_success_verdict_passes(self, orch_dir):
|
||||
self._write_log(
|
||||
setup_work_item_dir,
|
||||
orch_dir,
|
||||
"---\nstaging_status: SUCCESS\ntimestamp: 2026-06-05T00:00:00Z\n---\n\nAll staging tests passed.\n",
|
||||
)
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035")
|
||||
passed, reason = check_staging_status("orchestrator", "ORCH-035")
|
||||
assert passed is True
|
||||
assert "SUCCESS" in reason
|
||||
|
||||
def test_failed_verdict_fails(self, setup_work_item_dir):
|
||||
def test_failed_verdict_fails(self, orch_dir):
|
||||
self._write_log(
|
||||
setup_work_item_dir,
|
||||
orch_dir,
|
||||
"---\nstaging_status: FAILED\ntimestamp: 2026-06-05T00:00:00Z\n---\n\n2 tests failed.\n",
|
||||
)
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035")
|
||||
passed, reason = check_staging_status("orchestrator", "ORCH-035")
|
||||
assert passed is False
|
||||
assert "FAILED" in reason
|
||||
|
||||
def test_no_file_fails(self, setup_work_item_dir):
|
||||
def test_no_file_fails_for_self_hosting(self, orch_dir):
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035")
|
||||
passed, reason = check_staging_status("orchestrator", "ORCH-035")
|
||||
assert passed is False
|
||||
assert "not found" in reason.lower()
|
||||
|
||||
def test_no_field_fails(self, setup_work_item_dir):
|
||||
def test_no_field_fails(self, orch_dir):
|
||||
# Frontmatter present but no staging_status field -> must NOT pass.
|
||||
self._write_log(
|
||||
setup_work_item_dir,
|
||||
orch_dir,
|
||||
"---\nversion: v0.0.3\n---\n\nStatus: all good (prose only).\n",
|
||||
)
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035")
|
||||
passed, reason = check_staging_status("orchestrator", "ORCH-035")
|
||||
assert passed is False
|
||||
|
||||
def test_prose_only_no_frontmatter_fails(self, setup_work_item_dir):
|
||||
def test_prose_only_no_frontmatter_fails(self, orch_dir):
|
||||
# Prose mentioning SUCCESS but no machine-readable frontmatter -> fail.
|
||||
self._write_log(
|
||||
setup_work_item_dir,
|
||||
orch_dir,
|
||||
"# Staging Log\n\nStatus: SUCCESS (prose, not frontmatter).\n",
|
||||
)
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035")
|
||||
passed, reason = check_staging_status("orchestrator", "ORCH-035")
|
||||
assert passed is False
|
||||
|
||||
def test_origin_main_success_passes_when_absent_in_worktree(self, monkeypatch):
|
||||
@@ -513,7 +530,7 @@ class TestCheckStagingStatus:
|
||||
lambda repo, wi: "---\nstaging_status: SUCCESS\n---\n\nAll good.\n",
|
||||
)
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035-main")
|
||||
passed, reason = check_staging_status("orchestrator", "ORCH-035-main")
|
||||
assert passed is True
|
||||
assert "SUCCESS" in reason
|
||||
|
||||
@@ -523,7 +540,7 @@ class TestCheckStagingStatus:
|
||||
lambda repo, wi: "---\nstaging_status: FAILED\n---\n\nboom.\n",
|
||||
)
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035-main")
|
||||
passed, reason = check_staging_status("orchestrator", "ORCH-035-main")
|
||||
assert passed is False
|
||||
assert "FAILED" in reason
|
||||
|
||||
@@ -532,10 +549,70 @@ class TestCheckStagingStatus:
|
||||
"src.qg.checks._staging_log_from_main", lambda repo, wi: None
|
||||
)
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035-absent")
|
||||
passed, reason = check_staging_status("orchestrator", "ORCH-035-absent")
|
||||
assert passed is False
|
||||
assert "not found" in reason.lower()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Non-self-hosting path -- instant pass, no file dependency
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_non_self_hosting_passes_immediately_no_file(self, tmp_path, monkeypatch):
|
||||
"""Non-self-hosting repo: gate is N/A even without a staging log file."""
|
||||
monkeypatch.setattr("src.qg.checks.settings.repos_dir", str(tmp_path))
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035")
|
||||
assert passed is True
|
||||
assert "N/A" in reason
|
||||
assert "enduro-trails" in reason
|
||||
|
||||
def test_non_self_hosting_passes_regardless_of_file_content(self, tmp_path, monkeypatch):
|
||||
"""Even a FAILED staging log must not block a non-self-hosting repo."""
|
||||
monkeypatch.setattr("src.qg.checks.settings.repos_dir", str(tmp_path))
|
||||
et_dir = tmp_path / "enduro-trails" / "docs" / "work-items" / "ET-035"
|
||||
et_dir.mkdir(parents=True)
|
||||
(et_dir / "15-staging-log.md").write_text(
|
||||
"---\nstaging_status: FAILED\n---\nShould be ignored.\n"
|
||||
)
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("enduro-trails", "ET-035")
|
||||
assert passed is True
|
||||
assert "N/A" in reason
|
||||
|
||||
def test_unknown_repo_also_passes_immediately(self, tmp_path, monkeypatch):
|
||||
"""Any repo that is not orchestrator gets N/A gate."""
|
||||
monkeypatch.setattr("src.qg.checks.settings.repos_dir", str(tmp_path))
|
||||
from src.qg.checks import check_staging_status
|
||||
passed, reason = check_staging_status("some-other-project", "XY-001")
|
||||
assert passed is True
|
||||
assert "N/A" in reason
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# is_self_hosting_repo helper
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_is_self_hosting_true_for_orchestrator(self):
|
||||
from src.qg.checks import is_self_hosting_repo
|
||||
assert is_self_hosting_repo("orchestrator") is True
|
||||
|
||||
def test_is_self_hosting_case_insensitive(self):
|
||||
from src.qg.checks import is_self_hosting_repo
|
||||
assert is_self_hosting_repo("Orchestrator") is True
|
||||
assert is_self_hosting_repo("ORCHESTRATOR") is True
|
||||
|
||||
def test_is_self_hosting_false_for_enduro_trails(self):
|
||||
from src.qg.checks import is_self_hosting_repo
|
||||
assert is_self_hosting_repo("enduro-trails") is False
|
||||
|
||||
def test_is_self_hosting_false_for_empty(self):
|
||||
from src.qg.checks import is_self_hosting_repo
|
||||
assert is_self_hosting_repo("") is False
|
||||
assert is_self_hosting_repo(None) is False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Stage machinery (regression: must not be broken)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def test_deploy_staging_qg_is_check_staging_status(self):
|
||||
assert get_qg_for_stage("deploy-staging") == "check_staging_status"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user