claude-dev-env 8.51.4 → 8.52.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,8 @@ import json
5
5
  import os
6
6
  import shutil
7
7
  import subprocess
8
+ import sys
9
+ import time
8
10
  from datetime import datetime, timezone
9
11
  from pathlib import Path
10
12
  from types import SimpleNamespace
@@ -222,16 +224,85 @@ def test_should_raise_windows_lock_error_other_than_contention(tmp_path: Path) -
222
224
  assert all_calls == [lock_descriptor]
223
225
 
224
226
 
225
- def test_should_launch_the_path_resolved_command_file(monkeypatch: pytest.MonkeyPatch) -> None:
226
- resolved_command = r"C:\Users\someone\AppData\Roaming\npm\claude.cmd"
227
- monkeypatch.setattr(shutil, "which", lambda name: resolved_command if name == "claude" else None)
227
+ GRANDCHILD_HEARTBEAT_SCRIPT = (
228
+ "import pathlib, sys, time\n"
229
+ "heartbeat = pathlib.Path(sys.argv[1])\n"
230
+ "for each_beat in range(300):\n"
231
+ " heartbeat.write_text(str(each_beat), encoding='utf-8')\n"
232
+ " time.sleep(0.1)\n"
233
+ )
234
+
235
+ CHILD_SCRIPT = (
236
+ "import subprocess, sys, time\n"
237
+ "subprocess.Popen([sys.executable, '-c', sys.argv[1], sys.argv[2]])\n"
238
+ "time.sleep(60)\n"
239
+ )
240
+
241
+
242
+ def test_should_end_grandchildren_when_the_job_times_out(tmp_path: Path) -> None:
243
+ heartbeat_file = tmp_path / "heartbeat.txt"
244
+
245
+ with pytest.raises(subprocess.TimeoutExpired):
246
+ support._run_captured_subprocess(
247
+ [sys.executable, "-c", CHILD_SCRIPT, GRANDCHILD_HEARTBEAT_SCRIPT, str(heartbeat_file)],
248
+ timeout=5,
249
+ )
250
+
251
+ beat_after_timeout = heartbeat_file.read_text(encoding="utf-8")
252
+ time.sleep(0.5)
253
+ assert heartbeat_file.read_text(encoding="utf-8") == beat_after_timeout
254
+
255
+
256
+ def test_should_end_grandchildren_when_the_broker_is_interrupted(
257
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
258
+ ) -> None:
259
+ heartbeat_file = tmp_path / "heartbeat.txt"
260
+ original_communicate = subprocess.Popen.communicate
261
+
262
+ def interrupted_communicate(process: subprocess.Popen[bytes], *args: object, **kwargs: object) -> object:
263
+ try:
264
+ return original_communicate(process, *args, timeout=5)
265
+ except subprocess.TimeoutExpired:
266
+ raise KeyboardInterrupt from None
267
+
268
+ monkeypatch.setattr(subprocess.Popen, "communicate", interrupted_communicate)
269
+
270
+ with pytest.raises(KeyboardInterrupt):
271
+ support._run_captured_subprocess(
272
+ [sys.executable, "-c", CHILD_SCRIPT, GRANDCHILD_HEARTBEAT_SCRIPT, str(heartbeat_file)],
273
+ )
274
+
275
+ beat_after_interrupt = heartbeat_file.read_text(encoding="utf-8")
276
+ time.sleep(0.5)
277
+ assert heartbeat_file.read_text(encoding="utf-8") == beat_after_interrupt
278
+
279
+
280
+ def _record_launches(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]:
228
281
  all_launched_argv: list[list[str]] = []
229
282
 
230
- def fake_run(all_argv: list[str], **options: object) -> subprocess.CompletedProcess[bytes]:
231
- all_launched_argv.append(list(all_argv))
232
- return subprocess.CompletedProcess(all_argv, 0)
283
+ class FakeProcess:
284
+ returncode = 0
285
+
286
+ def __init__(self, all_argv: list[str], **options: object) -> None:
287
+ all_launched_argv.append(list(all_argv))
288
+
289
+ def __enter__(self) -> "FakeProcess":
290
+ return self
291
+
292
+ def __exit__(self, *exception_info: object) -> None:
293
+ return None
233
294
 
234
- monkeypatch.setattr(support.subprocess, "run", fake_run)
295
+ def communicate(self, **options: object) -> tuple[None, None]:
296
+ return None, None
297
+
298
+ monkeypatch.setattr(support.subprocess, "Popen", FakeProcess)
299
+ return all_launched_argv
300
+
301
+
302
+ def test_should_launch_the_path_resolved_command_file(monkeypatch: pytest.MonkeyPatch) -> None:
303
+ resolved_command = r"C:\Users\someone\AppData\Roaming\npm\claude.cmd"
304
+ monkeypatch.setattr(shutil, "which", lambda name: resolved_command if name == "claude" else None)
305
+ all_launched_argv = _record_launches(monkeypatch)
235
306
 
