mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
20 Commits
add-apply-
...
add-resolv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c088a08e51 | ||
|
|
2ce806e411 | ||
|
|
45a1486f24 | ||
|
|
1f53c930fe | ||
|
|
dbf560d21b | ||
|
|
845f1b25ea | ||
|
|
031e20105e | ||
|
|
f03748226a | ||
|
|
27592c504a | ||
|
|
a4f577222a | ||
|
|
c2265e83c5 | ||
|
|
87925dd876 | ||
|
|
95884c1c74 | ||
|
|
cfd3911f2b | ||
|
|
abde56ff7e | ||
|
|
66b4e5d14b | ||
|
|
cb92518f1b | ||
|
|
486355bfd5 | ||
|
|
fba35a4be8 | ||
|
|
73e190e0f1 |
@@ -2,4 +2,4 @@
|
||||
"APP_MODE": "oss",
|
||||
"GITHUB_CLIENT_ID": "",
|
||||
"POSTHOG_CLIENT_KEY": "phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +187,9 @@ class CodeActAgent(Agent):
|
||||
)
|
||||
]
|
||||
elif isinstance(action, CmdRunAction) and action.source == 'user':
|
||||
content = [TextContent(text=f'User executed the command:\n{action.command}')]
|
||||
content = [
|
||||
TextContent(text=f'User executed the command:\n{action.command}')
|
||||
]
|
||||
return [
|
||||
Message(
|
||||
role='user',
|
||||
|
||||
@@ -24,6 +24,7 @@ class MessageAction(Action):
|
||||
@images_urls.setter
|
||||
def images_urls(self, value):
|
||||
self.image_urls = value
|
||||
|
||||
def __str__(self) -> str:
|
||||
ret = f'**MessageAction** (source={self.source})\n'
|
||||
ret += f'CONTENT: {self.content}'
|
||||
|
||||
@@ -69,7 +69,7 @@ def action_from_dict(action: dict) -> Action:
|
||||
# images_urls has been renamed to image_urls
|
||||
if 'images_urls' in args:
|
||||
args['image_urls'] = args.pop('images_urls')
|
||||
|
||||
|
||||
try:
|
||||
decoded_action = action_class(**args)
|
||||
if 'timeout' in action:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .patch import parse_patch
|
||||
from .apply import apply_diff
|
||||
from .patch import parse_patch
|
||||
|
||||
__all__ = ["parse_patch", "apply_diff"]
|
||||
__all__ = ['parse_patch', 'apply_diff']
|
||||
|
||||
@@ -10,33 +10,33 @@ from .snippets import remove, which
|
||||
|
||||
def _apply_diff_with_subprocess(diff, lines, reverse=False):
|
||||
# call out to patch program
|
||||
patchexec = which("patch")
|
||||
patchexec = which('patch')
|
||||
if not patchexec:
|
||||
raise SubprocessException("cannot find patch program", code=-1)
|
||||
raise SubprocessException('cannot find patch program', code=-1)
|
||||
|
||||
tempdir = tempfile.gettempdir()
|
||||
|
||||
filepath = os.path.join(tempdir, "wtp-" + str(hash(diff.header)))
|
||||
oldfilepath = filepath + ".old"
|
||||
newfilepath = filepath + ".new"
|
||||
rejfilepath = filepath + ".rej"
|
||||
patchfilepath = filepath + ".patch"
|
||||
with open(oldfilepath, "w") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
filepath = os.path.join(tempdir, 'wtp-' + str(hash(diff.header)))
|
||||
oldfilepath = filepath + '.old'
|
||||
newfilepath = filepath + '.new'
|
||||
rejfilepath = filepath + '.rej'
|
||||
patchfilepath = filepath + '.patch'
|
||||
with open(oldfilepath, 'w') as f:
|
||||
f.write('\n'.join(lines) + '\n')
|
||||
|
||||
with open(patchfilepath, "w") as f:
|
||||
with open(patchfilepath, 'w') as f:
|
||||
f.write(diff.text)
|
||||
|
||||
args = [
|
||||
patchexec,
|
||||
"--reverse" if reverse else "--forward",
|
||||
"--quiet",
|
||||
"--no-backup-if-mismatch",
|
||||
"-o",
|
||||
'--reverse' if reverse else '--forward',
|
||||
'--quiet',
|
||||
'--no-backup-if-mismatch',
|
||||
'-o',
|
||||
newfilepath,
|
||||
"-i",
|
||||
'-i',
|
||||
patchfilepath,
|
||||
"-r",
|
||||
'-r',
|
||||
rejfilepath,
|
||||
oldfilepath,
|
||||
]
|
||||
@@ -58,7 +58,7 @@ def _apply_diff_with_subprocess(diff, lines, reverse=False):
|
||||
|
||||
# do this last to ensure files get cleaned up
|
||||
if ret != 0:
|
||||
raise SubprocessException("patch program failed", code=ret)
|
||||
raise SubprocessException('patch program failed', code=ret)
|
||||
|
||||
return lines, rejlines
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ class HunkException(PatchingException):
|
||||
self.hunk = hunk
|
||||
if hunk is not None:
|
||||
super(HunkException, self).__init__(
|
||||
"{msg}, in hunk #{n}".format(msg=msg, n=hunk)
|
||||
'{msg}, in hunk #{n}'.format(msg=msg, n=hunk)
|
||||
)
|
||||
else:
|
||||
super(HunkException, self).__init__(msg)
|
||||
|
||||
@@ -8,67 +8,67 @@ from . import exceptions
|
||||
from .snippets import findall_regex, split_by_regex
|
||||
|
||||
header = namedtuple(
|
||||
"header",
|
||||
"index_path old_path old_version new_path new_version",
|
||||
'header',
|
||||
'index_path old_path old_version new_path new_version',
|
||||
)
|
||||
|
||||
diffobj = namedtuple("diffobj", "header changes text")
|
||||
Change = namedtuple("Change", "old new line hunk")
|
||||
diffobj = namedtuple('diffobj', 'header changes text')
|
||||
Change = namedtuple('Change', 'old new line hunk')
|
||||
|
||||
file_timestamp_str = "(.+?)(?:\t|:| +)(.*)"
|
||||
file_timestamp_str = '(.+?)(?:\t|:| +)(.*)'
|
||||
# .+? was previously [^:\t\n\r\f\v]+
|
||||
|
||||
# general diff regex
|
||||
diffcmd_header = re.compile("^diff.* (.+) (.+)$")
|
||||
unified_header_index = re.compile("^Index: (.+)$")
|
||||
unified_header_old_line = re.compile(r"^--- " + file_timestamp_str + "$")
|
||||
unified_header_new_line = re.compile(r"^\+\+\+ " + file_timestamp_str + "$")
|
||||
unified_hunk_start = re.compile(r"^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@(.*)$")
|
||||
unified_change = re.compile("^([-+ ])(.*)$")
|
||||
diffcmd_header = re.compile('^diff.* (.+) (.+)$')
|
||||
unified_header_index = re.compile('^Index: (.+)$')
|
||||
unified_header_old_line = re.compile(r'^--- ' + file_timestamp_str + '$')
|
||||
unified_header_new_line = re.compile(r'^\+\+\+ ' + file_timestamp_str + '$')
|
||||
unified_hunk_start = re.compile(r'^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@(.*)$')
|
||||
unified_change = re.compile('^([-+ ])(.*)$')
|
||||
|
||||
context_header_old_line = re.compile(r"^\*\*\* " + file_timestamp_str + "$")
|
||||
context_header_new_line = re.compile("^--- " + file_timestamp_str + "$")
|
||||
context_hunk_start = re.compile(r"^\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*$")
|
||||
context_hunk_old = re.compile(r"^\*\*\* (\d+),?(\d*) \*\*\*\*$")
|
||||
context_hunk_new = re.compile(r"^--- (\d+),?(\d*) ----$")
|
||||
context_change = re.compile("^([-+ !]) (.*)$")
|
||||
context_header_old_line = re.compile(r'^\*\*\* ' + file_timestamp_str + '$')
|
||||
context_header_new_line = re.compile('^--- ' + file_timestamp_str + '$')
|
||||
context_hunk_start = re.compile(r'^\*\*\*\*\*\*\*\*\*\*\*\*\*\*\*$')
|
||||
context_hunk_old = re.compile(r'^\*\*\* (\d+),?(\d*) \*\*\*\*$')
|
||||
context_hunk_new = re.compile(r'^--- (\d+),?(\d*) ----$')
|
||||
context_change = re.compile('^([-+ !]) (.*)$')
|
||||
|
||||
ed_hunk_start = re.compile(r"^(\d+),?(\d*)([acd])$")
|
||||
ed_hunk_end = re.compile("^.$")
|
||||
ed_hunk_start = re.compile(r'^(\d+),?(\d*)([acd])$')
|
||||
ed_hunk_end = re.compile('^.$')
|
||||
# much like forward ed, but no 'c' type
|
||||
rcs_ed_hunk_start = re.compile(r"^([ad])(\d+) ?(\d*)$")
|
||||
rcs_ed_hunk_start = re.compile(r'^([ad])(\d+) ?(\d*)$')
|
||||
|
||||
default_hunk_start = re.compile(r"^(\d+),?(\d*)([acd])(\d+),?(\d*)$")
|
||||
default_hunk_mid = re.compile("^---$")
|
||||
default_change = re.compile("^([><]) (.*)$")
|
||||
default_hunk_start = re.compile(r'^(\d+),?(\d*)([acd])(\d+),?(\d*)$')
|
||||
default_hunk_mid = re.compile('^---$')
|
||||
default_change = re.compile('^([><]) (.*)$')
|
||||
|
||||
# Headers
|
||||
|
||||
# git has a special index header and no end part
|
||||
git_diffcmd_header = re.compile("^diff --git a/(.+) b/(.+)$")
|
||||
git_header_index = re.compile(r"^index ([a-f0-9]+)..([a-f0-9]+) ?(\d*)$")
|
||||
git_header_old_line = re.compile("^--- (.+)$")
|
||||
git_header_new_line = re.compile(r"^\+\+\+ (.+)$")
|
||||
git_header_file_mode = re.compile(r"^(new|deleted) file mode \d{6}$")
|
||||
git_header_binary_file = re.compile("^Binary files (.+) and (.+) differ")
|
||||
git_binary_patch_start = re.compile(r"^GIT binary patch$")
|
||||
git_binary_literal_start = re.compile(r"^literal (\d+)$")
|
||||
git_binary_delta_start = re.compile(r"^delta (\d+)$")
|
||||
base85string = re.compile(r"^[0-9A-Za-z!#$%&()*+;<=>?@^_`{|}~-]+$")
|
||||
git_diffcmd_header = re.compile('^diff --git a/(.+) b/(.+)$')
|
||||
git_header_index = re.compile(r'^index ([a-f0-9]+)..([a-f0-9]+) ?(\d*)$')
|
||||
git_header_old_line = re.compile('^--- (.+)$')
|
||||
git_header_new_line = re.compile(r'^\+\+\+ (.+)$')
|
||||
git_header_file_mode = re.compile(r'^(new|deleted) file mode \d{6}$')
|
||||
git_header_binary_file = re.compile('^Binary files (.+) and (.+) differ')
|
||||
git_binary_patch_start = re.compile(r'^GIT binary patch$')
|
||||
git_binary_literal_start = re.compile(r'^literal (\d+)$')
|
||||
git_binary_delta_start = re.compile(r'^delta (\d+)$')
|
||||
base85string = re.compile(r'^[0-9A-Za-z!#$%&()*+;<=>?@^_`{|}~-]+$')
|
||||
|
||||
bzr_header_index = re.compile("=== (.+)")
|
||||
bzr_header_index = re.compile('=== (.+)')
|
||||
bzr_header_old_line = unified_header_old_line
|
||||
bzr_header_new_line = unified_header_new_line
|
||||
|
||||
svn_header_index = unified_header_index
|
||||
svn_header_timestamp_version = re.compile(r"\((?:working copy|revision (\d+))\)")
|
||||
svn_header_timestamp = re.compile(r".*(\(.*\))$")
|
||||
svn_header_timestamp_version = re.compile(r'\((?:working copy|revision (\d+))\)')
|
||||
svn_header_timestamp = re.compile(r'.*(\(.*\))$')
|
||||
|
||||
cvs_header_index = unified_header_index
|
||||
cvs_header_rcs = re.compile(r"^RCS file: (.+)(?:,\w{1}$|$)")
|
||||
cvs_header_timestamp = re.compile(r"(.+)\t([\d.]+)")
|
||||
cvs_header_timestamp_colon = re.compile(r":([\d.]+)\t(.+)")
|
||||
old_cvs_diffcmd_header = re.compile("^diff.* (.+):(.*) (.+):(.*)$")
|
||||
cvs_header_rcs = re.compile(r'^RCS file: (.+)(?:,\w{1}$|$)')
|
||||
cvs_header_timestamp = re.compile(r'(.+)\t([\d.]+)')
|
||||
cvs_header_timestamp_colon = re.compile(r':([\d.]+)\t(.+)')
|
||||
old_cvs_diffcmd_header = re.compile('^diff.* (.+):(.*) (.+):(.*)$')
|
||||
|
||||
|
||||
def parse_patch(text):
|
||||
@@ -97,7 +97,7 @@ def parse_patch(text):
|
||||
break
|
||||
|
||||
for diff in diffs:
|
||||
difftext = "\n".join(diff) + "\n"
|
||||
difftext = '\n'.join(diff) + '\n'
|
||||
h = parse_header(diff)
|
||||
d = parse_diff(diff)
|
||||
if h or d:
|
||||
@@ -133,10 +133,10 @@ def parse_scm_header(text):
|
||||
if res:
|
||||
old_path = res.old_path
|
||||
new_path = res.new_path
|
||||
if old_path.startswith("a/"):
|
||||
if old_path.startswith('a/'):
|
||||
old_path = old_path[2:]
|
||||
|
||||
if new_path.startswith("b/"):
|
||||
if new_path.startswith('b/'):
|
||||
new_path = new_path[2:]
|
||||
|
||||
return header(
|
||||
@@ -240,10 +240,10 @@ def parse_git_header(text):
|
||||
new_path = binary.group(2)
|
||||
|
||||
if old_path and new_path:
|
||||
if old_path.startswith("a/"):
|
||||
if old_path.startswith('a/'):
|
||||
old_path = old_path[2:]
|
||||
|
||||
if new_path.startswith("b/"):
|
||||
if new_path.startswith('b/'):
|
||||
new_path = new_path[2:]
|
||||
return header(
|
||||
index_path=None,
|
||||
@@ -256,19 +256,19 @@ def parse_git_header(text):
|
||||
# if we go through all of the text without finding our normal info,
|
||||
# use the cmd if available
|
||||
if cmd_old_path and cmd_new_path and old_version and new_version:
|
||||
if cmd_old_path.startswith("a/"):
|
||||
if cmd_old_path.startswith('a/'):
|
||||
cmd_old_path = cmd_old_path[2:]
|
||||
|
||||
if cmd_new_path.startswith("b/"):
|
||||
if cmd_new_path.startswith('b/'):
|
||||
cmd_new_path = cmd_new_path[2:]
|
||||
|
||||
return header(
|
||||
index_path=None,
|
||||
# wow, I kind of hate this:
|
||||
# assume /dev/null if the versions are zeroed out
|
||||
old_path="/dev/null" if old_version == "0000000" else cmd_old_path,
|
||||
old_path='/dev/null' if old_version == '0000000' else cmd_old_path,
|
||||
old_version=old_version,
|
||||
new_path="/dev/null" if new_version == "0000000" else cmd_new_path,
|
||||
new_path='/dev/null' if new_version == '0000000' else cmd_new_path,
|
||||
new_version=new_version,
|
||||
)
|
||||
|
||||
@@ -569,10 +569,10 @@ def parse_default_diff(text):
|
||||
kind = c.group(1)
|
||||
line = c.group(2)
|
||||
|
||||
if kind == "<" and (r != old_len or r == 0):
|
||||
if kind == '<' and (r != old_len or r == 0):
|
||||
changes.append(Change(old + r, None, line, hunk_n))
|
||||
r += 1
|
||||
elif kind == ">" and (i != new_len or i == 0):
|
||||
elif kind == '>' and (i != new_len or i == 0):
|
||||
changes.append(Change(None, new + i, line, hunk_n))
|
||||
i += 1
|
||||
|
||||
@@ -627,13 +627,13 @@ def parse_unified_diff(text):
|
||||
kind = c.group(1)
|
||||
line = c.group(2)
|
||||
|
||||
if kind == "-" and (r != old_len or r == 0):
|
||||
if kind == '-' and (r != old_len or r == 0):
|
||||
changes.append(Change(old + r, None, line, hunk_n))
|
||||
r += 1
|
||||
elif kind == "+" and (i != new_len or i == 0):
|
||||
elif kind == '+' and (i != new_len or i == 0):
|
||||
changes.append(Change(None, new + i, line, hunk_n))
|
||||
i += 1
|
||||
elif kind == " ":
|
||||
elif kind == ' ':
|
||||
if r != old_len and i != new_len:
|
||||
changes.append(Change(old + r, new + i, line, hunk_n))
|
||||
r += 1
|
||||
@@ -667,7 +667,7 @@ def parse_context_diff(text):
|
||||
k = 0
|
||||
parts = split_by_regex(hunk, context_hunk_new)
|
||||
if len(parts) != 2:
|
||||
raise exceptions.ParseException("Context diff invalid", hunk_n)
|
||||
raise exceptions.ParseException('Context diff invalid', hunk_n)
|
||||
|
||||
old_hunk = parts[0]
|
||||
new_hunk = parts[1]
|
||||
@@ -695,7 +695,7 @@ def parse_context_diff(text):
|
||||
|
||||
# now have old and new set, can start processing?
|
||||
if len(old_hunk) > 0 and len(new_hunk) == 0:
|
||||
msg = "Got unexpected change in removal hunk: "
|
||||
msg = 'Got unexpected change in removal hunk: '
|
||||
# only removes left?
|
||||
while len(old_hunk) > 0:
|
||||
c = context_change.match(old_hunk[0])
|
||||
@@ -707,22 +707,22 @@ def parse_context_diff(text):
|
||||
kind = c.group(1)
|
||||
line = c.group(2)
|
||||
|
||||
if kind == "-" and (j != old_len or j == 0):
|
||||
if kind == '-' and (j != old_len or j == 0):
|
||||
changes.append(Change(old + j, None, line, hunk_n))
|
||||
j += 1
|
||||
elif kind == " " and (
|
||||
elif kind == ' ' and (
|
||||
(j != old_len and k != new_len) or (j == 0 or k == 0)
|
||||
):
|
||||
changes.append(Change(old + j, new + k, line, hunk_n))
|
||||
j += 1
|
||||
k += 1
|
||||
elif kind == "+" or kind == "!":
|
||||
elif kind == '+' or kind == '!':
|
||||
raise exceptions.ParseException(msg + kind, hunk_n)
|
||||
|
||||
continue
|
||||
|
||||
if len(old_hunk) == 0 and len(new_hunk) > 0:
|
||||
msg = "Got unexpected change in removal hunk: "
|
||||
msg = 'Got unexpected change in removal hunk: '
|
||||
# only insertions left?
|
||||
while len(new_hunk) > 0:
|
||||
c = context_change.match(new_hunk[0])
|
||||
@@ -734,16 +734,16 @@ def parse_context_diff(text):
|
||||
kind = c.group(1)
|
||||
line = c.group(2)
|
||||
|
||||
if kind == "+" and (k != new_len or k == 0):
|
||||
if kind == '+' and (k != new_len or k == 0):
|
||||
changes.append(Change(None, new + k, line, hunk_n))
|
||||
k += 1
|
||||
elif kind == " " and (
|
||||
elif kind == ' ' and (
|
||||
(j != old_len and k != new_len) or (j == 0 or k == 0)
|
||||
):
|
||||
changes.append(Change(old + j, new + k, line, hunk_n))
|
||||
j += 1
|
||||
k += 1
|
||||
elif kind == "-" or kind == "!":
|
||||
elif kind == '-' or kind == '!':
|
||||
raise exceptions.ParseException(msg + kind, hunk_n)
|
||||
continue
|
||||
|
||||
@@ -765,17 +765,17 @@ def parse_context_diff(text):
|
||||
if not (oc or nc):
|
||||
del old_hunk[0]
|
||||
del new_hunk[0]
|
||||
elif okind == " " and nkind == " " and oline == nline:
|
||||
elif okind == ' ' and nkind == ' ' and oline == nline:
|
||||
changes.append(Change(old + j, new + k, oline, hunk_n))
|
||||
j += 1
|
||||
k += 1
|
||||
del old_hunk[0]
|
||||
del new_hunk[0]
|
||||
elif okind == "-" or okind == "!" and (j != old_len or j == 0):
|
||||
elif okind == '-' or okind == '!' and (j != old_len or j == 0):
|
||||
changes.append(Change(old + j, None, oline, hunk_n))
|
||||
j += 1
|
||||
del old_hunk[0]
|
||||
elif nkind == "+" or nkind == "!" and (k != new_len or k == 0):
|
||||
elif nkind == '+' or nkind == '!' and (k != new_len or k == 0):
|
||||
changes.append(Change(None, new + k, nline, hunk_n))
|
||||
k += 1
|
||||
del new_hunk[0]
|
||||
@@ -821,7 +821,7 @@ def parse_ed_diff(text):
|
||||
old_end = int(o.group(2)) if len(o.group(2)) else old
|
||||
|
||||
hunk_kind = o.group(3)
|
||||
if hunk_kind == "d":
|
||||
if hunk_kind == 'd':
|
||||
k = 0
|
||||
while old_end >= old:
|
||||
changes.append(Change(old + k, None, None, hunk_n))
|
||||
@@ -832,7 +832,7 @@ def parse_ed_diff(text):
|
||||
|
||||
while len(hunk) > 0:
|
||||
e = ed_hunk_end.match(hunk[0])
|
||||
if not e and hunk_kind == "c":
|
||||
if not e and hunk_kind == 'c':
|
||||
k = 0
|
||||
while old_end >= old:
|
||||
changes.append(Change(old + k, None, None, hunk_n))
|
||||
@@ -852,7 +852,7 @@ def parse_ed_diff(text):
|
||||
)
|
||||
i += 1
|
||||
j += 1
|
||||
if not e and hunk_kind == "a":
|
||||
if not e and hunk_kind == 'a':
|
||||
changes.append(
|
||||
Change(
|
||||
None,
|
||||
@@ -900,7 +900,7 @@ def parse_rcs_ed_diff(text):
|
||||
old = int(o.group(2))
|
||||
size = int(o.group(3))
|
||||
|
||||
if hunk_kind == "a":
|
||||
if hunk_kind == 'a':
|
||||
old += total_change_size + 1
|
||||
total_change_size += size
|
||||
while size > 0 and len(hunk) > 0:
|
||||
@@ -910,7 +910,7 @@ def parse_rcs_ed_diff(text):
|
||||
|
||||
del hunk[0]
|
||||
|
||||
elif hunk_kind == "d":
|
||||
elif hunk_kind == 'd':
|
||||
total_change_size -= size
|
||||
while size > 0:
|
||||
changes.append(Change(old + j, None, None, hunk_n))
|
||||
@@ -938,8 +938,8 @@ def parse_git_binary_diff(text):
|
||||
# the sizes are used as latch-up
|
||||
new_size = 0
|
||||
old_size = 0
|
||||
old_encoded = ""
|
||||
new_encoded = ""
|
||||
old_encoded = ''
|
||||
new_encoded = ''
|
||||
for line in lines:
|
||||
if cmd_old_path is None and cmd_new_path is None:
|
||||
hm = git_diffcmd_header.match(line)
|
||||
@@ -978,11 +978,11 @@ def parse_git_binary_diff(text):
|
||||
change = Change(None, 0, added_data, None)
|
||||
changes.append(change)
|
||||
new_size = 0
|
||||
new_encoded = ""
|
||||
new_encoded = ''
|
||||
else:
|
||||
# Invalid line format
|
||||
new_size = 0
|
||||
new_encoded = ""
|
||||
new_encoded = ''
|
||||
|
||||
# the second is removed file
|
||||
if old_size == 0:
|
||||
@@ -1006,10 +1006,10 @@ def parse_git_binary_diff(text):
|
||||
change = Change(0, None, None, removed_data)
|
||||
changes.append(change)
|
||||
old_size = 0
|
||||
old_encoded = ""
|
||||
old_encoded = ''
|
||||
else:
|
||||
# Invalid line format
|
||||
old_size = 0
|
||||
old_encoded = ""
|
||||
old_encoded = ''
|
||||
|
||||
return changes
|
||||
|
||||
@@ -54,7 +54,7 @@ def which(program):
|
||||
if is_exe(program):
|
||||
return program
|
||||
else:
|
||||
for path in os.environ["PATH"].split(os.pathsep):
|
||||
for path in os.environ['PATH'].split(os.pathsep):
|
||||
path = path.strip('"')
|
||||
exe_file = os.path.join(path, program)
|
||||
if is_exe(exe_file):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is a Python repo for openhands-resolver, a library that attempts to resolve github issues with the AI agent OpenHands.
|
||||
|
||||
- Setup: `poetry install --with test --with dev`
|
||||
- Testing: `poetry run pytest tests/test_*.py`
|
||||
- Testing: `poetry run pytest tests/test_*.py`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
This is a node repo for an RSS parser.
|
||||
- Setup: `yes | npm install`
|
||||
- Testing: `SKIP_BROWSER_TESTS=1 npm test`
|
||||
- Writing Tests: Add to the `test` directory.
|
||||
- Writing Tests: Add to the `test` directory.
|
||||
|
||||
@@ -3,7 +3,7 @@ The feedback may be addressed to specific code files. In this case the file loca
|
||||
Please update the code based on the feedback for the repository in /workspace.
|
||||
An environment has been set up for you to start working. You may assume all necessary tools are installed.
|
||||
|
||||
# Issues addressed
|
||||
# Issues addressed
|
||||
{{ issues }}
|
||||
|
||||
# Review comments
|
||||
@@ -21,4 +21,4 @@ You SHOULD INCLUDE PROPER INDENTATION in your edit commands.{% if repo_instructi
|
||||
Some basic information about this repository:
|
||||
{{ repo_instruction }}{% endif %}
|
||||
|
||||
When you think you have fixed the issue through code changes, please finish the interaction.
|
||||
When you think you have fixed the issue through code changes, please finish the interaction.
|
||||
|
||||
@@ -14,4 +14,4 @@ For all changes to actual application code (e.g. in Python or Javascript), add a
|
||||
Run the tests, and if they pass you are done!
|
||||
You do NOT need to write new tests if there are only changes to documentation or configuration files.
|
||||
|
||||
When you think you have fixed the issue through code changes, please call the finish action to end the interaction.
|
||||
When you think you have fixed the issue through code changes, please call the finish action to end the interaction.
|
||||
|
||||
@@ -10,4 +10,4 @@ You SHOULD INCLUDE PROPER INDENTATION in your edit commands.{% if repo_instructi
|
||||
Some basic information about this repository:
|
||||
{{ repo_instruction }}{% endif %}
|
||||
|
||||
When you think you have fixed the issue through code changes, please finish the interaction.
|
||||
When you think you have fixed the issue through code changes, please finish the interaction.
|
||||
|
||||
@@ -316,7 +316,7 @@ async def resolve_issue(
|
||||
issue_number: int,
|
||||
comment_id: int | None,
|
||||
reset_logger: bool = False,
|
||||
) -> None:
|
||||
) -> ResolverOutput:
|
||||
"""Resolve a single github issue.
|
||||
|
||||
Args:
|
||||
@@ -412,9 +412,9 @@ async def resolve_issue(
|
||||
data = ResolverOutput.model_validate_json(line)
|
||||
if data.issue.number == issue_number:
|
||||
logger.warning(
|
||||
f'Issue {issue_number} was already processed. Skipping.'
|
||||
f'Issue {issue_number} was already processed. Returning existing output.'
|
||||
)
|
||||
return
|
||||
return data
|
||||
|
||||
output_fp = open(output_file, 'a')
|
||||
|
||||
@@ -458,6 +458,7 @@ async def resolve_issue(
|
||||
finally:
|
||||
output_fp.close()
|
||||
logger.info('Finished.')
|
||||
return output
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -7,6 +7,7 @@ import subprocess
|
||||
import jinja2
|
||||
import litellm
|
||||
import requests
|
||||
from pydantic import BaseModel
|
||||
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
@@ -426,21 +427,36 @@ def update_existing_pull_request(
|
||||
return pr_url
|
||||
|
||||
|
||||
def process_single_issue(
|
||||
class ProcessIssueResult(BaseModel):
|
||||
success: bool
|
||||
url: str | None = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
def create_pull_request_from_resolver_output(
|
||||
output_dir: str,
|
||||
resolver_output: ResolverOutput,
|
||||
github_token: str,
|
||||
github_username: str,
|
||||
github_username: str | None,
|
||||
pr_type: str,
|
||||
llm_config: LLMConfig,
|
||||
fork_owner: str | None,
|
||||
send_on_failure: bool,
|
||||
) -> None:
|
||||
) -> ProcessIssueResult:
|
||||
if github_username is None:
|
||||
return ProcessIssueResult(
|
||||
success=False,
|
||||
error='GITHUB_USERNAME environment variable not set',
|
||||
)
|
||||
|
||||
if not resolver_output.success and not send_on_failure:
|
||||
print(
|
||||
f'Issue {resolver_output.issue.number} was not successfully resolved. Skipping PR creation.'
|
||||
)
|
||||
return
|
||||
return ProcessIssueResult(
|
||||
success=False,
|
||||
error='Issue was not successfully resolved',
|
||||
)
|
||||
|
||||
issue_type = resolver_output.issue_type
|
||||
|
||||
@@ -465,26 +481,30 @@ def process_single_issue(
|
||||
|
||||
make_commit(patched_repo_dir, resolver_output.issue, issue_type)
|
||||
|
||||
if issue_type == 'pr':
|
||||
update_existing_pull_request(
|
||||
github_issue=resolver_output.issue,
|
||||
github_token=github_token,
|
||||
github_username=github_username,
|
||||
patch_dir=patched_repo_dir,
|
||||
additional_message=resolver_output.success_explanation,
|
||||
llm_config=llm_config,
|
||||
)
|
||||
else:
|
||||
send_pull_request(
|
||||
github_issue=resolver_output.issue,
|
||||
github_token=github_token,
|
||||
github_username=github_username,
|
||||
patch_dir=patched_repo_dir,
|
||||
pr_type=pr_type,
|
||||
llm_config=llm_config,
|
||||
fork_owner=fork_owner,
|
||||
additional_message=resolver_output.success_explanation,
|
||||
)
|
||||
try:
|
||||
if issue_type == 'pr':
|
||||
url = update_existing_pull_request(
|
||||
github_issue=resolver_output.issue,
|
||||
github_token=github_token,
|
||||
github_username=github_username,
|
||||
patch_dir=patched_repo_dir,
|
||||
additional_message=resolver_output.success_explanation,
|
||||
llm_config=llm_config,
|
||||
)
|
||||
else:
|
||||
url = send_pull_request(
|
||||
github_issue=resolver_output.issue,
|
||||
github_token=github_token,
|
||||
github_username=github_username,
|
||||
patch_dir=patched_repo_dir,
|
||||
pr_type=pr_type,
|
||||
llm_config=llm_config,
|
||||
fork_owner=fork_owner,
|
||||
additional_message=resolver_output.success_explanation,
|
||||
)
|
||||
return ProcessIssueResult(success=True, url=url)
|
||||
except Exception as e:
|
||||
return ProcessIssueResult(success=False, error=str(e))
|
||||
|
||||
|
||||
def process_all_successful_issues(
|
||||
@@ -499,7 +519,7 @@ def process_all_successful_issues(
|
||||
for resolver_output in load_all_resolver_outputs(output_path):
|
||||
if resolver_output.success:
|
||||
print(f'Processing issue {resolver_output.issue.number}')
|
||||
process_single_issue(
|
||||
create_pull_request_from_resolver_output(
|
||||
output_dir,
|
||||
resolver_output,
|
||||
github_token,
|
||||
@@ -616,7 +636,7 @@ def main():
|
||||
resolver_output = load_single_resolver_output(output_path, issue_number)
|
||||
if not github_username:
|
||||
raise ValueError('Github username is required.')
|
||||
process_single_issue(
|
||||
create_pull_request_from_resolver_output(
|
||||
my_args.output_dir,
|
||||
resolver_output,
|
||||
github_token,
|
||||
|
||||
28
openhands/server/data_models/issue_models.py
Normal file
28
openhands/server/data_models/issue_models.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ResolveIssueDataModel(BaseModel):
|
||||
owner: str = Field(..., description='Github owner of the repo')
|
||||
repo: str = Field(..., description='Github repository name')
|
||||
token: str = Field(..., description='Github token to access the repository')
|
||||
username: str = Field(..., description='Github username to access the repository')
|
||||
max_iterations: int = Field(50, description='Maximum number of iterations to run')
|
||||
issue_type: Literal['issue', 'pr'] = Field(
|
||||
..., description='Type of issue to resolve (issue or pr)'
|
||||
)
|
||||
issue_number: int = Field(..., description='Issue number to resolve')
|
||||
comment_id: int | None = Field(
|
||||
None, description='Optional ID of a specific comment to focus on'
|
||||
)
|
||||
# PR-related fields
|
||||
pr_type: Literal['branch', 'draft', 'ready'] = Field(
|
||||
'draft', description='Type of PR to create (branch, draft, ready)'
|
||||
)
|
||||
fork_owner: str | None = Field(None, description='Optional owner to fork to')
|
||||
send_on_failure: bool = Field(
|
||||
False, description='Whether to send PR even if resolution failed'
|
||||
)
|
||||
|
||||
|
||||
@@ -5,28 +5,10 @@ import tempfile
|
||||
import time
|
||||
import uuid
|
||||
import warnings
|
||||
from typing import Any
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
from pathspec import PathSpec
|
||||
from pathspec.patterns import GitWildMatchPattern
|
||||
|
||||
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
|
||||
from openhands.security.options import SecurityAnalyzers
|
||||
from openhands.server.data_models.feedback import FeedbackDataModel, store_feedback
|
||||
from openhands.server.github import (
|
||||
GITHUB_CLIENT_ID,
|
||||
GITHUB_CLIENT_SECRET,
|
||||
UserVerifier,
|
||||
authenticate_github_user,
|
||||
)
|
||||
from openhands.storage import get_file_store
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
import litellm
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from fastapi import (
|
||||
BackgroundTasks,
|
||||
@@ -40,6 +22,8 @@ from fastapi import (
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.security import HTTPBearer
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pathspec import PathSpec
|
||||
from pathspec.patterns import GitWildMatchPattern
|
||||
from pydantic import BaseModel
|
||||
|
||||
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
|
||||
@@ -63,9 +47,33 @@ from openhands.events.serialization import event_to_dict
|
||||
from openhands.events.stream import AsyncEventStreamWrapper
|
||||
from openhands.llm import bedrock
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
|
||||
from openhands.security.options import SecurityAnalyzers
|
||||
from openhands.server.auth.auth import get_sid_from_token, sign_token
|
||||
from openhands.server.data_models.feedback import FeedbackDataModel, store_feedback
|
||||
from openhands.server.github import (
|
||||
GITHUB_CLIENT_ID,
|
||||
GITHUB_CLIENT_SECRET,
|
||||
UserVerifier,
|
||||
authenticate_github_user,
|
||||
)
|
||||
from openhands.server.middleware import LocalhostCORSMiddleware, NoCacheMiddleware
|
||||
from openhands.server.session import SessionManager
|
||||
from openhands.storage import get_file_store
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
import litellm
|
||||
|
||||
|
||||
from openhands.resolver.resolve_issue import resolve_issue as resolve_github_issue
|
||||
from openhands.resolver.send_pull_request import (
|
||||
create_pull_request_from_resolver_output,
|
||||
)
|
||||
from openhands.server.data_models.issue_models import (
|
||||
ResolveIssueDataModel,
|
||||
)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
@@ -452,6 +460,108 @@ async def get_security_analyzers():
|
||||
return sorted(SecurityAnalyzers.keys())
|
||||
|
||||
|
||||
@app.post('/api/resolver/resolve-issue')
|
||||
async def resolve_issue(request: Request) -> dict[str, str | dict[str, Any]]:
|
||||
"""Resolve a GitHub issue using OpenHands and create a pull request.
|
||||
|
||||
This endpoint:
|
||||
1. Analyzes the issue content and comments
|
||||
2. Makes necessary code changes
|
||||
3. Creates a pull request or branch with the changes
|
||||
|
||||
Args:
|
||||
request: The incoming request object
|
||||
|
||||
Returns:
|
||||
A dictionary containing the resolution results with keys:
|
||||
- status: 'success' or 'error'
|
||||
- output: The resolver output (on success)
|
||||
- result: PR creation result (on success)
|
||||
- message: Error message (on error)
|
||||
"""
|
||||
try:
|
||||
# Get LLM config from current session
|
||||
llm_config = config.get_llm_config()
|
||||
|
||||
# Get runtime container image from config
|
||||
runtime_container_image = config.sandbox.runtime_container_image
|
||||
if not runtime_container_image:
|
||||
raise ValueError('Runtime container image not configured')
|
||||
|
||||
# Get GitHub token from environment
|
||||
github_token = os.environ.get('GITHUB_TOKEN')
|
||||
if not github_token:
|
||||
raise ValueError('GITHUB_TOKEN environment variable not set')
|
||||
|
||||
# Get GitHub username from environment
|
||||
github_username = os.environ.get('GITHUB_USERNAME')
|
||||
|
||||
# Parse request data
|
||||
body = await request.json()
|
||||
data = ResolveIssueDataModel(**body)
|
||||
|
||||
# Create temporary output directory for any intermediate files
|
||||
output_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
# Process the issue resolution
|
||||
resolver_output = await resolve_github_issue(
|
||||
owner=data.owner,
|
||||
repo=data.repo,
|
||||
token=data.token,
|
||||
username=data.username,
|
||||
max_iterations=data.max_iterations,
|
||||
output_dir=output_dir,
|
||||
llm_config=llm_config,
|
||||
runtime_container_image=runtime_container_image,
|
||||
prompt_template='', # Using default for now
|
||||
issue_type=data.issue_type,
|
||||
repo_instruction=None,
|
||||
issue_number=data.issue_number,
|
||||
comment_id=data.comment_id,
|
||||
reset_logger=True,
|
||||
)
|
||||
finally:
|
||||
# Cleanup temp directory
|
||||
if os.path.exists(output_dir):
|
||||
import shutil
|
||||
|
||||
shutil.rmtree(output_dir)
|
||||
|
||||
if not resolver_output:
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': f'No resolver output generated for issue {data.issue_number}',
|
||||
}
|
||||
|
||||
# Create pull request
|
||||
result = create_pull_request_from_resolver_output(
|
||||
output_dir=output_dir,
|
||||
resolver_output=resolver_output,
|
||||
github_token=github_token,
|
||||
github_username=github_username,
|
||||
pr_type=data.pr_type,
|
||||
llm_config=llm_config,
|
||||
fork_owner=data.fork_owner,
|
||||
send_on_failure=data.send_on_failure,
|
||||
)
|
||||
|
||||
if result.success:
|
||||
return {
|
||||
'status': 'success',
|
||||
'output': resolver_output.model_dump(),
|
||||
'result': {'url': result.url},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'status': 'error',
|
||||
'output': resolver_output.model_dump(),
|
||||
'message': result.error or 'Unknown error',
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {'status': 'error', 'message': str(e)}
|
||||
|
||||
|
||||
FILES_TO_IGNORE = [
|
||||
'.git/',
|
||||
'.DS_Store',
|
||||
|
||||
@@ -95,6 +95,7 @@ reportlab = "*"
|
||||
[tool.coverage.run]
|
||||
concurrency = ["gevent"]
|
||||
|
||||
|
||||
[tool.poetry.group.runtime.dependencies]
|
||||
jupyterlab = "*"
|
||||
notebook = "*"
|
||||
@@ -125,6 +126,7 @@ ignore = ["D1"]
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
|
||||
[tool.poetry.group.evaluation.dependencies]
|
||||
streamlit = "*"
|
||||
whatthepatch = "*"
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
from openhands.resolver.issue_definitions import IssueHandler
|
||||
from openhands.resolver.github_issue import GithubIssue
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.resolver.github_issue import GithubIssue
|
||||
from openhands.resolver.issue_definitions import IssueHandler
|
||||
|
||||
|
||||
def test_guess_success_multiline_explanation():
|
||||
# Mock data
|
||||
issue = GithubIssue(
|
||||
owner="test",
|
||||
repo="test",
|
||||
owner='test',
|
||||
repo='test',
|
||||
number=1,
|
||||
title="Test Issue",
|
||||
body="Test body",
|
||||
title='Test Issue',
|
||||
body='Test body',
|
||||
thread_comments=None,
|
||||
review_comments=None,
|
||||
)
|
||||
history = [MessageAction(content="Test message")]
|
||||
llm_config = LLMConfig(model="test", api_key="test")
|
||||
history = [MessageAction(content='Test message')]
|
||||
llm_config = LLMConfig(model='test', api_key='test')
|
||||
|
||||
# Create a mock response with multi-line explanation
|
||||
mock_response = """--- success
|
||||
@@ -31,7 +31,7 @@ The PR successfully addressed the issue by:
|
||||
Automatic fix generated by OpenHands 🙌"""
|
||||
|
||||
# Create a handler instance
|
||||
handler = IssueHandler("test", "test", "test")
|
||||
handler = IssueHandler('test', 'test', 'test')
|
||||
|
||||
# Mock the litellm.completion call
|
||||
def mock_completion(*args, **kwargs):
|
||||
@@ -61,11 +61,11 @@ Automatic fix generated by OpenHands 🙌"""
|
||||
|
||||
# Verify the results
|
||||
assert success is True
|
||||
assert "The PR successfully addressed the issue by:" in explanation
|
||||
assert "Fixed bug A" in explanation
|
||||
assert "Added test B" in explanation
|
||||
assert "Updated documentation C" in explanation
|
||||
assert "Automatic fix generated by OpenHands" in explanation
|
||||
assert 'The PR successfully addressed the issue by:' in explanation
|
||||
assert 'Fixed bug A' in explanation
|
||||
assert 'Added test B' in explanation
|
||||
assert 'Updated documentation C' in explanation
|
||||
assert 'Automatic fix generated by OpenHands' in explanation
|
||||
finally:
|
||||
# Restore the original function
|
||||
litellm.completion = original_completion
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
from openhands.resolver.issue_definitions import IssueHandler, PRHandler
|
||||
from openhands.resolver.github_issue import GithubIssue, ReviewThread
|
||||
from openhands.events.action.message import MessageAction
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.resolver.github_issue import GithubIssue, ReviewThread
|
||||
from openhands.resolver.issue_definitions import IssueHandler, PRHandler
|
||||
|
||||
|
||||
def test_get_converted_issues_initializes_review_comments():
|
||||
# Mock the necessary dependencies
|
||||
with patch("requests.get") as mock_get:
|
||||
with patch('requests.get') as mock_get:
|
||||
# Mock the response for issues
|
||||
mock_issues_response = MagicMock()
|
||||
mock_issues_response.json.return_value = [
|
||||
{"number": 1, "title": "Test Issue", "body": "Test Body"}
|
||||
{'number': 1, 'title': 'Test Issue', 'body': 'Test Body'}
|
||||
]
|
||||
# Mock the response for comments
|
||||
mock_comments_response = MagicMock()
|
||||
@@ -26,7 +27,7 @@ def test_get_converted_issues_initializes_review_comments():
|
||||
] # Need two comment responses because we make two API calls
|
||||
|
||||
# Create an instance of IssueHandler
|
||||
handler = IssueHandler("test-owner", "test-repo", "test-token")
|
||||
handler = IssueHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Get converted issues
|
||||
issues = handler.get_converted_issues()
|
||||
@@ -39,35 +40,35 @@ def test_get_converted_issues_initializes_review_comments():
|
||||
|
||||
# Verify other fields are set correctly
|
||||
assert issues[0].number == 1
|
||||
assert issues[0].title == "Test Issue"
|
||||
assert issues[0].body == "Test Body"
|
||||
assert issues[0].owner == "test-owner"
|
||||
assert issues[0].repo == "test-repo"
|
||||
assert issues[0].title == 'Test Issue'
|
||||
assert issues[0].body == 'Test Body'
|
||||
assert issues[0].owner == 'test-owner'
|
||||
assert issues[0].repo == 'test-repo'
|
||||
|
||||
|
||||
def test_pr_handler_guess_success_with_thread_comments():
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create a mock issue with thread comments but no review comments
|
||||
issue = GithubIssue(
|
||||
owner="test-owner",
|
||||
repo="test-repo",
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=1,
|
||||
title="Test PR",
|
||||
body="Test Body",
|
||||
thread_comments=["First comment", "Second comment"],
|
||||
closing_issues=["Issue description"],
|
||||
title='Test PR',
|
||||
body='Test Body',
|
||||
thread_comments=['First comment', 'Second comment'],
|
||||
closing_issues=['Issue description'],
|
||||
review_comments=None,
|
||||
thread_ids=None,
|
||||
head_branch="test-branch",
|
||||
head_branch='test-branch',
|
||||
)
|
||||
|
||||
# Create mock history
|
||||
history = [MessageAction(content="Fixed the issue by implementing X and Y")]
|
||||
history = [MessageAction(content='Fixed the issue by implementing X and Y')]
|
||||
|
||||
# Create mock LLM config
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Mock the LLM response
|
||||
mock_response = MagicMock()
|
||||
@@ -84,7 +85,7 @@ The changes successfully address the feedback."""
|
||||
]
|
||||
|
||||
# Test the guess_success method
|
||||
with patch("litellm.completion", return_value=mock_response):
|
||||
with patch('litellm.completion', return_value=mock_response):
|
||||
success, success_list, explanation = handler.guess_success(
|
||||
issue, history, llm_config
|
||||
)
|
||||
@@ -92,39 +93,39 @@ The changes successfully address the feedback."""
|
||||
# Verify the results
|
||||
assert success is True
|
||||
assert success_list == [True]
|
||||
assert "successfully address" in explanation
|
||||
assert 'successfully address' in explanation
|
||||
|
||||
|
||||
def test_pr_handler_get_converted_issues_with_comments():
|
||||
# Mock the necessary dependencies
|
||||
with patch("requests.get") as mock_get:
|
||||
with patch('requests.get') as mock_get:
|
||||
# Mock the response for PRs
|
||||
mock_prs_response = MagicMock()
|
||||
mock_prs_response.json.return_value = [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Test PR",
|
||||
"body": "Test Body fixes #1",
|
||||
"head": {"ref": "test-branch"},
|
||||
'number': 1,
|
||||
'title': 'Test PR',
|
||||
'body': 'Test Body fixes #1',
|
||||
'head': {'ref': 'test-branch'},
|
||||
}
|
||||
]
|
||||
|
||||
# Mock the response for PR comments
|
||||
mock_comments_response = MagicMock()
|
||||
mock_comments_response.json.return_value = [
|
||||
{"body": "First comment"},
|
||||
{"body": "Second comment"},
|
||||
{'body': 'First comment'},
|
||||
{'body': 'Second comment'},
|
||||
]
|
||||
|
||||
# Mock the response for PR metadata (GraphQL)
|
||||
mock_graphql_response = MagicMock()
|
||||
mock_graphql_response.json.return_value = {
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequest": {
|
||||
"closingIssuesReferences": {"edges": []},
|
||||
"reviews": {"nodes": []},
|
||||
"reviewThreads": {"edges": []},
|
||||
'data': {
|
||||
'repository': {
|
||||
'pullRequest': {
|
||||
'closingIssuesReferences': {'edges': []},
|
||||
'reviews': {'nodes': []},
|
||||
'reviewThreads': {'edges': []},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +139,7 @@ def test_pr_handler_get_converted_issues_with_comments():
|
||||
# Mock the response for fetching the external issue referenced in PR body
|
||||
mock_external_issue_response = MagicMock()
|
||||
mock_external_issue_response.json.return_value = {
|
||||
"body": "This is additional context from an externally referenced issue."
|
||||
'body': 'This is additional context from an externally referenced issue.'
|
||||
}
|
||||
|
||||
mock_get.side_effect = [
|
||||
@@ -150,11 +151,11 @@ def test_pr_handler_get_converted_issues_with_comments():
|
||||
]
|
||||
|
||||
# Mock the post request for GraphQL
|
||||
with patch("requests.post") as mock_post:
|
||||
with patch('requests.post') as mock_post:
|
||||
mock_post.return_value = mock_graphql_response
|
||||
|
||||
# Create an instance of PRHandler
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Get converted issues
|
||||
prs = handler.get_converted_issues()
|
||||
@@ -163,43 +164,43 @@ def test_pr_handler_get_converted_issues_with_comments():
|
||||
assert len(prs) == 1
|
||||
|
||||
# Verify that thread_comments are set correctly
|
||||
assert prs[0].thread_comments == ["First comment", "Second comment"]
|
||||
assert prs[0].thread_comments == ['First comment', 'Second comment']
|
||||
|
||||
# Verify other fields are set correctly
|
||||
assert prs[0].number == 1
|
||||
assert prs[0].title == "Test PR"
|
||||
assert prs[0].body == "Test Body fixes #1"
|
||||
assert prs[0].owner == "test-owner"
|
||||
assert prs[0].repo == "test-repo"
|
||||
assert prs[0].head_branch == "test-branch"
|
||||
assert prs[0].title == 'Test PR'
|
||||
assert prs[0].body == 'Test Body fixes #1'
|
||||
assert prs[0].owner == 'test-owner'
|
||||
assert prs[0].repo == 'test-repo'
|
||||
assert prs[0].head_branch == 'test-branch'
|
||||
assert prs[0].closing_issues == [
|
||||
"This is additional context from an externally referenced issue."
|
||||
'This is additional context from an externally referenced issue.'
|
||||
]
|
||||
|
||||
|
||||
def test_pr_handler_guess_success_only_review_comments():
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create a mock issue with only review comments
|
||||
issue = GithubIssue(
|
||||
owner="test-owner",
|
||||
repo="test-repo",
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=1,
|
||||
title="Test PR",
|
||||
body="Test Body",
|
||||
title='Test PR',
|
||||
body='Test Body',
|
||||
thread_comments=None,
|
||||
closing_issues=["Issue description"],
|
||||
review_comments=["Please fix the formatting", "Add more tests"],
|
||||
closing_issues=['Issue description'],
|
||||
review_comments=['Please fix the formatting', 'Add more tests'],
|
||||
thread_ids=None,
|
||||
head_branch="test-branch",
|
||||
head_branch='test-branch',
|
||||
)
|
||||
|
||||
# Create mock history
|
||||
history = [MessageAction(content="Fixed the formatting and added more tests")]
|
||||
history = [MessageAction(content='Fixed the formatting and added more tests')]
|
||||
|
||||
# Create mock LLM config
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Mock the LLM response
|
||||
mock_response = MagicMock()
|
||||
@@ -216,7 +217,7 @@ The changes successfully address the review comments."""
|
||||
]
|
||||
|
||||
# Test the guess_success method
|
||||
with patch("litellm.completion", return_value=mock_response):
|
||||
with patch('litellm.completion', return_value=mock_response):
|
||||
success, success_list, explanation = handler.guess_success(
|
||||
issue, history, llm_config
|
||||
)
|
||||
@@ -224,32 +225,32 @@ The changes successfully address the review comments."""
|
||||
# Verify the results
|
||||
assert success is True
|
||||
assert success_list == [True]
|
||||
assert "successfully address" in explanation
|
||||
assert 'successfully address' in explanation
|
||||
|
||||
|
||||
def test_pr_handler_guess_success_no_comments():
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create a mock issue with no comments
|
||||
issue = GithubIssue(
|
||||
owner="test-owner",
|
||||
repo="test-repo",
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=1,
|
||||
title="Test PR",
|
||||
body="Test Body",
|
||||
title='Test PR',
|
||||
body='Test Body',
|
||||
thread_comments=None,
|
||||
closing_issues=["Issue description"],
|
||||
closing_issues=['Issue description'],
|
||||
review_comments=None,
|
||||
thread_ids=None,
|
||||
head_branch="test-branch",
|
||||
head_branch='test-branch',
|
||||
)
|
||||
|
||||
# Create mock history
|
||||
history = [MessageAction(content="Fixed the issue")]
|
||||
history = [MessageAction(content='Fixed the issue')]
|
||||
|
||||
# Create mock LLM config
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Test that it returns appropriate message when no comments are present
|
||||
success, success_list, explanation = handler.guess_success(
|
||||
@@ -257,29 +258,29 @@ def test_pr_handler_guess_success_no_comments():
|
||||
)
|
||||
assert success is False
|
||||
assert success_list is None
|
||||
assert explanation == "No feedback was found to process"
|
||||
assert explanation == 'No feedback was found to process'
|
||||
|
||||
|
||||
def test_get_issue_comments_with_specific_comment_id():
|
||||
# Mock the necessary dependencies
|
||||
with patch("requests.get") as mock_get:
|
||||
with patch('requests.get') as mock_get:
|
||||
# Mock the response for comments
|
||||
mock_comments_response = MagicMock()
|
||||
mock_comments_response.json.return_value = [
|
||||
{"id": 123, "body": "First comment"},
|
||||
{"id": 456, "body": "Second comment"},
|
||||
{'id': 123, 'body': 'First comment'},
|
||||
{'id': 456, 'body': 'Second comment'},
|
||||
]
|
||||
|
||||
mock_get.return_value = mock_comments_response
|
||||
|
||||
# Create an instance of IssueHandler
|
||||
handler = IssueHandler("test-owner", "test-repo", "test-token")
|
||||
handler = IssueHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Get comments with a specific comment_id
|
||||
specific_comment = handler._get_issue_comments(issue_number=1, comment_id=123)
|
||||
|
||||
# Verify only the specific comment is returned
|
||||
assert specific_comment == ["First comment"]
|
||||
assert specific_comment == ['First comment']
|
||||
|
||||
|
||||
def test_pr_handler_get_converted_issues_with_specific_thread_comment():
|
||||
@@ -287,50 +288,50 @@ def test_pr_handler_get_converted_issues_with_specific_thread_comment():
|
||||
specific_comment_id = 123
|
||||
|
||||
# Mock GraphQL response for review threads
|
||||
with patch("requests.get") as mock_get:
|
||||
with patch('requests.get') as mock_get:
|
||||
# Mock the response for PRs
|
||||
mock_prs_response = MagicMock()
|
||||
mock_prs_response.json.return_value = [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Test PR",
|
||||
"body": "Test Body",
|
||||
"head": {"ref": "test-branch"},
|
||||
'number': 1,
|
||||
'title': 'Test PR',
|
||||
'body': 'Test Body',
|
||||
'head': {'ref': 'test-branch'},
|
||||
}
|
||||
]
|
||||
|
||||
# Mock the response for PR comments
|
||||
mock_comments_response = MagicMock()
|
||||
mock_comments_response.json.return_value = [
|
||||
{"body": "First comment", "id": 123},
|
||||
{"body": "Second comment", "id": 124},
|
||||
{'body': 'First comment', 'id': 123},
|
||||
{'body': 'Second comment', 'id': 124},
|
||||
]
|
||||
|
||||
# Mock the response for PR metadata (GraphQL)
|
||||
mock_graphql_response = MagicMock()
|
||||
mock_graphql_response.json.return_value = {
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequest": {
|
||||
"closingIssuesReferences": {"edges": []},
|
||||
"reviews": {"nodes": []},
|
||||
"reviewThreads": {
|
||||
"edges": [
|
||||
'data': {
|
||||
'repository': {
|
||||
'pullRequest': {
|
||||
'closingIssuesReferences': {'edges': []},
|
||||
'reviews': {'nodes': []},
|
||||
'reviewThreads': {
|
||||
'edges': [
|
||||
{
|
||||
"node": {
|
||||
"id": "review-thread-1",
|
||||
"isResolved": False,
|
||||
"comments": {
|
||||
"nodes": [
|
||||
'node': {
|
||||
'id': 'review-thread-1',
|
||||
'isResolved': False,
|
||||
'comments': {
|
||||
'nodes': [
|
||||
{
|
||||
"fullDatabaseId": 121,
|
||||
"body": "Specific review comment",
|
||||
"path": "file1.txt",
|
||||
'fullDatabaseId': 121,
|
||||
'body': 'Specific review comment',
|
||||
'path': 'file1.txt',
|
||||
},
|
||||
{
|
||||
"fullDatabaseId": 456,
|
||||
"body": "Another review comment",
|
||||
"path": "file2.txt",
|
||||
'fullDatabaseId': 456,
|
||||
'body': 'Another review comment',
|
||||
'path': 'file2.txt',
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -356,11 +357,11 @@ def test_pr_handler_get_converted_issues_with_specific_thread_comment():
|
||||
]
|
||||
|
||||
# Mock the post request for GraphQL
|
||||
with patch("requests.post") as mock_post:
|
||||
with patch('requests.post') as mock_post:
|
||||
mock_post.return_value = mock_graphql_response
|
||||
|
||||
# Create an instance of PRHandler
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Get converted issues
|
||||
prs = handler.get_converted_issues(comment_id=specific_comment_id)
|
||||
@@ -369,17 +370,17 @@ def test_pr_handler_get_converted_issues_with_specific_thread_comment():
|
||||
assert len(prs) == 1
|
||||
|
||||
# Verify that thread_comments are set correctly
|
||||
assert prs[0].thread_comments == ["First comment"]
|
||||
assert prs[0].thread_comments == ['First comment']
|
||||
assert prs[0].review_comments == []
|
||||
assert prs[0].review_threads == []
|
||||
|
||||
# Verify other fields are set correctly
|
||||
assert prs[0].number == 1
|
||||
assert prs[0].title == "Test PR"
|
||||
assert prs[0].body == "Test Body"
|
||||
assert prs[0].owner == "test-owner"
|
||||
assert prs[0].repo == "test-repo"
|
||||
assert prs[0].head_branch == "test-branch"
|
||||
assert prs[0].title == 'Test PR'
|
||||
assert prs[0].body == 'Test Body'
|
||||
assert prs[0].owner == 'test-owner'
|
||||
assert prs[0].repo == 'test-repo'
|
||||
assert prs[0].head_branch == 'test-branch'
|
||||
|
||||
|
||||
def test_pr_handler_get_converted_issues_with_specific_review_thread_comment():
|
||||
@@ -387,50 +388,50 @@ def test_pr_handler_get_converted_issues_with_specific_review_thread_comment():
|
||||
specific_comment_id = 123
|
||||
|
||||
# Mock GraphQL response for review threads
|
||||
with patch("requests.get") as mock_get:
|
||||
with patch('requests.get') as mock_get:
|
||||
# Mock the response for PRs
|
||||
mock_prs_response = MagicMock()
|
||||
mock_prs_response.json.return_value = [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Test PR",
|
||||
"body": "Test Body",
|
||||
"head": {"ref": "test-branch"},
|
||||
'number': 1,
|
||||
'title': 'Test PR',
|
||||
'body': 'Test Body',
|
||||
'head': {'ref': 'test-branch'},
|
||||
}
|
||||
]
|
||||
|
||||
# Mock the response for PR comments
|
||||
mock_comments_response = MagicMock()
|
||||
mock_comments_response.json.return_value = [
|
||||
{"body": "First comment", "id": 120},
|
||||
{"body": "Second comment", "id": 124},
|
||||
{'body': 'First comment', 'id': 120},
|
||||
{'body': 'Second comment', 'id': 124},
|
||||
]
|
||||
|
||||
# Mock the response for PR metadata (GraphQL)
|
||||
mock_graphql_response = MagicMock()
|
||||
mock_graphql_response.json.return_value = {
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequest": {
|
||||
"closingIssuesReferences": {"edges": []},
|
||||
"reviews": {"nodes": []},
|
||||
"reviewThreads": {
|
||||
"edges": [
|
||||
'data': {
|
||||
'repository': {
|
||||
'pullRequest': {
|
||||
'closingIssuesReferences': {'edges': []},
|
||||
'reviews': {'nodes': []},
|
||||
'reviewThreads': {
|
||||
'edges': [
|
||||
{
|
||||
"node": {
|
||||
"id": "review-thread-1",
|
||||
"isResolved": False,
|
||||
"comments": {
|
||||
"nodes": [
|
||||
'node': {
|
||||
'id': 'review-thread-1',
|
||||
'isResolved': False,
|
||||
'comments': {
|
||||
'nodes': [
|
||||
{
|
||||
"fullDatabaseId": specific_comment_id,
|
||||
"body": "Specific review comment",
|
||||
"path": "file1.txt",
|
||||
'fullDatabaseId': specific_comment_id,
|
||||
'body': 'Specific review comment',
|
||||
'path': 'file1.txt',
|
||||
},
|
||||
{
|
||||
"fullDatabaseId": 456,
|
||||
"body": "Another review comment",
|
||||
"path": "file1.txt",
|
||||
'fullDatabaseId': 456,
|
||||
'body': 'Another review comment',
|
||||
'path': 'file1.txt',
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -456,11 +457,11 @@ def test_pr_handler_get_converted_issues_with_specific_review_thread_comment():
|
||||
]
|
||||
|
||||
# Mock the post request for GraphQL
|
||||
with patch("requests.post") as mock_post:
|
||||
with patch('requests.post') as mock_post:
|
||||
mock_post.return_value = mock_graphql_response
|
||||
|
||||
# Create an instance of PRHandler
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Get converted issues
|
||||
prs = handler.get_converted_issues(comment_id=specific_comment_id)
|
||||
@@ -475,17 +476,17 @@ def test_pr_handler_get_converted_issues_with_specific_review_thread_comment():
|
||||
assert isinstance(prs[0].review_threads[0], ReviewThread)
|
||||
assert (
|
||||
prs[0].review_threads[0].comment
|
||||
== "Specific review comment\n---\nlatest feedback:\nAnother review comment\n"
|
||||
== 'Specific review comment\n---\nlatest feedback:\nAnother review comment\n'
|
||||
)
|
||||
assert prs[0].review_threads[0].files == ["file1.txt"]
|
||||
assert prs[0].review_threads[0].files == ['file1.txt']
|
||||
|
||||
# Verify other fields are set correctly
|
||||
assert prs[0].number == 1
|
||||
assert prs[0].title == "Test PR"
|
||||
assert prs[0].body == "Test Body"
|
||||
assert prs[0].owner == "test-owner"
|
||||
assert prs[0].repo == "test-repo"
|
||||
assert prs[0].head_branch == "test-branch"
|
||||
assert prs[0].title == 'Test PR'
|
||||
assert prs[0].body == 'Test Body'
|
||||
assert prs[0].owner == 'test-owner'
|
||||
assert prs[0].repo == 'test-repo'
|
||||
assert prs[0].head_branch == 'test-branch'
|
||||
|
||||
|
||||
def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs():
|
||||
@@ -493,50 +494,50 @@ def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs():
|
||||
specific_comment_id = 123
|
||||
|
||||
# Mock GraphQL response for review threads
|
||||
with patch("requests.get") as mock_get:
|
||||
with patch('requests.get') as mock_get:
|
||||
# Mock the response for PRs
|
||||
mock_prs_response = MagicMock()
|
||||
mock_prs_response.json.return_value = [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Test PR fixes #3",
|
||||
"body": "Test Body",
|
||||
"head": {"ref": "test-branch"},
|
||||
'number': 1,
|
||||
'title': 'Test PR fixes #3',
|
||||
'body': 'Test Body',
|
||||
'head': {'ref': 'test-branch'},
|
||||
}
|
||||
]
|
||||
|
||||
# Mock the response for PR comments
|
||||
mock_comments_response = MagicMock()
|
||||
mock_comments_response.json.return_value = [
|
||||
{"body": "First comment", "id": 120},
|
||||
{"body": "Second comment", "id": 124},
|
||||
{'body': 'First comment', 'id': 120},
|
||||
{'body': 'Second comment', 'id': 124},
|
||||
]
|
||||
|
||||
# Mock the response for PR metadata (GraphQL)
|
||||
mock_graphql_response = MagicMock()
|
||||
mock_graphql_response.json.return_value = {
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequest": {
|
||||
"closingIssuesReferences": {"edges": []},
|
||||
"reviews": {"nodes": []},
|
||||
"reviewThreads": {
|
||||
"edges": [
|
||||
'data': {
|
||||
'repository': {
|
||||
'pullRequest': {
|
||||
'closingIssuesReferences': {'edges': []},
|
||||
'reviews': {'nodes': []},
|
||||
'reviewThreads': {
|
||||
'edges': [
|
||||
{
|
||||
"node": {
|
||||
"id": "review-thread-1",
|
||||
"isResolved": False,
|
||||
"comments": {
|
||||
"nodes": [
|
||||
'node': {
|
||||
'id': 'review-thread-1',
|
||||
'isResolved': False,
|
||||
'comments': {
|
||||
'nodes': [
|
||||
{
|
||||
"fullDatabaseId": specific_comment_id,
|
||||
"body": "Specific review comment that references #6",
|
||||
"path": "file1.txt",
|
||||
'fullDatabaseId': specific_comment_id,
|
||||
'body': 'Specific review comment that references #6',
|
||||
'path': 'file1.txt',
|
||||
},
|
||||
{
|
||||
"fullDatabaseId": 456,
|
||||
"body": "Another review comment referencing #7",
|
||||
"path": "file2.txt",
|
||||
'fullDatabaseId': 456,
|
||||
'body': 'Another review comment referencing #7',
|
||||
'path': 'file2.txt',
|
||||
},
|
||||
]
|
||||
},
|
||||
@@ -557,13 +558,13 @@ def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs():
|
||||
# Mock the response for fetching the external issue referenced in PR body
|
||||
mock_external_issue_response_in_body = MagicMock()
|
||||
mock_external_issue_response_in_body.json.return_value = {
|
||||
"body": "External context #1."
|
||||
'body': 'External context #1.'
|
||||
}
|
||||
|
||||
# Mock the response for fetching the external issue referenced in review thread
|
||||
mock_external_issue_response_review_thread = MagicMock()
|
||||
mock_external_issue_response_review_thread.json.return_value = {
|
||||
"body": "External context #2."
|
||||
'body': 'External context #2.'
|
||||
}
|
||||
|
||||
mock_get.side_effect = [
|
||||
@@ -576,11 +577,11 @@ def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs():
|
||||
]
|
||||
|
||||
# Mock the post request for GraphQL
|
||||
with patch("requests.post") as mock_post:
|
||||
with patch('requests.post') as mock_post:
|
||||
mock_post.return_value = mock_graphql_response
|
||||
|
||||
# Create an instance of PRHandler
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Get converted issues
|
||||
prs = handler.get_converted_issues(comment_id=specific_comment_id)
|
||||
@@ -595,52 +596,52 @@ def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs():
|
||||
assert isinstance(prs[0].review_threads[0], ReviewThread)
|
||||
assert (
|
||||
prs[0].review_threads[0].comment
|
||||
== "Specific review comment that references #6\n---\nlatest feedback:\nAnother review comment referencing #7\n"
|
||||
== 'Specific review comment that references #6\n---\nlatest feedback:\nAnother review comment referencing #7\n'
|
||||
)
|
||||
assert prs[0].closing_issues == [
|
||||
"External context #1.",
|
||||
"External context #2.",
|
||||
'External context #1.',
|
||||
'External context #2.',
|
||||
] # Only includes references inside comment ID and body PR
|
||||
|
||||
# Verify other fields are set correctly
|
||||
assert prs[0].number == 1
|
||||
assert prs[0].title == "Test PR fixes #3"
|
||||
assert prs[0].body == "Test Body"
|
||||
assert prs[0].owner == "test-owner"
|
||||
assert prs[0].repo == "test-repo"
|
||||
assert prs[0].head_branch == "test-branch"
|
||||
assert prs[0].title == 'Test PR fixes #3'
|
||||
assert prs[0].body == 'Test Body'
|
||||
assert prs[0].owner == 'test-owner'
|
||||
assert prs[0].repo == 'test-repo'
|
||||
assert prs[0].head_branch == 'test-branch'
|
||||
|
||||
|
||||
def test_pr_handler_get_converted_issues_with_duplicate_issue_refs():
|
||||
# Mock the necessary dependencies
|
||||
with patch("requests.get") as mock_get:
|
||||
with patch('requests.get') as mock_get:
|
||||
# Mock the response for PRs
|
||||
mock_prs_response = MagicMock()
|
||||
mock_prs_response.json.return_value = [
|
||||
{
|
||||
"number": 1,
|
||||
"title": "Test PR",
|
||||
"body": "Test Body fixes #1",
|
||||
"head": {"ref": "test-branch"},
|
||||
'number': 1,
|
||||
'title': 'Test PR',
|
||||
'body': 'Test Body fixes #1',
|
||||
'head': {'ref': 'test-branch'},
|
||||
}
|
||||
]
|
||||
|
||||
# Mock the response for PR comments
|
||||
mock_comments_response = MagicMock()
|
||||
mock_comments_response.json.return_value = [
|
||||
{"body": "First comment addressing #1"},
|
||||
{"body": "Second comment addressing #2"},
|
||||
{'body': 'First comment addressing #1'},
|
||||
{'body': 'Second comment addressing #2'},
|
||||
]
|
||||
|
||||
# Mock the response for PR metadata (GraphQL)
|
||||
mock_graphql_response = MagicMock()
|
||||
mock_graphql_response.json.return_value = {
|
||||
"data": {
|
||||
"repository": {
|
||||
"pullRequest": {
|
||||
"closingIssuesReferences": {"edges": []},
|
||||
"reviews": {"nodes": []},
|
||||
"reviewThreads": {"edges": []},
|
||||
'data': {
|
||||
'repository': {
|
||||
'pullRequest': {
|
||||
'closingIssuesReferences': {'edges': []},
|
||||
'reviews': {'nodes': []},
|
||||
'reviewThreads': {'edges': []},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,13 +655,13 @@ def test_pr_handler_get_converted_issues_with_duplicate_issue_refs():
|
||||
# Mock the response for fetching the external issue referenced in PR body
|
||||
mock_external_issue_response_in_body = MagicMock()
|
||||
mock_external_issue_response_in_body.json.return_value = {
|
||||
"body": "External context #1."
|
||||
'body': 'External context #1.'
|
||||
}
|
||||
|
||||
# Mock the response for fetching the external issue referenced in review thread
|
||||
mock_external_issue_response_in_comment = MagicMock()
|
||||
mock_external_issue_response_in_comment.json.return_value = {
|
||||
"body": "External context #2."
|
||||
'body': 'External context #2.'
|
||||
}
|
||||
|
||||
mock_get.side_effect = [
|
||||
@@ -673,11 +674,11 @@ def test_pr_handler_get_converted_issues_with_duplicate_issue_refs():
|
||||
]
|
||||
|
||||
# Mock the post request for GraphQL
|
||||
with patch("requests.post") as mock_post:
|
||||
with patch('requests.post') as mock_post:
|
||||
mock_post.return_value = mock_graphql_response
|
||||
|
||||
# Create an instance of PRHandler
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Get converted issues
|
||||
prs = handler.get_converted_issues()
|
||||
@@ -687,18 +688,18 @@ def test_pr_handler_get_converted_issues_with_duplicate_issue_refs():
|
||||
|
||||
# Verify that thread_comments are set correctly
|
||||
assert prs[0].thread_comments == [
|
||||
"First comment addressing #1",
|
||||
"Second comment addressing #2",
|
||||
'First comment addressing #1',
|
||||
'Second comment addressing #2',
|
||||
]
|
||||
|
||||
# Verify other fields are set correctly
|
||||
assert prs[0].number == 1
|
||||
assert prs[0].title == "Test PR"
|
||||
assert prs[0].body == "Test Body fixes #1"
|
||||
assert prs[0].owner == "test-owner"
|
||||
assert prs[0].repo == "test-repo"
|
||||
assert prs[0].head_branch == "test-branch"
|
||||
assert prs[0].title == 'Test PR'
|
||||
assert prs[0].body == 'Test Body fixes #1'
|
||||
assert prs[0].owner == 'test-owner'
|
||||
assert prs[0].repo == 'test-repo'
|
||||
assert prs[0].head_branch == 'test-branch'
|
||||
assert prs[0].closing_issues == [
|
||||
"External context #1.",
|
||||
"External context #2.",
|
||||
'External context #1.',
|
||||
'External context #2.',
|
||||
]
|
||||
|
||||
@@ -1,94 +1,97 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import requests
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from openhands.resolver.issue_definitions import PRHandler
|
||||
from openhands.resolver.github_issue import ReviewThread
|
||||
|
||||
|
||||
def test_handle_nonexistent_issue_reference():
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Mock the requests.get to simulate a 404 error
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Client Error: Not Found")
|
||||
|
||||
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
|
||||
'404 Client Error: Not Found'
|
||||
)
|
||||
|
||||
with patch('requests.get', return_value=mock_response):
|
||||
# Call the method with a non-existent issue reference
|
||||
result = handler._PRHandler__get_context_from_external_issues_references(
|
||||
closing_issues=[],
|
||||
closing_issue_numbers=[],
|
||||
issue_body="This references #999999", # Non-existent issue
|
||||
issue_body='This references #999999', # Non-existent issue
|
||||
review_comments=[],
|
||||
review_threads=[],
|
||||
thread_comments=None
|
||||
thread_comments=None,
|
||||
)
|
||||
|
||||
|
||||
# The method should return an empty list since the referenced issue couldn't be fetched
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_handle_rate_limit_error():
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Mock the requests.get to simulate a rate limit error
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
|
||||
"403 Client Error: Rate Limit Exceeded"
|
||||
'403 Client Error: Rate Limit Exceeded'
|
||||
)
|
||||
|
||||
|
||||
with patch('requests.get', return_value=mock_response):
|
||||
# Call the method with an issue reference
|
||||
result = handler._PRHandler__get_context_from_external_issues_references(
|
||||
closing_issues=[],
|
||||
closing_issue_numbers=[],
|
||||
issue_body="This references #123",
|
||||
issue_body='This references #123',
|
||||
review_comments=[],
|
||||
review_threads=[],
|
||||
thread_comments=None
|
||||
thread_comments=None,
|
||||
)
|
||||
|
||||
|
||||
# The method should return an empty list since the request was rate limited
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_handle_network_error():
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Mock the requests.get to simulate a network error
|
||||
with patch('requests.get', side_effect=requests.exceptions.ConnectionError("Network Error")):
|
||||
with patch(
|
||||
'requests.get', side_effect=requests.exceptions.ConnectionError('Network Error')
|
||||
):
|
||||
# Call the method with an issue reference
|
||||
result = handler._PRHandler__get_context_from_external_issues_references(
|
||||
closing_issues=[],
|
||||
closing_issue_numbers=[],
|
||||
issue_body="This references #123",
|
||||
issue_body='This references #123',
|
||||
review_comments=[],
|
||||
review_threads=[],
|
||||
thread_comments=None
|
||||
thread_comments=None,
|
||||
)
|
||||
|
||||
|
||||
# The method should return an empty list since the network request failed
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_successful_issue_reference():
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Mock a successful response
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = {"body": "This is the referenced issue body"}
|
||||
|
||||
mock_response.json.return_value = {'body': 'This is the referenced issue body'}
|
||||
|
||||
with patch('requests.get', return_value=mock_response):
|
||||
# Call the method with an issue reference
|
||||
result = handler._PRHandler__get_context_from_external_issues_references(
|
||||
closing_issues=[],
|
||||
closing_issue_numbers=[],
|
||||
issue_body="This references #123",
|
||||
issue_body='This references #123',
|
||||
review_comments=[],
|
||||
review_threads=[],
|
||||
thread_comments=None
|
||||
thread_comments=None,
|
||||
)
|
||||
|
||||
|
||||
# The method should return a list with the referenced issue body
|
||||
assert result == ["This is the referenced issue body"]
|
||||
assert result == ['This is the referenced issue body']
|
||||
|
||||
@@ -2,13 +2,13 @@ from openhands.resolver.issue_definitions import IssueHandler
|
||||
|
||||
|
||||
def test_extract_issue_references():
|
||||
handler = IssueHandler("test-owner", "test-repo", "test-token")
|
||||
handler = IssueHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Test basic issue reference
|
||||
assert handler._extract_issue_references("Fixes #123") == [123]
|
||||
assert handler._extract_issue_references('Fixes #123') == [123]
|
||||
|
||||
# Test multiple issue references
|
||||
assert handler._extract_issue_references("Fixes #123, #456") == [123, 456]
|
||||
assert handler._extract_issue_references('Fixes #123, #456') == [123, 456]
|
||||
|
||||
# Test issue references in code blocks should be ignored
|
||||
assert handler._extract_issue_references("""
|
||||
@@ -22,13 +22,21 @@ def test_extract_issue_references():
|
||||
""") == [789]
|
||||
|
||||
# Test issue references in inline code should be ignored
|
||||
assert handler._extract_issue_references("This `#123` should be ignored but #456 should be extracted") == [456]
|
||||
assert handler._extract_issue_references(
|
||||
'This `#123` should be ignored but #456 should be extracted'
|
||||
) == [456]
|
||||
|
||||
# Test issue references in URLs should be ignored
|
||||
assert handler._extract_issue_references("Check http://example.com/#123 but #456 should be extracted") == [456]
|
||||
assert handler._extract_issue_references(
|
||||
'Check http://example.com/#123 but #456 should be extracted'
|
||||
) == [456]
|
||||
|
||||
# Test issue references in markdown links should be extracted
|
||||
assert handler._extract_issue_references("[Link to #123](http://example.com) and #456") == [123, 456]
|
||||
assert handler._extract_issue_references(
|
||||
'[Link to #123](http://example.com) and #456'
|
||||
) == [123, 456]
|
||||
|
||||
# Test issue references with text around them
|
||||
assert handler._extract_issue_references("Issue #123 is fixed and #456 is pending") == [123, 456]
|
||||
assert handler._extract_issue_references(
|
||||
'Issue #123 is fixed and #456 is pending'
|
||||
) == [123, 456]
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from openhands.resolver.issue_definitions import PRHandler
|
||||
from openhands.resolver.github_issue import GithubIssue, ReviewThread
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.events.action.message import MessageAction
|
||||
from openhands.resolver.github_issue import GithubIssue, ReviewThread
|
||||
from openhands.resolver.issue_definitions import PRHandler
|
||||
|
||||
|
||||
def test_guess_success_review_threads_litellm_call():
|
||||
"""Test that the litellm.completion() call for review threads contains the expected content."""
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create a mock issue with review threads
|
||||
issue = GithubIssue(
|
||||
owner="test-owner",
|
||||
repo="test-repo",
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=1,
|
||||
title="Test PR",
|
||||
body="Test Body",
|
||||
title='Test PR',
|
||||
body='Test Body',
|
||||
thread_comments=None,
|
||||
closing_issues=["Issue 1 description", "Issue 2 description"],
|
||||
closing_issues=['Issue 1 description', 'Issue 2 description'],
|
||||
review_comments=None,
|
||||
review_threads=[
|
||||
ReviewThread(
|
||||
comment="Please fix the formatting\n---\nlatest feedback:\nAdd docstrings",
|
||||
files=["/src/file1.py", "/src/file2.py"],
|
||||
comment='Please fix the formatting\n---\nlatest feedback:\nAdd docstrings',
|
||||
files=['/src/file1.py', '/src/file2.py'],
|
||||
),
|
||||
ReviewThread(
|
||||
comment="Add more tests\n---\nlatest feedback:\nAdd test cases",
|
||||
files=["/tests/test_file.py"],
|
||||
comment='Add more tests\n---\nlatest feedback:\nAdd test cases',
|
||||
files=['/tests/test_file.py'],
|
||||
),
|
||||
],
|
||||
thread_ids=["1", "2"],
|
||||
head_branch="test-branch",
|
||||
thread_ids=['1', '2'],
|
||||
head_branch='test-branch',
|
||||
)
|
||||
|
||||
# Create mock history with a detailed response
|
||||
@@ -47,7 +47,7 @@ def test_guess_success_review_threads_litellm_call():
|
||||
]
|
||||
|
||||
# Create mock LLM config
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Mock the LLM response
|
||||
mock_response = MagicMock()
|
||||
@@ -64,7 +64,7 @@ The changes successfully address the feedback."""
|
||||
]
|
||||
|
||||
# Test the guess_success method
|
||||
with patch("litellm.completion") as mock_completion:
|
||||
with patch('litellm.completion') as mock_completion:
|
||||
mock_completion.return_value = mock_response
|
||||
success, success_list, explanation = handler.guess_success(
|
||||
issue, history, llm_config
|
||||
@@ -75,63 +75,63 @@ The changes successfully address the feedback."""
|
||||
|
||||
# Check first call
|
||||
first_call = mock_completion.call_args_list[0]
|
||||
first_prompt = first_call[1]["messages"][0]["content"]
|
||||
first_prompt = first_call[1]['messages'][0]['content']
|
||||
assert (
|
||||
"Issue descriptions:\n"
|
||||
+ json.dumps(["Issue 1 description", "Issue 2 description"], indent=4)
|
||||
'Issue descriptions:\n'
|
||||
+ json.dumps(['Issue 1 description', 'Issue 2 description'], indent=4)
|
||||
in first_prompt
|
||||
)
|
||||
assert (
|
||||
"Feedback:\nPlease fix the formatting\n---\nlatest feedback:\nAdd docstrings"
|
||||
'Feedback:\nPlease fix the formatting\n---\nlatest feedback:\nAdd docstrings'
|
||||
in first_prompt
|
||||
)
|
||||
assert (
|
||||
"Files locations:\n"
|
||||
+ json.dumps(["/src/file1.py", "/src/file2.py"], indent=4)
|
||||
'Files locations:\n'
|
||||
+ json.dumps(['/src/file1.py', '/src/file2.py'], indent=4)
|
||||
in first_prompt
|
||||
)
|
||||
assert "Last message from AI agent:\n" + history[0].content in first_prompt
|
||||
assert 'Last message from AI agent:\n' + history[0].content in first_prompt
|
||||
|
||||
# Check second call
|
||||
second_call = mock_completion.call_args_list[1]
|
||||
second_prompt = second_call[1]["messages"][0]["content"]
|
||||
second_prompt = second_call[1]['messages'][0]['content']
|
||||
assert (
|
||||
"Issue descriptions:\n"
|
||||
+ json.dumps(["Issue 1 description", "Issue 2 description"], indent=4)
|
||||
'Issue descriptions:\n'
|
||||
+ json.dumps(['Issue 1 description', 'Issue 2 description'], indent=4)
|
||||
in second_prompt
|
||||
)
|
||||
assert (
|
||||
"Feedback:\nAdd more tests\n---\nlatest feedback:\nAdd test cases"
|
||||
'Feedback:\nAdd more tests\n---\nlatest feedback:\nAdd test cases'
|
||||
in second_prompt
|
||||
)
|
||||
assert (
|
||||
"Files locations:\n" + json.dumps(["/tests/test_file.py"], indent=4)
|
||||
'Files locations:\n' + json.dumps(['/tests/test_file.py'], indent=4)
|
||||
in second_prompt
|
||||
)
|
||||
assert "Last message from AI agent:\n" + history[0].content in second_prompt
|
||||
assert 'Last message from AI agent:\n' + history[0].content in second_prompt
|
||||
|
||||
|
||||
def test_guess_success_thread_comments_litellm_call():
|
||||
"""Test that the litellm.completion() call for thread comments contains the expected content."""
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create a mock issue with thread comments
|
||||
issue = GithubIssue(
|
||||
owner="test-owner",
|
||||
repo="test-repo",
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=1,
|
||||
title="Test PR",
|
||||
body="Test Body",
|
||||
title='Test PR',
|
||||
body='Test Body',
|
||||
thread_comments=[
|
||||
"Please improve error handling",
|
||||
"Add input validation",
|
||||
"latest feedback:\nHandle edge cases",
|
||||
'Please improve error handling',
|
||||
'Add input validation',
|
||||
'latest feedback:\nHandle edge cases',
|
||||
],
|
||||
closing_issues=["Issue 1 description", "Issue 2 description"],
|
||||
closing_issues=['Issue 1 description', 'Issue 2 description'],
|
||||
review_comments=None,
|
||||
thread_ids=None,
|
||||
head_branch="test-branch",
|
||||
head_branch='test-branch',
|
||||
)
|
||||
|
||||
# Create mock history with a detailed response
|
||||
@@ -145,7 +145,7 @@ def test_guess_success_thread_comments_litellm_call():
|
||||
]
|
||||
|
||||
# Create mock LLM config
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Mock the LLM response
|
||||
mock_response = MagicMock()
|
||||
@@ -162,7 +162,7 @@ The changes successfully address the feedback."""
|
||||
]
|
||||
|
||||
# Test the guess_success method
|
||||
with patch("litellm.completion") as mock_completion:
|
||||
with patch('litellm.completion') as mock_completion:
|
||||
mock_completion.return_value = mock_response
|
||||
success, success_list, explanation = handler.guess_success(
|
||||
issue, history, llm_config
|
||||
@@ -171,77 +171,77 @@ The changes successfully address the feedback."""
|
||||
# Verify the litellm.completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]["messages"][0]["content"]
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
|
||||
# Check prompt content
|
||||
assert (
|
||||
"Issue descriptions:\n"
|
||||
+ json.dumps(["Issue 1 description", "Issue 2 description"], indent=4)
|
||||
'Issue descriptions:\n'
|
||||
+ json.dumps(['Issue 1 description', 'Issue 2 description'], indent=4)
|
||||
in prompt
|
||||
)
|
||||
assert "PR Thread Comments:\n" + "\n---\n".join(issue.thread_comments) in prompt
|
||||
assert "Last message from AI agent:\n" + history[0].content in prompt
|
||||
assert 'PR Thread Comments:\n' + '\n---\n'.join(issue.thread_comments) in prompt
|
||||
assert 'Last message from AI agent:\n' + history[0].content in prompt
|
||||
|
||||
|
||||
def test_check_feedback_with_llm():
|
||||
"""Test the _check_feedback_with_llm helper function."""
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create mock LLM config
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Test cases for different LLM responses
|
||||
test_cases = [
|
||||
{
|
||||
"response": "--- success\ntrue\n--- explanation\nChanges look good",
|
||||
"expected": (True, "Changes look good"),
|
||||
'response': '--- success\ntrue\n--- explanation\nChanges look good',
|
||||
'expected': (True, 'Changes look good'),
|
||||
},
|
||||
{
|
||||
"response": "--- success\nfalse\n--- explanation\nNot all issues fixed",
|
||||
"expected": (False, "Not all issues fixed"),
|
||||
'response': '--- success\nfalse\n--- explanation\nNot all issues fixed',
|
||||
'expected': (False, 'Not all issues fixed'),
|
||||
},
|
||||
{
|
||||
"response": "Invalid response format",
|
||||
"expected": (
|
||||
'response': 'Invalid response format',
|
||||
'expected': (
|
||||
False,
|
||||
"Failed to decode answer from LLM response: Invalid response format",
|
||||
'Failed to decode answer from LLM response: Invalid response format',
|
||||
),
|
||||
},
|
||||
{
|
||||
"response": "--- success\ntrue\n--- explanation\nMultiline\nexplanation\nhere",
|
||||
"expected": (True, "Multiline\nexplanation\nhere"),
|
||||
'response': '--- success\ntrue\n--- explanation\nMultiline\nexplanation\nhere',
|
||||
'expected': (True, 'Multiline\nexplanation\nhere'),
|
||||
},
|
||||
]
|
||||
|
||||
for case in test_cases:
|
||||
# Mock the LLM response
|
||||
mock_response = MagicMock()
|
||||
mock_response.choices = [MagicMock(message=MagicMock(content=case["response"]))]
|
||||
mock_response.choices = [MagicMock(message=MagicMock(content=case['response']))]
|
||||
|
||||
# Test the function
|
||||
with patch("litellm.completion", return_value=mock_response):
|
||||
with patch('litellm.completion', return_value=mock_response):
|
||||
success, explanation = handler._check_feedback_with_llm(
|
||||
"test prompt", llm_config
|
||||
'test prompt', llm_config
|
||||
)
|
||||
assert (success, explanation) == case["expected"]
|
||||
assert (success, explanation) == case['expected']
|
||||
|
||||
|
||||
def test_check_review_thread():
|
||||
"""Test the _check_review_thread helper function."""
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create test data
|
||||
review_thread = ReviewThread(
|
||||
comment="Please fix the formatting\n---\nlatest feedback:\nAdd docstrings",
|
||||
files=["/src/file1.py", "/src/file2.py"],
|
||||
comment='Please fix the formatting\n---\nlatest feedback:\nAdd docstrings',
|
||||
files=['/src/file1.py', '/src/file2.py'],
|
||||
)
|
||||
issues_context = json.dumps(
|
||||
["Issue 1 description", "Issue 2 description"], indent=4
|
||||
['Issue 1 description', 'Issue 2 description'], indent=4
|
||||
)
|
||||
last_message = "I have fixed the formatting and added docstrings"
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
last_message = 'I have fixed the formatting and added docstrings'
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Mock the LLM response
|
||||
mock_response = MagicMock()
|
||||
@@ -258,7 +258,7 @@ Changes look good"""
|
||||
]
|
||||
|
||||
# Test the function
|
||||
with patch("litellm.completion") as mock_completion:
|
||||
with patch('litellm.completion') as mock_completion:
|
||||
mock_completion.return_value = mock_response
|
||||
success, explanation = handler._check_review_thread(
|
||||
review_thread, issues_context, last_message, llm_config
|
||||
@@ -267,37 +267,37 @@ Changes look good"""
|
||||
# Verify the litellm.completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]["messages"][0]["content"]
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
|
||||
# Check prompt content
|
||||
assert "Issue descriptions:\n" + issues_context in prompt
|
||||
assert "Feedback:\n" + review_thread.comment in prompt
|
||||
assert 'Issue descriptions:\n' + issues_context in prompt
|
||||
assert 'Feedback:\n' + review_thread.comment in prompt
|
||||
assert (
|
||||
"Files locations:\n" + json.dumps(review_thread.files, indent=4) in prompt
|
||||
'Files locations:\n' + json.dumps(review_thread.files, indent=4) in prompt
|
||||
)
|
||||
assert "Last message from AI agent:\n" + last_message in prompt
|
||||
assert 'Last message from AI agent:\n' + last_message in prompt
|
||||
|
||||
# Check result
|
||||
assert success is True
|
||||
assert explanation == "Changes look good"
|
||||
assert explanation == 'Changes look good'
|
||||
|
||||
|
||||
def test_check_thread_comments():
|
||||
"""Test the _check_thread_comments helper function."""
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create test data
|
||||
thread_comments = [
|
||||
"Please improve error handling",
|
||||
"Add input validation",
|
||||
"latest feedback:\nHandle edge cases",
|
||||
'Please improve error handling',
|
||||
'Add input validation',
|
||||
'latest feedback:\nHandle edge cases',
|
||||
]
|
||||
issues_context = json.dumps(
|
||||
["Issue 1 description", "Issue 2 description"], indent=4
|
||||
['Issue 1 description', 'Issue 2 description'], indent=4
|
||||
)
|
||||
last_message = "I have added error handling and input validation"
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
last_message = 'I have added error handling and input validation'
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Mock the LLM response
|
||||
mock_response = MagicMock()
|
||||
@@ -314,7 +314,7 @@ Changes look good"""
|
||||
]
|
||||
|
||||
# Test the function
|
||||
with patch("litellm.completion") as mock_completion:
|
||||
with patch('litellm.completion') as mock_completion:
|
||||
mock_completion.return_value = mock_response
|
||||
success, explanation = handler._check_thread_comments(
|
||||
thread_comments, issues_context, last_message, llm_config
|
||||
@@ -323,34 +323,34 @@ Changes look good"""
|
||||
# Verify the litellm.completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]["messages"][0]["content"]
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
|
||||
# Check prompt content
|
||||
assert "Issue descriptions:\n" + issues_context in prompt
|
||||
assert "PR Thread Comments:\n" + "\n---\n".join(thread_comments) in prompt
|
||||
assert "Last message from AI agent:\n" + last_message in prompt
|
||||
assert 'Issue descriptions:\n' + issues_context in prompt
|
||||
assert 'PR Thread Comments:\n' + '\n---\n'.join(thread_comments) in prompt
|
||||
assert 'Last message from AI agent:\n' + last_message in prompt
|
||||
|
||||
# Check result
|
||||
assert success is True
|
||||
assert explanation == "Changes look good"
|
||||
assert explanation == 'Changes look good'
|
||||
|
||||
|
||||
def test_check_review_comments():
|
||||
"""Test the _check_review_comments helper function."""
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create test data
|
||||
review_comments = [
|
||||
"Please improve code readability",
|
||||
"Add comments to complex functions",
|
||||
"Follow PEP 8 style guide",
|
||||
'Please improve code readability',
|
||||
'Add comments to complex functions',
|
||||
'Follow PEP 8 style guide',
|
||||
]
|
||||
issues_context = json.dumps(
|
||||
["Issue 1 description", "Issue 2 description"], indent=4
|
||||
['Issue 1 description', 'Issue 2 description'], indent=4
|
||||
)
|
||||
last_message = "I have improved code readability and added comments"
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
last_message = 'I have improved code readability and added comments'
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Mock the LLM response
|
||||
mock_response = MagicMock()
|
||||
@@ -367,7 +367,7 @@ Changes look good"""
|
||||
]
|
||||
|
||||
# Test the function
|
||||
with patch("litellm.completion") as mock_completion:
|
||||
with patch('litellm.completion') as mock_completion:
|
||||
mock_completion.return_value = mock_response
|
||||
success, explanation = handler._check_review_comments(
|
||||
review_comments, issues_context, last_message, llm_config
|
||||
@@ -376,39 +376,39 @@ Changes look good"""
|
||||
# Verify the litellm.completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]["messages"][0]["content"]
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
|
||||
# Check prompt content
|
||||
assert "Issue descriptions:\n" + issues_context in prompt
|
||||
assert "PR Review Comments:\n" + "\n---\n".join(review_comments) in prompt
|
||||
assert "Last message from AI agent:\n" + last_message in prompt
|
||||
assert 'Issue descriptions:\n' + issues_context in prompt
|
||||
assert 'PR Review Comments:\n' + '\n---\n'.join(review_comments) in prompt
|
||||
assert 'Last message from AI agent:\n' + last_message in prompt
|
||||
|
||||
# Check result
|
||||
assert success is True
|
||||
assert explanation == "Changes look good"
|
||||
assert explanation == 'Changes look good'
|
||||
|
||||
|
||||
def test_guess_success_review_comments_litellm_call():
|
||||
"""Test that the litellm.completion() call for review comments contains the expected content."""
|
||||
# Create a PR handler instance
|
||||
handler = PRHandler("test-owner", "test-repo", "test-token")
|
||||
handler = PRHandler('test-owner', 'test-repo', 'test-token')
|
||||
|
||||
# Create a mock issue with review comments
|
||||
issue = GithubIssue(
|
||||
owner="test-owner",
|
||||
repo="test-repo",
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=1,
|
||||
title="Test PR",
|
||||
body="Test Body",
|
||||
title='Test PR',
|
||||
body='Test Body',
|
||||
thread_comments=None,
|
||||
closing_issues=["Issue 1 description", "Issue 2 description"],
|
||||
closing_issues=['Issue 1 description', 'Issue 2 description'],
|
||||
review_comments=[
|
||||
"Please improve code readability",
|
||||
"Add comments to complex functions",
|
||||
"Follow PEP 8 style guide",
|
||||
'Please improve code readability',
|
||||
'Add comments to complex functions',
|
||||
'Follow PEP 8 style guide',
|
||||
],
|
||||
thread_ids=None,
|
||||
head_branch="test-branch",
|
||||
head_branch='test-branch',
|
||||
)
|
||||
|
||||
# Create mock history with a detailed response
|
||||
@@ -422,7 +422,7 @@ def test_guess_success_review_comments_litellm_call():
|
||||
]
|
||||
|
||||
# Create mock LLM config
|
||||
llm_config = LLMConfig(model="test-model", api_key="test-key")
|
||||
llm_config = LLMConfig(model='test-model', api_key='test-key')
|
||||
|
||||
# Mock the LLM response
|
||||
mock_response = MagicMock()
|
||||
@@ -439,7 +439,7 @@ The changes successfully address the feedback."""
|
||||
]
|
||||
|
||||
# Test the guess_success method
|
||||
with patch("litellm.completion") as mock_completion:
|
||||
with patch('litellm.completion') as mock_completion:
|
||||
mock_completion.return_value = mock_response
|
||||
success, success_list, explanation = handler.guess_success(
|
||||
issue, history, llm_config
|
||||
@@ -448,13 +448,13 @@ The changes successfully address the feedback."""
|
||||
# Verify the litellm.completion() call
|
||||
mock_completion.assert_called_once()
|
||||
call_args = mock_completion.call_args
|
||||
prompt = call_args[1]["messages"][0]["content"]
|
||||
prompt = call_args[1]['messages'][0]['content']
|
||||
|
||||
# Check prompt content
|
||||
assert (
|
||||
"Issue descriptions:\n"
|
||||
+ json.dumps(["Issue 1 description", "Issue 2 description"], indent=4)
|
||||
'Issue descriptions:\n'
|
||||
+ json.dumps(['Issue 1 description', 'Issue 2 description'], indent=4)
|
||||
in prompt
|
||||
)
|
||||
assert "PR Review Comments:\n" + "\n---\n".join(issue.review_comments) in prompt
|
||||
assert "Last message from AI agent:\n" + history[0].content in prompt
|
||||
assert 'PR Review Comments:\n' + '\n---\n'.join(issue.review_comments) in prompt
|
||||
assert 'Last message from AI agent:\n' + history[0].content in prompt
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
from openhands.resolver.github_issue import GithubIssue
|
||||
from openhands.resolver.send_pull_request import make_commit
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
|
||||
def test_commit_message_with_quotes():
|
||||
# Create a temporary directory and initialize git repo
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
subprocess.run(["git", "init", temp_dir], check=True)
|
||||
subprocess.run(['git', 'init', temp_dir], check=True)
|
||||
|
||||
# Create a test file and add it to git
|
||||
test_file = os.path.join(temp_dir, "test.txt")
|
||||
with open(test_file, "w") as f:
|
||||
f.write("test content")
|
||||
test_file = os.path.join(temp_dir, 'test.txt')
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('test content')
|
||||
|
||||
subprocess.run(["git", "-C", temp_dir, "add", "test.txt"], check=True)
|
||||
subprocess.run(['git', '-C', temp_dir, 'add', 'test.txt'], check=True)
|
||||
|
||||
# Create a test issue with problematic title
|
||||
issue = GithubIssue(
|
||||
owner="test-owner",
|
||||
repo="test-repo",
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=123,
|
||||
title="Issue with 'quotes' and \"double quotes\" and <class 'ValueError'>",
|
||||
body="Test body",
|
||||
body='Test body',
|
||||
labels=[],
|
||||
assignees=[],
|
||||
state="open",
|
||||
created_at="2024-01-01T00:00:00Z",
|
||||
updated_at="2024-01-01T00:00:00Z",
|
||||
state='open',
|
||||
created_at='2024-01-01T00:00:00Z',
|
||||
updated_at='2024-01-01T00:00:00Z',
|
||||
closed_at=None,
|
||||
head_branch=None,
|
||||
thread_ids=None,
|
||||
)
|
||||
|
||||
# Make the commit
|
||||
make_commit(temp_dir, issue, "issue")
|
||||
make_commit(temp_dir, issue, 'issue')
|
||||
|
||||
# Get the commit message
|
||||
result = subprocess.run(
|
||||
["git", "-C", temp_dir, "log", "-1", "--pretty=%B"],
|
||||
['git', '-C', temp_dir, 'log', '-1', '--pretty=%B'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
@@ -48,7 +49,7 @@ def test_commit_message_with_quotes():
|
||||
|
||||
# The commit message should contain the quotes without excessive escaping
|
||||
expected = "Fix issue #123: Issue with 'quotes' and \"double quotes\" and <class 'ValueError'>"
|
||||
assert commit_msg == expected, f"Expected: {expected}\nGot: {commit_msg}"
|
||||
assert commit_msg == expected, f'Expected: {expected}\nGot: {commit_msg}'
|
||||
|
||||
|
||||
def test_pr_title_with_quotes(monkeypatch):
|
||||
@@ -56,39 +57,39 @@ def test_pr_title_with_quotes(monkeypatch):
|
||||
class MockResponse:
|
||||
def __init__(self, status_code=201):
|
||||
self.status_code = status_code
|
||||
self.text = ""
|
||||
self.text = ''
|
||||
|
||||
def json(self):
|
||||
return {"html_url": "https://github.com/test/test/pull/1"}
|
||||
return {'html_url': 'https://github.com/test/test/pull/1'}
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def mock_post(*args, **kwargs):
|
||||
# Verify that the PR title is not over-escaped
|
||||
data = kwargs.get("json", {})
|
||||
title = data.get("title", "")
|
||||
data = kwargs.get('json', {})
|
||||
title = data.get('title', '')
|
||||
expected = "Fix issue #123: Issue with 'quotes' and \"double quotes\" and <class 'ValueError'>"
|
||||
assert (
|
||||
title == expected
|
||||
), f"PR title was incorrectly escaped.\nExpected: {expected}\nGot: {title}"
|
||||
), f'PR title was incorrectly escaped.\nExpected: {expected}\nGot: {title}'
|
||||
return MockResponse()
|
||||
|
||||
class MockGetResponse:
|
||||
def __init__(self, status_code=200):
|
||||
self.status_code = status_code
|
||||
self.text = ""
|
||||
self.text = ''
|
||||
|
||||
def json(self):
|
||||
return {"default_branch": "main"}
|
||||
return {'default_branch': 'main'}
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
monkeypatch.setattr("requests.post", mock_post)
|
||||
monkeypatch.setattr("requests.get", lambda *args, **kwargs: MockGetResponse())
|
||||
monkeypatch.setattr('requests.post', mock_post)
|
||||
monkeypatch.setattr('requests.get', lambda *args, **kwargs: MockGetResponse())
|
||||
monkeypatch.setattr(
|
||||
"openhands.resolver.send_pull_request.branch_exists",
|
||||
'openhands.resolver.send_pull_request.branch_exists',
|
||||
lambda *args, **kwargs: False,
|
||||
)
|
||||
|
||||
@@ -97,69 +98,69 @@ def test_pr_title_with_quotes(monkeypatch):
|
||||
|
||||
def mock_run(*args, **kwargs):
|
||||
print(f"Running command: {args[0] if args else kwargs.get('args', [])}")
|
||||
if isinstance(args[0], list) and args[0][0] == "git":
|
||||
if "push" in args[0]:
|
||||
if isinstance(args[0], list) and args[0][0] == 'git':
|
||||
if 'push' in args[0]:
|
||||
return subprocess.CompletedProcess(
|
||||
args[0], returncode=0, stdout="", stderr=""
|
||||
args[0], returncode=0, stdout='', stderr=''
|
||||
)
|
||||
return original_run(*args, **kwargs)
|
||||
return original_run(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr("subprocess.run", mock_run)
|
||||
monkeypatch.setattr('subprocess.run', mock_run)
|
||||
|
||||
# Create a temporary directory and initialize git repo
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
print("Initializing git repo...")
|
||||
subprocess.run(["git", "init", temp_dir], check=True)
|
||||
print('Initializing git repo...')
|
||||
subprocess.run(['git', 'init', temp_dir], check=True)
|
||||
|
||||
# Add these lines to configure git
|
||||
subprocess.run(
|
||||
["git", "-C", temp_dir, "config", "user.name", "Test User"], check=True
|
||||
['git', '-C', temp_dir, 'config', 'user.name', 'Test User'], check=True
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "-C", temp_dir, "config", "user.email", "test@example.com"],
|
||||
['git', '-C', temp_dir, 'config', 'user.email', 'test@example.com'],
|
||||
check=True,
|
||||
)
|
||||
|
||||
# Create a test file and add it to git
|
||||
test_file = os.path.join(temp_dir, "test.txt")
|
||||
with open(test_file, "w") as f:
|
||||
f.write("test content")
|
||||
test_file = os.path.join(temp_dir, 'test.txt')
|
||||
with open(test_file, 'w') as f:
|
||||
f.write('test content')
|
||||
|
||||
print("Adding and committing test file...")
|
||||
subprocess.run(["git", "-C", temp_dir, "add", "test.txt"], check=True)
|
||||
print('Adding and committing test file...')
|
||||
subprocess.run(['git', '-C', temp_dir, 'add', 'test.txt'], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", temp_dir, "commit", "-m", "Initial commit"], check=True
|
||||
['git', '-C', temp_dir, 'commit', '-m', 'Initial commit'], check=True
|
||||
)
|
||||
|
||||
# Create a test issue with problematic title
|
||||
print("Creating test issue...")
|
||||
print('Creating test issue...')
|
||||
issue = GithubIssue(
|
||||
owner="test-owner",
|
||||
repo="test-repo",
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=123,
|
||||
title="Issue with 'quotes' and \"double quotes\" and <class 'ValueError'>",
|
||||
body="Test body",
|
||||
body='Test body',
|
||||
labels=[],
|
||||
assignees=[],
|
||||
state="open",
|
||||
created_at="2024-01-01T00:00:00Z",
|
||||
updated_at="2024-01-01T00:00:00Z",
|
||||
state='open',
|
||||
created_at='2024-01-01T00:00:00Z',
|
||||
updated_at='2024-01-01T00:00:00Z',
|
||||
closed_at=None,
|
||||
head_branch=None,
|
||||
thread_ids=None,
|
||||
)
|
||||
|
||||
# Try to send a PR - this will fail if the title is incorrectly escaped
|
||||
print("Sending PR...")
|
||||
from openhands.resolver.send_pull_request import send_pull_request
|
||||
print('Sending PR...')
|
||||
from openhands.core.config import LLMConfig
|
||||
from openhands.resolver.send_pull_request import send_pull_request
|
||||
|
||||
send_pull_request(
|
||||
github_issue=issue,
|
||||
github_token="dummy-token",
|
||||
github_username="test-user",
|
||||
github_token='dummy-token',
|
||||
github_username='test-user',
|
||||
patch_dir=temp_dir,
|
||||
llm_config=LLMConfig(model="test-model", api_key="test-key"),
|
||||
pr_type="ready",
|
||||
llm_config=LLMConfig(model='test-model', api_key='test-key'),
|
||||
pr_type='ready',
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ from openhands.resolver.send_pull_request import (
|
||||
load_single_resolver_output,
|
||||
make_commit,
|
||||
process_all_successful_issues,
|
||||
process_single_issue,
|
||||
create_pull_request_from_resolver_output,
|
||||
reply_to_comment,
|
||||
send_pull_request,
|
||||
update_existing_pull_request,
|
||||
@@ -607,7 +607,7 @@ def test_process_single_pr_update(
|
||||
)
|
||||
mock_initialize_repo.return_value = f'{mock_output_dir}/patches/pr_1'
|
||||
|
||||
process_single_issue(
|
||||
create_pull_request_from_resolver_output(
|
||||
mock_output_dir,
|
||||
resolver_output,
|
||||
github_token,
|
||||
@@ -639,7 +639,7 @@ def test_process_single_pr_update(
|
||||
@patch('openhands.resolver.send_pull_request.apply_patch')
|
||||
@patch('openhands.resolver.send_pull_request.send_pull_request')
|
||||
@patch('openhands.resolver.send_pull_request.make_commit')
|
||||
def test_process_single_issue(
|
||||
def test_create_pull_request_from_resolver_output(
|
||||
mock_make_commit,
|
||||
mock_send_pull_request,
|
||||
mock_apply_patch,
|
||||
@@ -679,7 +679,7 @@ def test_process_single_issue(
|
||||
mock_initialize_repo.return_value = f'{mock_output_dir}/patches/issue_1'
|
||||
|
||||
# Call the function
|
||||
process_single_issue(
|
||||
create_pull_request_from_resolver_output(
|
||||
mock_output_dir,
|
||||
resolver_output,
|
||||
github_token,
|
||||
@@ -714,7 +714,7 @@ def test_process_single_issue(
|
||||
@patch('openhands.resolver.send_pull_request.apply_patch')
|
||||
@patch('openhands.resolver.send_pull_request.send_pull_request')
|
||||
@patch('openhands.resolver.send_pull_request.make_commit')
|
||||
def test_process_single_issue_unsuccessful(
|
||||
def test_create_pull_request_from_resolver_output_unsuccessful(
|
||||
mock_make_commit,
|
||||
mock_send_pull_request,
|
||||
mock_apply_patch,
|
||||
@@ -748,7 +748,7 @@ def test_process_single_issue_unsuccessful(
|
||||
)
|
||||
|
||||
# Call the function
|
||||
process_single_issue(
|
||||
create_pull_request_from_resolver_output(
|
||||
mock_output_dir,
|
||||
resolver_output,
|
||||
github_token,
|
||||
@@ -767,9 +767,9 @@ def test_process_single_issue_unsuccessful(
|
||||
|
||||
|
||||
@patch('openhands.resolver.send_pull_request.load_all_resolver_outputs')
|
||||
@patch('openhands.resolver.send_pull_request.process_single_issue')
|
||||
@patch('openhands.resolver.send_pull_request.create_pull_request_from_resolver_output')
|
||||
def test_process_all_successful_issues(
|
||||
mock_process_single_issue, mock_load_all_resolver_outputs, mock_llm_config
|
||||
mock_create_pull_request, mock_load_all_resolver_outputs, mock_llm_config
|
||||
):
|
||||
# Create ResolverOutput objects with properly initialized GithubIssue instances
|
||||
resolver_output_1 = ResolverOutput(
|
||||
@@ -849,10 +849,10 @@ def test_process_all_successful_issues(
|
||||
)
|
||||
|
||||
# Assert that process_single_issue was called for successful issues only
|
||||
assert mock_process_single_issue.call_count == 2
|
||||
assert mock_create_pull_request.call_count == 2
|
||||
|
||||
# Check that the function was called with the correct arguments for successful issues
|
||||
mock_process_single_issue.assert_has_calls(
|
||||
mock_create_pull_request.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
'output_dir',
|
||||
@@ -945,7 +945,7 @@ def test_send_pull_request_branch_naming(
|
||||
|
||||
@patch('openhands.resolver.send_pull_request.argparse.ArgumentParser')
|
||||
@patch('openhands.resolver.send_pull_request.process_all_successful_issues')
|
||||
@patch('openhands.resolver.send_pull_request.process_single_issue')
|
||||
@patch('openhands.resolver.send_pull_request.create_pull_request_from_resolver_output')
|
||||
@patch('openhands.resolver.send_pull_request.load_single_resolver_output')
|
||||
@patch('os.path.exists')
|
||||
@patch('os.getenv')
|
||||
@@ -953,7 +953,7 @@ def test_main(
|
||||
mock_getenv,
|
||||
mock_path_exists,
|
||||
mock_load_single_resolver_output,
|
||||
mock_process_single_issue,
|
||||
mock_create_pull_request,
|
||||
mock_process_all_successful_issues,
|
||||
mock_parser,
|
||||
):
|
||||
@@ -999,7 +999,7 @@ def test_main(
|
||||
mock_getenv.assert_any_call('GITHUB_TOKEN')
|
||||
mock_path_exists.assert_called_with('/mock/output')
|
||||
mock_load_single_resolver_output.assert_called_with('/mock/output/output.jsonl', 42)
|
||||
mock_process_single_issue.assert_called_with(
|
||||
mock_create_pull_request.assert_called_with(
|
||||
'/mock/output',
|
||||
mock_resolver_output,
|
||||
'mock_token',
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import tempfile
|
||||
from unittest.mock import patch
|
||||
|
||||
from openhands.core.config import AppConfig
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from openhands.core.config import AppConfig, LLMConfig, SandboxConfig
|
||||
from openhands.resolver.github_issue import GithubIssue
|
||||
from openhands.resolver.resolver_output import ResolverOutput
|
||||
from openhands.resolver.send_pull_request import ProcessIssueResult
|
||||
|
||||
|
||||
# Mock the SessionManager to avoid asyncio issues
|
||||
@@ -8,6 +15,12 @@ class MockSessionManager:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
async def attach_to_conversation(self, sid):
|
||||
return {'id': sid}
|
||||
|
||||
async def detach_from_conversation(self, conversation):
|
||||
pass
|
||||
|
||||
|
||||
# Mock StaticFiles
|
||||
class MockStaticFiles:
|
||||
@@ -19,7 +32,11 @@ class MockStaticFiles:
|
||||
with patch('openhands.server.session.SessionManager', MockSessionManager), patch(
|
||||
'fastapi.staticfiles.StaticFiles', MockStaticFiles
|
||||
):
|
||||
from openhands.server.listen import is_extension_allowed, load_file_upload_config
|
||||
from openhands.server.listen import (
|
||||
app,
|
||||
is_extension_allowed,
|
||||
load_file_upload_config,
|
||||
)
|
||||
|
||||
|
||||
def test_load_file_upload_config():
|
||||
@@ -76,3 +93,160 @@ def test_is_extension_allowed_wildcard():
|
||||
assert is_extension_allowed('file.pdf')
|
||||
assert is_extension_allowed('file.doc')
|
||||
assert is_extension_allowed('file')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client():
|
||||
"""Create a test client for the FastAPI app."""
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config():
|
||||
"""Create a mock config for testing."""
|
||||
config = AppConfig(
|
||||
sandbox=SandboxConfig(runtime_container_image='test-image'),
|
||||
llms={'test': LLMConfig(model='test-model', api_key='test-key')},
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_resolve_issue():
|
||||
"""Create a mock for resolve_github_issue."""
|
||||
with patch('openhands.server.listen.resolve_github_issue') as mock:
|
||||
test_issue = GithubIssue(
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=123,
|
||||
title='Test Issue',
|
||||
body='Test body',
|
||||
)
|
||||
test_output = ResolverOutput(
|
||||
issue=test_issue,
|
||||
issue_type='issue',
|
||||
instruction='Test instruction',
|
||||
base_commit='abc123',
|
||||
git_patch='test patch',
|
||||
history=[],
|
||||
metrics={},
|
||||
success=True,
|
||||
success_explanation='Test success',
|
||||
error=None,
|
||||
comment_success=[],
|
||||
)
|
||||
mock.return_value = test_output
|
||||
yield mock
|
||||
|
||||
|
||||
def test_resolve_issue_endpoint(test_client, mock_config, mock_resolve_issue):
|
||||
"""Test the resolve issue endpoint."""
|
||||
# Create test data using Pydantic models
|
||||
test_issue = GithubIssue(
|
||||
owner='test-owner',
|
||||
repo='test-repo',
|
||||
number=123,
|
||||
title='Test Issue',
|
||||
body='Test body',
|
||||
)
|
||||
test_output = ResolverOutput(
|
||||
issue=test_issue,
|
||||
issue_type='issue',
|
||||
instruction='Test instruction',
|
||||
base_commit='abc123',
|
||||
git_patch='test patch',
|
||||
history=[],
|
||||
metrics={},
|
||||
success=True,
|
||||
success_explanation='Test success',
|
||||
error=None,
|
||||
comment_success=[],
|
||||
)
|
||||
|
||||
# Set environment variables before creating the test client
|
||||
with patch.dict(
|
||||
'os.environ', {'GITHUB_TOKEN': 'test-token', 'GITHUB_USERNAME': 'test-user'}
|
||||
):
|
||||
with patch('openhands.server.listen.config', mock_config), patch(
|
||||
'openhands.server.listen.get_sid_from_token', return_value='test-sid'
|
||||
), patch(
|
||||
'openhands.server.listen.create_pull_request_from_resolver_output',
|
||||
return_value=ProcessIssueResult(
|
||||
success=True, url='https://github.com/test/test/pull/123'
|
||||
),
|
||||
) as mock_send_pr:
|
||||
# Test successful resolution and PR creation
|
||||
request_data = {
|
||||
'owner': 'test-owner',
|
||||
'repo': 'test-repo',
|
||||
'token': 'test-token',
|
||||
'username': 'test-user',
|
||||
'max_iterations': 50,
|
||||
'issue_type': 'issue',
|
||||
'issue_number': 123,
|
||||
'comment_id': None,
|
||||
'pr_type': 'draft',
|
||||
'fork_owner': None,
|
||||
'send_on_failure': False,
|
||||
}
|
||||
|
||||
# Create a temp directory for our test
|
||||
with tempfile.TemporaryDirectory() as test_dir:
|
||||
# Mock tempfile.mkdtemp to return our test dir
|
||||
with patch('tempfile.mkdtemp', return_value=test_dir):
|
||||
response = test_client.post(
|
||||
'/api/resolver/resolve-issue',
|
||||
json=request_data,
|
||||
headers={'Authorization': 'Bearer test-token'},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()['status'] == 'success'
|
||||
assert (
|
||||
response.json()['result']['url']
|
||||
== 'https://github.com/test/test/pull/123'
|
||||
)
|
||||
|
||||
# Verify mocks were called correctly
|
||||
mock_resolve_issue.assert_called_once()
|
||||
call_args = mock_resolve_issue.call_args[1]
|
||||
assert call_args['owner'] == request_data['owner']
|
||||
assert call_args['repo'] == request_data['repo']
|
||||
assert call_args['issue_number'] == request_data['issue_number']
|
||||
|
||||
mock_send_pr.assert_called_once_with(
|
||||
output_dir=test_dir,
|
||||
resolver_output=test_output,
|
||||
github_token='test-token',
|
||||
github_username='test-user',
|
||||
pr_type='draft',
|
||||
llm_config=mock_config.get_llm_config(),
|
||||
fork_owner=None,
|
||||
send_on_failure=False,
|
||||
)
|
||||
|
||||
# Test error handling
|
||||
mock_resolve_issue.side_effect = Exception('Test error')
|
||||
response = test_client.post(
|
||||
'/api/resolver/resolve-issue',
|
||||
json=request_data,
|
||||
headers={'Authorization': 'Bearer test-token'},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()['status'] == 'error'
|
||||
assert response.json()['message'] == 'Test error'
|
||||
|
||||
# Test missing resolver output
|
||||
mock_resolve_issue.side_effect = None
|
||||
mock_resolve_issue.return_value = None
|
||||
response = test_client.post(
|
||||
'/api/resolver/resolve-issue',
|
||||
json=request_data,
|
||||
headers={'Authorization': 'Bearer test-token'},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()['status'] == 'error'
|
||||
assert (
|
||||
response.json()['message']
|
||||
== 'No resolver output generated for issue 123'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user