developer(ET): auto-commit from developer run_id=363
All checks were successful
CI / test (push) Successful in 23s
CI / test (pull_request) Successful in 23s

This commit is contained in:
2026-06-08 10:19:42 +00:00
parent 2087475d0d
commit 9e1fe82ef0
16 changed files with 1207 additions and 65 deletions

View File

@@ -8,9 +8,17 @@ builds a FRESH Settings() (the process-wide singleton is not mutated).
from src.config import Settings
def test_tracker_mode_defaults_to_edit(monkeypatch):
# No env var -> default "edit" (TC-01 / AC-1).
def test_tracker_mode_defaults_to_bump(monkeypatch):
# ORCH-067 (TC-01 / AC-1): the default flipped edit -> bump. With no env var
# the card now re-creates at the bottom of the chat out of the box; edit
# stays available via ORCH_TRACKER_MODE=edit (see test below).
monkeypatch.delenv("ORCH_TRACKER_MODE", raising=False)
assert Settings().tracker_mode == "bump"
def test_tracker_mode_reads_env_edit(monkeypatch):
# ORCH-067 (AC-4): edit mode is still available through the env var.
monkeypatch.setenv("ORCH_TRACKER_MODE", "edit")
assert Settings().tracker_mode == "edit"

View File

@@ -0,0 +1,206 @@
"""ORCH-067 — Group D: clickable issue number in ALL alerts (AC-13, AC-12).
Every orchestrator alert that mentions a work_item_id now renders it as a Plane
hyperlink via the shared ``link_for`` / ``plane_issue_link`` helpers, and degrades
fail-safe to the raw (escaped) number when data is missing. This covers the
dedicated notify_* helpers (notify_approve_requested, notify_error) and asserts
the engine/launcher/security_gate/reconciler alert sites are wired to ``link_for``
— the single DB-resolving helper those sites call. Network is isolated:
send_telegram is replaced with a recorder; the DB is a temp SQLite.
Test ids TC-13, TC-14, TC-15 from 04-test-plan.yaml.
"""
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_notify_links.db")
os.environ["ORCH_DB_PATH"] = _test_db
from types import SimpleNamespace # noqa: E402
import pytest # noqa: E402
import src.db as db_module # noqa: E402
import src.projects as projects_mod # noqa: E402
from src.db import init_db, get_db # noqa: E402
from src import notifications as N # noqa: E402
_ORCH_PROJECT_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
@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()
# Pin repo->project resolution so cross-file registry reloads can't strip
# 'orchestrator' and break the expected issue URL.
monkeypatch.setattr(
projects_mod, "get_project_by_repo",
lambda repo: (SimpleNamespace(plane_project_id=_ORCH_PROJECT_ID)
if repo == "orchestrator" else None),
)
yield
if os.path.exists(_test_db):
os.unlink(_test_db)
def _set(monkeypatch, **kw):
s = N._get_settings()
for k, v in kw.items():
monkeypatch.setattr(s, k, v, raising=False)
def _mk_task(wid="ORCH-067", repo="orchestrator", title="notify links",
plane_issue_id="iss-1", stage="development"):
conn = get_db()
cur = conn.execute(
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title, "
"plane_issue_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
("p1", wid, repo, "feature/ORCH-067-x", stage, title, plane_issue_id),
)
tid = cur.lastrowid
conn.commit()
conn.close()
return tid
def _record_send(monkeypatch):
calls = []
def _fake(text, disable_notification=False):
calls.append({"text": text, "silent": disable_notification})
return 1
monkeypatch.setattr(N, "send_telegram", _fake)
monkeypatch.setattr(N, "update_task_tracker", lambda task_id: None)
return calls
# --------------------------------------------------------------------------- #
# TC-13 / AC-13 — notify_approve_requested: number clickable, CTA + single ping
# --------------------------------------------------------------------------- #
def test_tc13_approve_requested_number_clickable(monkeypatch):
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme", gitea_public_url="https://git.example.org",
gitea_owner="orchteam")
tid = _mk_task(plane_issue_id="iss-1")
calls = _record_send(monkeypatch)
N.notify_approve_requested(tid)
assert len(calls) == 1 # exactly one notifying ping
assert calls[0]["silent"] is not True
text = calls[0]["text"]
expected = (
f"https://plane.example.org/acme/projects/{_ORCH_PROJECT_ID}"
f"/issues/iss-1/"
)
assert f'<a href="{expected}">ORCH-067</a>' in text # clickable number
assert "Approved" in text # call-to-action preserved
# --------------------------------------------------------------------------- #
# TC-14 / AC-13, AC-12 — notify_error: clickable when data present, else raw
# --------------------------------------------------------------------------- #
def test_tc14_notify_error_clickable(monkeypatch):
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
tid = _mk_task(plane_issue_id="iss-1")
calls = _record_send(monkeypatch)
N.notify_error(tid, "boom happened")
assert len(calls) == 1
text = calls[0]["text"]
assert ">ORCH-067</a>" in text # number is a link
assert "ERROR" in text and "boom happened" in text
def test_tc14_notify_error_degrades_raw_number(monkeypatch):
# No usable Plane base -> raw (unlinked) number, alert still sent, no crash.
_set(monkeypatch, plane_web_url="", plane_api_url="")
tid = _mk_task(plane_issue_id="iss-1")
calls = _record_send(monkeypatch)
N.notify_error(tid, "boom")
text = calls[0]["text"]
assert "ORCH-067" in text
assert "<a href=" not in text
def test_tc14_notify_error_escapes_error_text(monkeypatch):
# The error string is html-escaped so it can't break the <a>/HTML markup.
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
tid = _mk_task(plane_issue_id="iss-1")
calls = _record_send(monkeypatch)
N.notify_error(tid, "<script> & </script>")
text = calls[0]["text"]
assert "<script>" not in text
assert "&lt;script&gt;" in text and "&amp;" in text
# The clickable number's anchor is still well-formed.
assert text.count("<a href=") == text.count("</a>")
# --------------------------------------------------------------------------- #
# TC-15 / AC-13 — link_for is the DB-resolving helper the alert sites call
# --------------------------------------------------------------------------- #
def test_tc15_link_for_by_work_item_id(monkeypatch):
# Sites holding only a work_item_id (launcher deploy-fail, security_gate,
# reconciler, engine QG-fail) call link_for(wid) -> resolves repo + issue id
# from the DB and returns a clickable number.
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
_mk_task(wid="ORCH-067", plane_issue_id="iss-1")
out = N.link_for("ORCH-067")
expected = (
f"https://plane.example.org/acme/projects/{_ORCH_PROJECT_ID}"
f"/issues/iss-1/"
)
assert out == f'<a href="{expected}">ORCH-067</a>'
def test_tc15_link_for_by_task_id(monkeypatch):
# Sites holding a task_id (launcher agent-fail, engine) call link_for(wid, tid).
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
tid = _mk_task(wid="ORCH-067", plane_issue_id="iss-7")
out = N.link_for("ORCH-067", tid)
assert ">ORCH-067</a>" in out and "/issues/iss-7/" in out
def test_tc15_link_for_unknown_task_degrades(monkeypatch):
# No matching DB row -> raw number, never raises.
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
out = N.link_for("ORCH-999")
assert out == "ORCH-999"
assert "<a href=" not in out
@pytest.mark.parametrize("module_name", [
"src.stage_engine",
"src.agents.launcher",
"src.security_gate",
"src.reconciler",
])
def test_tc15_alert_modules_wire_link_for(module_name):
"""The representative alert modules call the shared link_for helper, so their
work_item_id alerts render a clickable number (not a bare string). Checked at
source level since some sites import link_for function-locally."""
import importlib
import inspect
mod = importlib.import_module(module_name)
src = inspect.getsource(mod)
assert "link_for(" in src, f"{module_name} must use link_for in its alerts"

