diff --git a/decnet/cli.py b/decnet/cli.py index 7226538..047ba9c 100644 --- a/decnet/cli.py +++ b/decnet/cli.py @@ -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.[/]") diff --git a/tests/test_cli.py b/tests/test_cli.py index 36ca5f4..23df3aa 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -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