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>
169 lines
7.2 KiB
Python
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",
|
|
}
|