View File

@@ -0,0 +1,101 @@
"""ORCH-067 — Group D: the shared plane_issue_link helper (AC-12).
``plane_issue_link(work_item_id, plane_issue_id=None, project_id=None, repo=None)``
is the single source of the clickable issue number for cards AND alerts. It
returns ``<a href=...>ORCH-NNN</a>`` when a usable Plane browser URL can be built,
and ``html.escape(work_item_id)`` otherwise. It must NEVER raise — including on
None arguments and a loopback base. No DB and no network are touched by this unit
(project_id is passed explicitly here), so these are pure settings-driven cases.
Test id TC-12 from 04-test-plan.yaml.
"""
import os
os.environ.setdefault("ORCH_PLANE_API_TOKEN", "test-token")
os.environ.setdefault("ORCH_GITEA_TOKEN", "test-token")
import pytest # noqa: E402
from src import notifications as N # noqa: E402
def _set(monkeypatch, **kw):
s = N._get_settings()
for k, v in kw.items():
monkeypatch.setattr(s, k, v, raising=False)
# --------------------------------------------------------------------------- #
# TC-12 / AC-12 — full data -> HTML link wrapping the number
# --------------------------------------------------------------------------- #
def test_tc12_full_data_returns_anchor(monkeypatch):
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
out = N.plane_issue_link("ORCH-067", plane_issue_id="iss-1",
project_id="proj-9")
expected = "https://plane.example.org/acme/projects/proj-9/issues/iss-1/"
assert out == f'<a href="{expected}">ORCH-067</a>'
def test_tc12_web_url_fallbacks_to_api_url(monkeypatch):
# plane_web_url empty -> non-loopback plane_api_url is used as the base.
_set(monkeypatch, plane_web_url="",
plane_api_url="https://plane-fallback.example.org",
plane_workspace_slug="acme")
out = N.plane_issue_link("ORCH-067", plane_issue_id="iss-1",
project_id="proj-9")
assert 'href="https://plane-fallback.example.org/acme/' in out
assert ">ORCH-067</a>" in out
# --------------------------------------------------------------------------- #
# TC-12 / AC-12 — insufficient data -> escaped number, NEVER an anchor
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("settings_kw,call_kw,reason", [
({"plane_web_url": "", "plane_api_url": ""},
{"plane_issue_id": "iss-1", "project_id": "proj-9"}, "no web base"),
({"plane_web_url": "http://localhost:8091", "plane_api_url": ""},
{"plane_issue_id": "iss-1", "project_id": "proj-9"}, "loopback base"),
({"plane_web_url": "https://plane.example.org", "plane_workspace_slug": ""},
{"plane_issue_id": "iss-1", "project_id": "proj-9"}, "no workspace"),
({"plane_web_url": "https://plane.example.org", "plane_workspace_slug": "acme"},
{"plane_issue_id": None, "project_id": "proj-9"}, "no issue id"),
({"plane_web_url": "https://plane.example.org", "plane_workspace_slug": "acme"},
{"plane_issue_id": "iss-1", "project_id": ""}, "no project id"),
])
def test_tc12_insufficient_data_returns_plain_number(monkeypatch, settings_kw,
call_kw, reason):
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_api_url="http://localhost:8091", plane_workspace_slug="acme")
_set(monkeypatch, **settings_kw)
out = N.plane_issue_link("ORCH-067", repo=None, **call_kw)
assert out == "ORCH-067", reason
assert "<a href=" not in out
# --------------------------------------------------------------------------- #
# TC-12 / AC-12 — html-escaping + never raises on hostile / None input
# --------------------------------------------------------------------------- #
def test_tc12_escapes_work_item_id_in_link(monkeypatch):
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
out = N.plane_issue_link("ORCH&<67>", plane_issue_id="iss-1",
project_id="proj-9")
assert ">ORCH&amp;&lt;67&gt;</a>" in out # label escaped inside the anchor
assert "<a href=" in out
def test_tc12_escapes_work_item_id_unlinked(monkeypatch):
_set(monkeypatch, plane_web_url="", plane_api_url="")
out = N.plane_issue_link("ORCH&<67>", plane_issue_id="iss-1",
project_id="proj-9")
assert out == "ORCH&amp;&lt;67&gt;" # escaped, no anchor
def test_tc12_none_args_never_raise(monkeypatch):
# All-None must not raise and must yield a (possibly empty) string.
out = N.plane_issue_link(None)
assert isinstance(out, str)
# None work_item_id -> empty label, no anchor.
assert "<a href=" not in out

