Skip to content

Fix rollback leaving directories apply created (#838) - #846

Merged
Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-rollback-prune-created-dirs
Oct 5, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 3 commits into
mainfrom
agent/fix-rollback-prune-created-dirs

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

LLM Description written by Claude Code:claude-opus-5-5

Fixes #838

Root cause

Agent-mode apply creates missing parent directories (create_dir_all in apply_file_patch_at) for files a patch adds. The agent-mode rollback engine (rollback_package_patch, used by rollback and remove, and fanned out to pnpm/vlt store copies) deleted those files with a single remove_file and never pruned the directories. In site-packages, an empty leftover directory still imports as a PEP 420 namespace package, so import six_safe / find_spec kept succeeding after a "successful" rollback.

Fix

crates/socket-patch-core/src/patch/rollback.rs is the only file changed. After deleting a patch-added file, a new prune_emptied_parents step:

  • removes stale __pycache__/<stem>.*.pyc bytecode for a deleted .py file, then the __pycache__ directory if it is now empty;
  • removes each parent directory that is now empty, deepest first, and stops at the first non-empty one. The package root is never removed;
  • lstats every directory between the package root and the file first. If any of them is a symlink or junction (on Windows, std reports name-surrogate reparse points as symlinks, not directories) or is not a real directory, nothing is pruned, so directories outside the package are never touched;
  • relaxes read-only parents (Go module cache) for the rmdir with the same DirWriteGuard apply uses, then restores their mode;
  • is best effort: the file delete already committed the rollback, so a directory that can't be removed stays in place and is not reported as a failure.

Apply doesn't record which directories it created. The test is "empty once the patch's files are gone": a package directory that holds only patch-added files cannot have existed with content before the patch.

Tests (red → green)

New unit tests in rollback.rs, landed in a commit of their own (Test rollback prunes dirs apply created) before the fix. On that commit, cargo test -p socket-patch-core --lib rollback::tests::test_rollback_new_file failed 5 tests and passed 1 (the symlink guard case, because nothing was pruned at all). With the fix, all 46 rollback::tests pass:

Local checks:

  • cargo clippy --workspace --all-features -D warnings is clean.
  • rustfmt --check on rollback.rs is clean.
  • cargo test --workspace --all-features --no-fail-fast: every failure is environmental. The sandbox runs as root, so read-only-chmod tests can't fail their writes, and the self-update/notifier fixtures need exec, network or a tty. None of those tests touch rollback.

CI on e08b11d is green: all suites completed with none failed. Bugbot's first review raised one finding, Windows junctions. That was a false positive: Rust's FileType::is_dir() is false for name-surrogate reparse points. Evidence is in the thread, which is now resolved. Bugbot's re-review of e08b11d found no new issues.

Per-issue checklist:

Not covered: files a patch adds are still not added to .dist-info/RECORD, so pip uninstall without a rollback leaves them behind (the related point in #838). That is a separate apply-side change and is not claimed here.

🤖 Generated with Claude Code


Generated by Claude Code

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

Regression tests for #838: rolling back a patch that added files under
new directories must remove those directories again, including a
__pycache__ that only holds bytecode for the deleted module.

Assisted-by: Claude Code:claude-opus-5-5
Rolling back or removing a patch that added files under new
directories deleted the files but left the directories. In
site-packages an empty leftover directory still imports as a
namespace package, so code probing for the module saw it as present.

Rollback now removes the deleted module's stale __pycache__ bytecode
and then every parent that is left empty, deepest first, stopping at
the package root. Symlinked directories are never removed or walked
through, and read-only parents are relaxed just for the rmdir.

Fixes #838

Assisted-by: Claude Code:claude-opus-5-5
@mikolalysenko
Mikola Lysenko (mikolalysenko) force-pushed the agent/fix-rollback-prune-created-dirs branch from 9bb3d4c to e08b11d Compare October 5, 2026 11:06
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/socket-patch-core/src/patch/rollback.rs
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

BugBot review


Generated by Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e08b11d. Configure here.

@mikolalysenko Mikola Lysenko (mikolalysenko) added the Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review label Oct 5, 2026
@mikolalysenko

Copy link
Copy Markdown
Collaborator Author

Ready for review.

  • Head: e08b11da
  • CI: 97/97 non-skipped check runs green on this head (3 skipped by path filters). Mergeable with no conflicts against main.
  • Bugbot: reviewed e08b11d and found no new issues. Its one earlier finding (Windows junctions) was refuted in-thread: FileType::is_dir() is false for name-surrogate reparse points, so the symlink guard covers them. The thread is resolved.
  • Reviewer focus: the prune logic in rollback.rs relies on "empty once the patch's files are gone" rather than recorded created dirs; check that heuristic is acceptable.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ready for review Agent-verified: mergeable, CI green, Bugbot clean — awaiting human review

Projects

None yet

3 participants