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>
109 lines
3.7 KiB
Python
109 lines
3.7 KiB
Python
"""ORCH-088 — GET /queue additive serial_gate block (AC-10 / TC-20).
|
|
|
|
The /queue payload must gain an additive ``serial_gate`` block WITHOUT changing
|
|
any pre-existing key (counts/max_concurrency/reconcile/reaper/post_deploy/
|
|
task_deps/recent ...).
|
|
"""
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
|
|
os.environ["ORCH_DB_PATH"] = os.path.join(tempfile.gettempdir(), "test_queue_endpoint.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 # noqa: E402
|
|
from src import config as cfg # noqa: E402
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def fresh_db(tmp_path, monkeypatch):
|
|
dbfile = tmp_path / "q.db"
|
|
monkeypatch.setattr(db.settings, "db_path", str(dbfile))
|
|
monkeypatch.setattr(cfg.settings, "serial_gate_enabled", True, raising=False)
|
|
init_db()
|
|
yield
|
|
|
|
|
|
def test_queue_has_serial_gate_block_and_keeps_existing_keys():
|
|
import asyncio
|
|
from src import main
|
|
|
|
payload = asyncio.run(main.queue())
|
|
|
|
# Pre-existing keys are all still present (no contract break).
|
|
for key in (
|
|
"counts", "max_concurrency", "poll_interval", "resilience", "reconcile",
|
|
"reaper", "post_deploy", "merge_verify", "task_deps", "recent",
|
|
):
|
|
assert key in payload, f"existing /queue key '{key}' must be preserved"
|
|
|
|
# New additive block.
|
|
assert "serial_gate" in payload
|
|
sg = payload["serial_gate"]
|
|
assert sg["enabled"] is True
|
|
assert "repos" in sg and "freeze_enabled" in sg
|
|
assert isinstance(sg["per_repo"], dict)
|
|
|
|
|
|
def test_queue_serial_gate_reflects_freeze():
|
|
import asyncio
|
|
from src import main
|
|
from src import serial_gate
|
|
|
|
serial_gate.set_repo_freeze("orchestrator", "DEGRADED", "ORCH-900")
|
|
payload = asyncio.run(main.queue())
|
|
per = payload["serial_gate"]["per_repo"]
|
|
assert "orchestrator" in per
|
|
assert per["orchestrator"]["frozen"] is True
|
|
assert per["orchestrator"]["frozen_reason"] == "DEGRADED"
|
|
|
|
|
|
# --- ORCH-019 (TC-13): additive bug_fast_track block -----------------------
|
|
def test_queue_has_bug_fast_track_block_and_keeps_existing_keys(monkeypatch):
|
|
import asyncio
|
|
from src import main
|
|
|
|
monkeypatch.setattr(cfg.settings, "bug_fast_track_enabled", True, raising=False)
|
|
payload = asyncio.run(main.queue())
|
|
|
|
# Pre-existing keys are all still present (no contract break).
|
|
for key in ("counts", "serial_gate", "coverage", "auto_labels", "stop", "recent"):
|
|
assert key in payload, f"existing /queue key '{key}' must be preserved"
|
|
|
|
assert "bug_fast_track" in payload
|
|
bft = payload["bug_fast_track"]
|
|
assert bft["enabled"] is True
|
|
assert set(bft) >= {
|
|
"enabled", "label", "repos",
|
|
"active_bug_tasks", "total_bug_tasks", "est_saved_architecture_runs",
|
|
}
|
|
|
|
|
|
def test_queue_bug_fast_track_counts_bug_tasks():
|
|
import asyncio
|
|
from src import main
|
|
|
|
conn = db.get_db()
|
|
conn.execute(
|
|
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title, track) "
|
|
"VALUES ('p1','ORCH-401','orchestrator','feature/x','development','t','bug')"
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title, track) "
|
|
"VALUES ('p2','ORCH-402','orchestrator','feature/y','done','t','bug')"
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title, track) "
|
|
"VALUES ('p3','ORCH-403','orchestrator','feature/z','development','t','full')"
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
bft = asyncio.run(main.queue())["bug_fast_track"]
|
|
assert bft["total_bug_tasks"] == 2 # two bug tasks total
|
|
assert bft["active_bug_tasks"] == 1 # one non-terminal bug task
|
|
assert bft["est_saved_architecture_runs"] == 2
|