From 357fac37d8ed39d62c6f75793821558848b67d36 Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 19 Jul 2026 02:07:08 +0000 Subject: [PATCH] test: replace /bin/sleep with portable Python sleep in test_gateway_init /bin/sleep does not exist at FHS paths on NixOS (the KVM self-hosted runner's OS). All 13 FileNotFoundError failures in TestSupervisor were caused by _DaemonSpec tuples spawning /bin/sleep directly. Replace with a module-level _SLEEP_30/_SLEEP_60 tuple using sys.executable so the supervisor tests run on any platform. Also fix TestMainEndToEnd._run() and remove the now-unnecessary setUpClass guard. --- tests/unit/test_gateway_init.py | 59 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/tests/unit/test_gateway_init.py b/tests/unit/test_gateway_init.py index 98607b6..284e000 100644 --- a/tests/unit/test_gateway_init.py +++ b/tests/unit/test_gateway_init.py @@ -2,9 +2,8 @@ Tests both the helper functions in `bot_bottle.gateway_init` and the supervisor's end-to-end signal / exit-code behavior. The -end-to-end tests use real subprocesses (`/bin/sleep`, -`/bin/sh -c '...'`) — short-lived, no docker required — so they -run under `tests/unit/` rather than `tests/integration/`.""" +end-to-end tests use real subprocesses — short-lived, no docker required +— so they run under `tests/unit/` rather than `tests/integration/`.""" from __future__ import annotations @@ -15,7 +14,6 @@ import sys import time import unittest import warnings -from pathlib import Path from unittest.mock import patch from bot_bottle.gateway_init import ( @@ -26,6 +24,11 @@ from bot_bottle.gateway_init import ( _selected_daemons, ) +# /bin/sleep does not exist on FHS-free systems (e.g. NixOS). Use a +# portable Python one-liner so supervisor tests run on any platform. +_SLEEP_30 = (sys.executable, "-c", "import time; time.sleep(30)") +_SLEEP_60 = (sys.executable, "-c", "import time; time.sleep(60)") + class TestEnvForDaemon(unittest.TestCase): """Scope egress-only credential env vars to the egress daemon. @@ -182,7 +185,7 @@ class TestSupervisor(unittest.TestCase): # up and the supervisor never set shutdown_at. specs = [ _DaemonSpec("crasher", ("/bin/sh", "-c", "exit 1")), - _DaemonSpec("longrun", ("/bin/sleep", "30")), + _DaemonSpec("longrun", _SLEEP_30), ] sup = _Supervisor(specs) sup.start_all() @@ -214,7 +217,7 @@ class TestSupervisor(unittest.TestCase): # signal-killed longrun's negative returncode. specs = [ _DaemonSpec("crasher", ("/bin/sh", "-c", "exit 1")), - _DaemonSpec("longrun", ("/bin/sleep", "30")), + _DaemonSpec("longrun", _SLEEP_30), ] sup = _Supervisor(specs) sup.start_all() @@ -259,7 +262,7 @@ class TestSupervisor(unittest.TestCase): ) specs = [ _DaemonSpec("egress", sighup_marker), - _DaemonSpec("other", ("/bin/sleep", "30")), + _DaemonSpec("other", _SLEEP_30), ] sup = _Supervisor(specs) sup.start_all() @@ -282,7 +285,7 @@ class TestSupervisor(unittest.TestCase): self._drive(sup) def test_forward_signal_unknown_daemon_no_op(self): - specs = [_DaemonSpec("a", ("/bin/sleep", "30"))] + specs = [_DaemonSpec("a", _SLEEP_30)] sup = _Supervisor(specs) sup.start_all() delivered = sup.forward_signal(signal.SIGHUP, "ghost") @@ -294,8 +297,8 @@ class TestSupervisor(unittest.TestCase): # Restart one daemon; the other (supervise, the MCP server # in production) must remain untouched. specs = [ - _DaemonSpec("git-gate", ("/bin/sleep", "30")), - _DaemonSpec("supervise", ("/bin/sleep", "30")), + _DaemonSpec("git-gate", _SLEEP_30), + _DaemonSpec("supervise", _SLEEP_30), ] sup = _Supervisor(specs) sup.start_all() @@ -319,8 +322,8 @@ class TestSupervisor(unittest.TestCase): def test_request_restart_is_drained_by_tick(self): specs = [ - _DaemonSpec("git-gate", ("/bin/sleep", "30")), - _DaemonSpec("supervise", ("/bin/sleep", "30")), + _DaemonSpec("git-gate", _SLEEP_30), + _DaemonSpec("supervise", _SLEEP_30), ] sup = _Supervisor(specs) sup.start_all() @@ -343,7 +346,7 @@ class TestSupervisor(unittest.TestCase): self._drive(sup) def test_repeated_restart_requests_coalesce(self): - specs = [_DaemonSpec("git-gate", ("/bin/sleep", "30"))] + specs = [_DaemonSpec("git-gate", _SLEEP_30)] sup = _Supervisor(specs) sup.start_all() time.sleep(0.1) @@ -366,7 +369,7 @@ class TestSupervisor(unittest.TestCase): self._drive(sup) def test_request_restart_unknown_daemon_no_op(self): - specs = [_DaemonSpec("a", ("/bin/sleep", "30"))] + specs = [_DaemonSpec("a", _SLEEP_30)] sup = _Supervisor(specs) sup.start_all() ok = sup.request_restart("ghost") @@ -376,7 +379,7 @@ class TestSupervisor(unittest.TestCase): self._drive(sup) def test_restart_unknown_daemon_no_op(self): - specs = [_DaemonSpec("a", ("/bin/sleep", "30"))] + specs = [_DaemonSpec("a", _SLEEP_30)] sup = _Supervisor(specs) sup.start_all() ok = sup.restart_daemon("ghost") @@ -385,7 +388,7 @@ class TestSupervisor(unittest.TestCase): self._drive(sup) def test_restart_during_shutdown_is_no_op(self): - specs = [_DaemonSpec("git-gate", ("/bin/sleep", "30"))] + specs = [_DaemonSpec("git-gate", _SLEEP_30)] sup = _Supervisor(specs) sup.start_all() sup.request_shutdown(reason="test") @@ -395,7 +398,7 @@ class TestSupervisor(unittest.TestCase): self._drive(sup) def test_pending_restart_dropped_during_shutdown(self): - specs = [_DaemonSpec("git-gate", ("/bin/sleep", "30"))] + specs = [_DaemonSpec("git-gate", _SLEEP_30)] sup = _Supervisor(specs) sup.start_all() time.sleep(0.1) @@ -413,8 +416,8 @@ class TestSupervisor(unittest.TestCase): # both should receive SIGTERM and exit. Signal-only # shutdown clamps to a zero supervisor exit code. specs = [ - _DaemonSpec("a", ("/bin/sleep", "60")), - _DaemonSpec("b", ("/bin/sleep", "60")), + _DaemonSpec("a", _SLEEP_60), + _DaemonSpec("b", _SLEEP_60), ] sup = _Supervisor(specs) sup.start_all() @@ -449,7 +452,7 @@ class TestSupervisor(unittest.TestCase): self.assertEqual(0, rc) def test_idempotent_shutdown_requests(self): - specs = [_DaemonSpec("a", ("/bin/sleep", "60"))] + specs = [_DaemonSpec("a", _SLEEP_60)] sup = _Supervisor(specs) sup.start_all() time.sleep(0.1) @@ -465,28 +468,22 @@ class TestSupervisor(unittest.TestCase): class TestMainEndToEnd(unittest.TestCase): """Run gateway_init.py as a real subprocess to cover the - signal-handler installation path. Skipped on platforms - without /bin/sleep + /bin/sh.""" - - @classmethod - def setUpClass(cls): - for p in ("/bin/sh", "/bin/sleep"): - if not Path(p).exists(): - raise unittest.SkipTest(f"missing {p}") + signal-handler installation path.""" def _run(self, daemons_csv: str, send_signal: int | None, wait_before_signal: float = 0.4, overall_timeout: float = 6.0) -> tuple[int, str]: """Spawn gateway_init.main() in a child process with the - DAEMONS list patched to harmless `sleep 30` commands. + DAEMONS list patched to harmless long-sleep commands. Returns (returncode, captured stdout).""" helper = ( "import os, runpy, sys\n" "from bot_bottle import gateway_init as si\n" + "sleep_cmd = (sys.executable, '-c', 'import time; time.sleep(30)')\n" "si._DAEMONS = (\n" - " si._DaemonSpec('alpha', ('/bin/sleep','30')),\n" - " si._DaemonSpec('beta', ('/bin/sleep','30')),\n" + " si._DaemonSpec('alpha', sleep_cmd),\n" + " si._DaemonSpec('beta', sleep_cmd),\n" ")\n" "sys.exit(si.main([]))\n" )