Files
orchestrator/tests/test_bug_fast_track.py
claude-bot 50bcae765a feat(bug-fast-track): cheaper/shorter pipeline route for bug-fix tasks (ORCH-019)
A task carrying the Plane `Bug` label takes a shortened route that skips the
`architecture` stage (one opus architect run + ADR + check_architecture_done),
replacing heavy analysis with a lite package (bug-report + mandatory regression
test plan). EVERY Quality Gate / sub-gate runs UNCHANGED — the route is a
scheduler property, not a gate (root invariant NFR-1): STAGE_TRANSITIONS /
QG_CHECKS / check_* / machine-verdict keys are byte-for-byte preserved.

- src/bug_fast_track.py: new leaf (never-raise) — bug_fast_track_applies (local,
  network-free, checked first), is_bug_task (labels.has_label, Plane API source),
  skips_architecture (pure DB-backed routing predicate), snapshot.
- src/db.py: additive idempotent tasks.track column (TEXT DEFAULT 'full') +
  set_task_track / get_task_track helpers (missing/NULL -> 'full', fail-safe).
- src/stage_engine.py: routing-override on the analysis-exit edge (track='bug' ->
  development/developer, skipping architect); brd-review-clock stamp extended to
  analysis->development. get_next_stage/get_agent_for_stage stay pure.
- src/webhooks/plane.py: classify task as bug in start_pipeline (applies-first
  short-circuit; never-raise -> full cycle on any error).
- src/main.py: additive bug_fast_track block in GET /queue + POST
  /bug-fast-track/escalate (reset 'bug'->'full' to return to the full cycle).
- src/config.py: bug_fast_track_enabled / _label / _repos flags (empty CSV ->
  self-hosting only).
- src/notifications.py: optional 🐞 marker on the bug-track card (never-raise).
- Prompts: analyst.md (lite bug package + escalation), reviewer.md (regression-
  test axis) — 52d canon preserved.
- Docs: CLAUDE.md, README.md (env + API + section), docs/architecture/README.md,
  CHANGELOG.md, .env.example.
- Tests: tests/test_bug_fast_track*.py + test_db_migrations.py + queue block
  (TC-01..TC-15). Full regression green (1551 passed).

Kill-switch ORCH_BUG_FAST_TRACK_ENABLED=false -> 1:1 pre-ORCH-019 (zero
regression; residual track column harmless).

Refs: ORCH-019

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 03:58:15 +03:00

169 lines
7.2 KiB
Python

