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>
98 lines
4.1 KiB
Python
98 lines
4.1 KiB
Python
"""ORCH-019 — Quality-Gate invariants on the bug-fast-track (root invariant NFR-1).
|
|
|
|
Covers (04-test-plan.yaml):
|
|
TC-07 The QG_CHECKS registry + the check_* signatures are NOT changed by the
|
|
bug-fast-track; the machine verdict-keys (verdict / result / deploy_status /
|
|
staging_status / security_status / coverage_status) are preserved by name
|
|
and case.
|
|
TC-12 check_analysis_complete does NOT special-case the bug track (ADR-001 D4):
|
|
a bug lite-package that still emits all 4 analysis files passes; the same
|
|
requirement holds for a non-bug task (no false block, no weakening).
|
|
"""
|
|
import os
|
|
import tempfile
|
|
|
|
os.environ.setdefault(
|
|
"ORCH_DB_PATH", os.path.join(tempfile.gettempdir(), "test_bft_gates.db")
|
|
)
|
|
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
|
|
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
|
|
|
|
from src.qg.checks import QG_CHECKS, check_analysis_complete # noqa: E402
|
|
|
|
|
|
# --- TC-07: registry + verdict-keys unchanged ------------------------------
|
|
def test_tc07_qg_checks_registry_unchanged():
|
|
# The exact registered gate set — a bug-fast-track must add/remove NOTHING.
|
|
expected = {
|
|
"check_analysis_complete",
|
|
"check_analysis_approved",
|
|
"check_architecture_done",
|
|
"check_ci_green",
|
|
"check_review_approved",
|
|
"check_reviewer_verdict",
|
|
"check_tests_local",
|
|
"check_tests_passed",
|
|
"check_staging_status",
|
|
"check_staging_image_fresh",
|
|
"check_deploy_status",
|
|
"check_branch_mergeable",
|
|
"check_security_gate",
|
|
"check_coverage_gate",
|
|
}
|
|
assert set(QG_CHECKS.keys()) == expected
|
|
|
|
|
|
def test_tc07_verdict_keys_preserved():
|
|
"""The frontmatter machine verdict-keys are parsed by exact name/case. ORCH-019
|
|
touches none of the parsers, so the literal keys must still be present."""
|
|
import inspect
|
|
from src.qg import checks as checks_mod
|
|
src = inspect.getsource(checks_mod)
|
|
for key in ("verdict:", "result:", "deploy_status:", "staging_status:"):
|
|
assert key in src, f"verdict key '{key}' must be preserved in qg.checks"
|
|
# security_status / coverage_status live in their own leaves but are read via
|
|
# the same unified frontmatter contract — assert they survive there.
|
|
import inspect as _i
|
|
from src import security_gate, coverage_gate
|
|
assert "security_status" in _i.getsource(security_gate)
|
|
assert "coverage_status" in _i.getsource(coverage_gate)
|
|
|
|
|
|
# --- TC-12: analysis gate not weakened, no false block ---------------------
|
|
def _seed_analysis_docs(repo_root, work_item_id, files):
|
|
d = os.path.join(repo_root, "docs", "work-items", work_item_id)
|
|
os.makedirs(d, exist_ok=True)
|
|
for fn in files:
|
|
with open(os.path.join(d, fn), "w") as fh:
|
|
fh.write("stub\n")
|
|
|
|
|
|
def test_tc12_bug_lite_package_with_all_four_passes(monkeypatch, tmp_path):
|
|
from src.qg import checks as checks_mod
|
|
monkeypatch.setattr(checks_mod, "_repo_path", lambda repo, branch=None: str(tmp_path))
|
|
_seed_analysis_docs(
|
|
str(tmp_path), "ORCH-bug",
|
|
["01-brd.md", "02-trz.md", "03-acceptance-criteria.md", "04-test-plan.yaml"],
|
|
)
|
|
ok, reason = check_analysis_complete("orchestrator", "ORCH-bug", "feature/x")
|
|
assert ok is True, reason
|
|
|
|
|
|
def test_tc12_missing_file_still_fails_for_any_track(monkeypatch, tmp_path):
|
|
"""The gate is NOT weakened for bugs: a package missing 02/03 still fails —
|
|
exactly as for a non-bug task (the gate never reads tasks.track)."""
|
|
from src.qg import checks as checks_mod
|
|
monkeypatch.setattr(checks_mod, "_repo_path", lambda repo, branch=None: str(tmp_path))
|
|
_seed_analysis_docs(str(tmp_path), "ORCH-bug", ["01-brd.md", "04-test-plan.yaml"])
|
|
ok, reason = check_analysis_complete("orchestrator", "ORCH-bug", "feature/x")
|
|
assert ok is False
|
|
assert "02-trz.md" in reason and "03-acceptance-criteria.md" in reason
|
|
|
|
|
|
def test_tc12_signature_has_no_track_param():
|
|
import inspect
|
|
params = list(inspect.signature(check_analysis_complete).parameters)
|
|
# byte-for-byte signature: (repo, work_item_id, branch=None) — no track-awareness.
|
|
assert params == ["repo", "work_item_id", "branch"]
|