Files
orchestrator/tests/test_bug_fast_track_composition.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

88 lines
3.9 KiB
Python

"""ORCH-019 — composition with ORCH-088 serial-gate / ORCH-089 auto-label (AC-9).
Covers (04-test-plan.yaml):
TC-14 A bug-fast-track task is an ORDINARY repo task for the serial gate
(ORCH-088): it counts as an active task and is gated like any other — it
does NOT bypass serialisation. autoApprove/autoDeploy (ORCH-089) apply on
the bug track (scope is repo-based, track-agnostic).
"""
import os
import tempfile
import pytest
os.environ["ORCH_DB_PATH"] = os.path.join(tempfile.gettempdir(), "test_bft_composition.db")
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
import src.db as db # noqa: E402
from src.db import init_db, get_db, enqueue_job, claim_next_job # noqa: E402
from src import serial_gate, labels, config as cfg # noqa: E402
@pytest.fixture(autouse=True)
def fresh_db(tmp_path, monkeypatch):
dbfile = tmp_path / "comp.db"
monkeypatch.setattr(db.settings, "db_path", str(dbfile))
monkeypatch.setattr(cfg.settings, "serial_gate_enabled", True, raising=False)
monkeypatch.setattr(cfg.settings, "serial_gate_repos", "", raising=False)
monkeypatch.setattr(cfg.settings, "serial_gate_freeze_enabled", False, raising=False)
monkeypatch.setattr(cfg.settings, "task_deps_enabled", False, raising=False)
monkeypatch.setattr(cfg.settings, "bug_fast_track_enabled", True, raising=False)
monkeypatch.setattr(cfg.settings, "auto_label_enabled", True, raising=False)
init_db()
yield
def _make_task(work_item_id, stage="analysis", repo="orchestrator", track="full"):
conn = get_db()
cur = conn.execute(
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title, track) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
(work_item_id, work_item_id, repo, f"feature/{work_item_id}", stage, work_item_id, track),
)
tid = cur.lastrowid
conn.commit()
conn.close()
return tid
def test_tc14_bug_task_counts_as_active_in_serial_gate():
# An EARLIER bug task A (unfinished) must gate a later task B's analyst-job —
# a bug task does NOT bypass the serial gate.
_make_task("ORCH-301", stage="development", track="bug") # active bug predecessor
b = _make_task("ORCH-302", stage="analysis", track="full") # new task
enqueue_job("analyst", "orchestrator", "B", task_id=b)
assert claim_next_job() is None, "a bug task must gate a later analyst-job (no bypass)"
# The bug task is the active task in the snapshot.
per = serial_gate.snapshot()["per_repo"]["orchestrator"]
assert per["active_task"]["work_item_id"] == "ORCH-301"
def test_tc14_bug_task_itself_gated_behind_predecessor():
# The bug task is also HELD behind an earlier non-bug task (symmetry).
_make_task("ORCH-310", stage="development", track="full") # active predecessor
b = _make_task("ORCH-311", stage="analysis", track="bug") # new BUG task
enqueue_job("analyst", "orchestrator", "bug-B", task_id=b)
assert claim_next_job() is None, "a bug task is itself serialised behind the predecessor"
def test_tc14_bug_task_claimable_once_predecessor_done():
a = _make_task("ORCH-320", stage="development", track="full")
b = _make_task("ORCH-321", stage="analysis", track="bug")
jid = enqueue_job("analyst", "orchestrator", "bug-B", task_id=b)
assert claim_next_job() is None
# Finish A -> the bug task's analyst-job is now claimable.
conn = get_db()
conn.execute("UPDATE tasks SET stage='done' WHERE id=?", (a,))
conn.commit()
conn.close()
claimed = claim_next_job()
assert claimed is not None and claimed["id"] == jid
def test_tc14_auto_label_applies_track_agnostic(monkeypatch):
# autoApprove/autoDeploy scope is repo-based, independent of the bug track.
assert labels.auto_approve_applies("orchestrator") is True
assert labels.auto_deploy_applies("orchestrator") is True