Compare commits
4 Commits
fix/tracke
...
fix/tests-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
757745a221 | ||
| 34894f4684 | |||
|
|
4e4cc6c724 | ||
| b222d7af27 |
173
src/qg/checks.py
173
src/qg/checks.py
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
|
import subprocess
|
||||||
import httpx
|
import httpx
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
|
|
||||||
@@ -137,7 +138,16 @@ def check_review_approved(repo: str, pr_number: int) -> tuple[bool, str]:
|
|||||||
|
|
||||||
def check_tests_passed(repo: str, work_item_id: str, branch: str | None = None) -> tuple[bool, str]:
|
def check_tests_passed(repo: str, work_item_id: str, branch: str | None = None) -> tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Check if test report exists and contains PASS indicator.
|
Gate the testing -> deploy transition on the tester's MACHINE-READABLE verdict
|
||||||
|
in 13-test-report.md frontmatter, NOT on a naive substring search of the body.
|
||||||
|
|
||||||
|
ET-013 fix: the previous implementation did `if "PASS" in content`, so a report
|
||||||
|
explicitly marked `verdict: BLOCKED` / `status: blocked` but whose prose mentioned
|
||||||
|
"23 passed" / "✅ PASS" / "All checks passed" was treated as a pass, and an
|
||||||
|
unfinished feature reached Done. This mirrors check_reviewer_verdict (S-5) and
|
||||||
|
check_deploy_status (БАГ 8): read ONLY the YAML frontmatter `verdict:` / `status:`
|
||||||
|
fields, never the body.
|
||||||
|
|
||||||
File: docs/work-items/<work_item_id>/13-test-report.md
|
File: docs/work-items/<work_item_id>/13-test-report.md
|
||||||
"""
|
"""
|
||||||
repo_path = _repo_path(repo, branch)
|
repo_path = _repo_path(repo, branch)
|
||||||
@@ -149,12 +159,67 @@ def check_tests_passed(repo: str, work_item_id: str, branch: str | None = None)
|
|||||||
try:
|
try:
|
||||||
with open(report_path, "r") as f:
|
with open(report_path, "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
if "PASS" in content or "All tests passed" in content:
|
|
||||||
return True, "Test report indicates PASS"
|
|
||||||
return False, "Test report exists but no PASS indicator found"
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
return False, f"Error reading test report: {e}"
|
return False, f"Error reading test report: {e}"
|
||||||
|
|
||||||
|
return _parse_tests_verdict(content)
|
||||||
|
|
||||||
|
|
||||||
|
# Positive / negative verdict tokens, derived from REAL tester reports in
|
||||||
|
# enduro-trails (ET-001..ET-014). The tester is inconsistent: most write
|
||||||
|
# `verdict: PASS`, but ET-006 used `verdict: ready-to-deploy` (with `status: PASSED`),
|
||||||
|
# ET-007 `verdict: PASS — ready-to-deploy`, ET-008 `verdict: stage:ready-to-deploy`
|
||||||
|
# (with `status: pass`). ET-013 (the bug) used `verdict: BLOCKED` / `status: blocked`.
|
||||||
|
# We therefore match known positive/negative TOKENS inside the normalized
|
||||||
|
# verdict/status fields, and treat a negative token as authoritative (a BLOCKED/FAILED
|
||||||
|
# report never passes, even if another field looks positive).
|
||||||
|
_TESTS_NEGATIVE_TOKENS = ("BLOCKED", "FAILED", "FAIL", "REQUEST_CHANGES", "REJECT", "RED")
|
||||||
|
_TESTS_POSITIVE_TOKENS = ("PASSED", "PASS", "READY-TO-DEPLOY", "READY_TO_DEPLOY", "GREEN", "APPROVED")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tests_verdict(content: str) -> tuple[bool, str]:
|
||||||
|
"""Map a 13-test-report.md body to a quality-gate verdict by reading ONLY the
|
||||||
|
machine-readable `verdict:` (and corroborating `status:`) YAML frontmatter fields.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- No frontmatter / bad YAML / neither field present -> (False, reason).
|
||||||
|
- A negative token (BLOCKED/FAILED/...) in verdict OR status -> (False) and is
|
||||||
|
authoritative (ET-013 main case: verdict BLOCKED wins over any prose PASS).
|
||||||
|
- Otherwise a positive token (PASS/PASSED/READY-TO-DEPLOY/...) in verdict OR
|
||||||
|
status -> (True).
|
||||||
|
- Anything else (unrecognized / empty verdict) -> (False, reason).
|
||||||
|
"""
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
if not content.startswith("---"):
|
||||||
|
return False, "No YAML frontmatter in test report (cannot read machine verdict)"
|
||||||
|
|
||||||
|
parts = content.split("---", 2)
|
||||||
|
if len(parts) < 3:
|
||||||
|
return False, "Malformed YAML frontmatter in test report"
|
||||||
|
|
||||||
|
try:
|
||||||
|
fm = yaml.safe_load(parts[1]) or {}
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
return False, f"Invalid YAML frontmatter in test report: {e}"
|
||||||
|
if not isinstance(fm, dict):
|
||||||
|
return False, "Malformed YAML frontmatter in test report (not a mapping)"
|
||||||
|
|
||||||
|
verdict = str(fm.get("verdict", "") or "").upper().strip()
|
||||||
|
status = str(fm.get("status", "") or "").upper().strip()
|
||||||
|
|
||||||
|
if not verdict and not status:
|
||||||
|
return False, "No machine-readable verdict/status in test report frontmatter"
|
||||||
|
|
||||||
|
fields = f"{verdict} {status}"
|
||||||
|
for neg in _TESTS_NEGATIVE_TOKENS:
|
||||||
|
if neg in fields:
|
||||||
|
return False, f"Test verdict: {verdict or status} ({neg})"
|
||||||
|
for pos in _TESTS_POSITIVE_TOKENS:
|
||||||
|
if pos in fields:
|
||||||
|
return True, f"Test verdict: {verdict or status} (PASS)"
|
||||||
|
|
||||||
|
return False, f"No recognized PASS verdict in frontmatter (verdict={verdict!r}, status={status!r})"
|
||||||
|
|
||||||
|
|
||||||
def check_analysis_approved(repo: str, work_item_id: str, branch: str | None = None) -> tuple[bool, str]:
|
def check_analysis_approved(repo: str, work_item_id: str, branch: str | None = None) -> tuple[bool, str]:
|
||||||
@@ -281,6 +346,64 @@ def check_tests_local(repo: str, branch: str) -> tuple[bool, str]:
|
|||||||
return False, f"Local test run error: {e}"
|
return False, f"Local test run error: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_deploy_status(content: str) -> tuple[bool, str]:
|
||||||
|
"""Parse a 14-deploy-log.md body and map its `deploy_status:` frontmatter to a
|
||||||
|
quality-gate verdict. Reads ONLY the machine-readable YAML field, never prose.
|
||||||
|
|
||||||
|
deploy_status: SUCCESS -> (True, "Deploy status: SUCCESS")
|
||||||
|
deploy_status: FAILED -> (False, "Deploy status: FAILED")
|
||||||
|
missing field / no frontmatter / bad YAML -> (False, <reason>)
|
||||||
|
"""
|
||||||
|
import yaml
|
||||||
|
status = None
|
||||||
|
if content.startswith("---"):
|
||||||
|
parts = content.split("---", 2)
|
||||||
|
if len(parts) >= 3:
|
||||||
|
try:
|
||||||
|
fm = yaml.safe_load(parts[1]) or {}
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
return False, f"Invalid YAML frontmatter in deploy log: {e}"
|
||||||
|
status = str(fm.get("deploy_status", "")).upper().strip()
|
||||||
|
if status == "SUCCESS":
|
||||||
|
return True, "Deploy status: SUCCESS"
|
||||||
|
if status == "FAILED":
|
||||||
|
return False, "Deploy status: FAILED"
|
||||||
|
return False, f"No machine-readable deploy_status in frontmatter (got: {status!r})"
|
||||||
|
|
||||||
|
|
||||||
|
def _deploy_log_from_main(repo: str, work_item_id: str) -> str | None:
|
||||||
|
"""Best-effort read of 14-deploy-log.md from origin/main on the shared clone.
|
||||||
|
|
||||||
|
The deployer writes 14-deploy-log.md and merges the deploy artifacts into main
|
||||||
|
via a separate PR (see ET-013), so the file lands in origin/main, NOT in the
|
||||||
|
feature branch worktree the gate normally reads. This recovers it from main.
|
||||||
|
|
||||||
|
Degrades gracefully: any git failure (no clone, network/fetch error, file
|
||||||
|
absent in main) returns None instead of raising, so the caller falls back to
|
||||||
|
the plain "not found" verdict. Never raises.
|
||||||
|
"""
|
||||||
|
repo_clone = os.path.join(settings.repos_dir, repo)
|
||||||
|
if not os.path.isdir(os.path.join(repo_clone, ".git")):
|
||||||
|
return None
|
||||||
|
rel = f"docs/work-items/{work_item_id}/14-deploy-log.md"
|
||||||
|
try:
|
||||||
|
# Refresh origin/main so we see freshly-merged deploy artifacts.
|
||||||
|
subprocess.run(
|
||||||
|
["git", "-C", repo_clone, "fetch", "origin", "main"],
|
||||||
|
check=False, capture_output=True, timeout=30,
|
||||||
|
)
|
||||||
|
show = subprocess.run(
|
||||||
|
["git", "-C", repo_clone, "show", f"origin/main:{rel}"],
|
||||||
|
check=False, capture_output=True, text=True, timeout=15,
|
||||||
|
)
|
||||||
|
except (subprocess.SubprocessError, OSError) as e:
|
||||||
|
logger.warning("deploy-log origin/main lookup failed for %s/%s: %s", repo, work_item_id, e)
|
||||||
|
return None
|
||||||
|
if show.returncode != 0:
|
||||||
|
return None
|
||||||
|
return show.stdout
|
||||||
|
|
||||||
|
|
||||||
def check_deploy_status(repo: str, work_item_id: str, branch: str | None = None) -> tuple[bool, str]:
|
def check_deploy_status(repo: str, work_item_id: str, branch: str | None = None) -> tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
БАГ 8 fix: gate the deploy -> done transition on the deployer's machine-readable
|
БАГ 8 fix: gate the deploy -> done transition on the deployer's machine-readable
|
||||||
@@ -291,32 +414,30 @@ def check_deploy_status(repo: str, work_item_id: str, branch: str | None = None)
|
|||||||
frontmatter. Returns:
|
frontmatter. Returns:
|
||||||
(True, ...) -> deploy_status: SUCCESS
|
(True, ...) -> deploy_status: SUCCESS
|
||||||
(False, ...) -> deploy_status: FAILED, missing field, or no frontmatter
|
(False, ...) -> deploy_status: FAILED, missing field, or no frontmatter
|
||||||
|
|
||||||
|
ET-013 path-sync fix: the deployer writes 14-deploy-log.md and merges the deploy
|
||||||
|
artifacts into main via a SEPARATE PR, so the log lands in origin/main, not in
|
||||||
|
the feature-branch worktree this gate reads via _repo_path(repo, branch). If the
|
||||||
|
file is absent in the worktree we fall back to reading it from origin/main on the
|
||||||
|
shared clone. Lookup order: worktree -> origin/main -> not found.
|
||||||
"""
|
"""
|
||||||
import yaml
|
|
||||||
repo_path = _repo_path(repo, branch)
|
repo_path = _repo_path(repo, branch)
|
||||||
log_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/14-deploy-log.md")
|
log_path = os.path.join(repo_path, f"docs/work-items/{work_item_id}/14-deploy-log.md")
|
||||||
|
|
||||||
if not os.path.isfile(log_path):
|
if os.path.isfile(log_path):
|
||||||
return False, "Deploy log not found (14-deploy-log.md)"
|
try:
|
||||||
try:
|
with open(log_path, "r") as f:
|
||||||
with open(log_path, "r") as f:
|
content = f.read()
|
||||||
content = f.read()
|
except OSError as e:
|
||||||
status = None
|
return False, f"Error reading deploy log: {e}"
|
||||||
if content.startswith("---"):
|
return _parse_deploy_status(content)
|
||||||
parts = content.split("---", 2)
|
|
||||||
if len(parts) >= 3:
|
# Not in the feature worktree — the deployer may have merged it into main.
|
||||||
try:
|
main_content = _deploy_log_from_main(repo, work_item_id)
|
||||||
fm = yaml.safe_load(parts[1]) or {}
|
if main_content is not None:
|
||||||
except yaml.YAMLError as e:
|
return _parse_deploy_status(main_content)
|
||||||
return False, f"Invalid YAML frontmatter in deploy log: {e}"
|
|
||||||
status = str(fm.get("deploy_status", "")).upper().strip()
|
return False, "Deploy log not found (14-deploy-log.md)"
|
||||||
if status == "SUCCESS":
|
|
||||||
return True, "Deploy status: SUCCESS"
|
|
||||||
if status == "FAILED":
|
|
||||||
return False, "Deploy status: FAILED"
|
|
||||||
return False, f"No machine-readable deploy_status in frontmatter (got: {status!r})"
|
|
||||||
except OSError as e:
|
|
||||||
return False, f"Error reading deploy log: {e}"
|
|
||||||
|
|
||||||
|
|
||||||
# Registry for dynamic lookup by name
|
# Registry for dynamic lookup by name
|
||||||
|
|||||||
166
tests/test_qg.py
166
tests/test_qg.py
@@ -167,23 +167,110 @@ class TestCheckReviewApproved:
|
|||||||
|
|
||||||
|
|
||||||
class TestCheckTestsPassed:
|
class TestCheckTestsPassed:
|
||||||
def test_report_with_pass(self, setup_work_item_dir):
|
"""ET-013 fix: testing -> deploy gate reads the tester's MACHINE-READABLE verdict
|
||||||
repo_dir = setup_work_item_dir
|
in 13-test-report.md frontmatter (verdict:/status:), NOT a substring of the body.
|
||||||
wi_dir = repo_dir / "docs" / "work-items" / "ET-001"
|
Mirrors check_reviewer_verdict / check_deploy_status. The old `if "PASS" in content`
|
||||||
wi_dir.mkdir(parents=True)
|
let a `verdict: BLOCKED` report whose prose said "23 passed"/"✅ PASS" pass the gate,
|
||||||
(wi_dir / "13-test-report.md").write_text("# Test Report\n\nResult: PASS\n")
|
shipping an unfinished feature to Done."""
|
||||||
|
|
||||||
|
def _write(self, repo_dir, content, wi="ET-001"):
|
||||||
|
wi_dir = repo_dir / "docs" / "work-items" / wi
|
||||||
|
wi_dir.mkdir(parents=True)
|
||||||
|
(wi_dir / "13-test-report.md").write_text(content)
|
||||||
|
|
||||||
|
def test_verdict_pass_passes(self, setup_work_item_dir):
|
||||||
|
# Most common real form (ET-001/002/005/009/011/012/014).
|
||||||
|
self._write(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"---\ntype: test-report\nverdict: PASS\nstatus: pass\n---\n\n# Test Report\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
|
assert passed is True
|
||||||
|
assert "PASS" in reason
|
||||||
|
|
||||||
|
def test_verdict_pass_ready_to_deploy_passes(self, setup_work_item_dir):
|
||||||
|
# ET-007 real form: "PASS — ready-to-deploy".
|
||||||
|
self._write(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"---\nverdict: PASS — ready-to-deploy\nstatus: PASS\n---\n\nbody\n",
|
||||||
|
)
|
||||||
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
assert passed is True
|
assert passed is True
|
||||||
|
|
||||||
def test_report_without_pass(self, setup_work_item_dir):
|
def test_verdict_ready_to_deploy_with_status_passed_passes(self, setup_work_item_dir):
|
||||||
repo_dir = setup_work_item_dir
|
# ET-006 real form: verdict has no PASS word, but status: PASSED.
|
||||||
wi_dir = repo_dir / "docs" / "work-items" / "ET-001"
|
self._write(
|
||||||
wi_dir.mkdir(parents=True)
|
setup_work_item_dir,
|
||||||
(wi_dir / "13-test-report.md").write_text("# Test Report\n\nResult: FAIL\n")
|
"---\nverdict: ready-to-deploy\nstatus: PASSED\n---\n\nbody\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
|
assert passed is True
|
||||||
|
|
||||||
|
def test_verdict_stage_ready_to_deploy_with_status_pass_passes(self, setup_work_item_dir):
|
||||||
|
# ET-008 real form: verdict: stage:ready-to-deploy, status: pass.
|
||||||
|
self._write(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"---\nverdict: stage:ready-to-deploy\nstatus: pass\n---\n\nbody\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
|
assert passed is True
|
||||||
|
|
||||||
|
def test_blocked_verdict_with_pass_in_body_fails(self, setup_work_item_dir):
|
||||||
|
# THE ET-013 BUG: verdict BLOCKED but body is full of "PASS"/"passed".
|
||||||
|
self._write(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"---\ntype: test-report\nstatus: blocked\nverdict: BLOCKED\n---\n\n"
|
||||||
|
"23 passed\n✅ PASS (часть AC-18)\nAll checks passed\n",
|
||||||
|
)
|
||||||
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
assert passed is False
|
assert passed is False
|
||||||
|
assert "BLOCKED" in reason
|
||||||
|
|
||||||
|
def test_failed_verdict_fails(self, setup_work_item_dir):
|
||||||
|
self._write(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"---\nverdict: FAILED\nstatus: failed\n---\n\nbody\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
|
assert passed is False
|
||||||
|
assert "FAILED" in reason
|
||||||
|
|
||||||
|
def test_passed_count_in_body_but_blocked_verdict_fails(self, setup_work_item_dir):
|
||||||
|
# Body says "23 passed" but frontmatter verdict BLOCKED -> substring no longer fools.
|
||||||
|
self._write(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"---\nverdict: BLOCKED\n---\n\nTests: 23 passed, 0 failed.\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
|
assert passed is False
|
||||||
|
|
||||||
|
def test_no_frontmatter_fails(self, setup_work_item_dir):
|
||||||
|
# Old format / prose only -> no machine verdict -> fail.
|
||||||
|
self._write(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"# Test Report\n\nResult: PASS\nAll tests passed.\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
|
assert passed is False
|
||||||
|
|
||||||
|
def test_no_verdict_field_fails(self, setup_work_item_dir):
|
||||||
|
# Frontmatter present but neither verdict nor status -> fail.
|
||||||
|
self._write(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"---\ntype: test-report\nversion: 1\n---\n\nResult: PASS\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
|
assert passed is False
|
||||||
|
|
||||||
|
def test_invalid_yaml_fails_no_exception(self, setup_work_item_dir):
|
||||||
|
# Broken YAML frontmatter -> False with reason, never raises.
|
||||||
|
self._write(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"---\nverdict: [unclosed\n : : :\n---\n\nbody PASS\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
|
assert passed is False
|
||||||
|
assert "YAML" in reason or "frontmatter" in reason.lower()
|
||||||
|
|
||||||
def test_no_report(self, setup_work_item_dir):
|
def test_no_report(self, setup_work_item_dir):
|
||||||
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
passed, reason = check_tests_passed("enduro-trails", "ET-001")
|
||||||
@@ -242,6 +329,65 @@ class TestCheckDeployStatus:
|
|||||||
passed, reason = check_deploy_status("enduro-trails", "ET-011")
|
passed, reason = check_deploy_status("enduro-trails", "ET-011")
|
||||||
assert passed is False
|
assert passed is False
|
||||||
|
|
||||||
|
# --- ET-013 path-sync fix: log written to origin/main via separate PR ---
|
||||||
|
|
||||||
|
def test_origin_main_success_passes_when_absent_in_worktree(self, monkeypatch):
|
||||||
|
# Deployer merged 14-deploy-log.md into main via a separate PR; it is NOT
|
||||||
|
# in the feature worktree. Gate must recover it from origin/main -> PASS.
|
||||||
|
# (This is the exact ET-013 regression.)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"src.qg.checks._deploy_log_from_main",
|
||||||
|
lambda repo, wi: "---\ndeploy_status: SUCCESS\nversion: v0.0.5\n---\n\nLive.\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_deploy_status("enduro-trails", "ET-013")
|
||||||
|
assert passed is True
|
||||||
|
assert "SUCCESS" in reason
|
||||||
|
|
||||||
|
def test_origin_main_failed_fails(self, monkeypatch):
|
||||||
|
# A genuine FAILED log in main must still fail.
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"src.qg.checks._deploy_log_from_main",
|
||||||
|
lambda repo, wi: "---\ndeploy_status: FAILED\nversion: v0.0.5\n---\n\nboom.\n",
|
||||||
|
)
|
||||||
|
passed, reason = check_deploy_status("enduro-trails", "ET-013")
|
||||||
|
assert passed is False
|
||||||
|
assert "FAILED" in reason
|
||||||
|
|
||||||
|
def test_absent_everywhere_fails(self, monkeypatch):
|
||||||
|
# Not in worktree and origin/main lookup yields nothing -> not found.
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"src.qg.checks._deploy_log_from_main", lambda repo, wi: None
|
||||||
|
)
|
||||||
|
passed, reason = check_deploy_status("enduro-trails", "ET-013")
|
||||||
|
assert passed is False
|
||||||
|
assert "not found" in reason.lower()
|
||||||
|
|
||||||
|
@patch("src.qg.checks.subprocess.run")
|
||||||
|
@patch("src.qg.checks.os.path.isdir", return_value=True)
|
||||||
|
def test_fetch_failure_degrades_no_exception(self, mock_isdir, mock_run):
|
||||||
|
# git fetch/show raising (e.g. network) must degrade to "not found",
|
||||||
|
# never propagate an exception out of the gate.
|
||||||
|
import subprocess as _sp
|
||||||
|
mock_run.side_effect = _sp.TimeoutExpired(cmd="git", timeout=30)
|
||||||
|
passed, reason = check_deploy_status("enduro-trails", "ET-013")
|
||||||
|
assert passed is False
|
||||||
|
assert "not found" in reason.lower()
|
||||||
|
|
||||||
|
def test_worktree_log_short_circuits_main_lookup(self, setup_work_item_dir, monkeypatch):
|
||||||
|
# If the log IS present in the worktree, origin/main must NOT be consulted.
|
||||||
|
self._write_log(
|
||||||
|
setup_work_item_dir,
|
||||||
|
"---\ndeploy_status: SUCCESS\nversion: v0.0.3\n---\n\nDeployed OK.\n",
|
||||||
|
)
|
||||||
|
called = {"n": 0}
|
||||||
|
def _boom(repo, wi):
|
||||||
|
called["n"] += 1
|
||||||
|
return None
|
||||||
|
monkeypatch.setattr("src.qg.checks._deploy_log_from_main", _boom)
|
||||||
|
passed, reason = check_deploy_status("enduro-trails", "ET-011")
|
||||||
|
assert passed is True
|
||||||
|
assert called["n"] == 0
|
||||||
|
|
||||||
def test_deploy_stage_qg_is_check_deploy_status(self):
|
def test_deploy_stage_qg_is_check_deploy_status(self):
|
||||||
assert get_qg_for_stage("deploy") == "check_deploy_status"
|
assert get_qg_for_stage("deploy") == "check_deploy_status"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user