kin: auto-commit after pipeline
This commit is contained in:
parent
e0511eac0c
commit
143a393ba7
3 changed files with 616 additions and 0 deletions
177
tests/test_kin_096_regression.py
Normal file
177
tests/test_kin_096_regression.py
Normal 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"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue