test(issue31): CLAUDE_CONFIG_DIR and partial-failure purges
Config-only and config+project overrides: a live session is refused and a
stopped one cleaned up. A key that cannot be removed keeps its session file;
a job dir that cannot be emptied keeps state.json; both are reported and
removed on retry. 5 of these fail on 8edb8d6.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
57830cf00a
commit
872d5f12fb
1 file changed
+88
-4
@@ -103,9 +103,10 @@ def _session_file(cfg: Path, pid: int, sid: str, started_ms=None, key=True) -> l
|
||||
return made
|
||||
|
||||
|
||||
def _tree(h: Path, ws: Path, sid: str, tmp_root: Path, pid=None) -> dict:
|
||||
"""Everything a claude session `sid` owns, as claude 2.1 lays it out."""
|
||||
cfg = h / ".claude"
|
||||
def _tree(h: Path, ws: Path, sid: str, tmp_root: Path, pid=None, cfg=None) -> dict:
|
||||
"""Everything a claude session `sid` owns, as claude 2.1 lays it out
|
||||
(`cfg`: a CLAUDE_CONFIG_DIR root instead of <home>/.claude)."""
|
||||
cfg = cfg or h / ".claude"
|
||||
key = workspace_key(str(ws))
|
||||
proj = cfg / "projects" / key
|
||||
proj.mkdir(parents=True, exist_ok=True)
|
||||
@@ -306,8 +307,11 @@ def test_session_file_liveness_fails_safe(monkeypatch, live):
|
||||
assert claude_mod._session_file_live({"startedAt": now_ms}, _dead_pid()) is False
|
||||
assert claude_mod._session_file_live({"startedAt": now_ms}, None) is True
|
||||
assert claude_mod._session_file_live({"startedAt": now_ms}, 0) is True
|
||||
assert claude_mod._session_file_live({"startedAt": 0}, live.pid) is True, "no usable startedAt: live"
|
||||
hour_ago = int((time.time() - 3600) * 1000)
|
||||
assert claude_mod._session_file_live({"startedAt": hour_ago}, live.pid) is False, "reused pid"
|
||||
monkeypatch.setenv("PATH", "/nonexistent")
|
||||
assert claude_mod._session_file_live({"startedAt": 0}, live.pid) is True, "ps unavailable: live"
|
||||
assert claude_mod._session_file_live({"startedAt": hour_ago}, live.pid) is True, "ps unavailable: live"
|
||||
|
||||
|
||||
def test_claude_purge_takes_the_stale_key_with_its_session_file(home, tmp_path, conv, other, live):
|
||||
@@ -327,6 +331,86 @@ def test_claude_purge_takes_the_stale_key_with_its_session_file(home, tmp_path,
|
||||
assert set(purged) == {str(stale[0]), _masked(stale[1])}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("project_override", [False, True])
|
||||
def test_claude_config_dir_moves_the_config_root(home, tmp_path, monkeypatch, conv, live, project_override):
|
||||
"""CLAUDE_CONFIG_DIR moves sessions/, file-history/, ... and projects/:
|
||||
a live session there is refused, a stopped one is cleaned up. With
|
||||
CLAUDE_PROJECT_DIR naming the same projects/ the result is the same."""
|
||||
ws = tmp_path / "ws"
|
||||
ws.mkdir()
|
||||
cfg = tmp_path / "cfg"
|
||||
monkeypatch.setenv("CLAUDE_CONFIG_DIR", str(cfg))
|
||||
if project_override:
|
||||
monkeypatch.setenv("CLAUDE_PROJECT_DIR", str(cfg / "projects"))
|
||||
else:
|
||||
monkeypatch.delenv("CLAUDE_PROJECT_DIR")
|
||||
t = _tree(home, ws, conv, tmp_path / "ctmp", pid=live.pid, cfg=cfg)
|
||||
adapter = get_adapter("claude")
|
||||
ctx = _ctx(ws, home)
|
||||
|
||||
assert adapter.conversation_in_use(conv, ctx) is True
|
||||
res = _agents_cli(["purge", "claude", conv, str(ws)], ws)
|
||||
assert res.returncode == 4, res.stdout + res.stderr
|
||||
assert all(p.exists() for p in t.values())
|
||||
|
||||
live.kill()
|
||||
live.wait()
|
||||
purged = adapter.purge_artifacts(conv, ctx)
|
||||
assert not any(p.exists() for p in t.values()), {k: str(p) for k, p in t.items() if p.exists()}
|
||||
assert str(t["file_history"]) in purged and str(t["jsonl"]) in purged
|
||||
assert adapter.residual_artifacts(conv, ctx) == []
|
||||
assert not (home / ".claude" / "file-history").exists(), "the default root is not used"
|
||||
|
||||
|
||||
def _failing_remove(monkeypatch, victim: Path):
|
||||
from lib_py.agents.adapters import claude as claude_mod
|
||||
real = os.remove
|
||||
|
||||
def remove(path, *a, **kw):
|
||||
if os.path.realpath(path) == os.path.realpath(str(victim)):
|
||||
raise PermissionError(13, "denied", str(path))
|
||||
return real(path, *a, **kw)
|
||||
monkeypatch.setattr(claude_mod.os, "remove", remove)
|
||||
|
||||
|
||||
def test_a_key_that_cannot_be_removed_keeps_its_session_file(home, tmp_path, monkeypatch, conv):
|
||||
ws = tmp_path / "ws"
|
||||
ws.mkdir()
|
||||
sfile, key = _session_file(home / ".claude", _dead_pid(), conv)
|
||||
adapter = get_adapter("claude")
|
||||
ctx = _ctx(ws, home)
|
||||
with monkeypatch.context() as m:
|
||||
_failing_remove(m, key)
|
||||
purged = adapter.purge_artifacts(conv, ctx)
|
||||
|
||||
assert key.exists() and sfile.exists(), "the session file is what ties the key to this session"
|
||||
assert purged == []
|
||||
assert adapter.residual_artifacts(conv, ctx) == sorted([str(sfile), _masked(key)])
|
||||
|
||||
assert set(adapter.purge_artifacts(conv, ctx)) == {_masked(key), str(sfile)}
|
||||
assert adapter.residual_artifacts(conv, ctx) == []
|
||||
|
||||
|
||||
def test_a_job_dir_that_cannot_be_emptied_keeps_its_state_json(home, tmp_path, monkeypatch, conv):
|
||||
ws = tmp_path / "ws"
|
||||
ws.mkdir()
|
||||
t = _tree(home, ws, conv, tmp_path / "ctmp")
|
||||
adapter = get_adapter("claude")
|
||||
ctx = _ctx(ws, home)
|
||||
with monkeypatch.context() as m:
|
||||
_failing_remove(m, t["job"] / "timeline.jsonl")
|
||||
purged = adapter.purge_artifacts(conv, ctx)
|
||||
|
||||
assert (t["job"] / "state.json").exists(), "state.json proves the dir is this session's"
|
||||
assert str(t["job"]) not in purged
|
||||
assert not t["file_history"].exists()
|
||||
assert adapter.residual_artifacts(conv, ctx) == [str(t["job"])]
|
||||
|
||||
assert adapter.purge_artifacts(conv, ctx) == [str(t["job"])]
|
||||
assert not t["job"].exists()
|
||||
assert adapter.residual_artifacts(conv, ctx) == []
|
||||
|
||||
|
||||
def test_claude_tmp_root_rules(tmp_path, monkeypatch):
|
||||
from lib_py.agents.adapters.claude import _tmp_roots
|
||||
name = f"claude-{UID}"
|
||||
|
||||
Reference in new issue
Block a user