fix(agents): any spelling of a claude uuid is checked as claude's lowercase id (Issue #31, review)
_is_uuid took an uppercase or mixed-case uuid, and the paths built from it are the live lowercase session's own files on macOS's default case-insensitive APFS. The in-use checks compare exactly, though: session files and command lines carry the lowercase id, so `purge claude <UPPER>` found no holder and could remove a live session's files. _canonical now lowercases a canonical uuid, and the paths, the session-file and job matches, the in-use check and the residual all use it; a command line is searched for both spellings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
60779ba1f8
commit
c0248b88be
2 files changed
+60
-13
No files matched your search
@@ -290,6 +290,43 @@ def test_claude_rejects_malformed_uuid(home, tmp_path, conv, bad):
|
||||
assert memory.exists() and all(p.exists() for p in named)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spell", [str.upper, lambda u: u[:18].upper() + u[18:]], ids=["upper", "mixed"])
|
||||
def test_any_spelling_of_the_uuid_is_the_lowercase_session(home, tmp_path, conv, live, spell):
|
||||
"""claude writes its ids in lowercase, and on macOS's default
|
||||
case-insensitive APFS another spelling names the very same files. Every
|
||||
check then uses the lowercase id: the live holder is seen, by its session
|
||||
file or its command line, and a purge takes the session's own paths."""
|
||||
ws = tmp_path / "ws"
|
||||
ws.mkdir()
|
||||
t = _tree(home, ws, conv, tmp_path / "ctmp", pid=live.pid)
|
||||
other = spell(conv)
|
||||
assert other != conv
|
||||
adapter = get_adapter("claude")
|
||||
ctx = _ctx(ws, home)
|
||||
|
||||
assert adapter.conversation_in_use(other, ctx) is True
|
||||
for args in (["purge", "claude", other, str(ws)], ["in-use", "claude", other, str(ws)]):
|
||||
res = _agents_cli(args, ws)
|
||||
assert res.returncode == 4, res.stdout + res.stderr
|
||||
assert all(p.exists() for p in t.values())
|
||||
|
||||
live.kill()
|
||||
live.wait()
|
||||
named = subprocess.Popen(["sh", "-c", "sleep 120", conv])
|
||||
try:
|
||||
time.sleep(0.05)
|
||||
assert adapter.conversation_in_use(other, ctx) is True, "a command line naming the lowercase id"
|
||||
finally:
|
||||
named.kill()
|
||||
named.wait()
|
||||
|
||||
assert adapter.has_artifacts(other, ctx) is True
|
||||
purged = adapter.purge_artifacts(other, ctx)
|
||||
assert not any(p.exists() for p in t.values()), {k: str(p) for k, p in t.items() if p.exists()}
|
||||
assert purged and all(other not in p for p in purged)
|
||||
assert adapter.residual_artifacts(other, ctx) == []
|
||||
|
||||
|
||||
def test_claude_purge_never_follows_a_linked_dir_below_a_root(home, tmp_path, conv):
|
||||
"""A symlinked <key> (tmp, projects) or file-history/ below its root is
|
||||
not followed: what lies behind it stays and is reported as residual."""
|
||||
|
||||
Reference in new issue
Block a user