kin: auto-commit after pipeline

This commit is contained in:
Gros Frumos 2026-03-17 16:21:33 +02:00
parent e0511eac0c
commit 143a393ba7
3 changed files with 616 additions and 0 deletions

View file

@ -0,0 +1,177 @@
"""
Regression tests for KIN-096:
Pipeline из веб-интерфейса не передаёт SSH_AUTH_SOCK в subprocess агентов.
Root causes fixed:
(1) agents/runner.py _build_claude_env() уже содержит glob-детекцию
SSH_AUTH_SOCK из /private/tmp/com.apple.launchd.*/Listeners.
(2) agents/runner.py _build_claude_env() SSH_AGENT_PID detection (строки 74-77)
был логическим no-op (env = os.environ.copy(), поэтому проверка
'SSH_AGENT_PID not in env' == 'SSH_AGENT_PID not in os.environ').
(3) web/api.py _launch_pipeline_subprocess() должна добавить аналогичную
glob-детекцию SSH_AUTH_SOCK, чтобы промежуточный subprocess получал
SSH env при старте из launchd.
Acceptance criteria:
- При наличии SSH_AUTH_SOCK в os.environ subprocess получает его в env.
- При отсутствии SSH_AUTH_SOCK и наличии macOS-сокета детектируется через glob.
- При отсутствии SSH_AUTH_SOCK и пустом glob subprocess стартует без него (graceful).
"""
import os
from unittest.mock import patch, MagicMock
import pytest
from agents.runner import _build_claude_env
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_popen_mock():
proc = MagicMock()
proc.pid = 12345
return proc
# ---------------------------------------------------------------------------
# _build_claude_env: SSH_AUTH_SOCK detection (agents/runner.py)
# ---------------------------------------------------------------------------
class TestBuildClaudeEnvSSH:
"""Тесты SSH env в _build_claude_env (agents/runner.py)."""
def test_ssh_auth_sock_preserved_when_present_in_environ(self):
"""Если SSH_AUTH_SOCK уже есть в os.environ — он попадает в env без изменений."""
sock_path = "/tmp/ssh-agent-test.sock"
with patch.dict("os.environ", {"SSH_AUTH_SOCK": sock_path}, clear=False):
env = _build_claude_env()
assert env.get("SSH_AUTH_SOCK") == sock_path
def test_ssh_auth_sock_detected_via_glob_when_absent(self):
"""Если SSH_AUTH_SOCK отсутствует в os.environ — детектируется через glob.
Примечание: в runner.py glob импортируется локально внутри if-блока,
поэтому патчим glob.glob напрямую, а не agents.runner.glob.
"""
fake_sock = "/private/tmp/com.apple.launchd.ABCDEF/Listeners"
env_without_sock = {k: v for k, v in os.environ.items() if k != "SSH_AUTH_SOCK"}
with patch.dict("os.environ", env_without_sock, clear=True):
with patch("glob.glob", return_value=[fake_sock]) as mock_glob:
env = _build_claude_env()
assert env.get("SSH_AUTH_SOCK") == fake_sock
mock_glob.assert_called_with("/private/tmp/com.apple.launchd.*/Listeners")
def test_ssh_auth_sock_absent_when_glob_finds_nothing(self):
"""Graceful degradation: если glob ничего не нашёл — SSH_AUTH_SOCK не устанавливается."""
env_without_sock = {k: v for k, v in os.environ.items() if k != "SSH_AUTH_SOCK"}
with patch.dict("os.environ", env_without_sock, clear=True):
with patch("glob.glob", return_value=[]):
env = _build_claude_env()
assert "SSH_AUTH_SOCK" not in env
def test_ssh_agent_pid_is_inherited_via_environ_copy(self):
"""SSH_AGENT_PID попадает в env через os.environ.copy() — без специального кода.
Проверяет что корректный путь работает: env=os.environ.copy()
автоматически включает SSH_AGENT_PID, если он был в окружении.
"""
with patch.dict("os.environ", {"SSH_AGENT_PID": "9999"}, clear=False):
env = _build_claude_env()
assert env.get("SSH_AGENT_PID") == "9999"
def test_ssh_agent_pid_absent_when_not_in_environ(self):
"""Если SSH_AGENT_PID нет в os.environ, он не появляется в env."""
env_without_pid = {k: v for k, v in os.environ.items() if k != "SSH_AGENT_PID"}
with patch.dict("os.environ", env_without_pid, clear=True):
with patch("glob.glob", return_value=[]):
env = _build_claude_env()
assert "SSH_AGENT_PID" not in env
# ---------------------------------------------------------------------------
# _launch_pipeline_subprocess: SSH_AUTH_SOCK передаётся в Popen (web/api.py)
# ---------------------------------------------------------------------------
class TestLaunchPipelineSubprocessSSH:
"""Тесты передачи SSH-переменных в subprocess из _launch_pipeline_subprocess."""
def test_ssh_auth_sock_passed_to_subprocess_when_in_environ(self):
"""KIN-096: SSH_AUTH_SOCK из os.environ попадает в env Popen через copy()."""
from web.api import _launch_pipeline_subprocess
sock_path = "/tmp/ssh-TEST-known.sock"
with patch.dict("os.environ", {"SSH_AUTH_SOCK": sock_path}, clear=False):
with patch("web.api.subprocess.Popen", return_value=_make_popen_mock()) as mock_popen:
_launch_pipeline_subprocess("P1-001")
assert mock_popen.called
kwargs = mock_popen.call_args[1]
env_passed = kwargs.get("env", {})
assert env_passed.get("SSH_AUTH_SOCK") == sock_path, (
"KIN-096: SSH_AUTH_SOCK должен передаваться в Popen env через os.environ.copy()"
)
def test_kin_noninteractive_always_set_in_subprocess_env(self):
"""KIN_NONINTERACTIVE=1 всегда присутствует в env Popen — независимо от SSH."""
from web.api import _launch_pipeline_subprocess
with patch("web.api.subprocess.Popen", return_value=_make_popen_mock()) as mock_popen:
_launch_pipeline_subprocess("P1-001")
kwargs = mock_popen.call_args[1]
env_passed = kwargs.get("env", {})
assert env_passed.get("KIN_NONINTERACTIVE") == "1"
def test_popen_failure_does_not_propagate_exception(self):
"""Если Popen бросает исключение, _launch_pipeline_subprocess не пробрасывает его."""
from web.api import _launch_pipeline_subprocess
with patch("web.api.subprocess.Popen", side_effect=OSError("no such file")):
# не должно бросать
_launch_pipeline_subprocess("P1-001")
def test_subprocess_launched_without_ssh_auth_sock_does_not_raise(self):
"""Graceful degradation: при отсутствии SSH_AUTH_SOCK Popen вызывается без ошибок.
Тест воспроизводит сценарий launchd-среды без SSH_AUTH_SOCK.
После применения фикса (glob-детекция в _launch_pipeline_subprocess)
macOS-сокет будет добавлен; до применения subprocess стартует без него.
В обоих случаях исключений быть не должно.
"""
from web.api import _launch_pipeline_subprocess
env_without_sock = {k: v for k, v in os.environ.items() if k != "SSH_AUTH_SOCK"}
with patch.dict("os.environ", env_without_sock, clear=True):
with patch("web.api.subprocess.Popen", return_value=_make_popen_mock()) as mock_popen:
_launch_pipeline_subprocess("P1-001")
assert mock_popen.called, "Popen должен быть вызван даже без SSH_AUTH_SOCK в окружении"
def test_ssh_auth_sock_detected_via_glob_in_launch_subprocess(self):
"""KIN-096 FIX VERIFICATION: glob-детекция SSH_AUTH_SOCK в _launch_pipeline_subprocess.
После применения фикса (добавление import glob + glob.glob детекции в
_launch_pipeline_subprocess), SSH_AUTH_SOCK должен определяться
автоматически из macOS launchd-сокета.
ВНИМАНИЕ: этот тест проверяет фикс, который ещё НЕ применён в web/api.py.
При текущем коде (без фикса) тест упадёт, т.к. glob-детекция отсутствует.
"""
from web.api import _launch_pipeline_subprocess
fake_sock = "/private/tmp/com.apple.launchd.XYZ123/Listeners"
env_without_sock = {k: v for k, v in os.environ.items() if k != "SSH_AUTH_SOCK"}
with patch.dict("os.environ", env_without_sock, clear=True):
with patch("glob.glob", return_value=[fake_sock]):
with patch("web.api.subprocess.Popen", return_value=_make_popen_mock()) as mock_popen:
_launch_pipeline_subprocess("P1-001")
kwargs = mock_popen.call_args[1]
env_passed = kwargs.get("env", {})
assert env_passed.get("SSH_AUTH_SOCK") == fake_sock, (
"KIN-096: после фикса SSH_AUTH_SOCK должен детектироваться через glob "
"в _launch_pipeline_subprocess когда его нет в os.environ"
)