agentic-dev-loop 1.0.0a49__tar.gz → 1.0.0a50__tar.gz

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.

Potentially problematic release.


This version of agentic-dev-loop might be problematic. Click here for more details.

Files changed (104) hide show
  1. {agentic_dev_loop-1.0.0a49/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a50}/PKG-INFO +5 -2
  2. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/README.md +4 -1
  3. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/pyproject.toml +1 -1
  4. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/isolation.py +35 -16
  5. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50/src/agentic_dev_loop.egg-info}/PKG-INFO +5 -2
  6. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_containers.py +5 -1
  7. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_isolation.py +69 -7
  8. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/LICENSE +0 -0
  9. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/setup.cfg +0 -0
  10. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/__init__.py +0 -0
  11. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/__init__.py +0 -0
  12. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/config.py +0 -0
  13. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/paths.py +0 -0
  14. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/results.py +0 -0
  15. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/core/verdict.py +0 -0
  16. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/__init__.py +0 -0
  17. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/actions.py +0 -0
  18. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  19. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  20. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/agents.py +0 -0
  21. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  22. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  23. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
  24. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
  25. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/cli.py +0 -0
  26. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/containers.py +0 -0
  27. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/deps.py +0 -0
  28. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  29. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  30. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  31. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  32. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  33. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  34. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/graph.py +0 -0
  35. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  36. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/llm.py +0 -0
  37. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/loaders.py +0 -0
  38. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/mounts.py +0 -0
  39. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/outputs.py +0 -0
  40. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/presets.py +0 -0
  41. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/proc.py +0 -0
  42. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/registry.py +0 -0
  43. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  44. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  45. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/secrets.py +0 -0
  46. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/stages.py +0 -0
  47. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  48. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  49. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/py.typed +0 -0
  50. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/viz/__init__.py +0 -0
  51. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/viz/__main__.py +0 -0
  52. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop/viz/server.py +0 -0
  53. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
  54. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  55. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  56. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  57. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  58. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_028_declared_caches.py +0 -0
  59. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_029_merge_on_conflict.py +0 -0
  60. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_033_brief_step.py +0 -0
  61. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_035_finalize_nudge.py +0 -0
  62. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  63. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_041_brief_coverage.py +0 -0
  64. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_046_brief_prompt.py +0 -0
  65. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_actions.py +0 -0
  66. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_agent_error_guide.py +0 -0
  67. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_agents.py +0 -0
  68. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_aidlc.py +0 -0
  69. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_cli.py +0 -0
  70. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_config.py +0 -0
  71. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_deps.py +0 -0
  72. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_dry_run.py +0 -0
  73. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_e2e_fixtures.py +0 -0
  74. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_egress_proxy.py +0 -0
  75. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_graph.py +0 -0
  76. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_hostboundary.py +0 -0
  77. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_hostboundary_integration.py +0 -0
  78. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_isolation_integration.py +0 -0
  79. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_isolation_pbt.py +0 -0
  80. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_llm.py +0 -0
  81. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_loaders.py +0 -0
  82. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_module_contract.py +0 -0
  83. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_outputs.py +0 -0
  84. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_outputs_pbt.py +0 -0
  85. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_proc.py +0 -0
  86. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_publish.py +0 -0
  87. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_qa_prompt.py +0 -0
  88. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_registry.py +0 -0
  89. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_repo_ascii.py +0 -0
  90. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_results.py +0 -0
  91. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_sdk_compat.py +0 -0
  92. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_secrets.py +0 -0
  93. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_secrets_integration.py +0 -0
  94. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_secrets_pipeline.py +0 -0
  95. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_source_resolution.py +0 -0
  96. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_stages.py +0 -0
  97. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_step_workspace.py +0 -0
  98. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_sync_back.py +0 -0
  99. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_trigger_script.py +0 -0
  100. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_unit5_config.py +0 -0
  101. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_unit5_pbt.py +0 -0
  102. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_unit5_pure.py +0 -0
  103. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_verdict.py +0 -0
  104. {agentic_dev_loop-1.0.0a49 → agentic_dev_loop-1.0.0a50}/tests/test_viz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a49
3
+ Version: 1.0.0a50
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -169,7 +169,10 @@ engine-owned object to its owning run and a `live`, `orphaned`,
169
169
  registry-owned orphans whose owner pid is dead (never unregistered names).
