kin/tests/test_kin_096_regression.py
2026-03-17 16:21:33 +02:00

177 lines
9.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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"
)