diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 30de2694..c7b8c7dd 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -5,28 +5,28 @@ --- -## 2026-09-28 — Pre-commit hook fail-open при потере маркеров (Open) +## 2026-09-28 — Pre-commit hook fail-open при потере маркеров (Fixed) - **Локация:** `.githooks/pre-commit:31-53` (`find_project_root` + `run_script`). - **Симптом:** если ни `.git`, ни `KNOWN_ISSUES.md` не найдены (переименование, копия дерева, битый `.git`), fallback указывает мимо проекта; все 9 гейтов печатают ⏭️ «скрипт не найден» и возвращают True → «All pre-commit checks passed», exit 0, коммит идёт без единой проверки. - **Repro (Verified 2026-09-28):** копия хука в `%TEMP%` (без маркеров) → 9× «скрипт не найден», итог PASS. - **Guard:** fallback-ветвь обязана fail-closed (sys.exit(1) с явным «project root not found»), либо `run_script` считает missing-script провалом, когда пропущены ВСЕ скрипты; regression-тест: исполнение с `__file__` в markerless-tmpdir → exit ≠ 0. -- **Статус:** 🟡 Fixed-pending-verification (branch `fix/redteam-open-triple`: `find_project_root() -> Path | None`, `run_script` fail-closed; `tests/test_hook_root.py` 3/3 green + full suite 1940 passed). +- **Статус:** ✅ Fixed (fix `0b6ca7c4`, merge `e8811af1` = PR #56: `find_project_root() -> Path | None`, `run_script` fail-closed; `tests/test_hook_root.py` 3/3 green; PR #56 CI all green incl. clean-state + ubuntu/windows tests). -## 2026-09-28 — silent_subprocess: STARTUPINFO ctor outside narrowed try (Open) +## 2026-09-28 — silent_subprocess: STARTUPINFO ctor outside narrowed try (Fixed) - **Локация:** `src/core/silent_subprocess.py:32-33` (S1), `:51` (S2 — unwrapped `setdefault`). - **Симптом:** `subprocess.STARTUPINFO()` на L33 вне `try`; на экзотическом win32-билде без `STARTUPINFO` — `AttributeError` из `apply()` на импорте (S2/L51 тот же путь без обёртки; S4/L67 в безопасности — вызов внутри try). - **Контекст:** на CPython/win32 `STARTUPINFO` всегда есть; все реальные `creationflags=`-вызывающие передают int — практический риск ≈ 0. - **Guard:** перенести конструирование внутрь try (S1) + обернуть L51 как L67; regression-тест: monkeypatch `subprocess.STARTUPINFO = ` → `apply()` не бросает. -- **Статус:** 🟡 Fixed-pending-verification (low; branch `fix/redteam-open-triple`: ctor inside try + `si = None` init, `:51` wrapped like `:66-69`; `tests/test_silent_subprocess.py` 3/3 green + full suite 1940 passed). +- **Статус:** ✅ Fixed (fix `0b6ca7c4`, merge `e8811af1` = PR #56: ctor inside try + `si = None` init, `:51` wrapped like `:66-69`; `tests/test_silent_subprocess.py` 3/3 green; PR #56 CI all green). Tails (branch `fix/redteam-tails`): модуль был INERT — заведён в entry point (`src/main.py` import + `apply()` at startup, как требует docstring модуля) + TypeError-guard на не-классовый `Popen` (тестовые шимы); liveness доказан `tests/test_silent_subprocess_wired.py` (fresh-процесс: импорт `src.main` → `_APPLIED=True`, на win32 `Popen=_SilentPopen`). -## 2026-09-28 — o1_holdout_gate: hung query hangs whole gate, no timeout (Open) +## 2026-09-28 — o1_holdout_gate: hung query hangs whole gate, no timeout (Fixed) - **Локация:** `scripts/o1_holdout_gate.py:129-156` (`_run_all`), вызов L106. - **Симптом:** 15 запросов идут последовательно в одном loop без `wait_for`/глобального капа; один зависший `hybrid_search_async` вешает весь гейт навсегда (единственный `timeout=10` — git-rev диагностика, L99-101). - **Guard:** per-query `asyncio.wait_for(..., timeout=120)` + timeout → fail-row (как `degraded`); regression — фейковый searcher с висящим запросом → гейт падает за ~120с, а не висит. -- **Статус:** 🟡 Fixed-pending-verification (medium — CI-stall; branch `fix/redteam-open-triple`: per-query `wait_for(timeout=120)` + `timed_out` fail-row in both gates; `tests/test_holdout_harness_timeout.py` 6/6 green + full suite 1940 passed). +- **Статус:** ✅ Fixed (fix `0b6ca7c4`, merge `e8811af1` = PR #56: per-query `wait_for(timeout=120)` + `timed_out` fail-row in both gates; `tests/test_holdout_harness_timeout.py` 6/6 green incl. positive controls; PR #56 CI all green). ## 2026-09-28 — Ретриевер-замеры без сброса реранкер-кэша недействительны (Open) diff --git a/src/core/silent_subprocess.py b/src/core/silent_subprocess.py index e5e52ab0..c7cd7914 100644 --- a/src/core/silent_subprocess.py +++ b/src/core/silent_subprocess.py @@ -41,19 +41,26 @@ def _silent_startupinfo(): _orig_popen = subprocess.Popen - class _SilentPopen(_orig_popen): # type: ignore[misc] - def __init__(self, *args, **kwargs): - kwargs.setdefault("creationflags", _CNW) - # CREATE_NO_WINDOW может быть скомбинирован — OR, не замена - try: - kwargs["creationflags"] |= _CNW - except TypeError: - pass - try: - kwargs.setdefault("startupinfo", _silent_startupinfo()) - except (AttributeError, OSError, TypeError): - pass - super().__init__(*args, **kwargs) + try: + + class _SilentPopen(_orig_popen): # type: ignore[misc] + def __init__(self, *args, **kwargs): + kwargs.setdefault("creationflags", _CNW) + # CREATE_NO_WINDOW может быть скомбинирован — OR, не замена + try: + kwargs["creationflags"] |= _CNW + except TypeError: + pass + try: + kwargs.setdefault("startupinfo", _silent_startupinfo()) + except (AttributeError, OSError, TypeError): + pass + super().__init__(*args, **kwargs) + + except TypeError: + # Popen — не класс (тестовый шим/экзотика): патчить нечего, + # стартовый импорт не должен падать (ср. S1/S2-стиль выше). + return _orig_run = subprocess.run _orig_check_output = subprocess.check_output diff --git a/src/main.py b/src/main.py index 0a2eeda8..ae8c75a5 100644 --- a/src/main.py +++ b/src/main.py @@ -2,6 +2,15 @@ Главная точка входа в приложение. """ +# Guard от мигающих консолей на Windows: патчит subprocess до любых spawn'ов. +# Идемпотентен (повторный apply() — no-op); на non-win32 — только флаг. +try: + from src.core.silent_subprocess import apply as _apply_silent_subprocess + + _apply_silent_subprocess() +except Exception: + pass + import logging import logging.handlers import os diff --git a/tests/test_silent_subprocess_wired.py b/tests/test_silent_subprocess_wired.py new file mode 100644 index 00000000..a5f97d40 --- /dev/null +++ b/tests/test_silent_subprocess_wired.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Liveness: silent_subprocess is wired into the entry point (2026-09-29). + +PR #56 shipped src/core/silent_subprocess.py INERT — nothing imported it. +fix/redteam-tails wires it into src/main.py (import + apply() at startup). + +Proves liveness in a FRESH interpreter (no conftest Popen-shim at import +time — the autouse _no_console_windows fixture replaces Popen with a plain +function, which would mask the real startup path): the child imports +src.main as the server would, then asserts the guard is applied — and on +win32 that subprocess.Popen is the silent wrapper. +""" + +import subprocess +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parent.parent + +_CHILD = ( + "import subprocess, sys; " + "import src.main; " + "import src.core.silent_subprocess as mod; " + "assert 'src.core.silent_subprocess' in sys.modules, 'not wired'; " + "assert mod._APPLIED is True, 'not applied'; " + "print('WIRED_OK'); " + "print('POPEN=' + type(subprocess.Popen).__name__ + ':' + subprocess.Popen.__name__)" +) + + +def test_entry_point_wires_silent_subprocess_fresh_process(): + proc = subprocess.run( + [sys.executable, "-c", _CHILD], + cwd=str(PROJECT_ROOT), + capture_output=True, + text=True, + timeout=120, + ) + assert proc.returncode == 0, f"child startup import failed:\n{proc.stderr[-2000:]}" + assert "WIRED_OK" in proc.stdout, f"guard not applied after startup import:\n{proc.stdout}" + if sys.platform == "win32": + assert "_SilentPopen" in proc.stdout, ( + f"Popen is not the silent wrapper after startup import:\n{proc.stdout}" + )