- Fix F1: Prepare recovery worktree detached at remote_head without git checkout -B to avoid exit 128 when branch is held by source worktree - Fix F2: Pass recovery_sanctioned=True in bind_session_lock and assess_same_issue_lease_conflict - Fix F3: Add SOURCE_RECOVER_DIRTY_ORPHANED to SANCTIONED_LOCK_SOURCES - Fix F4: Stop after Phase 4 dirty apply when conflicts exist; do not finalize session binding - Fix F5: Dynamically query competing live locks and workflow leases in MCP server - Fix F6: Fail closed on recovery worktree resume when HEAD does not match expected remote_head - Fix F7: Fail closed on remote HEAD observation failure rather than copying expected_remote_head pin - Fix F8: Enforce foreign overwrite protection requiring same claimant or sanctioned reclaim - Fix F9: Add real multi-worktree integration tests for prepare_recovery_worktree and lock rebind - Fix TestWorktreeStart: Bypass session lock check for dry-run and review/pr-* branches in scripts/worktree-start
This commit is contained in:
@@ -305,7 +305,8 @@ class CrashSafeRecovery(unittest.TestCase):
|
||||
local_head_contents={"c.py": b"local-base"},
|
||||
remote_head_contents={"c.py": b"remote-changed"},
|
||||
)
|
||||
self.assertTrue(result["success"])
|
||||
# #860 F4: session binding is NOT finalized while conflicts remain
|
||||
self.assertFalse(result["success"])
|
||||
self.assertEqual(result["outcome"], dorec.CONFLICTS_PRESENT)
|
||||
sidecar = os.path.join(self.recovery, "c.py.recovered-dirty")
|
||||
self.assertTrue(os.path.isfile(sidecar))
|
||||
@@ -403,10 +404,8 @@ class JournalSymlinkRefusal(unittest.TestCase):
|
||||
fh.write("{}")
|
||||
link = os.path.join(tmp, "link.json")
|
||||
os.symlink(real, link)
|
||||
# Point journal path helper via env
|
||||
key = "symlink-test"
|
||||
jdir = tmp
|
||||
# Craft path that is a symlink by saving then replacing
|
||||
path = dorec._journal_path(key, journal_dir=jdir)
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump({"idempotency_key": key}, fh)
|
||||
@@ -418,5 +417,67 @@ class JournalSymlinkRefusal(unittest.TestCase):
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
class RealGitMultiWorktreeIntegration(unittest.TestCase):
|
||||
def setUp(self):
|
||||
import subprocess
|
||||
self.tmp = tempfile.mkdtemp(prefix="git-integration-")
|
||||
self.repo = os.path.join(self.tmp, "repo")
|
||||
os.makedirs(self.repo, exist_ok=True)
|
||||
subprocess.run(["git", "init"], cwd=self.repo, check=True, capture_output=True)
|
||||
subprocess.run(["git", "config", "user.name", "Test User"], cwd=self.repo, check=True)
|
||||
subprocess.run(["git", "config", "user.email", "[email protected]"], cwd=self.repo, check=True)
|
||||
with open(os.path.join(self.repo, "init.txt"), "w") as fh:
|
||||
fh.write("init")
|
||||
subprocess.run(["git", "add", "."], cwd=self.repo, check=True)
|
||||
subprocess.run(["git", "commit", "-m", "init"], cwd=self.repo, check=True)
|
||||
branch = "fix/issue-999-test"
|
||||
subprocess.run(["git", "branch", branch], cwd=self.repo, check=True)
|
||||
self.branches = os.path.join(self.repo, "branches")
|
||||
self.source = os.path.join(self.branches, "issue-999-test")
|
||||
subprocess.run(["git", "worktree", "add", self.source, branch], cwd=self.repo, check=True)
|
||||
self.dirty_path = os.path.join(self.source, "dirty.txt")
|
||||
with open(self.dirty_path, "w") as fh:
|
||||
fh.write("dirty-data")
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def test_prepare_recovery_worktree_detached_no_exit_128(self):
|
||||
import subprocess
|
||||
head_sha = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=self.repo, text=True).strip()
|
||||
rec_wt = os.path.join(self.branches, "recovery-issue-999-test")
|
||||
res = dorec.prepare_recovery_worktree(
|
||||
canonical_repo_root=self.repo,
|
||||
recovery_worktree_path=rec_wt,
|
||||
branch_name="fix/issue-999-test",
|
||||
remote_head=head_sha,
|
||||
)
|
||||
self.assertTrue(res["success"], res.get("reasons"))
|
||||
self.assertTrue(os.path.isdir(rec_wt))
|
||||
|
||||
def test_real_lock_rebind_recovery_sanctioned(self):
|
||||
lock_dir = os.path.join(self.tmp, "locks")
|
||||
rec_wt = os.path.join(self.branches, "recovery-issue-999-test")
|
||||
os.makedirs(rec_wt, exist_ok=True)
|
||||
record = {
|
||||
"remote": "prgs",
|
||||
"org": "Example-Org",
|
||||
"repo": "Example-Repo",
|
||||
"issue_number": 999,
|
||||
"branch_name": "fix/issue-999-test",
|
||||
"worktree_path": rec_wt,
|
||||
"claimant": {"username": "author-user", "profile": "prgs-author"},
|
||||
}
|
||||
record_src = dict(record)
|
||||
record_src["worktree_path"] = self.source
|
||||
issue_lock_store.bind_session_lock(record_src, lock_dir=lock_dir)
|
||||
path = issue_lock_store.bind_session_lock(
|
||||
record,
|
||||
lock_dir=lock_dir,
|
||||
recovery_sanctioned=True,
|
||||
)
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user