"""ORCH-087: effort-in-stage-line (BR-EFF), honest done-time (BR-G5), deterministic stage labels (G2) and deploy-cycle label (G3). Telegram/Plane fully isolated (render is pure DB). Covers TC-06, TC-11..TC-15 and the confirm_deploy live-overlay label. """ import os import tempfile os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token") os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token") _test_db = os.path.join(tempfile.gettempdir(), "test_orchestrator_eff_time.db") os.environ["ORCH_DB_PATH"] = _test_db import pytest # noqa: E402 import src.db as db_module # noqa: E402 from src.db import init_db, get_db # noqa: E402 from src import notifications as N # noqa: E402 @pytest.fixture(autouse=True) def setup_db(monkeypatch): monkeypatch.setattr(db_module.settings, "db_path", _test_db, raising=False) if os.path.exists(_test_db): os.unlink(_test_db) init_db() # No live overlay in render-only tests unless a test opts in. monkeypatch.setattr(N._get_settings(), "tracker_live_status", False, raising=False) yield if os.path.exists(_test_db): os.unlink(_test_db) def _mk_task(stage="development", wid="ORCH-087", title="eff/time test", brd_start=None, brd_end=None, created=None, updated=None): conn = get_db() cur = conn.execute( "INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title, " "brd_review_started_at, brd_review_ended_at) VALUES (?,?,?,?,?,?,?,?)", ("p1", wid, "orchestrator", "feature/ORCH-087-x", stage, title, brd_start, brd_end), ) tid = cur.lastrowid if created or updated: conn.execute( "UPDATE tasks SET created_at=COALESCE(?, created_at), " "updated_at=COALESCE(?, updated_at) WHERE id=?", (created, updated, tid), ) conn.commit() conn.close() return tid def _mk_run(tid, agent, started, finished, *, effort=None, model="tokenator/claude-opus-4-8", in_tok=10, out_tok=5, cost=0.0, exit_code=0): conn = get_db() conn.execute( "INSERT INTO agent_runs (task_id, agent, started_at, finished_at, " "exit_code, input_tokens, output_tokens, cost_usd, model, effort) " "VALUES (?,?,?,?,?,?,?,?,?,?)", (tid, agent, started, finished, exit_code, in_tok, out_tok, cost, model, effort), ) conn.commit() conn.close() # --------------------------------------------------------------------------- # # G2: plane_status_label deterministic for every stage (TC-06) # --------------------------------------------------------------------------- # def test_plane_status_label_all_stages(): """TC-06/AC-2.2: every stage maps to its own label; deploy -> Awaiting Deploy.""" cases = { "created": "To Analyse", "analysis": "Analysis", "architecture": "Architecture", "development": "Development", "review": "Code-Review", "testing": "Testing", "done": "Done", } for stage, expected in cases.items(): assert N.plane_status_label({"stage": stage}) == expected deploy = N.plane_status_label({"stage": "deploy"}) assert "Awaiting Deploy" in deploy # In Review derives from the brd-clock on the analysis stage. in_review = N.plane_status_label( {"stage": "analysis", "brd_review_started_at": "2026-06-04 10:00:00", "brd_review_ended_at": None} ) assert "In Review" in in_review def test_confirm_deploy_label_registered(): """G3/AC-3.x: the deploy-cycle gains a confirm_deploy overlay label.""" assert "confirm_deploy" in N._LIVE_BRANCH_LABELS assert "Confirm Deploy" in N._LIVE_BRANCH_LABELS["confirm_deploy"] # confirm_deploy is a REAL dedicated status -> no base-alias suppression. assert "confirm_deploy" not in N._LIVE_BRANCH_BASE # --------------------------------------------------------------------------- # # BR-EFF: effort rendered next to the model (TC-11, TC-12) # --------------------------------------------------------------------------- # @pytest.mark.parametrize("agent,label,effort", [ ("developer", "Разработка", "xhigh"), ("tester", "Тестирование", "medium"), ("deployer", "Внедрение", "medium"), ("analyst", "Анализ", "high"), ("architect", "Архитектура", "high"), ("reviewer", "Код ревью", "high"), ]) def test_stage_line_shows_effort(agent, label, effort): """TC-11/AC-E.2,AC-E.3: stage line shows '· model · effort' for each role.""" tid = _mk_task(stage="done") _mk_run(tid, agent, "2026-06-04 09:00:00", "2026-06-04 09:10:00", effort=effort) text = N.render_task_tracker(tid) line = [ln for ln in text.splitlines() if ln.startswith(f"✅ {label}")][0] assert line.rstrip().endswith(f"opus-4-8 · {effort}") def test_stage_line_omits_empty_effort(): """TC-12/AC-E.4: NULL effort -> suffix omitted, render does not crash.""" tid = _mk_task(stage="analysis") _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00", effort=None) text = N.render_task_tracker(tid) line = [ln for ln in text.splitlines() if ln.startswith("✅ Анализ")][0] # Ends at the model (no trailing effort segment). assert line.rstrip().endswith("opus-4-8") # --------------------------------------------------------------------------- # # BR-G5: honest done-time (TC-13, TC-14, TC-15) # --------------------------------------------------------------------------- # def test_done_review_time_capped(): """TC-13/AC-5.1: a ~6h open brd_review window is NOT shown as ~6h.""" # 6h review window (10:00 -> 16:00) with default 2h cap. tid = _mk_task( stage="done", brd_start="2026-06-04 10:00:00", brd_end="2026-06-04 16:00:00", created="2026-06-04 09:00:00", updated="2026-06-04 16:30:00", ) _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:30:00", effort="high") text = N.render_task_tracker(tid) time_line = [ln for ln in text.splitlines() if ln.startswith("⏱")][0] # Capped to ~2h (120м), marked with '~'; the raw 360m is NOT shown as твоё. assert "твоё ~120м" in time_line assert "твоё 360м" not in time_line def test_done_review_time_under_cap_uncapped(): """AC-5.1: a normal short review window is shown verbatim (no '~').""" tid = _mk_task( stage="done", brd_start="2026-06-04 10:00:00", brd_end="2026-06-04 10:08:00", created="2026-06-04 09:00:00", updated="2026-06-04 10:30:00", ) _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:30:00", effort="high") text = N.render_task_tracker(tid) time_line = [ln for ln in text.splitlines() if ln.startswith("⏱")][0] assert "твоё 8м" in time_line assert "~" not in time_line def test_done_time_line_labels_and_agent_sum(): """TC-14,TC-15/AC-5.2,AC-5.3: agents=Σ runs; wall labelled 'общее с ожиданием'.""" tid = _mk_task( stage="done", created="2026-06-04 09:00:00", updated="2026-06-04 11:00:00", # wall 120m ) # Two runs: 10m + 6m = 16m of agent time. _mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00", effort="high") _mk_run(tid, "deployer", "2026-06-04 10:50:00", "2026-06-04 10:56:00", effort="medium") text = N.render_task_tracker(tid) time_line = [ln for ln in text.splitlines() if ln.startswith("⏱")][0] # agents = 16m (exact Σ), wall = 120m labelled as "общее с ожиданием". assert "Агенты 16м" in time_line # Агенты 16м assert "общее с ожиданием 120м" in time_line # общее с ожиданием 120м # wall (120m) != agents (16m) -> not presented as a sum. assert "Всего" not in time_line # no old "Всего"