fix(cli): keep FileNotFoundError handling on decnet api

Popen moved inside the try so a missing uvicorn falls through to the
existing error message instead of crashing the CLI. test_cli was still
patching the old subprocess.run entrypoint; switched both api command
tests to patch subprocess.Popen / os.killpg to match the current path.
This commit is contained in:
2026-04-17 19:09:15 -04:00
parent 6301504c0e
commit 3cc5ba36e8
2 changed files with 21 additions and 14 deletions

View File

@@ -106,19 +106,20 @@ def api(
# can signal the whole tree on Ctrl+C. Without this, only the supervisor
# receives SIGINT from the terminal and worker children may survive and
# be respawned — the "forkbomb" ANTI hit during testing.
proc = subprocess.Popen(_cmd, env=_env, start_new_session=True) # nosec B603 B404
try:
proc.wait()
except KeyboardInterrupt:
proc = subprocess.Popen(_cmd, env=_env, start_new_session=True) # nosec B603 B404
try:
os.killpg(proc.pid, signal.SIGTERM)
proc.wait()
except KeyboardInterrupt:
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
proc.wait()
except ProcessLookupError:
pass
os.killpg(proc.pid, signal.SIGTERM)
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
proc.wait()
except ProcessLookupError:
pass
except (FileNotFoundError, subprocess.SubprocessError):
console.print("[red]Failed to start API. Ensure 'uvicorn' is installed in the current environment.[/]")

View File

@@ -325,13 +325,19 @@ class TestCorrelateCommand:
# ── api command ───────────────────────────────────────────────────────────────
class TestApiCommand:
@patch("subprocess.run", side_effect=KeyboardInterrupt)
def test_api_keyboard_interrupt(self, mock_run):
@patch("os.killpg")
@patch("subprocess.Popen")
def test_api_keyboard_interrupt(self, mock_popen, mock_killpg):
proc = MagicMock()
proc.wait.side_effect = [KeyboardInterrupt, 0]
proc.pid = 4321
mock_popen.return_value = proc
result = runner.invoke(app, ["api"])
assert result.exit_code == 0
mock_killpg.assert_called()
@patch("subprocess.run", side_effect=FileNotFoundError)
def test_api_not_found(self, mock_run):
@patch("subprocess.Popen", side_effect=FileNotFoundError)
def test_api_not_found(self, mock_popen):
result = runner.invoke(app, ["api"])
assert result.exit_code == 0