View File

@@ -241,6 +241,9 @@ def test_first_call_sends_message_and_stores_id(monkeypatch):
def test_second_call_edits_existing_message(monkeypatch):
# ORCH-067: the default flipped to bump; this case asserts the edit-mode
# contract, so pin edit mode explicitly.
monkeypatch.setattr(N._get_settings(), "tracker_mode", "edit", raising=False)
tid = _mk_task(stage="development")
_mk_run(tid, "analyst", "2026-06-04 09:00:00", "2026-06-04 09:10:00",
in_tok=10, out_tok=5, cost=0.1)
@@ -602,9 +605,15 @@ def test_render_stage_labels_are_russian():
for ru in ("Анализ", "Архитектура", "Разработка", "Код ревью",
"Тестирование", "Внедрение"):
assert ru in text, f"missing russian label {ru!r}"
# ORCH-067: the new '📍 <Plane-status>' line intentionally carries the ENGLISH
# ORCH-066 Plane status name (e.g. 'Awaiting Deploy'); the russian-only rule
# (BR-11) applies to the STAGE label lines, so exclude the status line here.
stage_lines = "\n".join(
ln for ln in text.splitlines() if not ln.startswith("\U0001f4cd")
)
for en in ("Analysis", "Architecture", "Development", "Review",
"Testing", "Deploy"):
assert en not in text, f"english label leaked: {en!r}"
assert en not in stage_lines, f"english label leaked: {en!r}"
def test_render_done_says_vnedreno_not_deployed():