170
170
  A SIGTERM to a running pipeline still closes the sandbox (the graph's
171
171
  `finally` runs); SIGKILL is unhandleable, so its containers are reclaimed by
172
- the startup reaper on the next run or by `containers --reap`. Pass
172
+ the startup reaper on the next run or by `containers --reap`. The reaper
173
+ and `--reap` prune a registry entry only after the daemon confirms the
174
+ object is gone; a surviving object (a failed removal) keeps its entry so
175
+ the next startup retries it. Pass
173
176
  `--state-dir` to point at a custom `isolation.state_dir`.
174
177
 
175
178
  **What a run looks like** (the shipped self-loop example):
@@ -125,7 +125,10 @@ engine-owned object to its owning run and a `live`, `orphaned`,
125
125
  registry-owned orphans whose owner pid is dead (never unregistered names).
126
126
  A SIGTERM to a running pipeline still closes the sandbox (the graph's
127
127
  `finally` runs); SIGKILL is unhandleable, so its containers are reclaimed by
128
- the startup reaper on the next run or by `containers --reap`. Pass
128
+ the startup reaper on the next run or by `containers --reap`. The reaper
129
+ and `--reap` prune a registry entry only after the daemon confirms the
130
+ object is gone; a surviving object (a failed removal) keeps its entry so
131
+ the next startup retries it. Pass
129
132
  `--state-dir` to point at a custom `isolation.state_dir`.
130
133
 
131
134
  **What a run looks like** (the shipped self-loop example):
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "1.0.0a49"
7
+ version = "1.0.0a50"
8
8
  description = "Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1632,23 +1632,12 @@ class DockerBackend(Sandbox):
1632
1632
  _write_registry(self._iso_state_dir, remaining)
1633
1633
 
1634
1634
  def _container_gone(self, name: str) -> bool:
1635
- """True only when the daemon confirms the container no longer exists.
1636
- A nonzero daemon exit keeps the entry."""
1637
- res = self._runner.run(
1638
- ["docker", "ps", "-a", "--filter", f"name={name}",
1639
- "--format", "{{.Names}}"], timeout=30)
1640
- if res.exit_code != 0:
1641
- return False
1642
- return name not in res.stdout.splitlines()
1635
+ """Thin delegation to the module-level confirm helper (050 D2)."""
1636
+ return _container_gone(self._runner, name)
1643
1637
 
1644
1638
  def _network_gone(self, name: str) -> bool:
1645
- """True only when the daemon reports the network is missing. Any
1646
- other inspect failure (daemon unreachable) keeps the entry."""
1647
- res = self._runner.run(
1648
- ["docker", "network", "inspect", name], timeout=30)
1649
- if res.exit_code == 0:
1650
- return False
1651
- return "No such network" in f"{res.stderr} {res.stdout}"
1639
+ """Thin delegation to the module-level confirm helper (050 D2)."""
1640
+ return _network_gone(self._runner, name)
1652
1641
 
1653
1642
  # -- internal -----------------------------------------------------------
1654
1643
 
@@ -1882,6 +1871,28 @@ def _pid_alive_matches(pid: int, recorded_start: str | None) -> bool:
1882
1871
  return current == recorded_start
1883
1872
 
1884
1873
 
1874
+ def _container_gone(runner: CommandRunner, name: str) -> bool:
1875
+ """True only when the daemon confirms the container no longer exists.
1876
+ A nonzero daemon exit keeps the entry."""
1877
+ res = runner.run(
1878
+ ["docker", "ps", "-a", "--filter", f"name={name}",
1879
+ "--format", "{{.Names}}"], timeout=30)
1880
+ if res.exit_code != 0:
1881
+ return False
1882
+ return name not in res.stdout.splitlines()
1883
+
1884
+
1885
+ def _network_gone(runner: CommandRunner, name: str) -> bool:
1886
+ """True only when the daemon reports the network is missing. Any
1887
+ other inspect failure (daemon unreachable) keeps the entry."""
1888
+ res = runner.run(
1889
+ ["docker", "network", "inspect", name], timeout=30)
1890
+ if res.exit_code == 0:
1891
+ return False
1892
+ return "No such network" in f"{res.stderr} {res.stdout}"
1893
+
1894
+
1895
+
1885
1896
  def reap_orphaned_containers(state_dir: str | Path,
1886
1897
  runner: CommandRunner | None = None,
1887
1898
  *, announce: bool = True) -> list[str]:
@@ -1889,7 +1900,9 @@ def reap_orphaned_containers(state_dir: str | Path,
1889
1900
  and the per-run internal network whose owning CLI process is gone (crashed
1890
1901
  before close()). Exact by construction - only engine registry names are
1891
1902
  touched, and a live owner (pid alive with the same start time) is left
1892
- alone. Best-effort: never raises."""
1903
+ alone. A surviving object (rm failure) keeps its entry so the next
1904
+ startup reaper retries; only a daemon-confirmed absence prunes it (050
1905
+ D1). Best-effort: never raises."""
1893
1906
  runner = runner or _default_runner
1894
1907
  entries = _read_registry(Path(state_dir))
1895
1908
  if not entries:
@@ -1909,6 +1922,9 @@ def reap_orphaned_containers(state_dir: str | Path,
1909
1922
  remaining.append(entry)
1910
1923
  continue
1911
1924
  runner.run(["docker", "network", "rm", name], timeout=60)
1925
+ if not _network_gone(runner, name):
1926
+ remaining.append(entry)
1927
+ continue
1912
1928
  if announce:
1913
1929
  _announce(f"reaped orphaned sandbox network {name}")
1914
1930
  reaped.append(name)
@@ -1924,6 +1940,9 @@ def reap_orphaned_containers(state_dir: str | Path,
1924
1940
  remaining.append(entry)
1925
1941
  continue
1926
1942
  runner.run(["docker", "rm", "-f", name], timeout=60)
1943
+ if not _container_gone(runner, name):
1944
+ remaining.append(entry)
1945
+ continue
1927
1946
  if announce:
1928
1947
  _announce(f"reaped orphaned sandbox container {name}")
1929
1948
  reaped.append(name)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 1.0.0a49
3
+ Version: 1.0.0a50
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -169,7 +169,10 @@ engine-owned object to its owning run and a `live`, `orphaned`,
169
169
  registry-owned orphans whose owner pid is dead (never unregistered names).
170
170
  A SIGTERM to a running pipeline still closes the sandbox (the graph's
171
171
  `finally` runs); SIGKILL is unhandleable, so its containers are reclaimed by
172
- the startup reaper on the next run or by `containers --reap`. Pass
172
+ the startup reaper on the next run or by `containers --reap`. The reaper
173
+ and `--reap` prune a registry entry only after the daemon confirms the
174
+ object is gone; a surviving object (a failed removal) keeps its entry so
175
+ the next startup retries it. Pass
173
176
  `--state-dir` to point at a custom `isolation.state_dir`.
174
177
 
175
178
  **What a run looks like** (the shipped self-loop example):
@@ -123,7 +123,11 @@ class TestReapReturnValue:
123
123
  _write(tmp_path / "state", [_entry(name, proc.pid, None)])
124
124
  runner = FakeRunner(ok(""))
125
125
  assert reap_orphaned_containers(tmp_path / "state", runner) == [name]
126
- assert runner.calls == [["docker", "rm", "-f", name]]
126
+ assert runner.calls == [
127
+ ["docker", "rm", "-f", name],
128
+ ["docker", "ps", "-a", "--filter", f"name={name}",
129
+ "--format", "{{.Names}}"],
130
+ ]
127
131
 
128
132
  def test_reap_live_owner_returns_empty(self, tmp_path):
129
133
  name = f"{'b' * 32}-exec"
@@ -2251,7 +2251,11 @@ class TestReaper:
2251
2251
  runner = FakeRunner()
2252
2252
  reaped = reap_orphaned_containers(tmp_path / "state", runner)
2253
2253
  assert reaped == [name]
2254
- assert runner.calls == [["docker", "rm", "-f", name]]
2254
+ assert runner.calls == [
2255
+ ["docker", "rm", "-f", name],
2256
+ ["docker", "ps", "-a", "--filter", f"name={name}",
2257
+ "--format", "{{.Names}}"],
2258
+ ]
2255
2259
  assert self._read(tmp_path / "state") == []
2256
2260
 
2257
2261
  def test_live_pid_left_alone(self, tmp_path):
@@ -2282,9 +2286,13 @@ class TestReaper:
2282
2286
  "run_id": "r", "name": name, "kind": "network",
2283
2287
  "pid": proc.pid, "process_start": None, "created_at": "now",
2284
2288
  }])
