fix(webhooks): source-backed 00-business-request.md instead of hardcoded TBD (ORCH-119)
All checks were successful
CI / test (push) Successful in 1m12s
CI / test (pull_request) Successful in 1m13s

The Description section of 00-business-request.md always read the literal `TBD`,
losing the source-backed Plane-issue request context. Render the ACTUAL issue
`description` on both creation paths:

- Direct path A (serial_gate N/A): start_pipeline passes `description` to
  _create_initial_docs.
- Deferred path B (ORCH-088, dominates on self-hosting): persist `description`
  durable in the additive `tasks.description` column inside the same atomic INSERT
  in create_task_atomic (race-safe vs ORCH-053 anti-dup claim), read it in
  launcher._spawn -> _materialize_deferred_branch at claim (no network in the hot
  claim path, NFR-4).

Pure render helper _render_business_request with a fail-safe fallback marker for
empty/None/unreadable descriptions (never breaks task creation); Gitea 422 stays a
no-op (idempotent). STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict keys
and the base CREATE TABLE tasks are byte-for-byte unchanged; the ORCH-088
anti-stale-base invariant is preserved (only the data source is enriched).

Tests: tests/test_orch119_business_request.py (TC-01 mandatory red->green
regression; TC-02..TC-07). Updated the ORCH-088 serial-gate spy for the additive
_create_initial_docs arg.

Refs: ORCH-119
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 14:22:27 +03:00
parent 615830e843
commit 43908518b7
6 changed files with 364 additions and 13 deletions

View File