View File

@@ -0,0 +1,159 @@
"""ORCH-067 — Group A: bump is the DEFAULT tracker mode (AC-1..AC-4, AC-15).
The default flipped edit -> bump: out of the box the live card is re-created at
the BOTTOM of the chat (delete old + send new silent + repoint id), one card per
task. edit stays available via ORCH_TRACKER_MODE=edit. Network is isolated: the
low-level send/edit/delete helpers are patched per case; the DB is a temp SQLite.
Test ids TC-01..TC-04 + TC-17 from 04-test-plan.yaml.
"""
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_bump_default.db")
os.environ["ORCH_DB_PATH"] = _test_db
import pytest # noqa: E402
import src.db as db_module # noqa: E402
from src.config import Settings # noqa: E402
from src.db import ( # noqa: E402
init_db, get_db, get_tracker_message_id, set_tracker_message_id,
)
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()
yield
if os.path.exists(_test_db):
os.unlink(_test_db)
def _mk_task(stage="development", wid="ORCH-067"):
conn = get_db()
cur = conn.execute(
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title) "
"VALUES (?, ?, ?, ?, ?, ?)",
("p1", wid, "orchestrator", "feature/ORCH-067-x", stage, "bump default"),
)
tid = cur.lastrowid
conn.commit()
conn.close()
return tid
# --------------------------------------------------------------------------- #
# TC-01 / AC-1 — default tracker_mode == "bump"
# --------------------------------------------------------------------------- #
def test_tc01_default_tracker_mode_is_bump(monkeypatch):
monkeypatch.delenv("ORCH_TRACKER_MODE", raising=False)
assert Settings().tracker_mode == "bump"
# --------------------------------------------------------------------------- #
# TC-02 / AC-2, AC-15 — repeat update: delete(old) -> send(silent) -> repoint
# --------------------------------------------------------------------------- #
def test_tc02_repeat_delete_send_silent_repoint(monkeypatch):
# No env -> resolves to the new bump default (no explicit mode pin).
monkeypatch.setattr(N._get_settings(), "tracker_mode", "bump", raising=False)
tid = _mk_task()
set_tracker_message_id(tid, 100)
order = []
monkeypatch.setattr(N, "delete_telegram",
lambda mid: order.append(("delete", mid)) or True)
monkeypatch.setattr(N, "send_telegram",
lambda text, disable_notification=False:
order.append(("send", disable_notification)) or 200)
N.update_task_tracker(tid)
# delete(old) strictly before send; the new card is SILENT (disable=True).
assert order == [("delete", 100), ("send", True)]
assert get_tracker_message_id(tid) == 200 # one card -> repointed
# --------------------------------------------------------------------------- #
# TC-03 / AC-3 — transient send None must NOT wipe the pointer / duplicate
# --------------------------------------------------------------------------- #
def test_tc03_send_none_keeps_pointer_no_dupe(monkeypatch):
monkeypatch.setattr(N._get_settings(), "tracker_mode", "bump", raising=False)
tid = _mk_task()
set_tracker_message_id(tid, 100)
sends = []
monkeypatch.setattr(N, "delete_telegram", lambda mid: True)
monkeypatch.setattr(N, "send_telegram",
lambda text, disable_notification=False:
sends.append(1) or None)
N.update_task_tracker(tid) # must not raise
assert len(sends) == 1 # exactly one (failed) attempt, no retry
assert get_tracker_message_id(tid) == 100 # pointer preserved, not None
# --------------------------------------------------------------------------- #
# TC-04 / AC-4 — edit mode still reachable via env -> editMessageText path
# --------------------------------------------------------------------------- #
def test_tc04_edit_mode_still_available(monkeypatch):
monkeypatch.setattr(N._get_settings(), "tracker_mode", "edit", raising=False)
tid = _mk_task()
set_tracker_message_id(tid, 777)
edited = {}
monkeypatch.setattr(N, "edit_telegram",
lambda mid, text: edited.update(mid=mid) or N.EDIT_OK)
monkeypatch.setattr(
N, "send_telegram",
lambda *a, **k: (_ for _ in ()).throw(
AssertionError("edit mode must not send when edit succeeds")),
)
N.update_task_tracker(tid)
assert edited["mid"] == 777 # edited in place, no new card
def test_tc04b_edit_mode_resolution_case_insensitive(monkeypatch):
"""Anything other than 'bump' resolves to edit (e.g. 'EDIT')."""
monkeypatch.setattr(N._get_settings(), "tracker_mode", "EDIT", raising=False)
tid = _mk_task()
set_tracker_message_id(tid, 5)
edited = {}
monkeypatch.setattr(N, "edit_telegram",
lambda mid, text: edited.update(mid=mid) or N.EDIT_OK)
monkeypatch.setattr(N, "send_telegram",
lambda *a, **k: (_ for _ in ()).throw(
AssertionError("should edit, not send")))
N.update_task_tracker(tid)
assert edited["mid"] == 5
# --------------------------------------------------------------------------- #
# TC-17 / AC-15 — first bump call: NO delete, silent send, id stored
# --------------------------------------------------------------------------- #
def test_tc17_first_call_silent_no_delete(monkeypatch):
monkeypatch.setattr(N._get_settings(), "tracker_mode", "bump", raising=False)
tid = _mk_task(stage="analysis")
sends = []
monkeypatch.setattr(N, "send_telegram",
lambda text, disable_notification=False:
sends.append(disable_notification) or 555)
monkeypatch.setattr(N, "delete_telegram",
lambda mid: (_ for _ in ()).throw(
AssertionError("delete must not run on first call")))
N.update_task_tracker(tid)
assert sends == [True] # exactly one SILENT send
assert get_tracker_message_id(tid) == 555 # id stored