236
307
  completion = support.subprocess_runner(["claude", "-p"], input=b"", encoding="utf-8", errors="replace")
237
308
 
@@ -241,7 +312,7 @@ def test_should_launch_the_path_resolved_command_file(monkeypatch: pytest.Monkey
241
312
 
242
313
  def test_should_refuse_to_launch_a_bare_command_missing_from_path(monkeypatch: pytest.MonkeyPatch) -> None:
243
314
  monkeypatch.setattr(shutil, "which", lambda name: None)
244
- monkeypatch.setattr(support.subprocess, "run", lambda *arguments, **options: pytest.fail("a missing command ran"))
315
+ monkeypatch.setattr(support.subprocess, "Popen", lambda *arguments, **options: pytest.fail("a missing command ran"))
245
316
 
246
317
  with pytest.raises(FileNotFoundError) as raised:
247
318
  support.subprocess_runner(["claude", "-p"])
@@ -256,7 +327,7 @@ def test_should_refuse_a_batch_file_argument_that_cmd_would_parse(
256
327
  ) -> None:
257
328
  resolved_command = r"C:\Users\someone\AppData\Roaming\npm\claude.cmd"
258
329
  monkeypatch.setattr(shutil, "which", lambda name: resolved_command)
259
- monkeypatch.setattr(support.subprocess, "run", lambda *arguments, **options: pytest.fail("a batch file ran"))
330
+ monkeypatch.setattr(support.subprocess, "Popen", lambda *arguments, **options: pytest.fail("a batch file ran"))
260
331
 
261
332
  with pytest.raises(OSError) as raised:
262
333
  support.subprocess_runner(["claude", "-p", f"fix{metacharacter}test"])
@@ -269,7 +340,7 @@ def test_should_refuse_a_batch_file_argument_that_cmd_would_parse(
269
340
  def test_should_refuse_metacharacters_for_an_uppercase_bat_extension(monkeypatch: pytest.MonkeyPatch) -> None:
270
341
  resolved_command = r"C:\tools\CLAUDE.BAT"
271
342
  monkeypatch.setattr(shutil, "which", lambda name: resolved_command)
272
- monkeypatch.setattr(support.subprocess, "run", lambda *arguments, **options: pytest.fail("a batch file ran"))
343
+ monkeypatch.setattr(support.subprocess, "Popen", lambda *arguments, **options: pytest.fail("a batch file ran"))
273
344
 
274
345
  with pytest.raises(OSError) as raised:
275
346
  support.subprocess_runner(["claude", "a & b"])
@@ -280,13 +351,7 @@ def test_should_refuse_metacharacters_for_an_uppercase_bat_extension(monkeypatch
280
351
  def test_should_launch_a_batch_file_with_flags_only(monkeypatch: pytest.MonkeyPatch) -> None:
281
352
  resolved_command = r"C:\Users\someone\AppData\Roaming\npm\claude.cmd"
282
353
  monkeypatch.setattr(shutil, "which", lambda name: resolved_command)
283
- all_launched_argv: list[list[str]] = []
284
-
285
- def fake_run(all_argv: list[str], **options: object) -> subprocess.CompletedProcess[bytes]:
286
- all_launched_argv.append(list(all_argv))
287
- return subprocess.CompletedProcess(all_argv, 0)
288
-
289
- monkeypatch.setattr(support.subprocess, "run", fake_run)
354
+ all_launched_argv = _record_launches(monkeypatch)
290
355
 
291
356
  support.subprocess_runner(["claude", "-p", "--output-format", "json", "--model", "opus"], input=b"a & b")
292
357
 
@@ -296,13 +361,7 @@ def test_should_launch_a_batch_file_with_flags_only(monkeypatch: pytest.MonkeyPa
296
361
  def test_should_pass_metacharacters_to_an_executable_unchanged(monkeypatch: pytest.MonkeyPatch) -> None:
297
362
  resolved_command = "/usr/local/bin/claude"
298
363
  monkeypatch.setattr(shutil, "which", lambda name: resolved_command)
299
- all_launched_argv: list[list[str]] = []
300
-
301
- def fake_run(all_argv: list[str], **options: object) -> subprocess.CompletedProcess[bytes]:
302
- all_launched_argv.append(list(all_argv))
303
- return subprocess.CompletedProcess(all_argv, 0)
304
-
305
- monkeypatch.setattr(support.subprocess, "run", fake_run)
364
+ all_launched_argv = _record_launches(monkeypatch)
306
365
 
307
366
  support.subprocess_runner(["claude", "-p", 'say "a & b" | 100%!'])
308
367