2285
- runner = FakeRunner()
2286
- reap_orphaned_containers(tmp_path / "state", runner)
2287
- assert runner.calls == [["docker", "network", "rm", name]]
2289
+ runner = FakeRunner(ok(""), fail(1, "No such network"))
2290
+ reaped = reap_orphaned_containers(tmp_path / "state", runner)
2291
+ assert reaped == [name]
2292
+ assert runner.calls == [
2293
+ ["docker", "network", "rm", name],
2294
+ ["docker", "network", "inspect", name],
2295
+ ]
2288
2296
  assert self._read(tmp_path / "state") == []
2289
2297
 
2290
2298
  def test_malformed_name_skipped(self, tmp_path):
@@ -2300,11 +2308,65 @@ class TestReaper:
2300
2308
  proc.wait()
2301
2309
  name = f"{'e' * 32}-exec"
2302
2310
  self._write(tmp_path / "state", [self._entry(name, proc.pid)])
2303
- runner = FakeRunner(fail(1, "No such container"))
2304
- reap_orphaned_containers(tmp_path / "state", runner)
2305
- assert runner.calls == [["docker", "rm", "-f", name]]
2311
+ runner = FakeRunner(fail(1, "No such container"), ok(""))
2312
+ reaped = reap_orphaned_containers(tmp_path / "state", runner)
2313
+ assert reaped == [name]
2314
+ assert runner.calls == [
2315
+ ["docker", "rm", "-f", name],
2316
+ ["docker", "ps", "-a", "--filter", f"name={name}",
2317
+ "--format", "{{.Names}}"],
2318
+ ]
2306
2319
  assert self._read(tmp_path / "state") == []
2307
2320
 
2321
+ def test_rm_failure_container_survives_retained(self, tmp_path):
2322
+ proc = subprocess.Popen([sys.executable, "-c", "pass"])
2323
+ proc.wait()
2324
+ name = f"{'a' * 32}-exec"
2325
+ entry = self._entry(name, proc.pid)
2326
+ self._write(tmp_path / "state", [entry])
2327
+ runner = FakeRunner(fail(1, "daemon busy"), ok(name + "\n"))
2328
+ reaped = reap_orphaned_containers(tmp_path / "state", runner)
2329
+ assert reaped == []
2330
+ assert self._read(tmp_path / "state") == [entry]
2331
+ assert runner.calls == [
2332
+ ["docker", "rm", "-f", name],
2333
+ ["docker", "ps", "-a", "--filter", f"name={name}",
2334
+ "--format", "{{.Names}}"],
2335
+ ]
2336
+
2337
+ def test_rm_failure_network_survives_retained(self, tmp_path):
2338
+ proc = subprocess.Popen([sys.executable, "-c", "pass"])
2339
+ proc.wait()
2340
+ name = f"{'a' * 32}-net"
2341
+ entry = {"run_id": "r", "name": name, "kind": "network",
2342
+ "pid": proc.pid, "process_start": None, "created_at": "now"}
2343
+ self._write(tmp_path / "state", [entry])
2344
+ runner = FakeRunner(fail(1, "daemon busy"), ok(name))
2345
+ reaped = reap_orphaned_containers(tmp_path / "state", runner)
2346
+ assert reaped == []
2347
+ assert self._read(tmp_path / "state") == [entry]
2348
+ assert runner.calls == [
2349
+ ["docker", "network", "rm", name],
2350
+ ["docker", "network", "inspect", name],
2351
+ ]
2352
+
2353
+ def test_missing_network_tolerated(self, tmp_path):
2354
+ proc = subprocess.Popen([sys.executable, "-c", "pass"])
2355
+ proc.wait()
2356
+ name = f"{'a' * 32}-net"
2357
+ entry = {"run_id": "r", "name": name, "kind": "network",
2358
+ "pid": proc.pid, "process_start": None, "created_at": "now"}
2359
+ self._write(tmp_path / "state", [entry])
2360
+ runner = FakeRunner(fail(1, "No such network"),
2361
+ fail(1, "No such network"))
2362
+ reaped = reap_orphaned_containers(tmp_path / "state", runner)
2363
+ assert reaped == [name]
2364
+ assert self._read(tmp_path / "state") == []
2365
+ assert runner.calls == [
2366
+ ["docker", "network", "rm", name],
2367
+ ["docker", "network", "inspect", name],
2368
+ ]
2369
+
2308
2370
  def test_no_registry_is_a_noop(self, tmp_path):
2309
2371
  runner = FakeRunner()
2310
2372
  reap_orphaned_containers(tmp_path / "state", runner)