work_item: ORCH-117
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
title: "Sandbox-only fail-closed изоляция записи в Plane (регресс ORCH-114)"
framework: pytest
scope: >
  Покрывает fail-closed гард записи в Plane: блок прод-записи из тест-процесса даже при живом
  боевом токене (обязательный регресс ORCH-114), sandbox-only-разрешение под явным opt-in,
  default-deny по умолчанию, отличие тест-процесса от боевого/staging рантайма, перехват на
  момент вызова (устойчивость к захвату токена на импорте), аудит. ВНЕ покрытия: реальные сетевые
  вызовы к боевому Plane (запрещены самим фиксом) — всё через мок httpx; выбор механизма детекта —
  зона ADR (тесты проверяют поведение, не реализацию).
notes: >
  TC-01 — ОБЯЗАТЕЛЬНЫЙ регресс инцидента ORCH-114: красный до фикса, зелёный после. Все три
  примитива записи (update_issue_state / add_comment / _set_issue_state_direct) проверяются на
  блок/разрешение через мок httpx.patch/httpx.post (никаких реальных сетевых вызовов). Полный
  регресс tests/ должен оставаться зелёным (autouse fail-closed-фикстура не ломает существующие
  тесты, большинство из которых уже мокируют plane_*/add_comment). Боевой ID проекта в тестах —
  7a79f0a9-5278-49cd-9007-9a338f238f9c; sandbox — 8c5a3025-4f9d-4190-b79f-fa06276bb27e.

tests:
  - id: TC-01
    type: integration
    description: "РЕГРЕСС ORCH-114: pytest-env + живой прод-токен → notify_stage_change('ORCH-114','deploy','done') на боевой проект НЕ делает ни одного httpx.patch/post (мок httpx не вызван для prod-URL / гард блокирует). Красный до фикса, зелёный после."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-02
    type: unit
    description: "update_issue_state в тест-процессе с целевым боевым проектом 7a79f0a9-… → блок (httpx.patch не вызван); аудит-причина prod-project-in-test."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-03
    type: unit
    description: "add_comment в тест-процессе с боевым проектом → блок (httpx.post не вызван)."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-04
    type: unit
    description: "_set_issue_state_direct в тест-процессе с боевым проектом → блок (httpx.patch не вызван). Покрывает все set_issue_* (Done/In Review/Blocked/…), сводящиеся к этому примитиву."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-05
    type: unit
    description: "Default-deny: без явного opt-in запись в тест-процессе блокируется для ЛЮБОГО целевого проекта (в т.ч. sandbox)."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-06
    type: unit
    description: "Sandbox-разрешение: opt-in включён + целевой проект SANDBOX 8c5a3025-… → реальный httpx-вызов разрешён и адресован sandbox-URL (мок подтверждает вызов)."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-07
    type: unit
    description: "Sandbox-only даже с opt-in: opt-in включён, но целевой проект боевой → блок (allowlist sandbox-only), независимо от opt-in."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-08
    type: unit
    description: "Fail-closed при неопределённости: целевой project_id неразрешим/пуст в тест-процессе → блок (NFR-1 'не знаю ⇒ не пишу')."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-09
    type: unit
    description: "Устойчивость к захвату на импорте: PLANE_HEADERS содержит реальный токен, env/settings не подменяются постфактум → гард всё равно блокирует прод-запись на момент вызова (не зависит от os.environ.setdefault / подмены plane_api_token)."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-10
    type: unit
    description: "Нулевая регрессия боевого рантайма: при имитации НЕ-pytest процесса гард = no-op, httpx.patch/post вызывается с прежним URL/headers/payload (запись в Plane как до ORCH-117)."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-11
    type: unit
    description: "Staging != pytest: имитация staging-рантайма (sandbox-проект, не тест-процесс) → запись в SANDBOX проходит (детект тест-процесса не срабатывает ложно)."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-12
    type: unit
    description: "Аудит: на блокировку эмитится структурный WARNING/ERROR с project_id/work_item/операцией/причиной (caplog); на разрешённую sandbox-запись — audit-INFO."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-13
    type: integration
    description: "Дефолтная autouse-страховка conftest: репрезентативный advance стадии в обычном тесте не делает реальной записи в боевой Plane (страховка активна по умолчанию для всего сьюта)."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-14
    type: unit
    description: "Kill-switch без чёрного хода: при выключенном kill-switch гарда запись в БОЕВОЙ проект из pytest всё равно не разрешается молча (реальная sandbox-запись управляется только opt-in+allowlist)."
    module: tests/test_orch117_plane_write_isolation.py
    expected: PASS

  - id: TC-15
    type: integration
    description: "Полный регресс tests/ зелёный — внедрённая autouse fail-closed-фикстура не ломает существующие тесты (smoke: pytest tests/ -q)."
    module: tests/
    expected: PASS
