work_item: ORCH-095
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-09
model_used: claude-opus-4-8
title: "HTML-безопасность динамических полей render_task_tracker (фикс инъекции «<1м»)"
framework: pytest
scope: >
  Покрывается: HTML-безопасность всех подставляемых данных в render_task_tracker
  (длительности < 1 мин, токены/стоимость, имя модели/эффорт, статус-лейбл, заголовок со
  спецсимволами), сохранность намеренной разметки (<a href> номер задачи, _done_link),
  возобновление обновлений застрявшей карточки, never-raise. Вне покрытия: реальная сеть к
  Telegram Bot API (мокируется httpx), изменения STAGE_TRANSITIONS/QG_CHECKS/схемы БД (не
  трогаются).
notes: >
  Тесты — изоляция от сети: httpx.post/get мокируются; БД — временная SQLite-фикстура с
  задачей и agent_runs (стадия < 60 с). Полный регресс pytest tests/ -q должен оставаться
  зелёным, включая существующие test_telegram_tracker.py / test_tracker_*.py /
  test_notifications_orphans.py / test_notify_issue_links.py. Регрессом считается: красный
  любой существующий тест трекера, заэкранированная намеренная разметка, двойное
  экранирование, непойманное исключение в пути рендера.

tests:
  - id: TC-01
    type: unit
    description: "_fmt_minutes для длительности < 60 с (напр. 30) не возвращает сырой '<1м': результат HTML-безопасен (&lt;1м либо переформулированный '~0м'/'< 1 мин' без сырого '<')."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-02
    type: unit
    description: "_fmt_minutes для граничных входов (0, None, нечисловое, ровно 60, большое значение) — never-raise и HTML-безопасный вывод во всех ветках."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-03
    type: unit
    description: "render_task_tracker для задачи со стадией < 1 мин: в выходном тексте нет неэкранированного '<' из данных длительности; подстрока длительности безопасна для parse_mode=HTML."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-04
    type: unit
    description: "render_task_tracker с заголовком, содержащим спецсимволы '<', '>', '&' (напр. 'A <b>x</b> & <1'): спецсимволы данных присутствуют только экранированными (&lt;/&gt;/&amp;), не как сырые теги; двойного экранирования (&amp;lt;) нет."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-05
    type: unit
    description: "Статус-лейбл (_card_status_label) и имя модели/эффорт, попадающие в текст карточки, экранированы (defence-in-depth): спецсимволы в них не ломают HTML."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-06
    type: unit
    description: "Метрики токенов/стоимости (fmt_tokens/fmt_cost) в карточке HTML-безопасны: '$' и числовой формат не порождают сырых тегов."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-07
    type: unit
    description: "Регресс намеренной разметки: кликабельный номер задачи (plane_issue_link -> <a href>) присутствует в выводе как валидный незаэкранированный <a>-тег; href/label не задвоены экранированием."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-08
    type: unit
    description: "Регресс _done_link: для завершённой задачи строка '🔗 PR #n · 📦 Внедрено' рендерится валидной (ссылочная разметка не экранирована)."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-09
    type: integration
    description: "update_task_tracker (edit-режим) с замоканным editMessageText: текст карточки со стадией < 1 мин принимается (мок ассертит отсутствие 'can't parse entities'-триггера, т.е. нет сырого '<1м' в payload text)."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-10
    type: integration
    description: "Возобновление застрявшей карточки (AC-4): после фикса валидный рендер проходит edit-путь без EDIT_FAILED из-за parse-ошибки; защита от дублей сохранена — транзиентный (network) фейл по-прежнему НЕ плодит новую карточку."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-11
    type: unit
    description: "never-raise: render_task_tracker на 'битых' входах (отсутствует задача, None-заголовок, нечисловые длительности) возвращает fallback-строку, не выбрасывает исключение."
    module: tests/test_tracker_html_escape.py
    expected: PASS

  - id: TC-12
    type: integration
    description: "Полный регресс существующих тестов трекера (test_telegram_tracker.py, test_tracker_issue_link.py, test_tracker_status_line.py, test_notifications_orphans.py, test_notify_issue_links.py) остаётся зелёным после фикса."
    module: tests/test_telegram_tracker.py
    expected: PASS
