Lift the two HUMAN gates that block an autonomous batch run (epic ORCH-088):
the BRD gate (analysis: manual Approved) and the prod-deploy gate (deploy
Phase A: manual Confirm Deploy, ORCH-059). Selective (a Plane label on the
issue), declarative, reversible, and WITHOUT touching a single technical check.
Additive, mirroring the conditional sub-gates (ORCH-035/043/058/088): leaf
src/labels.py (never-raise) + two point insertions + config flags.
STAGE_TRANSITIONS / QG_CHECKS / check_* / DB schema are NOT touched.
- autoApprove: врезка in _handle_analysis_approved_flow (files_ok branch) ->
set_issue_approved + log/Telegram/Plane-comment + advance_stage(
finished_agent=None) — the SAME path a human Approved takes (approved-via-
status -> analysis->architecture + mark_brd_review_ended). No duplicated
transition logic; re-entrancy safe.
- autoDeploy: врезка in _handle_self_deploy_phase_a after advance to deploy +
clear_state -> log/Telegram/Plane-comment + _handle_self_deploy_phase_b
(INITIATED marker, Deploying, finalizer). Only the indicative human steps are
skipped. BR-5 holds structurally: Phase A is reached only after the green edge
sub-gates, so autoDeploy can never deploy a broken build.
- plane_sync: fetch_issue_labels (None on error != []), get_project_labels
({normalized_name->uuid}, TTL cache, ambiguity sentinel), set_issue_approved.
- config flags: auto_label_enabled (kill-switch), auto_approve_label/
auto_deploy_label, auto_label_repos (empty -> self-hosting only),
auto_label_states_ttl_s. applies() (local) checked FIRST; has_label (network)
only when applies==True -> zero network / zero regression when disabled (AC-8).
- Fail-safe (never auto on doubt), transparency via log+Telegram+Plane+card,
read-only auto_labels block in GET /queue.
- Tests TC-01..TC-26 across 7 modules; docs (CLAUDE.md, architecture README,
CHANGELOG) updated in the same PR.
Refs: ORCH-089
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
6.8 KiB
Python
151 lines
6.8 KiB
Python
"""ORCH-089 — src/labels.py: auto-mode pure logic (never-raise, fail-safe).
|
|
|
|
Covers (04-test-plan.yaml):
|
|
TC-01 has_label True when the label is present on the issue.
|
|
TC-02 has_label False when the label is absent.
|
|
TC-03 has_label fail-safe (no auto, never-raise) on Plane API error/timeout.
|
|
TC-04 label-name matching is normalized (case/space); ambiguity -> no auto.
|
|
TC-05 auto_approve_applies / auto_deploy_applies: CSV scope + self-hosting.
|
|
TC-06 auto_label_enabled=False -> applies() False -> has_label never reached
|
|
(no network call on the gate).
|
|
"""
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_labels.db"))
|
|
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
|
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
|
|
|
from src import labels # noqa: E402
|
|
from src import plane_sync # noqa: E402
|
|
from src import config as cfg # noqa: E402
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def enabled_self_hosting(monkeypatch):
|
|
monkeypatch.setattr(cfg.settings, "auto_label_enabled", True, raising=False)
|
|
monkeypatch.setattr(cfg.settings, "auto_approve_label", "autoApprove", raising=False)
|
|
monkeypatch.setattr(cfg.settings, "auto_deploy_label", "autoDeploy", raising=False)
|
|
monkeypatch.setattr(cfg.settings, "auto_label_repos", "", raising=False)
|
|
# Keep _resolve_project_id offline-deterministic.
|
|
monkeypatch.setattr(plane_sync, "_resolve_project_id", lambda w=None, p=None: "proj-1")
|
|
yield
|
|
|
|
|
|
# --- TC-01 / TC-02 ---------------------------------------------------------
|
|
def test_tc01_has_label_present(monkeypatch):
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
|
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
|
assert labels.has_label("ORCH-1", "autoApprove") is True
|
|
|
|
|
|
def test_tc02_has_label_absent(monkeypatch):
|
|
# Issue carries a different label uuid than the project's autoApprove uuid.
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-OTHER"])
|
|
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
|
assert labels.has_label("ORCH-1", "autoApprove") is False
|
|
|
|
|
|
def test_tc02_has_label_empty_issue_labels(monkeypatch):
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: [])
|
|
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
|
assert labels.has_label("ORCH-1", "autoApprove") is False
|
|
|
|
|
|
# --- TC-03: fail-safe / never-raise ----------------------------------------
|
|
def test_tc03_fetch_none_failsafe(monkeypatch):
|
|
# fetch returns None (could-not-read) -> False, no auto.
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: None)
|
|
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
|
assert labels.has_label("ORCH-1", "autoApprove") is False
|
|
|
|
|
|
def test_tc03_fetch_raises_failsafe(monkeypatch):
|
|
def boom(*a, **k):
|
|
raise RuntimeError("plane down")
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", boom)
|
|
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
|
# Never raises; degrades to no auto.
|
|
assert labels.has_label("ORCH-1", "autoApprove") is False
|
|
|
|
|
|
def test_tc03_project_map_empty_failsafe(monkeypatch):
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
|
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {})
|
|
assert labels.has_label("ORCH-1", "autoApprove") is False
|
|
|
|
|
|
# --- TC-04: normalization + ambiguity --------------------------------------
|
|
def test_tc04_normalized_match(monkeypatch):
|
|
# Issue label uuid-A; project maps a differently-cased/spaced name to it.
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
|
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
|
# Sought name has different case + surrounding spaces.
|
|
assert labels.has_label("ORCH-1", " AUTOapprove ") is True
|
|
|
|
|
|
def test_tc04_ambiguous_name_no_auto(monkeypatch):
|
|
# Two distinct project labels collapse to the same normalized name -> ambiguous
|
|
# sentinel from get_project_labels -> has_label False.
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
|
monkeypatch.setattr(
|
|
plane_sync, "get_project_labels",
|
|
lambda pid: {"autoapprove": "__AMBIGUOUS__"},
|
|
)
|
|
assert labels.has_label("ORCH-1", "autoApprove") is False
|
|
|
|
|
|
def test_tc04_empty_label_name(monkeypatch):
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-A"])
|
|
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"autoapprove": "uuid-A"})
|
|
assert labels.has_label("ORCH-1", "") is False
|
|
|
|
|
|
# --- TC-05: scope (CSV + self-hosting) -------------------------------------
|
|
def test_tc05_empty_csv_self_hosting_only(monkeypatch):
|
|
monkeypatch.setattr(cfg.settings, "auto_label_repos", "", raising=False)
|
|
assert labels.auto_approve_applies("orchestrator") is True
|
|
assert labels.auto_deploy_applies("orchestrator") is True
|
|
# Non-self repo with empty CSV -> not in scope.
|
|
assert labels.auto_approve_applies("enduro-trails") is False
|
|
assert labels.auto_deploy_applies("enduro-trails") is False
|
|
|
|
|
|
def test_tc05_csv_membership(monkeypatch):
|
|
monkeypatch.setattr(cfg.settings, "auto_label_repos", "enduro-trails, foo", raising=False)
|
|
assert labels.auto_approve_applies("enduro-trails") is True
|
|
assert labels.auto_deploy_applies("foo") is True
|
|
# orchestrator is NOT in the explicit CSV -> out of scope.
|
|
assert labels.auto_approve_applies("orchestrator") is False
|
|
|
|
|
|
# --- TC-06: kill-switch -> applies False, no network -----------------------
|
|
def test_tc06_killswitch_applies_false(monkeypatch):
|
|
monkeypatch.setattr(cfg.settings, "auto_label_enabled", False, raising=False)
|
|
assert labels.auto_approve_applies("orchestrator") is False
|
|
assert labels.auto_deploy_applies("orchestrator") is False
|
|
|
|
|
|
def test_tc06_killswitch_gate_makes_no_network(monkeypatch):
|
|
"""The gate idiom `applies(repo) and has_label(...)` short-circuits before any
|
|
network call when the kill-switch is off (AC-8)."""
|
|
monkeypatch.setattr(cfg.settings, "auto_label_enabled", False, raising=False)
|
|
called = {"fetch": 0}
|
|
|
|
def spy(*a, **k):
|
|
called["fetch"] += 1
|
|
return ["uuid-A"]
|
|
monkeypatch.setattr(plane_sync, "fetch_issue_labels", spy)
|
|
|
|
repo = "orchestrator"
|
|
fired = labels.auto_approve_applies(repo) and labels.has_label("ORCH-1", "autoApprove")
|
|
assert fired is False
|
|
assert called["fetch"] == 0 # has_label never reached -> zero network
|
|
|
|
|
|
def test_snapshot_never_raises():
|
|
snap = labels.snapshot()
|
|
assert set(snap) >= {"enabled", "approve_label", "deploy_label", "repos"}
|