Level A — merge/deploy serialization within one repo: reuse the existing ORCH-043/065 merge-lease (no new mechanism); the only new logic is an unconditional pre-merge rebase in check_branch_mergeable — under the held lease, auto_rebase_onto_main is ALWAYS called when premerge_rebase_always (default True), not just when the branch is behind. No-op on an up-to-date branch (rebase keeps HEAD, force-with-lease -> "Everything up-to-date", CI not triggered). Kill-switch off -> ORCH-043 behaviour 1:1. Level B — declarative task dependencies: additive job_deps table (CREATE ... IF NOT EXISTS, no live-DB migration); claim_next_job gate (NOT EXISTS) defers a job whose depends-on tasks are not yet 'done' without occupying a max_concurrency slot; inert on empty job_deps -> zero regression. New leaf src/task_deps.py (never-raise): is_task_ready (fail-open), DFS cycle detection + Blocked/alert, declare/ingest_plane_relations (db source never hits the network on the hot path), snapshot. Telegram waiting-line, /queue observability, reconciler skip + cycle backstop, reaper untouched. Invariants unchanged: STAGE_TRANSITIONS, QG_CHECKS registry (dep gate is a claim_next_job врезка, not a registered QG), DB schema of existing tables, HTTP endpoints; non-self repos remain a no-op on empty deps/scope. Flags: ORCH_PREMERGE_REBASE_ALWAYS, ORCH_TASK_DEPS_ENABLED, ORCH_TASK_DEPS_SOURCE. Docs: docs/architecture/README.md, CLAUDE.md, .env.example, CHANGELOG.md, adr-0015. Tests: tests/test_orch026_*.py (64 tests); full suite 991 green. Refs: ORCH-026 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
119 lines
4.6 KiB
Python
119 lines
4.6 KiB
Python
"""ORCH-026 conditionality / self-hosting safety (TC-A06, TC-A07).
|
|
|
|
TC-A06 kill-switch / out-of-scope: with the flag off (or for a repo outside the
|
|
merge-gate scope) the merge path behaves 1:1 as before ORCH-026 — no-op.
|
|
TC-A07 self-hosting safety: the new Level-A logic never pushes to main; the only
|
|
force op stays --force-with-lease on the task branch; STAGE_TRANSITIONS
|
|
and the QG_CHECKS registry are unchanged.
|
|
"""
|
|
import os
|
|
import tempfile
|
|
|
|
os.environ.setdefault("ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_orch026_cond.db"))
|
|
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
|
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
|
|
|
from src import merge_gate # noqa: E402
|
|
from src.qg import checks # noqa: E402
|
|
|
|
|
|
# ----------------------------------------------------------------- TC-A06
|
|
def test_out_of_scope_repo_is_noop_even_with_flag_on(monkeypatch):
|
|
"""A repo outside merge_gate scope -> N/A pass, regardless of premerge flag."""
|
|
monkeypatch.setattr(checks.settings, "merge_gate_enabled", True, raising=False)
|
|
monkeypatch.setattr(checks.settings, "merge_gate_repos", "orchestrator", raising=False)
|
|
monkeypatch.setattr(checks.settings, "premerge_rebase_always", True, raising=False)
|
|
|
|
# enduro-trails is NOT in the scope -> no lease, no rebase, just N/A.
|
|
called = {"acquire": 0, "rebase": 0}
|
|
monkeypatch.setattr(merge_gate, "acquire_merge_lease",
|
|
lambda *a, **k: (called.__setitem__("acquire", called["acquire"] + 1), (True, "x"))[1],
|
|
raising=False)
|
|
monkeypatch.setattr(merge_gate, "auto_rebase_onto_main",
|
|
lambda *a, **k: (called.__setitem__("rebase", called["rebase"] + 1), (True, "x"))[1],
|
|
raising=False)
|
|
ok, reason = checks.check_branch_mergeable("enduro-trails", "ET-1", "feature/e")
|
|
assert ok is True
|
|
assert "N/A" in reason
|
|
assert called["acquire"] == 0 and called["rebase"] == 0
|
|
|
|
|
|
def test_task_deps_kill_switch_omits_gate(monkeypatch):
|
|
"""task_deps_enabled=False -> claim_next_job query is the ORCH-1 query (no gate)."""
|
|
import src.db as db
|
|
monkeypatch.setattr(db.settings, "task_deps_enabled", False, raising=False)
|
|
# Inspect the SQL the claim builds by stubbing the connection.
|
|
captured = {}
|
|
|
|
class _FakeConn:
|
|
def execute(self, sql, *a):
|
|
captured.setdefault("sql", sql)
|
|
|
|
class _R:
|
|
def fetchone(self_inner):
|
|
return None
|
|
return _R()
|
|
|
|
def commit(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
monkeypatch.setattr(db, "get_db", lambda: _FakeConn())
|
|
db.claim_next_job()
|
|
assert "NOT EXISTS" not in captured["sql"], "gate must be omitted when disabled"
|
|
|
|
|
|
def test_task_deps_enabled_adds_gate(monkeypatch):
|
|
import src.db as db
|
|
monkeypatch.setattr(db.settings, "task_deps_enabled", True, raising=False)
|
|
captured = {}
|
|
|
|
class _FakeConn:
|
|
def execute(self, sql, *a):
|
|
captured.setdefault("sql", sql)
|
|
|
|
class _R:
|
|
def fetchone(self_inner):
|
|
return None
|
|
return _R()
|
|
|
|
def commit(self):
|
|
pass
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
monkeypatch.setattr(db, "get_db", lambda: _FakeConn())
|
|
db.claim_next_job()
|
|
assert "NOT EXISTS" in captured["sql"], "gate must be present when enabled"
|
|
assert "job_deps" in captured["sql"]
|
|
|
|
|
|
# ----------------------------------------------------------------- TC-A07
|
|
def test_stage_transitions_unchanged():
|
|
"""ORCH-026 must not touch the state machine (AC-A5)."""
|
|
from src.stages import STAGE_TRANSITIONS
|
|
# The canonical happy-path edges must still exist exactly.
|
|
assert STAGE_TRANSITIONS["deploy-staging"]["next"] == "deploy"
|
|
assert STAGE_TRANSITIONS["deploy"]["next"] == "done"
|
|
assert STAGE_TRANSITIONS["development"]["next"] == "review"
|
|
|
|
|
|
def test_qg_registry_has_no_new_dep_gate():
|
|
"""The dependency gate is врезка in claim_next_job, NOT a registered QG."""
|
|
from src.qg.checks import QG_CHECKS
|
|
joined = " ".join(QG_CHECKS.keys())
|
|
assert "task_dep" not in joined and "dependency" not in joined
|
|
|
|
|
|
def test_premerge_only_force_with_lease_on_branch():
|
|
"""auto_rebase_onto_main never pushes to main; force is --force-with-lease only."""
|
|
import inspect
|
|
src = inspect.getsource(merge_gate.auto_rebase_onto_main)
|
|
assert "--force-with-lease" in src
|
|
# No raw 'push origin main' / force-push to main in the rebase path.
|
|
assert "push origin main" not in src
|
|
assert "--force " not in src # plain --force (not -with-lease) is forbidden
|