@@ -3,6 +3,7 @@
Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу.
## [Unreleased]
- **Source-backed `00-business-request.md` вместо хардкода `TBD`** (ORCH-119, `fix`, Bug-трек): раздел «Description» файла `00-business-request.md` теперь несёт **фактический текст запроса** из Plane-issue (`description`/`description_stripped`) вместо литерала `TBD` — терялся source-backed контекст запроса. Фикс работает на **обоих** путях создания: прямой (путь A, `serial_gate` не применим — `start_pipeline` передаёт `description` в `_create_initial_docs`) и **отложенный срез ветки** (путь B, ORCH-088, доминирует на self-hosting `orchestrator`). Для пути B `description` **персистится durable** при создании задачи (аддитивная колонка `tasks.description` через `_ensure_column`, зеркало `tasks.title`, записывается **внутри того же атомарного INSERT** `create_task_atomic` — race-safe относительно анти-dup-claim ORCH-053) и читается из строки `tasks` в `launcher._spawn``_materialize_deferred_branch` на момент claim (без сетевого вызова в горячем пути, NFR-4). **Fail-safe (FR-4):** пустое/whitespace/`None`/нечитаемое описание → явный безопасный маркер `_(описание отсутствует в источнике)_` через чистый рендер-хелпер `_render_business_request` (never-raise; создание задачи не падает). **Идемпотентность:** Gitea 422 (файл существует) → no-op, ранее записанное тело не перезаписывается. **Инвариант (AC-5):** `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict-ключи — байт-в-байт; единственное изменение схемы — аддитивная `tasks.description` (базовый `CREATE TABLE tasks` не тронут); анти-stale-base инвариант ORCH-088 цел (момент/условие среза не меняются — только источник данных дополняется). Обратимость — revert PR (колонка остаётся инертной). Покрытие — `tests/test_orch119_business_request.py` (TC-01 обязательный регресс red→green; TC-02…TC-07). ADR: `docs/work-items/ORCH-119/06-adr/ADR-001-source-backed-business-request-doc.md`.
- **Детерминированный test-раннер вместо LLM-тестера на `testing`** (ORCH-116, `feat`): второй реализованный срез determinization-roadmap (ORCH-118 A5, `needs-hybrid-fallback`) — на стадии `testing` для self-hosting `orchestrator` **LLM-агент `tester` заменён детерминированным кодом** (`src/test_runner.py`). PASS/FAIL-ядро агента было деривируемым (регресс `pytest` + read-only smoke → `result:`); каждый прогон жёг токены/время opus-агента (~60150k / 520 мин) и встраивал недетерминизм LLM в точку ветвления `testing → deploy-staging` / `testing → development`. **Инвариант (NFR-1):** это замена *продюсера* артефакта, **не** гейта — контракт `13-test-report.md`, гейт `check_tests_passed`/`_parse_tests_verdict`, `STAGE_TRANSITIONS`, machine-verdict `result:` (+ legacy `verdict:`/`status:`), схема БД — **байт-в-байт не тронуты**. Аддитивно, под kill-switch, never-raise, fail-closed, скоуп self-hosting, гибрид (LLM строго off-control-path). Эталон — `src/staging_runner.py` (ORCH-115). ADR: `docs/work-items/ORCH-116/06-adr/ADR-001-deterministic-test-runner.md`, сквозной `docs/architecture/adr/adr-0050-deterministic-test-runner.md`.
- **Перехват в `launch_job` до `_spawn` (D1):** `if job.agent=="tester" and test_runner.should_intercept(job)``_run_test_runner_job` (зеркало `_run_staging_runner_job`, прецедент `deploy-finalizer`/`post-deploy-monitor`/`staging-runner` `launcher.py:397/402/405`): синхронно ведёт `jobs`-строку через `mark_job`, возвращает `None` (нет `agent_runs`, нет токенов). Дискриминатор — роль `tester` **И** стадия задачи `testing` (defense-in-depth: `tester` — единственный агент входа в `testing`, коллизии стадий нет, в отличие от общей роли `deployer`) **И** `applies(repo)`; `should_intercept` never-raise → `False` → штатный `_spawn` (fail-safe к LLM-пути).
- **Leaf `src/test_runner.py` (новый, чистый never-raise):** по образцу `staging_runner`/`self_deploy`/`proc_group` (на импорте только `config`/`proc_group`; `db`/`git_worktree`/`self_deploy`/`qg.checks`/`stage_engine`/`notifications` — лениво). `applies(repo)` = kill-switch `test_runner_enabled` + скоуп `test_runner_repos` (пусто → self-hosting only) **И** резолв тест-контракта `_has_test_contract` (BR-9: репо без контракта → `False` → LLM-tester — enduro-trails 1:1 как до ORCH-116, даже если руками добавлен в CSV). Исполняет регресс `python -m pytest <test_runner_target>` **в worktree ветки** (`git_worktree.get_worktree_path`, анти checkout-гонка ORCH-112) через `proc_group.run_in_process_group` (tree-kill, таймаут `test_runner_timeout_s=900`, малформ/непозитив → дефолт + WARNING) + опц. **read-only smoke** (`/health`/`/status`/`/queue` + блок `serial_gate`, stdlib `urllib`; транзиентная недостижимость — ограниченный ретрай, не-200/нет блока — немедленный FAIL; `test_runner_smoke_enabled`). Маппит exit-код **единым** контрактом `self_deploy.map_exit_code_to_status` в токенах `result:` (`0→PASS`/иначе/None→`FAIL`, fail-closed; smoke-провал AND-ится в `FAIL`); пишет `13-test-report.md` (тот же machine-key `result:` UPPERCASE + 52c-схема, `author_agent: test-runner`/`model_used: n/a`) + best-effort push в **фичеветку**; вызывает **существующий** `advance_stage(current_stage="testing", finished_agent="tester")` — без новых рёбер/исходов (transition-lease ORCH-114 берётся внутри `advance_stage` — граница O1).

View File

@@ -512,7 +512,8 @@ class AgentLauncher:
return None
def _materialize_deferred_branch(
self, repo: str, branch: str, work_item_id: str | None, title: str | None
self, repo: str, branch: str, work_item_id: str | None, title: str | None,
description: str | None = None,
) -> None:
"""ORCH-088 (ADR-001 D1): create the deferred Gitea branch + initial docs.
@@ -524,6 +525,12 @@ class AgentLauncher:
Both are idempotent (409/422 -> no-op) so a re-claim after a restart is safe.
A transient Gitea error PROPAGATES so the caller (_spawn) fails the launch and
the queue worker requeues the job for a later tick (never a half-cut state).
ORCH-119 (FR-3 / AC-3): ``description`` (read from the tasks row by ``_spawn``,
durable since task creation) is passed through to ``_create_initial_docs`` so
the artifact materialised at claim carries the real request text, not ``TBD``.
The branch-cut MOMENT/CONDITION are unchanged — only the data source is enriched
(ORCH-088 anti-stale-base invariant preserved, NFR-3).
"""
import asyncio
from ..webhooks.plane import _create_gitea_branch, _create_initial_docs
@@ -535,7 +542,9 @@ class AgentLauncher:
)
asyncio.run(_create_gitea_branch(repo, branch))
if work_item_id:
asyncio.run(_create_initial_docs(repo, branch, work_item_id, name))
asyncio.run(
_create_initial_docs(repo, branch, work_item_id, name, description)
)
def _spawn(self, agent: str, repo: str, task_content: str = None,
task_id: int = None, job_id: int = None) -> int:
@@ -556,9 +565,14 @@ class AgentLauncher:
raise FileNotFoundError(f"Repo not found: {local_repo_path}")
# Determine branch (needed before we touch the worktree / task file).
# ORCH-119: also read the durable `description` so the deferred branch
# materialisation (path B) renders the real request text into
# 00-business-request.md instead of `TBD`. No network call (read from the
# tasks row) -> the hot claim path stays network-free (NFR-4).
_br_row = (
get_db().execute(
"SELECT branch, work_item_id, title FROM tasks WHERE id=?", (task_id,)
"SELECT branch, work_item_id, title, description FROM tasks WHERE id=?",
(task_id,),
).fetchone()
if task_id else None
)
@@ -580,7 +594,7 @@ class AgentLauncher:
_applies = False
if _applies:
self._materialize_deferred_branch(
repo, agent_branch, _br_row[1], _br_row[2]
repo, agent_branch, _br_row[1], _br_row[2], _br_row[3]
)
# ORCH-41: resolve the Plane project uuid for this repo so per-project

View File

@@ -123,6 +123,16 @@ def init_db():
# ("🛠️ ET-012 · <title>"). Populated from the Plane work-item name at task
# creation; falls back to the work_item_id when absent. Idempotent ALTER.
_ensure_column(conn, "tasks", "title", "TEXT")
# ORCH-119 (08-data-requirements.md): durable source-backed Plane-issue
# `description` (plain-text, preferably description_stripped). Mirrors tasks.title
# 1:1 — additive, idempotent (_ensure_column is a no-op once present) -> safe on
# the live shared prod DB (enduro untouched). Written inside the atomic INSERT in
# create_task_atomic so it is race-safe vs the ORCH-053 anti-dup claim (no window
# where the task exists but the description is missing). The deferred branch cut
# (path B, ORCH-088, dominates on self-hosting) reads it from the tasks row at
# claim time and renders it into 00-business-request.md instead of the historic
# hardcoded `TBD`. NULL for pre-existing rows -> renders the safe fallback marker.
_ensure_column(conn, "tasks", "description", "TEXT")
# Telegram live tracker: "BRD review" is the only HUMAN gate time — the delta
# between "BRD ready / approve requested" and the analysis->architecture
# advance (human flipped Plane to Approved). Persisted on the task so the
@@ -651,6 +661,7 @@ def create_task_atomic(
branch: str,
stage: str,
title: str,
description: str | None = None,
) -> tuple[dict, bool]:
"""ORCH-053 (AC-4): atomically claim creation of a task for a plane_id.
@@ -665,6 +676,14 @@ def create_task_atomic(
* ``created=False`` -> a task for this plane_id already existed (the other
racer won); ``row`` is the existing task and the caller must NOT duplicate
the follow-up work.
ORCH-119 (ADR-001 D1): ``description`` (the source-backed Plane-issue text) is
persisted durable INSIDE this same atomic INSERT — never a separate UPDATE — so
there is no race window (ORCH-053) where the task exists but the description is
missing. The parameter is additive with a default so the other callers (e.g. the
F-2 reconciler) stay backward-compatible (NULL description -> render falls back to
a safe marker). The deferred branch cut (path B, ORCH-088) reads it from the row
at claim time.
"""
with _CREATE_TASK_LOCK:
conn = get_db()
@@ -677,9 +696,11 @@ def create_task_atomic(
return dict(existing), False
cur = conn.execute(
"INSERT INTO tasks "
"(plane_id, work_item_id, repo, branch, stage, plane_issue_id, title) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
(plane_id, work_item_id, repo, branch, stage, plane_id, title),
"(plane_id, work_item_id, repo, branch, stage, plane_issue_id, title, "
"description) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(plane_id, work_item_id, repo, branch, stage, plane_id, title,
description),
)
conn.commit()
row = conn.execute(

View File

@@ -637,8 +637,12 @@ async def start_pipeline(data: dict, project_id: str = ""):
# process-wide lock. If the F-2 reconciler and this live webhook race on the
# same plane_id, exactly one wins (created=True); the loser sees the existing
# task and returns WITHOUT creating a second branch / worktree / analyst job.
# ORCH-119 (FR-3 / AC-3): persist `description` DURABLE inside the same atomic
# INSERT (mirror of `title`) so the deferred branch cut (path B, ORCH-088 — dominates
# on self-hosting) can read it from the tasks row at claim time and render the real
# request text into 00-business-request.md instead of the hardcoded `TBD`.
task_row, created = create_task_atomic(
plane_id, work_item_id, repo, branch, "analysis", name
plane_id, work_item_id, repo, branch, "analysis", name, description
)
if not created:
logger.info(
@@ -706,8 +710,10 @@ async def start_pipeline(data: dict, project_id: str = ""):
return
# Create initial docs structure via Gitea API (create file)
# ORCH-119 (FR-2 / AC-2): direct path A — pass the source-backed `description`
# so the artifact is created with the real request text in one call.
try:
await _create_initial_docs(repo, branch, work_item_id, name)
await _create_initial_docs(repo, branch, work_item_id, name, description)
except Exception as e:
logger.error(f"Failed to create initial docs: {e}")
else:
@@ -914,15 +920,59 @@ async def _create_gitea_branch(repo: str, branch: str):
logger.info(f"Created branch '{branch}' in {owner}/{repo}")
async def _create_initial_docs(repo: str, branch: str, work_item_id: str, name: str):
"""Create initial business request doc in the feature branch."""
# ORCH-119 (FR-4 / AC-4): explicit safe marker used when the source description is
# empty / whitespace / None / unreadable — NOT the historic bare `TBD` bug.
_BUSINESS_REQUEST_FALLBACK = "_(описание отсутствует в источнике)_"
def _render_business_request(
work_item_id: str, name: str, description: str | None
) -> str:
"""ORCH-119 (FR-1 / BR-1): render the body of ``00-business-request.md`` from the
source-backed Plane-issue ``description``.
Pure & network-free so it is unit-testable without Gitea (TC-01/TC-02/TC-06). The
Description section carries the ACTUAL request text plain-text and verbatim —
multi-line content and markdown special chars are preserved (the doc is
informational and NOT gate-parsed, NFR-2); only surrounding whitespace is trimmed
for the emptiness check. Empty / whitespace / None / any failure degrades to an
explicit safe marker (``_BUSINESS_REQUEST_FALLBACK``) instead of the historic bare
``TBD`` (FR-4 / AC-4); never raises. The header (``# Business Request: {name}``)
and ``Work Item ID`` are unchanged.
"""
try:
body = (description or "").strip()
if not body:
body = _BUSINESS_REQUEST_FALLBACK
except Exception: # noqa: BLE001 - never let rendering break task creation
body = _BUSINESS_REQUEST_FALLBACK
return (
f"# Business Request: {name}\n\n"
f"Work Item ID: {work_item_id}\n\n"
f"## Description\n\n{body}\n"
)
async def _create_initial_docs(
repo: str, branch: str, work_item_id: str, name: str,
description: str | None = None,
):
"""Create initial business request doc in the feature branch.
ORCH-119: the Description section now carries the source-backed Plane-issue
``description`` (rendered via ``_render_business_request``) instead of the historic
hardcoded ``TBD``. ``description`` is additive with a default so existing callers /
a re-claim stay backward-compatible; empty/None degrades to a safe marker.
Idempotent: Gitea 422 (file already exists) -> no-op, the previously written body
is NOT overwritten (AC-6 / TC-06).
"""
owner = settings.gitea_owner
file_path = f"docs/work-items/{work_item_id}/00-business-request.md"
url = f"{settings.gitea_url}/api/v1/repos/{owner}/{repo}/contents/{file_path}"
headers = {"Authorization": f"token {settings.gitea_token}"}
import base64
content = f"# Business Request: {name}\n\nWork Item ID: {work_item_id}\n\n## Description\n\nTBD\n"
content = _render_business_request(work_item_id, name, description)
encoded = base64.b64encode(content.encode()).decode()
payload = {

View File

@@ -0,0 +1,263 @@
"""ORCH-119: source-backed ``00-business-request.md`` (fix the hardcoded ``TBD``).
The Description section of ``00-business-request.md`` must carry the ACTUAL Plane-issue
``description`` instead of the historic hardcoded literal ``TBD``. Because the
self-hosting ``orchestrator`` repo cuts its branch lazily at analyst-job claim (the
deferred path B, ORCH-088), the description must be DURABLE-persisted on the ``tasks``
row at creation time (mirror of ``tasks.title``) so it survives the gap between task
creation and claim.
These tests are network-free: the Gitea ``contents`` POST (httpx) and the deferred
branch-cut coroutines are mocked; ``create_task_atomic`` runs against a real isolated
SQLite DB. Mapping to ``04-test-plan.yaml``:
* TC-01 — MANDATORY regression: render contains the real description, not ``TBD``.
* TC-02 — fallback: empty/whitespace/None -> explicit safe marker, never raises.
* TC-03 — deferred path B: description persisted + rendered at materialisation.
* TC-04 — direct path A: ``_create_initial_docs`` writes the real description.
* TC-05 — schema backward-compat: ``_ensure_column`` is additive + idempotent.
* TC-06 — data integrity: multi-line markdown preserved; Gitea 422 -> no-op.
* TC-07 — anti-regression: gates / STAGE_TRANSITIONS / QG_CHECKS unchanged.
"""
import asyncio
import base64
import os
import tempfile
from types import SimpleNamespace
import pytest
_test_db = os.path.join(tempfile.gettempdir(), "test_orch119_business_request.db")
os.environ["ORCH_DB_PATH"] = _test_db
os.environ["ORCH_REPOS_DIR"] = tempfile.gettempdir()
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, create_task_atomic # noqa: E402
from src.webhooks import plane # noqa: E402
@pytest.fixture(autouse=True)
def fresh_db(monkeypatch):
monkeypatch.setattr(_db.settings, "db_path", _test_db)
if os.path.exists(_test_db):
os.unlink(_test_db)
init_db()
yield
# ---------------------------------------------------------------------------
# TC-01 (MANDATORY regression): the rendered body carries the real request
# text, NOT the historic hardcoded ``TBD``. Red before the fix
# (``_render_business_request`` did not exist / body was ``TBD``).
# ---------------------------------------------------------------------------
def test_tc01_render_contains_real_description():
desc = "Users need source-backed business requests captured from the Plane issue."
out = plane._render_business_request("ORCH-119", "Source-backed request", desc)
# The real request text reaches the artifact body...
assert desc in out
# ...the header / Work Item ID are still present (unchanged contract)...
assert "# Business Request: Source-backed request" in out
assert "Work Item ID: ORCH-119" in out
# ...and the Description body is NOT the bare ``TBD`` bug.
body = out.split("## Description", 1)[1]
assert "TBD" not in body
# ---------------------------------------------------------------------------
# TC-02 (fallback): empty / whitespace / None description -> explicit safe
# marker (never the bare ``TBD`` bug), and the renderer never raises.
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("desc", ["", " ", "\n\t \n", None])
def test_tc02_fallback_marker_no_raise(desc):
out = plane._render_business_request("ORCH-119", "Name", desc)
assert "описание отсутствует в источнике" in out
body = out.split("## Description", 1)[1]
assert "TBD" not in body
# ---------------------------------------------------------------------------
# TC-03 (deferred path B / self-hosting): description is persisted DURABLE on
# the tasks row at creation and rendered into the artifact when the
# deferred branch is materialised at claim (launcher).
# ---------------------------------------------------------------------------
def test_tc03_deferred_path_persists_and_renders(monkeypatch):
desc = "A durable source-backed description for the deferred path B (claim-time cut)."
row, created = create_task_atomic(
"plane-b", "ORCH-201", "orchestrator",
"feature/ORCH-201-x", "analysis", "Title B", desc,
)
assert created
# Durable: description survives in the tasks row (readable without the network).
got = get_db().execute(
"SELECT description FROM tasks WHERE id=?", (row["id"],)
).fetchone()
assert got[0] == desc
captured = {}
async def _branch_spy(repo, branch):
captured["branch_cut"] = (repo, branch)
async def _docs_spy(repo, branch, work_item_id, name, description=None):
captured["description"] = description
# _materialize_deferred_branch imports these names from webhooks.plane at call
# time, so patching the source module attributes intercepts them.
monkeypatch.setattr(plane, "_create_gitea_branch", _branch_spy)
monkeypatch.setattr(plane, "_create_initial_docs", _docs_spy)
from src.agents.launcher import AgentLauncher
AgentLauncher()._materialize_deferred_branch(
"orchestrator", "feature/ORCH-201-x", "ORCH-201", "Title B", desc
)
assert captured.get("branch_cut") == ("orchestrator", "feature/ORCH-201-x")
assert captured.get("description") == desc
# ---------------------------------------------------------------------------
# TC-04 (direct path A / serial gate not applicable): _create_initial_docs
# writes the real description into the Gitea contents body.
# ---------------------------------------------------------------------------
def test_tc04_direct_path_renders_description(monkeypatch):
desc = "Direct path A description that must reach the artifact body verbatim."
captured = {}
class _Resp:
status_code = 201
def raise_for_status(self):
pass
class _Client:
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return False
async def post(self, url, json=None, headers=None, timeout=None):
captured["content"] = base64.b64decode(json["content"]).decode()
return _Resp()
monkeypatch.setattr(
plane, "httpx", SimpleNamespace(AsyncClient=lambda *a, **k: _Client())
)
asyncio.run(
plane._create_initial_docs("orchestrator", "feature/x", "ORCH-119", "Name", desc)
)
assert desc in captured["content"]
assert "## Description\n\nTBD\n" not in captured["content"]
# ---------------------------------------------------------------------------
# TC-05 (schema backward-compat): _ensure_column adds tasks.description on a
# pre-existing table without it; idempotent on re-run; creation works.
# ---------------------------------------------------------------------------
def test_tc05_schema_backward_compat(monkeypatch, tmp_path):
db_path = str(tmp_path / "bc.db")
monkeypatch.setattr(_db.settings, "db_path", db_path)
# Simulate an OLD tasks table WITHOUT the description column.
conn = _db.get_db()
conn.executescript(
"""
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plane_id TEXT, work_item_id TEXT, repo TEXT NOT NULL,
branch TEXT, stage TEXT DEFAULT 'created', plane_issue_id TEXT, title TEXT
);
"""
)
conn.commit()
conn.close()
# init_db must add the column idempotently and never fail.
init_db()
cols = [r[1] for r in _db.get_db().execute("PRAGMA table_info(tasks)").fetchall()]
assert "description" in cols
init_db() # re-run: _ensure_column is a no-op (idempotent)
# Task creation works and persists the description.
row, created = create_task_atomic(
"p1", "ORCH-1", "orchestrator", "b", "analysis", "T", "a real description"
)
assert created
got = get_db().execute(
"SELECT description FROM tasks WHERE id=?", (row["id"],)
).fetchone()
assert got[0] == "a real description"
# ---------------------------------------------------------------------------
# TC-06 (data integrity): multi-line markdown with special chars is preserved
# verbatim (no truncation/escaping); Gitea 422 (file exists) -> no-op
# (single create attempt, body NOT overwritten).
# ---------------------------------------------------------------------------
def test_tc06_multiline_preserved_and_idempotent_422(monkeypatch):
desc = (
"# Heading\n\n- bullet with `inline code`\n"
"- second *italic* and __bold__\n\n"
"Paragraph with special chars: <>&\"' and a trailing word."
)
out = plane._render_business_request("ORCH-119", "N", desc)
assert desc in out # preserved verbatim, no truncation/escaping
calls = []
class _Resp:
status_code = 422 # Gitea: file already exists
def raise_for_status(self):
raise AssertionError("422 must be a no-op, never raised")
class _Client:
async def __aenter__(self):
return self
async def __aexit__(self, *a):
return False
async def post(self, url, json=None, headers=None, timeout=None):
calls.append(json)
return _Resp()
monkeypatch.setattr(
plane, "httpx", SimpleNamespace(AsyncClient=lambda *a, **k: _Client())
)
asyncio.run(
plane._create_initial_docs("orchestrator", "b", "ORCH-119", "N", desc)
)
# Exactly one create attempt; no follow-up PUT/overwrite of the existing body.
assert len(calls) == 1
# ---------------------------------------------------------------------------
# TC-07 (anti-regression): the pipeline machinery is untouched —
# 00-business-request.md stays informational (not gate-parsed).
# ---------------------------------------------------------------------------
def test_tc07_gates_unchanged():
from src.stages import STAGE_TRANSITIONS
from src.qg.checks import QG_CHECKS
# Stage graph intact.
for st in ("created", "analysis", "architecture", "development",
"review", "testing", "deploy-staging", "deploy", "done"):
assert st in STAGE_TRANSITIONS
# The named checks still exist with their canonical names.
for chk in ("check_analysis_complete", "check_architecture_done",
"check_ci_green", "check_tests_passed"):
assert chk in QG_CHECKS
# 00-business-request.md is informational: no check is keyed on it.
assert not any("business" in k.lower() for k in QG_CHECKS)

View File

@@ -68,7 +68,9 @@ async def _drive_start_pipeline(monkeypatch, gate_applies: bool):
async def _branch_spy(repo, branch):
branch_calls.append((repo, branch))
async def _docs_spy(repo, branch, wi, name):
# ORCH-119: _create_initial_docs gained an additive `description` arg; the spy
# accepts it so the serial-gate invariant assertions below stay 1:1.
async def _docs_spy(repo, branch, wi, name, description=None):
docs_calls.append((repo, branch, wi, name))
monkeypatch.setattr(plane, "_create_gitea_branch", _branch_spy)