feat(drone): fix pr_handler: scope commit to branch_dir pathspec — prevents concurrent-PR index contamination (FPLAN-0190 Task A) (#351)
* feat(devpulse): watchdog: breadcrumb on exit + default timeout 1800s to 600s (FPLAN-0189) Co-Authored-By: @devpulse <devpulse@aipass> * feat(drone): fix pr_handler: scope commit to branch_dir pathspec — prevents concurrent-PR index contamination (FPLAN-0190 Task A) Co-Authored-By: @drone <drone@aipass> --------- Co-authored-by: @devpulse <devpulse@aipass> Co-authored-by: @drone <drone@aipass>
This commit is contained in:
co-authored by
@devpulse
@drone
parent
c941c3d6ac
commit
905f2a45fb
@@ -407,6 +407,7 @@ class TestPRHandler:
|
||||
call_count = 0
|
||||
|
||||
def mock_subprocess_run(cmd, **kwargs):
|
||||
"""Simulate git subprocess returning main branch and staged-nothing."""
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
result = MagicMock()
|
||||
@@ -448,6 +449,7 @@ class TestPRHandler:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
def mock_subprocess_run(cmd, **kwargs):
|
||||
"""Simulate git subprocess returning main branch, then early exit on no staged files."""
|
||||
result = MagicMock()
|
||||
result.stderr = ""
|
||||
result.stdout = ""
|
||||
@@ -478,6 +480,62 @@ class TestPRHandler:
|
||||
# Lock must always be released, even on early exit
|
||||
release_mock.assert_called_once_with(force=True)
|
||||
|
||||
def test_commit_uses_pathspec_not_whole_index(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""Commit is scoped to branch_dir — pre-staged files outside it are excluded.
|
||||
|
||||
Regression test for FPLAN-0190: concurrent drone @git pr calls could
|
||||
contaminate each other's commits because git commit with no pathspec
|
||||
commits the entire index, not just files staged in this invocation.
|
||||
"""
|
||||
registry = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry.write_text("{}", encoding="utf-8")
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
commit_cmd_seen: list[list[str]] = []
|
||||
|
||||
def mock_subprocess_run(cmd, **kwargs):
|
||||
"""Simulate git/gh subprocess calls, recording commit invocations."""
|
||||
r = MagicMock()
|
||||
r.stderr = ""
|
||||
r.stdout = ""
|
||||
if cmd[1:3] == ["rev-parse", "--abbrev-ref"]:
|
||||
r.returncode = 0
|
||||
r.stdout = "main\n"
|
||||
elif cmd[0] == "git" and cmd[1] == "add":
|
||||
r.returncode = 0
|
||||
elif cmd[1:3] == ["diff", "--cached"]:
|
||||
r.returncode = 1 # 1 means something is staged
|
||||
elif cmd[0] == "git" and cmd[1] == "commit":
|
||||
commit_cmd_seen.append(list(cmd))
|
||||
r.returncode = 0
|
||||
r.stdout = "[main abc1234] feat(api): test"
|
||||
elif cmd[0] == "git" and cmd[1] == "branch":
|
||||
r.returncode = 0
|
||||
elif cmd[0] == "git" and cmd[1] == "push":
|
||||
r.returncode = 0
|
||||
elif cmd[0] == "gh":
|
||||
r.returncode = 0
|
||||
r.stdout = "https://github.com/test/repo/pull/1"
|
||||
else:
|
||||
r.returncode = 0
|
||||
return r
|
||||
|
||||
with patch("aipass.drone.apps.handlers.git.pr_handler.subprocess.run", side_effect=mock_subprocess_run):
|
||||
with patch(
|
||||
"aipass.drone.apps.handlers.git.pr_handler.acquire_lock",
|
||||
return_value={"success": True, "message": "ok"},
|
||||
):
|
||||
with patch("aipass.drone.apps.handlers.git.pr_handler.release_lock"):
|
||||
create_pr("api", "test desc", tmp_path / "src" / "aipass" / "api")
|
||||
|
||||
# The commit command must include '--' separator + pathspec to scope to branch_dir
|
||||
assert commit_cmd_seen, "commit was never called"
|
||||
commit_cmd = commit_cmd_seen[0]
|
||||
assert "--" in commit_cmd, "commit missing '--' pathspec separator"
|
||||
pathspec_idx = commit_cmd.index("--")
|
||||
pathspec = commit_cmd[pathspec_idx + 1]
|
||||
assert "src/aipass/api" in pathspec, f"pathspec should target branch_dir, got: {pathspec}"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 5. git_module — command routing, unknown commands, help/introspection
|
||||
@@ -698,6 +756,7 @@ class TestTriggerFireIntegration:
|
||||
call_count = 0
|
||||
|
||||
def mock_run(cmd, **kwargs):
|
||||
"""Simulate git subprocess calls and count invocations."""
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
r = MagicMock()
|
||||
@@ -749,6 +808,7 @@ class TestTriggerFireIntegration:
|
||||
monkeypatch.chdir(tmp_path)
|
||||
|
||||
def mock_run(cmd, **kwargs):
|
||||
"""Simulate git/gh subprocess calls for trigger-failure resilience test."""
|
||||
r = MagicMock()
|
||||
r.stderr = ""
|
||||
if cmd[1:3] == ["rev-parse", "--abbrev-ref"]:
|
||||
@@ -790,6 +850,7 @@ class TestTriggerFireIntegration:
|
||||
call_idx = 0
|
||||
|
||||
def mock_run(cmd, **kwargs):
|
||||
"""Simulate gh pr merge and git pull subprocess calls."""
|
||||
nonlocal call_idx
|
||||
call_idx += 1
|
||||
r = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user