mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 08:11:07 -10:00
fix: reject checkout shorthand when creating branches (#4843)
* fix: reject branch shorthand during validation
* test: add regression coverage for branch-ref validation
Cover github_create_branch: rejects checkout shorthand (@{-1}) via literal
refs/heads/<name> validation, accepts ordinary slash names, and never uses
--branch (which would expand shorthand before checkout -B).
Co-authored-by: Mervin Praison <[email protected]>
* fix: clear rejected branch from sticky comment
* fix: preserve sticky branch after rejected creation
---------
Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com>
Co-authored-by: Mervin Praison <[email protected]>
This commit is contained in:
4 files changed
+104
-2
No files matched your search
@@ -136,7 +136,7 @@ def github_create_branch(branch_name: str) -> str:
|
||||
# Validate branch name to prevent misinterpretation as git options or invalid refs
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "check-ref-format", "--branch", branch_name],
|
||||
["git", "check-ref-format", f"refs/heads/{branch_name}"],
|
||||
check=True, capture_output=True, text=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
||||
@@ -255,6 +255,53 @@ class TestEnhancedStickyComment:
|
||||
# File should be clickable link
|
||||
assert "https://github.com/MervinPraison/PraisonAI/blob/feature/test/src/main.py" in body
|
||||
|
||||
def test_failed_branch_creation_clears_sticky_branch(self):
|
||||
"""A rejected branch must not remain rendered as active."""
|
||||
from praisonai.cli.commands.github import StickyComment
|
||||
|
||||
sticky = StickyComment(
|
||||
api_base="https://api.github.com",
|
||||
issue=112,
|
||||
token="fake",
|
||||
task_type="Issue",
|
||||
title="Test",
|
||||
run_url="",
|
||||
repo_name="owner/repo",
|
||||
)
|
||||
|
||||
sticky.set_branch("@{-1}")
|
||||
sticky.on_tool_end("agent", "github_create_branch", "Error: invalid branch name '@{-1}'")
|
||||
|
||||
assert sticky._branch_name is None
|
||||
assert "@{-1}" not in sticky._build_body()
|
||||
|
||||
def test_failed_branch_creation_restores_prior_sticky_branch_and_refreshes(self, monkeypatch):
|
||||
"""A failed later creation restores the displayed active branch."""
|
||||
from praisonai.cli.commands.github import StickyComment
|
||||
|
||||
sticky = StickyComment(
|
||||
api_base="https://api.github.com",
|
||||
issue=113,
|
||||
token="fake",
|
||||
task_type="Issue",
|
||||
title="Test",
|
||||
run_url="",
|
||||
repo_name="owner/repo",
|
||||
)
|
||||
sticky.set_branch("feature/active")
|
||||
sticky.on_tool_start("agent", "github_create_branch", {"branch_name": "@{-1}"})
|
||||
sticky.set_branch("@{-1}")
|
||||
refreshes = []
|
||||
monkeypatch.setattr(sticky, "_schedule_push", lambda: refreshes.append(True))
|
||||
|
||||
sticky.on_tool_end("agent", "github_create_branch", "Error: invalid branch name '@{-1}'")
|
||||
|
||||
assert sticky._branch_name == "feature/active"
|
||||
body = sticky._build_body()
|
||||
assert "[🌿 `feature/active`]" in body
|
||||
assert "[🌿 `@{-1}`]" not in body
|
||||
assert refreshes == [True]
|
||||
|
||||
|
||||
@pytest.mark.manual
|
||||
class TestEnhancedIntegration:
|
||||
|
||||
@@ -215,3 +215,44 @@ def test_slugify_and_auto_branch():
|
||||
name = github_tools._auto_branch_name("Fix parser")
|
||||
assert name.startswith("praisonai/fix-parser-")
|
||||
assert github_tools._branch_push_safety("praisonai/anything") is None
|
||||
|
||||
|
||||
def test_create_branch_validates_full_ref(monkeypatch):
|
||||
# Validation must check the literal ``refs/heads/<name>`` ref so Git cannot
|
||||
# expand checkout shorthand (e.g. ``@{-1}``) before ``checkout -B`` runs.
|
||||
responses = {
|
||||
("rev-parse", "--is-inside-work-tree"): ("true\n", 0),
|
||||
}
|
||||
calls = _call(monkeypatch, responses)
|
||||
_run_tool(github_tools.github_create_branch, "praisonai/fix")
|
||||
ref_checks = [c for c in calls if c[:2] == ["git", "check-ref-format"]]
|
||||
assert ref_checks, "expected a check-ref-format validation"
|
||||
for c in ref_checks:
|
||||
assert "--branch" not in c, "must not use --branch (expands shorthand)"
|
||||
assert "refs/heads/praisonai/fix" in c
|
||||
|
||||
|
||||
def test_create_branch_rejects_checkout_shorthand(monkeypatch):
|
||||
# ``@{-1}`` is a valid checkout shorthand but an invalid full ref, so it must
|
||||
# be rejected before any checkout runs (no destructive branch reset).
|
||||
responses = {
|
||||
("rev-parse", "--is-inside-work-tree"): ("true\n", 0),
|
||||
("check-ref-format", "refs/heads/@{-1}"): ("", 1),
|
||||
}
|
||||
calls = _call(monkeypatch, responses)
|
||||
result = _run_tool(github_tools.github_create_branch, "@{-1}")
|
||||
assert "invalid branch name" in result
|
||||
assert not [c for c in calls if c[:2] == ["git", "checkout"]]
|
||||
|
||||
|
||||
def test_create_branch_accepts_slash_names(monkeypatch):
|
||||
# Ordinary slash-containing branch names must still be created.
|
||||
responses = {
|
||||
("rev-parse", "--is-inside-work-tree"): ("true\n", 0),
|
||||
}
|
||||
calls = _call(monkeypatch, responses)
|
||||
result = _run_tool(github_tools.github_create_branch, "feature/new-thing")
|
||||
assert "Successfully" in result
|
||||
checkouts = [c for c in calls if c[:2] == ["git", "checkout"]]
|
||||
assert checkouts, "expected a checkout for a valid branch name"
|
||||
assert ["git", "checkout", "-B", "feature/new-thing"] in checkouts
|
||||
@@ -73,6 +73,7 @@ class StickyComment:
|
||||
|
||||
# Branch and PR tracking
|
||||
self._branch_name: Optional[str] = None
|
||||
self._branch_before_create: Optional[str] = None
|
||||
self._pr_url: Optional[str] = None
|
||||
self._pr_number: Optional[int] = None
|
||||
self._summary_lines: List[str] = []
|
||||
@@ -161,6 +162,8 @@ class StickyComment:
|
||||
def on_tool_start(self, agent_name: str, tool_name: str, tool_args: dict) -> None:
|
||||
with self._lock:
|
||||
self._current_tool = tool_name
|
||||
if tool_name == "github_create_branch":
|
||||
self._branch_before_create = self._branch_name
|
||||
label = self._tool_label(tool_name, tool_args)
|
||||
self._logs.append(f"🔧 {label}")
|
||||
# Extract file modifications from commands
|
||||
@@ -169,12 +172,23 @@ class StickyComment:
|
||||
self._schedule_push()
|
||||
|
||||
def on_tool_end(self, agent_name: str, tool_name: str, result: Any = None) -> None:
|
||||
refresh = False
|
||||
with self._lock:
|
||||
self._current_tool = None
|
||||
# A branch name is captured eagerly at tool start so the live
|
||||
# comment can show the operation immediately. Do not leave a
|
||||
# rejected branch rendered as active after validation or checkout
|
||||
# fails.
|
||||
if tool_name == "github_create_branch" and str(result).startswith("Error"):
|
||||
self._branch_name = self._branch_before_create
|
||||
refresh = True
|
||||
if tool_name == "github_create_branch":
|
||||
self._branch_before_create = None
|
||||
# Extract features from tool results
|
||||
if result and isinstance(result, str):
|
||||
self._extract_features_from_output(result)
|
||||
# no extra log for tool end – keeps comment clean
|
||||
if refresh:
|
||||
self._schedule_push()
|
||||
|
||||
def _extract_file_from_command(self, command: str) -> None:
|
||||
"""Extract file paths from shell commands to track modifications."""
|
||||
|
||||
Reference in new issue
Block a user