View File

@@ -0,0 +1,158 @@
"""ORCH-067 — Group C: clickable issue number in the live card (AC-10/AC-11/AC-14).
The issue number in the card header is now a Plane hyperlink
(``<a href=".../issues/<id>/">ORCH-NNN</a>``) when a usable browser URL can be
built, and degrades fail-safe to the html-escaped raw number when any piece is
missing (web base / non-loopback / workspace / project_id / plane_issue_id). The
card must NEVER break under parse_mode=HTML: a title with '<'/'&'/'>' stays
escaped while the <a> markup stays valid. Network is isolated (no HTTP from the
render path here); the DB is a temp SQLite.
Test ids TC-10, TC-11, TC-16 from 04-test-plan.yaml.
"""
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_card_link.db")
os.environ["ORCH_DB_PATH"] = _test_db
from types import SimpleNamespace # noqa: E402
import pytest # noqa: E402
import src.db as db_module # noqa: E402
import src.projects as projects_mod # noqa: E402
from src.db import init_db, get_db # noqa: E402
from src import notifications as N # noqa: E402
# orchestrator repo -> default project registry uuid (src/projects.py).
_ORCH_PROJECT_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
@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()
# Keep the render path fully offline (no live overlay HTTP).
monkeypatch.setattr(N._get_settings(), "tracker_live_status", False,
raising=False)
# Pin the repo->project resolution so cross-file tests that reload the
# ORCH_PROJECTS_JSON registry can't strip 'orchestrator' out from under us.
monkeypatch.setattr(
projects_mod, "get_project_by_repo",
lambda repo: (SimpleNamespace(plane_project_id=_ORCH_PROJECT_ID)
if repo == "orchestrator" else None),
)
yield
if os.path.exists(_test_db):
os.unlink(_test_db)
def _set(monkeypatch, **kw):
s = N._get_settings()
for k, v in kw.items():
monkeypatch.setattr(s, k, v, raising=False)
def _mk_task(wid="ORCH-067", repo="orchestrator", title="card link",
plane_issue_id="issue-uuid-1", stage="development"):
conn = get_db()
cur = conn.execute(
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title, "
"plane_issue_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
("p1", wid, repo, "feature/ORCH-067-x", stage, title, plane_issue_id),
)
tid = cur.lastrowid
conn.commit()
conn.close()
return tid
# --------------------------------------------------------------------------- #
# TC-10 / AC-10 — full data -> clickable <a> wrapping the issue number
# --------------------------------------------------------------------------- #
def test_tc10_card_number_is_clickable(monkeypatch):
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_api_url="http://localhost:8091", plane_workspace_slug="acme")
tid = _mk_task(plane_issue_id="abcd-issue-uuid")
text = N.render_task_tracker(tid)
expected_url = (
f"https://plane.example.org/acme/projects/{_ORCH_PROJECT_ID}"
f"/issues/abcd-issue-uuid/"
)
assert f'<a href="{expected_url}">ORCH-067</a>' in text
# --------------------------------------------------------------------------- #
# TC-11 / AC-11 — fail-safe: any missing piece -> escaped number, no <a>, no crash
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("override,reason", [
({"plane_web_url": "", "plane_api_url": ""}, "no web base"),
({"plane_web_url": "http://localhost:8091", "plane_api_url": ""}, "loopback base"),
({"plane_workspace_slug": ""}, "no workspace"),
])
def test_tc11_card_number_degrades_settings(monkeypatch, override, reason):
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_api_url="http://localhost:8091", plane_workspace_slug="acme")
_set(monkeypatch, **override)
tid = _mk_task(plane_issue_id="abcd-issue-uuid")
text = N.render_task_tracker(tid)
assert "ORCH-067" in text # raw number still shown
assert "<a href=" not in text, reason # but NOT a link
assert "localhost" not in text # never leak a loopback URL
def test_tc11_card_number_degrades_no_issue_id(monkeypatch):
# Missing plane_issue_id -> the number is shown unlinked, render survives.
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
tid = _mk_task(plane_issue_id=None)
text = N.render_task_tracker(tid)
assert "ORCH-067" in text
assert "<a href=" not in text
def test_tc11_card_number_degrades_unknown_repo(monkeypatch):
# repo not in the registry -> no project_id -> number unlinked, no crash.
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
tid = _mk_task(repo="not-a-real-repo", plane_issue_id="abcd-issue-uuid")
text = N.render_task_tracker(tid)
assert "ORCH-067" in text
assert "<a href=" not in text
# --------------------------------------------------------------------------- #
# TC-16 / AC-14 — HTML escaping: title with '<b>'/'&'/'>' stays safe + valid <a>
# --------------------------------------------------------------------------- #
def test_tc16_title_escaped_link_valid(monkeypatch):
_set(monkeypatch, plane_web_url="https://plane.example.org",
plane_workspace_slug="acme")
tid = _mk_task(title="<b>drop & </b> table >", plane_issue_id="iss-1")
text = N.render_task_tracker(tid)
# Raw title markup is escaped -> cannot break parse_mode=HTML.
assert "<b>" not in text
assert "&lt;b&gt;" in text
assert "&amp;" in text
# The card's own anchor markup stays well-formed (balanced tags).
assert text.count("<a href=") == text.count("</a>")
assert text.count("<a href=") >= 1 # the clickable number is present
def test_tc16_ampersand_in_work_item_id_escaped(monkeypatch):
# A '&' in the work_item_id is escaped in the (unlinked) fail-safe path too.
_set(monkeypatch, plane_web_url="", plane_api_url="",
plane_workspace_slug="acme")
tid = _mk_task(wid="ORCH&67", plane_issue_id="iss-1")
text = N.render_task_tracker(tid)
assert "ORCH&amp;67" in text
assert "<a href=" not in text # no link (no web base)