"""ORCH-019 — src/bug_fast_track.py: bug-fast-track pure logic (never-raise, fail-safe).
Covers (04-test-plan.yaml):
TC-01 is_bug_task() True for an issue carrying the `Bug` label (label read from
the Plane API via labels.has_label, NOT the webhook payload).
TC-02 is_bug_task() False on missing/ambiguous label or labels=None (fail-safe).
TC-03 bug_fast_track_applies(): the LOCAL scope (enabled + CSV repos) is checked
FIRST, before any network; disabled flag -> False without has_label.
TC-04 never-raise: an exception in the label apparatus degrades is_bug_task to
False (full cycle), never propagates.
"""
import os
import tempfile
import pytest
os.environ.setdefault(
"ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_bug_fast_track.db")
)
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
from src import bug_fast_track # 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, "bug_fast_track_enabled", True, raising=False)
monkeypatch.setattr(cfg.settings, "bug_fast_track_label", "Bug", raising=False)
monkeypatch.setattr(cfg.settings, "bug_fast_track_repos", "", raising=False)
# Keep _resolve_project_id offline-deterministic (mirrors test_labels.py).
monkeypatch.setattr(plane_sync, "_resolve_project_id", lambda w=None, p=None: "proj-1")
yield
# --- TC-01: classification True --------------------------------------------
def test_tc01_is_bug_task_true(monkeypatch):
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-BUG"])
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"bug": "uuid-BUG"})
assert bug_fast_track.is_bug_task("ORCH-1", "proj-1") is True
def test_tc01_label_from_plane_api_not_payload(monkeypatch):
"""The decision comes from labels.has_label (Plane API), independent of any
webhook payload field — a payload `type` is irrelevant."""
seen = {"fetch": 0}
def fetch(w, p=None):
seen["fetch"] += 1
return ["uuid-BUG"]
monkeypatch.setattr(plane_sync, "fetch_issue_labels", fetch)
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"bug": "uuid-BUG"})
assert bug_fast_track.is_bug_task("ORCH-1", "proj-1") is True
assert seen["fetch"] == 1 # the Plane API WAS consulted
# --- TC-02: fail-safe on absent / ambiguous / None -------------------------
def test_tc02_label_absent(monkeypatch):
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-OTHER"])
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"bug": "uuid-BUG"})
assert bug_fast_track.is_bug_task("ORCH-1", "proj-1") is False
def test_tc02_labels_none(monkeypatch):
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: None)
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"bug": "uuid-BUG"})
assert bug_fast_track.is_bug_task("ORCH-1", "proj-1") is False
def test_tc02_label_ambiguous(monkeypatch):
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-BUG"])
monkeypatch.setattr(
plane_sync, "get_project_labels", lambda pid: {"bug": "__AMBIGUOUS__"}
)
assert bug_fast_track.is_bug_task("ORCH-1", "proj-1") is False
def test_tc02_empty_label_config(monkeypatch):
monkeypatch.setattr(cfg.settings, "bug_fast_track_label", "", raising=False)
monkeypatch.setattr(plane_sync, "fetch_issue_labels", lambda w, p=None: ["uuid-BUG"])
monkeypatch.setattr(plane_sync, "get_project_labels", lambda pid: {"bug": "uuid-BUG"})
assert bug_fast_track.is_bug_task("ORCH-1", "proj-1") is False
# --- TC-03: local scope first (CSV + self-hosting + kill-switch) ------------
def test_tc03_empty_csv_self_hosting_only(monkeypatch):
monkeypatch.setattr(cfg.settings, "bug_fast_track_repos", "", raising=False)
assert bug_fast_track.bug_fast_track_applies("orchestrator") is True
assert bug_fast_track.bug_fast_track_applies("enduro-trails") is False
def test_tc03_csv_membership(monkeypatch):
monkeypatch.setattr(cfg.settings, "bug_fast_track_repos", "enduro-trails, foo", raising=False)
assert bug_fast_track.bug_fast_track_applies("enduro-trails") is True
assert bug_fast_track.bug_fast_track_applies("foo") is True
# orchestrator is NOT in the explicit CSV -> out of scope.
assert bug_fast_track.bug_fast_track_applies("orchestrator") is False
def test_tc03_killswitch_off_no_network(monkeypatch):
"""The gate idiom `applies(repo) and is_bug_task(...)` short-circuits before any
network call when the kill-switch is off (AC-6)."""
monkeypatch.setattr(cfg.settings, "bug_fast_track_enabled", False, raising=False)
called = {"fetch": 0}
def spy(*a, **k):
called["fetch"] += 1
return ["uuid-BUG"]
monkeypatch.setattr(plane_sync, "fetch_issue_labels", spy)
repo = "orchestrator"
fired = bug_fast_track.bug_fast_track_applies(repo) and bug_fast_track.is_bug_task(
"ORCH-1", "proj-1"
)
assert fired is False
assert called["fetch"] == 0 # is_bug_task never reached -> zero network
# --- TC-04: never-raise -----------------------------------------------------
def test_tc04_is_bug_task_never_raises(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: {"bug": "uuid-BUG"})
# Degrades to False (full cycle), no exception.
assert bug_fast_track.is_bug_task("ORCH-1", "proj-1") is False
def test_tc04_applies_never_raises(monkeypatch):
# A repos config whose access explodes still yields False, not a crash.
class _Poisoned:
bug_fast_track_enabled = True
@property
def bug_fast_track_repos(self):
raise RuntimeError("boom")
monkeypatch.setattr(bug_fast_track, "settings", _Poisoned(), raising=False)
assert bug_fast_track.bug_fast_track_applies("orchestrator") is False
# --- skips_architecture predicate ------------------------------------------
def test_skips_architecture_bug(monkeypatch):
assert bug_fast_track.skips_architecture("bug") is True
assert bug_fast_track.skips_architecture("BUG") is True
def test_skips_architecture_full(monkeypatch):
assert bug_fast_track.skips_architecture("full") is False
assert bug_fast_track.skips_architecture(None) is False
assert bug_fast_track.skips_architecture("") is False
def test_skips_architecture_killswitch_off(monkeypatch):
monkeypatch.setattr(cfg.settings, "bug_fast_track_enabled", False, raising=False)
# Even a stored 'bug' track is inert when the kill-switch is off (1:1 routing).
assert bug_fast_track.skips_architecture("bug") is False
# --- snapshot ---------------------------------------------------------------
def test_snapshot_never_raises():
snap = bug_fast_track.snapshot()
assert set(snap) >= {
"enabled", "label", "repos",
"active_bug_tasks", "total_bug_tasks", "est_saved_architecture_runs",
}