diff --git a/docs/work-items/ORCH-057/01-brd.md b/docs/work-items/ORCH-057/01-brd.md new file mode 100644 index 0000000..9d130a5 --- /dev/null +++ b/docs/work-items/ORCH-057/01-brd.md @@ -0,0 +1,140 @@ +--- +work_item: ORCH-057 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 01 — BRD (бизнес-требования): ORCH-057 — нормализация legacy root-owned файлов при миграции на uid 1000 (one-time + защита) + +Work Item: **ORCH-057** · Repo: **orchestrator** · Стадия: analysis + +## 1. Бизнес-контекст и проблема + +ORCH-040 перевёл оба контейнера (`orchestrator` 8500, `orchestrator-staging` 8501) с root +на `user: "1000:1000"` (slin). Изменён был **только** `docker-compose.yml`. Однако bind-mount +`/home/slin/repos → /repos` уже содержал файлы и каталоги, созданные **прежним root-контейнером** +(`root:root`). Смена `user:` владельца существующих файлов НЕ меняет. + +**Реальный инцидент (прод, 06.06, поймали на первом запуске ORCH-043).** Первый job под uid 1000 +упал на стадии **launch** (НЕ на коде задачи): + +``` +fatal: could not create leading directories of +'/repos/_wt/orchestrator/feature_ORCH-043-.../.git': Permission denied +``` + +Причина: `/repos/_wt/` и старые worktree-папки = `root:root` → uid 1000 не может создать рядом +новый каталог worktree. Установлено фактически: ошибка возникает в `src/git_worktree.py::ensure_worktree` +(вызов `git worktree add`), куда конвейер приходит из `src/agents/launcher.py::_spawn` (стр. 500) +и `_materialize_deferred_branch` (ORCH-088). Агент даже не стартует — падает создание worktree. + +**Ручной workaround (применён Стрим, прод снова рабочий, ОДНОРАЗОВО):** +``` +sudo chown -R 1000:1000 /home/slin/repos/_wt +sudo chown -R 1000:1000 /home/slin/repos/orchestrator/.git /home/slin/repos/enduro-trails/.git +sudo chown -R 1000:1000 /home/slin/repos/orchestrator # +data/runs/*.log (37 root-логов) +``` + +ADR-001 ORCH-040 упоминал «массовый chown старых root-файлов» лишь абстрактно («вне объёма кода», +«разовая операция Owner») и НЕ дал конкретной процедуры чистки legacy worktree — поэтому deployer +её не выполнил, и баг проявился в проде. Прод сейчас рабочий (ручной фикс наложен), но проблема +**воспроизведётся** на чистой среде, новом репо или после любого исторического запуска под root, +если её не закрыть кодом + процедурой. + +**Это follow-up / закрытие недоделанного AC ORCH-040** (legacy-файлы), а не новая фича. + +## 2. Объём (scope) + +### В объёме +- **Защита launcher (код):** при `Permission denied` на создании worktree выдавать **внятную, + диагностируемую** ошибку «legacy root-файлы в `/repos/_wt` — требуется нормализация прав» + с указанием команды, а НЕ сырой `git fatal`. +- **Раннее обнаружение (код):** детектирование наличия файлов с `uid != ` в + `ORCH_REPOS_DIR` (включая `_wt`, `.git/objects`, `.git/worktrees`, `data/runs`) при старте + контейнера / перед претензией на job — чтобы конвейер падал **внятно и заранее**, а не сырым + git-фаталом на launch. +- **Процедура нормализации (документация):** в `docs/operations/INFRA.md` (и собственный ADR + ORCH-057) — обязательная одноразовая процедура нормализации legacy root-файлов при миграции uid, + с точными командами и областью охвата (`_wt`, `.git`, `data/runs`). +- **Опционально (по решению архитектора):** механизм one-time нормализации при буте/деплое — + init-контейнер/хук под root, либо blocking-entrypoint-проверка. + +### Вне объёма +- Изменение логики конвейера, `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, схемы БД. +- Пересмотр самого решения ORCH-040 (uid 1000) — оно принято и остаётся. +- Перенос инстанса на другой хост / другой uid (отдельная задача при миграции хоста). +- Массовая ретроактивная переработка ADR-001 ORCH-040 (его история не переписывается; + допускается forward-breadcrumb-ссылка на ORCH-057 — решает архитектор). +- Выбор конкретного варианта реализации one-time нормализации (a/b/в) — зона архитектора (06-adr). + +## 3. Заинтересованные стороны + +- **Заказчик / Owner** — Слава (homenet542), инициатор; принимает результат. +- **Эксплуатация** — Стрим (применял ручной workaround); потребитель процедуры в INFRA.md. +- **Затронутые проекты** — `orchestrator` (self-hosting) и `enduro-trails` (общий инстанс, общая + очередь, общий bind-mount `/repos`): нормализация прав `/repos` касается обоих репо. + +## 4. Бизнес-требования (BR) + +- **BR-1** — После миграции контейнера на новый uid конвейер запускается **без ручного `chown`**: + либо авто-нормализация прав, либо **явная блокирующая ошибка с инструкцией** (никогда не сырой + `git fatal` на launch). +- **BR-2** — На свежей среде / новом репо / после исторического запуска под root проблема + **не воспроизводится** (детект + понятная диагностика срабатывают до падения агента). +- **BR-3** — `INFRA.md` и ADR содержат **конкретную процедуру** нормализации legacy root-файлов + (точные команды, область: `_wt`, `.git/objects`, `.git/worktrees`, `data/runs`), помеченную как + обязательный шаг миграции uid. +- **BR-4** — Несоответствие владельца наблюдаемо: оператор узнаёт о проблеме из лога/уведомления/ + read-only статуса, а не по падению задачи на launch. +- **BR-5** — Защита `ensure_worktree` распознаёт класс ошибки «нет прав на создание worktree» и + сообщает причину + лечащую команду (опц. — авто-самолечение, если процесс имеет права). + +## 5. Нефункциональные требования (NFR) + +- **NFR-1 (self-hosting безопасность)** — Решение **никогда** не перезапускает/не роняет + прод-контейнер `orchestrator`, не трогает `main`/force-push/прод-образ. Контейнер бежит под + uid 1000 (без root) → код **не может** делать `chown` без root; код ограничивается + детектом + внятной диагностикой/блокировкой, а фактический `chown` — операторская/init-процедура. +- **NFR-2 (общий инстанс)** — Нулевая регрессия для `enduro-trails`: feature под kill-switch и + scope-флагом (по образцу `serial_gate`/`coverage_gate`); выключено → поведение 1:1 как до ORCH-057. +- **NFR-3 (never-raise / fail-safe)** — Детект-леаф никогда не бросает наружу неожиданное исключение + и не блокирует старт сервиса по своей ошибке; деградирует в WARNING. +- **NFR-4 (идемпотентность)** — Повторный запуск детекта/нормализации на уже корректной среде — + no-op без побочных эффектов. +- **NFR-5 (обратимость)** — Поведение откатывается выключением kill-switch без миграций/правки схемы. +- **NFR-6 (наблюдаемость)** — Вердикт (есть/нет mismatch, сколько файлов, какие корни) логируется + структурно; при проблеме — Telegram с кликабельным номером задачи (если применимо) + read-only + отражение в `GET /queue`. + +## 6. Допущения и ограничения + +- Целевой uid:gid рантайма = `1000:1000` (slin), подтверждён ORCH-040 (P-3); на хосте `/repos`, + `/app/data` штатно `1000:1000`. +- Контейнер бежит под numeric uid 1000 без записи в `/etc/passwd` базового образа; в образе создан + реальный user `slin` (uid 1000) для `getpwuid()` (ORCH-058, Dockerfile). Под uid 1000 `chown` + чужих (root) файлов **невозможен** без CAP_CHOWN/root. +- `git config --system --add safe.directory '*'` уже в образе — git доверяет bind-mount. +- Корни проверки: `ORCH_REPOS_DIR` (`/repos`), включая `_wt`, `/.git/objects`, + `/.git/worktrees`, и `data/runs` (37 root-логов в инциденте). +- `start_pipeline` (ORCH-088) отложил срез ветки на момент claim analyst-job → детект уместен + и на старте сервиса, и перед claim'ом (точку выбирает архитектор). + +## 7. Критерии успеха + +После миграции uid (или на чистой среде) первый же job проходит launch без ручного `chown`, либо — +если права не нормализованы — конвейер выдаёт **понятную блокирующую диагностику** с командой +исправления вместо сырого `git fatal`. INFRA.md/ADR содержат воспроизводимую процедуру. +Для `enduro-trails` — нулевая регрессия. Детальные PASS/FAIL — в `03-acceptance-criteria.md`. + +## 8. Риски + +- Контейнер без root не может `chown` → авто-самолечение возможно только частично/при наличии прав; + основной гарант — детект+диагностика+процедура (детали — `10-tech-risks.md`, архитектор). +- Рекурсивный обход больших `.git/objects` / `_wt` может быть дорог → нужен дешёвый/семплированный + детект и кэш (как preflight TTL). +- Ложно-блокирующая ошибка может застопорить и enduro-trails (общий `/repos`) → строгий scope/fail-safe. +- Правка `docker-compose.yml`/entrypoint (init-контейнер) = деплой self → групповой риск (NFR-1), + обязательная страховка staging. diff --git a/docs/work-items/ORCH-057/02-trz.md b/docs/work-items/ORCH-057/02-trz.md new file mode 100644 index 0000000..6944f9d --- /dev/null +++ b/docs/work-items/ORCH-057/02-trz.md @@ -0,0 +1,117 @@ +--- +work_item: ORCH-057 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 02 — ТЗ (TRZ): ORCH-057 — нормализация legacy root-owned файлов при миграции на uid 1000 + +Work Item: **ORCH-057** · Repo: **orchestrator** · Стадия: analysis + +> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода. +> Архитектурное обоснование/выбор варианта one-time нормализации (init-контейнер vs blocking-entrypoint +> vs ансибл) — задача архитектора (`06-adr/`). Здесь — требования, контракты и ограничения. + +## 1. Сводка изменения + +Закрыть недоделанный AC ORCH-040 по legacy-файлам. Три слоя: +1. **Защита launcher** — `ensure_worktree` распознаёт `Permission denied`/git-fatal на создании + worktree и поднимает **внятную** ошибку с диагнозом «legacy root-файлы в `/repos/_wt` — нужна + нормализация прав» + лечащая команда (опц. авто-самолечение при наличии прав). +2. **Ранний детект** — новый чистый леаф находит файлы с `uid != target_uid` в `ORCH_REPOS_DIR` + (`_wt`, `.git/objects`, `.git/worktrees`, `data/runs`); вызывается на старте сервиса и/или перед + claim'ом job; never-raise, config-gated, с наблюдаемостью. +3. **Процедура** — `INFRA.md` + ADR ORCH-057: точные команды разовой нормализации как обязательный + шаг миграции uid. Опционально — one-time нормализация под root через init-механизм (решает архитектор). + +Инвариант: `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи / схема БД — +**байт-в-байт прежние**. Изменение аддитивно и обратимо kill-switch'ем. + +## 2. Задействованные модули / пути + +| Путь | Действие | +|------|----------| +| `src/git_worktree.py` (`ensure_worktree`, `remove_worktree`) | изменить — классификация `Permission denied`/git-fatal на `git worktree add` / `os.makedirs` → внятный actionable `RuntimeError` (опц. self-heal при правах) | +| `src/fs_normalize.py` | **создать** — чистый леаф (never-raise): `scan_ownership(roots, target_uid) -> результат`; опц. `normalize(...)` (chown только при наличии прав); хелпер `applies(repo)` + кэш (TTL, как preflight) | +| `src/config.py` | изменить — добавить флаги (см. §7); без правки существующих значений | +| `src/main.py` (`lifespan`) | изменить — добавить startup-вызов детекта (best-effort, never-fatal по образцу L-2/lease-reclaim), лог + Telegram при mismatch; read-only блок в `GET /queue` | +| `src/preflight.py` **или** `src/queue_worker.py` | изменить (на выбор архитектора) — опц. гейт claim'а job при обнаруженном mismatch, чтобы падать внятно ДО launch (по образцу preflight-гейта) | +| `docker-compose.yml` / `Dockerfile` / `scripts/*entrypoint*` | **кандидат** (решает архитектор) — one-time root-нормализация (init-контейнер/хук) ПЕРЕД стартом app; если выбрано — деплой self, обязательная staging-страховка | +| `docs/operations/INFRA.md` | изменить — раздел «Миграция uid: обязательная нормализация legacy root-файлов» (команды + область) | +| `docs/work-items/ORCH-057/06-adr/ADR-001-*.md` | создать (architect) — решение + процедура; опц. forward-breadcrumb из ADR-001 ORCH-040 (без переписывания истории) | +| `CHANGELOG.md` | изменить — запись о ORCH-057 | +| `tests/test_*` | создать — см. `04-test-plan.yaml` | + +## 3. Функциональные требования + +### FR-1 — Внятная ошибка `ensure_worktree` (BR-1, BR-5) +При неуспехе `git worktree add` / `os.makedirs(os.path.dirname(wt))` по причине отказа доступа +(`Permission denied`, `could not create leading directories`, `insufficient permission for adding an +object`) `ensure_worktree` поднимает `RuntimeError` с сообщением, которое: (а) называет корневую +причину (legacy root-owned файлы в `/repos/_wt` или `.git` после миграции uid ORCH-040); (б) указывает +лечащую команду (`chown -R : …`) или ссылку на процедуру INFRA.md; (в) НЕ является сырым +git stderr. Прочие (нет-прав-несвязанные) ошибки сохраняют текущий контракт (никакой подмены смысла). + +### FR-2 — Детект несоответствия владельца (BR-2, BR-4) +Леаф `fs_normalize.scan_ownership` обходит корни (`/repos/_wt`, `/.git/objects`, +`/.git/worktrees`, `data/runs`) и возвращает: есть ли файлы с `uid != target_uid`, их число +(или флаг «≥1»), список затронутых корней. Обход дешёвый/ограниченный (ранний выход при первом +mismatch для быстрого вердикта; полный подсчёт — опционально/семплировано). Результат кэшируется по +TTL (по образцу `preflight._cache`). `target_uid` = `os.getuid()` или конфиг (дефолт 1000). + +### FR-3 — Реакция на детект (BR-1, BR-4) +- **Startup (main.lifespan):** вызвать детект best-effort; при mismatch — структурный WARNING + + Telegram (если включён) с числом/корнями и лечащей командой. Никогда не падать на старте по + ошибке детекта (NFR-3). +- **Опц. гейт claim'а:** при обнаруженном mismatch и `target_uid` без прав на chown — не претендовать + на job (или претендовать и сразу честно фейлить с FR-1-сообщением), чтобы исход был внятным до launch. + Конкретную точку (preflight vs queue_worker) выбирает архитектор; требование — «внятно и заранее». + +### FR-4 — Опциональная авто-нормализация (BR-1) +`fs_normalize.normalize` выполняет `chown -R target_uid:target_gid` по корням **только если процесс +имеет на это право** (CAP_CHOWN/root). Под uid 1000 без прав — no-op + честный лог «нужна операторская +процедура» (НЕ ошибка). Включается отдельным флагом (`*_AUTO`), по умолчанию — выкл (детект-only). +Если архитектор выбирает init-контейнер под root — это и есть носитель FR-4 на буте. + +### FR-5 — Документированная процедура (BR-3) +`INFRA.md` получает раздел с точными командами разовой нормализации (`_wt`, оба `.git`, `data/runs`), +помеченный как **обязательный** шаг миграции uid и часть чеклиста деплоя self. ADR ORCH-057 фиксирует +решение и ссылается на процедуру; ADR-001 ORCH-040 опц. получает forward-ссылку. + +## 4. Изменения API + +Нет новых обязательных эндпоинтов. **Опционально** (наблюдаемость, решает архитектор): +- расширить `GET /queue` read-only блоком `fs_ownership` (`{enabled, target_uid, mismatch, roots, checked_at}`); +- ручной триггер `POST /fs-normalize/check` (форс-пересчёт детекта) — по образцу `POST /serial-gate/unfreeze`. + +## 5. Изменения схемы БД + +Нет. Состояние детекта — в памяти (TTL-кэш), как `preflight`. Таблицы/миграции/индексы не вводятся. + +## 6. Требования к новым/изменённым QG checks + +Нет. Это **не** stage-гейт и **не** под-гейт ребра. `QG_CHECKS` / `check_*` / `STAGE_TRANSITIONS` / +machine-verdict-ключи (`verdict:`/`result:`/`deploy_status:`/`staging_status:`/`security_status:`/ +`coverage_status:`) — не трогаются. (В описании баг-репорта «deploy-гейт ORCH-040» — это деплой-хук/ +процедура, а не зарегистрированный QG.) + +## 7. Совместимость / регресс + +- **Kill-switch** `ORCH_FS_NORMALIZE_ENABLED` (дефолт по решению архитектора; `False` → весь код инертен, + поведение 1:1 как до ORCH-057). +- **Scope** `ORCH_FS_NORMALIZE_REPOS` (CSV; пусто → **self-hosting only**, как `coverage_gate_repos` → + enduro-trails не затронут). Локальный `applies(repo)` проверяется ПЕРВЫМ (дешёвый обход только при applies). +- **Флаги** (рабочие имена, финал — за архитектором): `ORCH_FS_TARGET_UID` (дефолт 1000), + `ORCH_FS_NORMALIZE_AUTO` (дефолт `False` — детект-only; `True` → попытка chown при наличии прав), + `ORCH_FS_SCAN_ROOTS` (CSV переопределения корней), `ORCH_FS_SCAN_CACHE_TTL_S`. +- **Never-raise / fail-safe** — ошибка детекта/нормализации деградирует в WARNING, не блокирует старт + сервиса по своей вине; FR-1 меняет лишь **формулировку** ошибки worktree, не её факт. +- **Self-hosting** (NFR-1) — код только читает/детектит/диагностирует (и chown ТОЛЬКО при наличии прав); + не деплоит/не рестартит прод/не трогает `main`. Любое касание `docker-compose.yml`/entrypoint требует + staging-прогона (8501) перед прод-рестартом в окно тишины. +- **Обратимость** — выкл kill-switch → прежнее поведение; миграций/правки схемы нет. +- **Пайплайн-артефакты:** обновляются `01..04` (analysis), `06-adr/`+`07-infra-requirements.md`+`10-tech-risks.md` + (architecture), `12/13/15/14` (review/testing/staging/deploy), `INFRA.md`, `CHANGELOG.md`. diff --git a/docs/work-items/ORCH-057/03-acceptance-criteria.md b/docs/work-items/ORCH-057/03-acceptance-criteria.md new file mode 100644 index 0000000..db8a1b1 --- /dev/null +++ b/docs/work-items/ORCH-057/03-acceptance-criteria.md @@ -0,0 +1,99 @@ +--- +work_item: ORCH-057 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-057 — нормализация legacy root-owned файлов + +Work Item: **ORCH-057** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** +(что считается провалом). Любой машинный/ручной reviewer проверяет их буквально по файлам репозитория. + +--- + +## AC-1 — Конвейер стартует без ручного chown (или внятная блокирующая ошибка) + +**Условие:** после миграции контейнера на новый uid первый job не падает сырым git-фаталом на launch. +- **PASS:** при нормализованных правах worktree создаётся и агент стартует; при НЕнормализованных + правах конвейер выдаёт понятную блокирующую ошибку с диагнозом и лечащей командой (НЕ сырой + `fatal: could not create leading directories … Permission denied`). +- **FAIL:** на launch всплывает сырой git-fatal/Permission denied без диагноза причины и инструкции. + +--- + +## AC-2 — `ensure_worktree` даёт actionable-ошибку при отказе доступа + +**Условие:** `src/git_worktree.py::ensure_worktree` классифицирует ошибки прав. +- **PASS:** при `Permission denied`/`could not create leading directories`/`insufficient permission` + поднимается `RuntimeError`, текст которого называет причину (legacy root-файлы в `/repos/_wt`/`.git` + после миграции uid) и указывает команду/ссылку на процедуру; ошибки, не связанные с правами, + сохраняют прежний контракт. +- **FAIL:** сырой git stderr пробрасывается без диагноза; либо подменяется смысл не-прав-ошибок; + либо `ensure_worktree` падает необработанно. + +--- + +## AC-3 — Детект несоответствия владельца + +**Условие:** новый леаф `src/fs_normalize.py` обнаруживает файлы с `uid != target_uid` в корнях +(`/repos/_wt`, `/.git/objects`, `/.git/worktrees`, `data/runs`). +- **PASS:** на среде с root-файлами `scan_ownership` возвращает mismatch=True + затронутые корни; + на чистой (`1000:1000`) среде — mismatch=False (no-op, идемпотентно); леаф never-raise. +- **FAIL:** mismatch не обнаружен на грязной среде / ложный mismatch на чистой / леаф бросает наружу. + +--- + +## AC-4 — Наблюдаемость детекта + +**Условие:** результат детекта виден оператору без падения задачи. +- **PASS:** при mismatch — структурный лог-WARNING (число/корни/лечащая команда) и Telegram (если + включён); опц. read-only отражение в `GET /queue`. +- **FAIL:** mismatch обнаружен, но никак не сообщён; оператор узнаёт о проблеме только по упавшей задаче. + +--- + +## AC-5 — Self-hosting безопасность и нулевая регрессия enduro-trails + +**Условие:** изменение безопасно для общего инстанса. +- **PASS:** код не рестартит/не роняет прод, не трогает `main`/force-push/прод-образ; chown — только + при наличии прав; при выключенном kill-switch поведение 1:1 как до ORCH-057; при пустом scope-CSV + feature активен только для self-hosting (enduro-trails не затронут); регресс `pytest tests/ -q` зелёный. +- **FAIL:** любой рестарт/деградация прода из кода задачи; ненулевая регрессия enduro-trails; + поведение меняется при выключенном флаге; падение всего регресса. + +--- + +## AC-6 — Инварианты конвейера сохранены + +**Условие:** изменение аддитивно. +- **PASS:** `STAGE_TRANSITIONS`, `QG_CHECKS`, `check_*`, machine-verdict-ключи и схема БД — + байт-в-байт прежние; новые флаги аддитивны и обратимы. +- **FAIL:** затронут любой exit/под-гейт, изменён machine-key, добавлена миграция схемы. + +--- + +## AC-7 — Документированная процедура нормализации + +**Условие:** процедура воспроизводима. +- **PASS:** `INFRA.md` содержит раздел «Миграция uid: обязательная нормализация legacy root-файлов» + с точными командами (`_wt`, оба `.git`, `data/runs`) как обязательный шаг миграции; ADR ORCH-057 + фиксирует решение и ссылается на процедуру. +- **FAIL:** процедура отсутствует/абстрактна (как было в ORCH-040) либо не покрывает все корни. + +--- + +## Сводная матрица AC ↔ FR/BR +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-1, FR-3 | +| AC-2 | BR-1, BR-5 / FR-1 | +| AC-3 | BR-2 / FR-2 | +| AC-4 | BR-4 / FR-3 | +| AC-5 | NFR-1, NFR-2, NFR-5 / FR-4 | +| AC-6 | NFR-5 (инварианты) | +| AC-7 | BR-3 / FR-5 | diff --git a/docs/work-items/ORCH-057/04-test-plan.yaml b/docs/work-items/ORCH-057/04-test-plan.yaml new file mode 100644 index 0000000..8fe92bd --- /dev/null +++ b/docs/work-items/ORCH-057/04-test-plan.yaml @@ -0,0 +1,92 @@ +work_item: ORCH-057 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-10 +model_used: claude-opus-4-8 +title: "Нормализация legacy root-owned файлов при миграции на uid 1000 (детект + защита worktree)" +framework: pytest +scope: > + Покрывается: классификация ошибки прав в ensure_worktree (внятная actionable-ошибка), + детект несоответствия владельца (fs_normalize.scan_ownership), идемпотентность на чистой среде, + fail-safe/never-raise, scope/kill-switch (self-hosting only при пустом CSV), опц. self-heal-noop + без прав. ВНЕ покрытия: реальный chown под root (требует привилегий — проверяется на staging + вручную), правка docker-compose/entrypoint (инфра, ручная проверка на 8501). +notes: > + Все FS-зависимые тесты используют tmp_path и monkeypatch os.getuid/os.stat — без реального chown + и без записи в /repos. Telegram/Plane мокаются. Полный регресс tests/ должен оставаться зелёным; + STAGE_TRANSITIONS/QG_CHECKS/схема БД не затрагиваются — отдельные guard-тесты не требуются, но + существующие тесты на инварианты должны пройти без изменений. + +tests: + - id: TC-01 + type: unit + description: "ensure_worktree при git-fatal 'could not create leading directories / Permission denied' поднимает RuntimeError с диагнозом legacy-root + лечащей командой, а не сырой git stderr" + module: tests/test_git_worktree_perm.py + expected: PASS + + - id: TC-02 + type: unit + description: "ensure_worktree при ошибке, НЕ связанной с правами (например branch conflict), сохраняет прежний контракт сообщения (не подменяет смысл)" + module: tests/test_git_worktree_perm.py + expected: PASS + + - id: TC-03 + type: unit + description: "scan_ownership на дереве с файлом uid != target_uid возвращает mismatch=True и список затронутых корней" + module: tests/test_fs_normalize.py + expected: PASS + + - id: TC-04 + type: unit + description: "scan_ownership на чистом дереве (все файлы target_uid) возвращает mismatch=False (идемпотентный no-op)" + module: tests/test_fs_normalize.py + expected: PASS + + - id: TC-05 + type: unit + description: "scan_ownership never-raise: при недоступном/несуществующем корне деградирует в WARNING и не бросает наружу" + module: tests/test_fs_normalize.py + expected: PASS + + - id: TC-06 + type: unit + description: "applies(repo): пустой ORCH_FS_NORMALIZE_REPOS → True только для self-hosting репо (orchestrator), False для enduro-trails; непустой CSV — по списку" + module: tests/test_fs_normalize.py + expected: PASS + + - id: TC-07 + type: unit + description: "kill-switch ORCH_FS_NORMALIZE_ENABLED=False → scan/normalize инертны (no-op), поведение 1:1 как до ORCH-057" + module: tests/test_fs_normalize.py + expected: PASS + + - id: TC-08 + type: unit + description: "normalize без прав (uid 1000, чужие root-файлы, ORCH_FS_NORMALIZE_AUTO=True) → no-op + честный лог 'нужна операторская процедура', НЕ исключение" + module: tests/test_fs_normalize.py + expected: PASS + + - id: TC-09 + type: unit + description: "TTL-кэш детекта: повторный вызов в окне TTL не пере-сканирует дерево (по образцу preflight._cache); force/reset инвалидирует" + module: tests/test_fs_normalize.py + expected: PASS + + - id: TC-10 + type: integration + description: "startup-хук lifespan при mismatch вызывает send_telegram (мок) и логирует WARNING; при ошибке детекта старт сервиса не падает (never-fatal)" + module: tests/test_fs_normalize_startup.py + expected: PASS + + - id: TC-11 + type: integration + description: "опц. гейт claim'а: при обнаруженном mismatch без прав исход job внятный (FR-1-сообщение / не-claim) ДО launch, а не сырой git-fatal" + module: tests/test_fs_normalize_startup.py + expected: PASS + + - id: TC-12 + type: integration + description: "GET /queue (если реализован read-only блок fs_ownership) отдаёт {enabled,target_uid,mismatch,roots,checked_at} и не 5xx-ит при выключенном флаге" + module: tests/test_api_queue.py + expected: PASS