@@ -118,6 +118,182 @@ def test_detect_concurrent_is_noop_off_windows(_winp, tmp_path):
118118 assert cli_main ._detect_concurrent_hermes_instances (tmp_path ) == []
119119
120120
121+ # ---------------------------------------------------------------------------
122+ # Parent-chain exclusion (issue #30768 follow-up — the setuptools .exe
123+ # launcher on Windows is a separate native process that spawns python.exe;
124+ # excluding only ``os.getpid()`` flags the launcher as a concurrent instance.
125+ # ---------------------------------------------------------------------------
126+
127+
128+ def _fake_psutil_with_parent_chain (
129+ parent_chain : list [int ],
130+ proc_iter_rows : list ,
131+ ):
132+ """Build a psutil stand-in that has Process()/parent() AND process_iter().
133+
134+ ``parent_chain`` is the list of PIDs returned by successive ``.parent()``
135+ calls starting from the seed (``os.getpid()``); the last entry's
136+ ``.parent()`` returns ``None`` to terminate the walk.
137+ """
138+
139+ class _FakeProc :
140+ def __init__ (self , pid : int , chain : list [int ]):
141+ self .pid = pid
142+ self ._chain = chain
143+
144+ def parent (self ):
145+ if not self ._chain :
146+ return None
147+ next_pid = self ._chain [0 ]
148+ return _FakeProc (next_pid , self ._chain [1 :])
149+
150+ class _NoSuchProcess (Exception ):
151+ pass
152+
153+ class _AccessDenied (Exception ):
154+ pass
155+
156+ def _process (pid ):
157+ return _FakeProc (pid , list (parent_chain ))
158+
159+ return types .SimpleNamespace (
160+ Process = _process ,
161+ NoSuchProcess = _NoSuchProcess ,
162+ AccessDenied = _AccessDenied ,
163+ process_iter = lambda attrs : iter (proc_iter_rows ),
164+ )
165+
166+
167+ @patch .object (cli_main , "_is_windows" , return_value = True )
168+ def test_detect_concurrent_excludes_parent_chain (_winp , tmp_path ):
169+ """The .exe launcher (parent of os.getpid()) must NOT be flagged.
170+
171+ Simulates the real Windows topology: hermes.exe launcher (PID L) spawns
172+ python.exe (PID os.getpid()). Both run from the same shim path. With the
173+ old single-PID exclusion, L would be reported as a concurrent instance.
174+ """
175+ scripts_dir = tmp_path
176+ shim = scripts_dir / "hermes.exe"
177+ shim .write_bytes (b"" )
178+ me = os .getpid ()
179+ launcher_pid = me + 100 # the .exe launcher — our parent
180+
181+ rows = [
182+ _make_proc (me , str (shim ), "python.exe" ),
183+ _make_proc (launcher_pid , str (shim ), "hermes.exe" ),
184+ ]
185+ fake_psutil = _fake_psutil_with_parent_chain (
186+ parent_chain = [launcher_pid ],
187+ proc_iter_rows = rows ,
188+ )
189+ with patch .dict (sys .modules , {"psutil" : fake_psutil }):
190+ result = cli_main ._detect_concurrent_hermes_instances (scripts_dir )
191+
192+ # Both self AND the launcher are excluded; no false positive.
193+ assert result == []
194+
195+
196+ @patch .object (cli_main , "_is_windows" , return_value = True )
197+ def test_detect_concurrent_still_finds_unrelated_other_hermes (_winp , tmp_path ):
198+ """A sibling hermes.exe outside our ancestor chain must still be reported."""
199+ scripts_dir = tmp_path
200+ shim = scripts_dir / "hermes.exe"
201+ shim .write_bytes (b"" )
202+ me = os .getpid ()
203+ launcher_pid = me + 100 # our .exe launcher (parent — must be excluded)
204+ sibling_pid = me + 200 # an UNRELATED hermes.exe (must still be reported)
205+
206+ rows = [
207+ _make_proc (me , str (shim ), "python.exe" ),
208+ _make_proc (launcher_pid , str (shim ), "hermes.exe" ),
209+ _make_proc (sibling_pid , str (shim ), "hermes.exe" ),
210+ ]
211+ fake_psutil = _fake_psutil_with_parent_chain (
212+ parent_chain = [launcher_pid ],
213+ proc_iter_rows = rows ,
214+ )
215+ with patch .dict (sys .modules , {"psutil" : fake_psutil }):
216+ result = cli_main ._detect_concurrent_hermes_instances (scripts_dir )
217+
218+ assert result == [(sibling_pid , "hermes.exe" )]
219+
220+
221+ @patch .object (cli_main , "_is_windows" , return_value = True )
222+ def test_detect_concurrent_parent_chain_walks_deep (_winp , tmp_path ):
223+ """Multi-level ancestry (shell → launcher → python) is fully excluded."""
224+ scripts_dir = tmp_path
225+ shim = scripts_dir / "hermes.exe"
226+ shim .write_bytes (b"" )
227+ me = os .getpid ()
228+ parent_pid = me + 1
229+ grandparent_pid = me + 2
230+ greatgrandparent_pid = me + 3
231+
232+ rows = [
233+ _make_proc (me , str (shim ), "python.exe" ),
234+ _make_proc (parent_pid , str (shim ), "hermes.exe" ),
235+ _make_proc (grandparent_pid , str (shim ), "hermes.exe" ),
236+ _make_proc (greatgrandparent_pid , str (shim ), "hermes.exe" ),
237+ ]
238+ fake_psutil = _fake_psutil_with_parent_chain (
239+ parent_chain = [parent_pid , grandparent_pid , greatgrandparent_pid ],
240+ proc_iter_rows = rows ,
241+ )
242+ with patch .dict (sys .modules , {"psutil" : fake_psutil }):
243+ result = cli_main ._detect_concurrent_hermes_instances (scripts_dir )
244+
245+ assert result == []
246+
247+
248+ @patch .object (cli_main , "_is_windows" , return_value = True )
249+ def test_detect_concurrent_parent_walk_handles_cycle (_winp , tmp_path ):
250+ """A PID cycle in the parent chain must not hang the walk."""
251+ scripts_dir = tmp_path
252+ shim = scripts_dir / "hermes.exe"
253+ shim .write_bytes (b"" )
254+ me = os .getpid ()
255+ bogus_loop_pid = me + 1
256+
257+ rows = [_make_proc (me , str (shim ), "python.exe" )]
258+ # Chain that points back to ``me`` — the loop-detection branch must break.
259+ fake_psutil = _fake_psutil_with_parent_chain (
260+ parent_chain = [bogus_loop_pid , me , bogus_loop_pid ],
261+ proc_iter_rows = rows ,
262+ )
263+ with patch .dict (sys .modules , {"psutil" : fake_psutil }):
264+ result = cli_main ._detect_concurrent_hermes_instances (scripts_dir )
265+
266+ # No crash, no hang; self + bogus_loop_pid excluded; no others reported.
267+ assert result == []
268+
269+
270+ @patch .object (cli_main , "_is_windows" , return_value = True )
271+ def test_detect_concurrent_parent_walk_handles_stub_without_process (_winp , tmp_path ):
272+ """Partially-stubbed psutil (no Process attr) must NOT crash the helper.
273+
274+ The function documents itself as "never raises"; a unit-test stub that
275+ only models ``process_iter`` must still complete cleanly with a sensible
276+ result rather than escape ``AttributeError`` to the caller.
277+ """
278+ scripts_dir = tmp_path
279+ shim = scripts_dir / "hermes.exe"
280+ shim .write_bytes (b"" )
281+ me = os .getpid ()
282+ other_pid = me + 1
283+
284+ rows = [
285+ _make_proc (me , str (shim ), "hermes.exe" ),
286+ _make_proc (other_pid , str (shim ), "hermes.exe" ),
287+ ]
288+ # SimpleNamespace with ONLY process_iter — no Process / NoSuchProcess.
289+ fake_psutil = types .SimpleNamespace (process_iter = lambda attrs : iter (rows ))
290+ with patch .dict (sys .modules , {"psutil" : fake_psutil }):
291+ result = cli_main ._detect_concurrent_hermes_instances (scripts_dir )
292+
293+ # Parent-walk silently failed; self still excluded; other still reported.
294+ assert result == [(other_pid , "hermes.exe" )]
295+
296+
121297# ---------------------------------------------------------------------------
122298# _format_concurrent_instances_message
123299# ---------------------------------------------------------------------------
0 commit comments