View File

@@ -0,0 +1,216 @@
"""ORCH-067 — Group B: the Plane-status line on the live card (AC-5..AC-9).
The card now carries an explicit '📍 <Plane status>' line under the header that
follows the ORCH-066 status model. The OFFLINE core (stage->status + In Review
from the brd-clock + Awaiting Deploy) is pure/deterministic and never touches the
network; a best-effort LIVE overlay draws the branch statuses that are
indistinguishable offline (Needs Input / Blocked / …). Everything degrades to the
stage default and NEVER raises (AC-9). Network is isolated: the live-state read
(`_live_state_uuid_cached`) and `get_project_states` are patched per case; the DB
is a temp SQLite.
Test ids TC-05..TC-09 from 04-test-plan.yaml.
"""
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_status_line.db")
os.environ["ORCH_DB_PATH"] = _test_db
from types import SimpleNamespace # noqa: E402
import pytest # noqa: E402
import src.db as db_module # noqa: E402
import src.projects as projects_mod # noqa: E402
from src.db import init_db, get_db # noqa: E402
from src import notifications as N # noqa: E402
import src.plane_sync as plane_sync # noqa: E402
_ORCH_PROJECT_ID = "8da6aa25-a60e-44d6-a1e2-d8ae59aa7d6a"
@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()
# Live overlay OFF by default for the offline-core tests; cases that need it
# turn it back on explicitly. Keep the per-issue cache clean between cases.
monkeypatch.setattr(N._get_settings(), "tracker_live_status", False, raising=False)
N._LIVE_STATE_CACHE.clear()
# Pin repo->project resolution (cross-file ORCH_PROJECTS_JSON reloads must not
# strip 'orchestrator' and disable the live overlay under us).
monkeypatch.setattr(
projects_mod, "get_project_by_repo",
lambda repo: (SimpleNamespace(plane_project_id=_ORCH_PROJECT_ID)
if repo == "orchestrator" else None),
)
yield
if os.path.exists(_test_db):
os.unlink(_test_db)
def _mk_task(stage="development", wid="ORCH-067", repo="orchestrator",
plane_issue_id="issue-uuid-1", brd_started=None, brd_ended=None,
title="status line"):
conn = get_db()
cur = conn.execute(
"INSERT INTO tasks (plane_id, work_item_id, repo, branch, stage, title, "
"plane_issue_id, brd_review_started_at, brd_review_ended_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
("p1", wid, repo, "feature/ORCH-067-x", stage, title, plane_issue_id,
brd_started, brd_ended),
)
tid = cur.lastrowid
conn.commit()
conn.close()
return tid
def _status_line(text):
"""Extract the single '📍 ...' status line from rendered card text."""
for ln in text.splitlines():
if ln.startswith("\U0001f4cd"):
return ln
return None
# --------------------------------------------------------------------------- #
# TC-05 / AC-5 — render carries an explicit Plane-status line
# --------------------------------------------------------------------------- #
def test_tc05_render_has_status_line():
tid = _mk_task(stage="development")
text = N.render_task_tracker(tid)
line = _status_line(text)
assert line is not None # '📍 ...' present
assert line == "\U0001f4cd Development" # stage -> Plane status
# --------------------------------------------------------------------------- #
# TC-06 / AC-6 — stage -> Plane status mapping (ТЗ §2.2), parametrized
# --------------------------------------------------------------------------- #
@pytest.mark.parametrize("stage,expected", [
("created", "To Analyse"),
("analysis", "Analysis"),
("architecture", "Architecture"),
("development", "Development"),
("review", "Code-Review"),
("testing", "Testing"),
("deploy", "⏸️ Awaiting Deploy — ожидание Confirm Deploy"),
("done", "Done"),
])
def test_tc06_stage_to_plane_status(stage, expected):
# plane_status_label is pure/offline -> assert directly off a row-like dict.
assert N.plane_status_label({"stage": stage}) == expected
def test_tc06_unknown_stage_degrades_to_default():
# Anything unknown -> the safe stage default (To Analyse), never an error.
assert N.plane_status_label({"stage": "weird-stage"}) == "To Analyse"
assert N.plane_status_label({}) == "To Analyse"
# --------------------------------------------------------------------------- #
# TC-07 / AC-7 — In Review from the brd-clock, OFFLINE (no network)
# --------------------------------------------------------------------------- #
def test_tc07_in_review_from_brd_clock(monkeypatch):
# analysis + brd started + not ended -> '⏸️ In Review' (waiting BRD approve).
# Guard: any network read would fail this test -> prove it stays offline.
def _boom(*a, **k):
raise AssertionError("In Review must be resolved OFFLINE (no network)")
monkeypatch.setattr(N, "_live_state_uuid_cached", _boom)
tid = _mk_task(stage="analysis", brd_started="2026-06-08 10:00:00",
brd_ended=None)
text = N.render_task_tracker(tid)
assert _status_line(text) == "\U0001f4cd " + N._IN_REVIEW_LABEL
# The human-gate 'Подтверждение BRD' line with ⏸️/⏳ is still rendered.
assert N._BRD_LABEL in text
assert "" in text # ⏳ still-waiting marker
def test_tc07b_in_review_clears_once_brd_ended():
# Once the BRD review ended, analysis is back to the plain 'Analysis' status.
tid = _mk_task(stage="analysis", brd_started="2026-06-08 10:00:00",
brd_ended="2026-06-08 10:30:00")
assert _status_line(N.render_task_tracker(tid)) == "\U0001f4cd Analysis"
# --------------------------------------------------------------------------- #
# TC-08 / AC-8 — Awaiting Deploy (offline) + Needs Input (live overlay)
# --------------------------------------------------------------------------- #
def test_tc08_awaiting_deploy_offline():
# stage=deploy -> '⏸️ Awaiting Deploy' purely offline (no overlay needed).
tid = _mk_task(stage="deploy")
line = _status_line(N.render_task_tracker(tid))
assert line == "\U0001f4cd ⏸️ Awaiting Deploy — ожидание Confirm Deploy"
def test_tc08_needs_input_via_live_overlay(monkeypatch):
# Needs Input is NOT derivable offline -> drawn by the best-effort overlay
# reading the LIVE Plane status. Patch the live read + the state map.
monkeypatch.setattr(N._get_settings(), "tracker_live_status", True,
raising=False)
monkeypatch.setattr(N, "_live_state_uuid_cached",
lambda issue_id, project_id: "uuid-needs-input")
monkeypatch.setattr(
plane_sync, "get_project_states",
lambda project_id: {"needs_input": "uuid-needs-input"},
)
# repo='orchestrator' resolves to a real registry project_id -> overlay runs.
tid = _mk_task(stage="development", repo="orchestrator")
line = _status_line(N.render_task_tracker(tid))
assert line == "\U0001f4cd ❓ Needs Input — нужны уточнения"
def test_tc08b_overlay_no_match_keeps_offline_base(monkeypatch):
# Live status maps to no branch key -> the offline stage base is kept.
monkeypatch.setattr(N._get_settings(), "tracker_live_status", True,
raising=False)
monkeypatch.setattr(N, "_live_state_uuid_cached",
lambda issue_id, project_id: "uuid-in-progress")
monkeypatch.setattr(
plane_sync, "get_project_states",
lambda project_id: {"in_progress": "uuid-in-progress",
"needs_input": "uuid-needs-input"},
)
tid = _mk_task(stage="development", repo="orchestrator")
assert _status_line(N.render_task_tracker(tid)) == "\U0001f4cd Development"
# --------------------------------------------------------------------------- #
# TC-09 / AC-9, AC-16 — render never raises on broken/unreachable status data
# --------------------------------------------------------------------------- #
def test_tc09_render_survives_overlay_exception(monkeypatch):
# The live overlay blowing up must NOT escape render -> degrade to stage base.
monkeypatch.setattr(N._get_settings(), "tracker_live_status", True,
raising=False)
def _boom(*a, **k):
raise RuntimeError("plane down")
monkeypatch.setattr(N, "_live_state_uuid_cached", _boom)
tid = _mk_task(stage="development", repo="orchestrator")
text = N.render_task_tracker(tid) # must not raise
assert _status_line(text) == "\U0001f4cd Development"
def test_tc09b_card_status_label_never_raises(monkeypatch):
# _card_status_label swallows everything -> a usable default, never an error.
def _boom(*a, **k):
raise RuntimeError("boom")
monkeypatch.setattr(N, "plane_status_label", _boom)
assert N._card_status_label({"stage": "development"}) == "To Analyse"
def test_tc09c_plane_status_label_never_raises():
# Garbage row (None / object without keys) -> safe default, no exception.
assert N.plane_status_label(None) == "To Analyse"
assert N.plane_status_label(object()) == "To Analyse"