diff --git a/docs/GitHub-Issue-Debug-Guide.md b/docs/GitHub-Issue-Debug-Guide.md index 56d15092..e80e02e6 100644 --- a/docs/GitHub-Issue-Debug-Guide.md +++ b/docs/GitHub-Issue-Debug-Guide.md @@ -161,7 +161,6 @@ After execution, the script outputs results in JSON format, including the follow "prometheus_result": { "patch": "Generated code patch", "passed_reproducing_test": true, - "passed_build": true, "passed_existing_test": false, "passed_regression_test": true, "passed_reproduction_test": true, diff --git a/prometheus/app/api/routes/issue.py b/prometheus/app/api/routes/issue.py index 1748b32f..c966b500 100644 --- a/prometheus/app/api/routes/issue.py +++ b/prometheus/app/api/routes/issue.py @@ -60,6 +60,18 @@ async def answer_issue(issue: IssueRequest, request: Request) -> Response[IssueR code=400, message="workdir must be provided for user defined environment", ) + + # Validate build and test commands if required + if issue.run_build and not issue.build_commands: + raise ServerException( + code=400, message="No build commands available, please provide build commands" + ) + + if issue.run_existing_test and not issue.test_commands: + raise ServerException( + code=400, message="No test commands available, please provide test commands" + ) + # Ensure the repository is not currently being used if repository.is_working: raise ServerException( @@ -83,7 +95,6 @@ async def answer_issue(issue: IssueRequest, request: Request) -> Response[IssueR ( patch, passed_reproducing_test, - passed_build, passed_regression_test, passed_existing_test, issue_response, @@ -115,12 +126,11 @@ async def answer_issue(issue: IssueRequest, request: Request) -> Response[IssueR if ( patch, passed_reproducing_test, - passed_build, passed_regression_test, passed_existing_test, issue_response, issue_type, - ) == (None, False, False, False, False, None, None): + ) == (None, False, False, False, None, None): raise ServerException( code=500, message="Failed to process the issue. Please try again later.", @@ -135,7 +145,6 @@ async def answer_issue(issue: IssueRequest, request: Request) -> Response[IssueR data=IssueResponse( patch=patch, passed_reproducing_test=passed_reproducing_test, - passed_build=passed_build, passed_regression_test=passed_regression_test, passed_existing_test=passed_existing_test, issue_response=issue_response, diff --git a/prometheus/app/models/response/issue.py b/prometheus/app/models/response/issue.py index f9b31bca..72ecab48 100644 --- a/prometheus/app/models/response/issue.py +++ b/prometheus/app/models/response/issue.py @@ -6,7 +6,6 @@ class IssueResponse(BaseModel): patch: str | None = None passed_reproducing_test: bool - passed_build: bool passed_regression_test: bool passed_existing_test: bool issue_response: str | None = None diff --git a/prometheus/app/services/issue_service.py b/prometheus/app/services/issue_service.py index fae76b34..5b94743a 100644 --- a/prometheus/app/services/issue_service.py +++ b/prometheus/app/services/issue_service.py @@ -48,10 +48,7 @@ def answer_issue( dockerfile_content: Optional[str] = None, image_name: Optional[str] = None, workdir: Optional[str] = None, - ) -> ( - tuple[None, bool, bool, bool, bool, None, None] - | tuple[str, bool, bool, bool, bool, str, IssueType] - ): + ) -> tuple[None, bool, bool, bool, None, None] | tuple[str, bool, bool, bool, str, IssueType]: """ Processes an issue, generates patches if needed, runs optional builds and tests, and returning the results. @@ -76,9 +73,10 @@ def answer_issue( Tuple containing: - edit_patch (str): The generated patch for the issue. - passed_reproducing_test (bool): Whether the reproducing test passed. - - passed_build (bool): Whether the build passed. + - passed_regression_test (bool): Whether the regression tests passed. - passed_existing_test (bool): Whether the existing tests passed. - issue_response (str): Response generated for the issue. + - issue_type (IssueType): The type of the issue (BUG or QUESTION). """ # Set up a dedicated logger for this thread @@ -111,7 +109,6 @@ def answer_issue( kg=knowledge_graph, git_repo=repository, container=container, - build_commands=build_commands, test_commands=test_commands, ) @@ -131,7 +128,6 @@ def answer_issue( return ( output_state["edit_patch"], output_state["passed_reproducing_test"], - output_state["passed_build"], output_state["passed_regression_test"], output_state["passed_existing_test"], output_state["issue_response"], @@ -139,7 +135,7 @@ def answer_issue( ) except Exception as e: logger.error(f"Error in answer_issue: {str(e)}\n{traceback.format_exc()}") - return None, False, False, False, False, None, None + return None, False, False, False, None, None finally: logger.removeHandler(file_handler) file_handler.close() diff --git a/prometheus/docker/base_container.py b/prometheus/docker/base_container.py index bf486e17..e9043d4d 100644 --- a/prometheus/docker/base_container.py +++ b/prometheus/docker/base_container.py @@ -99,7 +99,9 @@ def update_files( Creates a tar archive of the new files and copies them into the workdir of the container. Args: - new_project_path: Path to the directory containing new files. + project_root_path: Path to the project root directory. + updated_files: List of file paths (relative to project_root_path) to update in the container. + removed_files: List of file paths (relative to project_root_path) to remove from the container. """ if not project_root_path.is_absolute(): raise ValueError("project_root_path {project_root_path} must be a absolute path") @@ -157,10 +159,10 @@ def execute_command(self, command: str) -> str: {command} timeout after {self.timeout} seconds ******************************************************************************* """ - timeout_command = f"timeout -k 5 {self.timeout}s {command}" - command = f'/bin/bash -l -c "{timeout_command}"' + bash_cmd = ["/bin/bash", "-lc", command] + full_cmd = ["timeout", "-k", "5", f"{self.timeout}s", *bash_cmd] self._logger.debug(f"Running command in container: {command}") - exec_result = self.container.exec_run(command, workdir=self.workdir) + exec_result = self.container.exec_run(full_cmd, workdir=self.workdir) exec_result_str = exec_result.output.decode("utf-8") if exec_result.exit_code in (124, 137): @@ -169,13 +171,11 @@ def execute_command(self, command: str) -> str: self._logger.debug(f"Command output:\n{exec_result_str}") return exec_result_str - def restart_container(self): - self._logger.info("Restarting the container") - if self.container: - self.container.stop(timeout=10) - self.container.remove(force=True) - - self.start_container() + def reset_repository(self): + """Reset the git repository in the container to a clean state.""" + self._logger.info("Resetting git repository in the container") + self.execute_command("git reset --hard") + self.execute_command("git clean -fd") def cleanup(self): """Clean up container resources and temporary files. diff --git a/prometheus/lang_graph/graphs/issue_graph.py b/prometheus/lang_graph/graphs/issue_graph.py index 512ec012..066be54e 100644 --- a/prometheus/lang_graph/graphs/issue_graph.py +++ b/prometheus/lang_graph/graphs/issue_graph.py @@ -30,7 +30,6 @@ def __init__( kg: KnowledgeGraph, git_repo: GitRepository, container: BaseContainer, - build_commands: Optional[Sequence[str]] = None, test_commands: Optional[Sequence[str]] = None, ): self.git_repo = git_repo @@ -52,7 +51,6 @@ def __init__( container=container, kg=kg, git_repo=git_repo, - build_commands=build_commands, test_commands=test_commands, ) diff --git a/prometheus/lang_graph/graphs/issue_state.py b/prometheus/lang_graph/graphs/issue_state.py index d63e73a1..931840ad 100644 --- a/prometheus/lang_graph/graphs/issue_state.py +++ b/prometheus/lang_graph/graphs/issue_state.py @@ -26,7 +26,6 @@ class IssueState(TypedDict): passed_regression_test: bool passed_reproducing_test: bool - passed_build: bool passed_existing_test: bool issue_response: str diff --git a/prometheus/lang_graph/nodes/context_extraction_node.py b/prometheus/lang_graph/nodes/context_extraction_node.py index 1e13c742..41f0b6d4 100644 --- a/prometheus/lang_graph/nodes/context_extraction_node.py +++ b/prometheus/lang_graph/nodes/context_extraction_node.py @@ -67,10 +67,16 @@ HUMAN_MESSAGE = """\ This is the original user query: + +--- BEGIN ORIGINAL QUERY --- {original_query} +--- END ORIGINAL QUERY --- The context or file content that you have seen so far (Some of the context may be IRRELEVANT to the query!!!): + +--- BEGIN CONTEXT --- {context} +--- END CONTEXT --- REMEMBER: Your task is to summarize the relevant contexts to a given query and return it in the specified format! """ @@ -112,16 +118,6 @@ def __init__(self, model: BaseChatModel, root_path: str): f"thread-{threading.get_ident()}.prometheus.lang_graph.nodes.context_extraction_node" ) - def get_human_message(self, state: ContextRetrievalState) -> str: - full_context_str = transform_tool_messages_to_str( - extract_last_tool_messages(state["context_provider_messages"]) - ) - original_query = state["query"] - return HUMAN_MESSAGE.format( - original_query=original_query, - context=full_context_str, - ) - def __call__(self, state: ContextRetrievalState): """ Extract relevant code contexts from the codebase based on the user query and existing context. @@ -130,9 +126,26 @@ def __call__(self, state: ContextRetrievalState): self._logger.info("Starting context extraction process") # Get Context List with existing context final_context = state.get("context", []) - # Get a human message - human_message = self.get_human_message(state) + + # Transform the tool messages to a single string + full_context_str = transform_tool_messages_to_str( + extract_last_tool_messages(state["context_provider_messages"]) + ) + + # return existing context if no new context is available + if not full_context_str: + self._logger.debug( + "No context available from tool messages, returning existing context" + ) + return {"context": final_context} + + # Format the human message + human_message = HUMAN_MESSAGE.format( + original_query=state["query"], + context=full_context_str, + ) self._logger.debug(human_message) + # Summarize the context based on the last messages and system prompt response = self.model.invoke({"human_prompt": human_message}) self._logger.debug(f"Model response: {response}") diff --git a/prometheus/lang_graph/nodes/context_retrieval_subgraph_node.py b/prometheus/lang_graph/nodes/context_retrieval_subgraph_node.py index 49627827..47bc7170 100644 --- a/prometheus/lang_graph/nodes/context_retrieval_subgraph_node.py +++ b/prometheus/lang_graph/nodes/context_retrieval_subgraph_node.py @@ -3,6 +3,7 @@ from typing import Dict, Sequence from langchain_core.language_models.chat_models import BaseChatModel +from langgraph.errors import GraphRecursionError from prometheus.graph.knowledge_graph import KnowledgeGraph from prometheus.lang_graph.subgraphs.context_retrieval_subgraph import ContextRetrievalSubgraph @@ -31,8 +32,12 @@ def __init__( def __call__(self, state: Dict) -> Dict[str, Sequence[Context]]: self._logger.info("Enter context retrieval subgraph") - output_state = self.context_retrieval_subgraph.invoke( - state[self.query_key_name], state["max_refined_query_loop"] - ) + try: + output_state = self.context_retrieval_subgraph.invoke( + state[self.query_key_name], state["max_refined_query_loop"] + ) + except GraphRecursionError as e: + self._logger.debug("Graph recursion limit reached, returning empty context") + raise e self._logger.info(f"Context retrieved: {output_state['context']}") return {self.context_key_name: output_state["context"]} diff --git a/prometheus/lang_graph/nodes/issue_bug_analyzer_message_node.py b/prometheus/lang_graph/nodes/issue_bug_analyzer_message_node.py index fcd78370..2a8a14c7 100644 --- a/prometheus/lang_graph/nodes/issue_bug_analyzer_message_node.py +++ b/prometheus/lang_graph/nodes/issue_bug_analyzer_message_node.py @@ -82,8 +82,6 @@ def format_human_message(self, state: Dict): ) elif "reproducing_test_fail_log" in state and state["reproducing_test_fail_log"]: edit_error = f"The patch failed to pass the bug exposing test cases:\n{state['reproducing_test_fail_log']}" - elif "build_fail_log" in state and state["build_fail_log"]: - edit_error = f"The patch failed to pass the build:\n{state['build_fail_log']}" elif "existing_test_fail_log" in state and state["existing_test_fail_log"]: edit_error = ( f"The patch failed to existing test cases:\n{state['existing_test_fail_log']}" diff --git a/prometheus/lang_graph/nodes/issue_bug_reproduction_context_message_node.py b/prometheus/lang_graph/nodes/issue_bug_reproduction_context_message_node.py index 3c4946a3..f66f2cb0 100644 --- a/prometheus/lang_graph/nodes/issue_bug_reproduction_context_message_node.py +++ b/prometheus/lang_graph/nodes/issue_bug_reproduction_context_message_node.py @@ -9,7 +9,7 @@ class IssueBugReproductionContextMessageNode: BUG_REPRODUCING_QUERY = """\ {issue_info} -OBJECTIVE: Find three relevant existing test cases that demonstrates similar functionality to the reported bug, +OBJECTIVE: Find 5 relevant existing test cases that demonstrates similar functionality to the reported bug, including ALL necessary imports, test setup, mocking, assertions, and any test method used in the test case. @@ -36,7 +36,7 @@ class IssueBugReproductionContextMessageNode: REQUIREMENTS: -- Return THREE complete, self-contained test cases most similar to bug scenario +- Return 5 complete, self-contained test cases most similar to bug scenario - Must include ALL necessary imports at the start of each test file - Must include full test method implementation - Must include ALL mock/fixture setup @@ -105,7 +105,7 @@ def test_file_permission_denied(self, mock_open, mock_access): 3. Tests with comparable mocking patterns 4. Tests demonstrating similar assertions -Find the THREE most relevant test cases with complete context, ensuring ALL necessary imports are included at the start of each test file. +Find the 5 most relevant test cases with complete context, ensuring ALL necessary imports are included at the start of each test file. """ def __init__(self): diff --git a/prometheus/lang_graph/nodes/issue_bug_responder_node.py b/prometheus/lang_graph/nodes/issue_bug_responder_node.py index 50578820..2e079d33 100644 --- a/prometheus/lang_graph/nodes/issue_bug_responder_node.py +++ b/prometheus/lang_graph/nodes/issue_bug_responder_node.py @@ -1,10 +1,10 @@ import logging import threading -from typing import Dict from langchain_core.language_models.chat_models import BaseChatModel from langchain_core.messages import HumanMessage, SystemMessage +from prometheus.lang_graph.subgraphs.issue_bug_state import IssueBugState from prometheus.utils.issue_util import format_issue_info @@ -53,15 +53,15 @@ def __init__(self, model: BaseChatModel): f"thread-{threading.get_ident()}.prometheus.lang_graph.nodes.issue_bug_responder_node" ) - def format_human_message(self, state: Dict) -> HumanMessage: + def format_human_message(self, state: IssueBugState) -> HumanMessage: verification_messages = [] # We only report successful verifications that were performed if state["passed_reproducing_test"]: verification_messages.append("✓ The bug reproducing test passed") - if state["passed_build"]: - verification_messages.append("✓ Build passes successfully") + if state["passed_regression_test"]: + verification_messages.append("✓ All selected regression tests passes successfully") if state["passed_existing_test"]: verification_messages.append("✓ All existing tests pass successfully") @@ -78,7 +78,7 @@ def format_human_message(self, state: Dict) -> HumanMessage: return HumanMessage(content=formatted_message) - def __call__(self, state: Dict): + def __call__(self, state: IssueBugState): messages = [ self.system_prompt, self.format_human_message(state), diff --git a/prometheus/lang_graph/nodes/issue_bug_subgraph_node.py b/prometheus/lang_graph/nodes/issue_bug_subgraph_node.py index c41fa52f..e1fe479f 100644 --- a/prometheus/lang_graph/nodes/issue_bug_subgraph_node.py +++ b/prometheus/lang_graph/nodes/issue_bug_subgraph_node.py @@ -24,7 +24,6 @@ def __init__( container: BaseContainer, kg: KnowledgeGraph, git_repo: GitRepository, - build_commands: Optional[Sequence[str]] = None, test_commands: Optional[Sequence[str]] = None, ): self._logger = logging.getLogger( @@ -37,7 +36,6 @@ def __init__( container=container, kg=kg, git_repo=git_repo, - build_commands=build_commands, test_commands=test_commands, ) @@ -46,6 +44,10 @@ def __call__(self, state: IssueState): self.container.build_docker_image() self.container.start_container() + # Run the build if needed + if state["run_build"]: + self.container.run_build() + self._logger.info("Enter IssueBugSubgraphNode") try: @@ -53,36 +55,32 @@ def __call__(self, state: IssueState): issue_title=state["issue_title"], issue_body=state["issue_body"], issue_comments=state["issue_comments"], - run_build=state["run_build"], run_existing_test=state["run_existing_test"], run_regression_test=state["run_regression_test"], run_reproduce_test=state["run_reproduce_test"], number_of_candidate_patch=state["number_of_candidate_patch"], ) - - self._logger.info(f"Generated patch:\n{output_state['edit_patch']}") - self._logger.info(f"passed_reproducing_test: {output_state['passed_reproducing_test']}") - self._logger.info(f"passed_build: {output_state['passed_build']}") - self._logger.info(f"passed_regression_test: {output_state['passed_regression_test']}") - self._logger.info(f"passed_existing_test: {output_state['passed_existing_test']}") - self._logger.info(f"issue_response:\n{output_state['issue_response']}") - return { - "edit_patch": output_state["edit_patch"], - "passed_reproducing_test": output_state["passed_reproducing_test"], - "passed_build": output_state["passed_build"], - "passed_regression_test": output_state["passed_regression_test"], - "passed_existing_test": output_state["passed_existing_test"], - "issue_response": output_state["issue_response"], - } except GraphRecursionError: self._logger.critical("Please increase the recursion limit of IssueBugSubgraph") return { "edit_patch": None, "passed_reproducing_test": False, - "passed_build": False, "passed_regression_test": False, "passed_existing_test": False, "issue_response": None, } finally: self.container.cleanup() + + self._logger.info(f"Generated patch:\n{output_state['edit_patch']}") + self._logger.info(f"passed_reproducing_test: {output_state['passed_reproducing_test']}") + self._logger.info(f"passed_regression_test: {output_state['passed_regression_test']}") + self._logger.info(f"passed_existing_test: {output_state['passed_existing_test']}") + self._logger.info(f"issue_response:\n{output_state['issue_response']}") + return { + "edit_patch": output_state["edit_patch"], + "passed_reproducing_test": output_state["passed_reproducing_test"], + "passed_regression_test": output_state["passed_regression_test"], + "passed_existing_test": output_state["passed_existing_test"], + "issue_response": output_state["issue_response"], + } diff --git a/prometheus/lang_graph/nodes/issue_not_verified_bug_subgraph_node.py b/prometheus/lang_graph/nodes/issue_not_verified_bug_subgraph_node.py index 347ef7c5..45fc621d 100644 --- a/prometheus/lang_graph/nodes/issue_not_verified_bug_subgraph_node.py +++ b/prometheus/lang_graph/nodes/issue_not_verified_bug_subgraph_node.py @@ -53,7 +53,7 @@ def __call__(self, state: Dict): return { "edit_patch": None, "passed_reproducing_test": False, - "passed_build": False, + "passed_regression_test": False, "passed_existing_test": False, } finally: @@ -64,6 +64,8 @@ def __call__(self, state: Dict): return { "edit_patch": output_state["final_patch"], "passed_reproducing_test": False, - "passed_build": False, + "passed_regression_test": True + if state["run_regression_test"] and state["selected_regression_tests"] + else False, "passed_existing_test": False, } diff --git a/prometheus/lang_graph/nodes/issue_question_subgraph_node.py b/prometheus/lang_graph/nodes/issue_question_subgraph_node.py index 288cf843..4bcaf0d2 100644 --- a/prometheus/lang_graph/nodes/issue_question_subgraph_node.py +++ b/prometheus/lang_graph/nodes/issue_question_subgraph_node.py @@ -49,7 +49,6 @@ def __call__(self, state: IssueState): return { "edit_patch": None, "passed_reproducing_test": False, - "passed_build": False, "passed_regression_test": False, "passed_existing_test": False, "issue_response": None, @@ -60,7 +59,6 @@ def __call__(self, state: IssueState): return { "edit_patch": output_state["edit_patch"], "passed_reproducing_test": output_state["passed_reproducing_test"], - "passed_build": output_state["passed_build"], "passed_regression_test": output_state["passed_regression_test"], "passed_existing_test": output_state["passed_existing_test"], "issue_response": output_state["issue_response"], diff --git a/prometheus/lang_graph/nodes/issue_verified_bug_subgraph_node.py b/prometheus/lang_graph/nodes/issue_verified_bug_subgraph_node.py index 41c0fd07..0288e270 100644 --- a/prometheus/lang_graph/nodes/issue_verified_bug_subgraph_node.py +++ b/prometheus/lang_graph/nodes/issue_verified_bug_subgraph_node.py @@ -1,6 +1,5 @@ import logging import threading -from typing import Optional, Sequence from langchain_core.language_models.chat_models import BaseChatModel from langgraph.errors import GraphRecursionError @@ -24,8 +23,6 @@ def __init__( container: BaseContainer, kg: KnowledgeGraph, git_repo: GitRepository, - build_commands: Optional[Sequence[str]] = None, - test_commands: Optional[Sequence[str]] = None, ): self._logger = logging.getLogger( f"thread-{threading.get_ident()}.prometheus.lang_graph.nodes.issue_verified_bug_subgraph_node" @@ -37,8 +34,6 @@ def __init__( container=container, kg=kg, git_repo=git_repo, - build_commands=build_commands, - test_commands=test_commands, ) def __call__(self, state: IssueBugState): @@ -48,7 +43,6 @@ def __call__(self, state: IssueBugState): issue_title=state["issue_title"], issue_body=state["issue_body"], issue_comments=state["issue_comments"], - run_build=state["run_build"], run_regression_test=state["run_regression_test"], run_existing_test=state["run_existing_test"], reproduced_bug_file=state["reproduced_bug_file"], @@ -63,26 +57,19 @@ def __call__(self, state: IssueBugState): return { "edit_patch": None, "passed_reproducing_test": False, - "passed_build": False, "passed_existing_test": False, + "passed_regression_test": False, } finally: self.git_repo.reset_repository() - # if all the tests passed - passed_reproducing_test = not bool(output_state["reproducing_test_fail_log"]) - # if the build passed - passed_build = state["run_build"] and not output_state["build_fail_log"] - # if the existing tests passed - passed_existing_test = ( - state["run_existing_test"] and not output_state["existing_test_fail_log"] - ) + + # Log the generated patch self._logger.info(f"edit_patch: {output_state['edit_patch']}") - self._logger.info(f"passed_reproducing_test: {passed_reproducing_test}") - self._logger.info(f"passed_build: {passed_build}") - self._logger.info(f"passed_existing_test: {passed_existing_test}") return { "edit_patch": output_state["edit_patch"], - "passed_reproducing_test": passed_reproducing_test, - "passed_build": passed_build, - "passed_existing_test": passed_existing_test, + "passed_reproducing_test": True if state["run_reproduce_test"] else False, + "passed_existing_test": True if state["run_existing_test"] else False, + "passed_regression_test": True + if state["run_regression_test"] and state["selected_regression_tests"] + else False, } diff --git a/prometheus/lang_graph/nodes/run_existing_tests_node.py b/prometheus/lang_graph/nodes/run_existing_tests_node.py new file mode 100644 index 00000000..1cddef6a --- /dev/null +++ b/prometheus/lang_graph/nodes/run_existing_tests_node.py @@ -0,0 +1,29 @@ +import logging +import threading + +from prometheus.docker.base_container import BaseContainer +from prometheus.lang_graph.subgraphs.run_existing_tests_state import RunExistingTestsState + + +class RunExistingTestsNode: + """ + The node to execute existing tests commands in the container. + """ + + def __init__(self, container: BaseContainer): + self._logger = logging.getLogger( + f"thread-{threading.get_ident()}.prometheus.lang_graph.nodes.run_regression_tests_node" + ) + self.container = container + + def __call__(self, state: RunExistingTestsState): + # Run the existing tests commands in the container + output = self.container.run_test() + + # Log the output + self._logger.info(f"Run existing tests output: {output}") + + # return the output + return { + "test_log": output, + } diff --git a/prometheus/lang_graph/nodes/run_existing_tests_structure_node.py b/prometheus/lang_graph/nodes/run_existing_tests_structure_node.py new file mode 100644 index 00000000..5cbbe094 --- /dev/null +++ b/prometheus/lang_graph/nodes/run_existing_tests_structure_node.py @@ -0,0 +1,76 @@ +import logging +import threading + +from langchain_core.language_models.chat_models import BaseChatModel +from langchain_core.prompts import ChatPromptTemplate +from pydantic import BaseModel + +from prometheus.lang_graph.subgraphs.run_existing_tests_state import RunExistingTestsState + + +class RunExistingTestsStructureOutput(BaseModel): + success: bool + + +class RunExistingTestsStructuredNode: + SYS_PROMPT = """\ +You are a test result parser. Your only task is to determine if all the executed tests passed successfully. + +Your task is to: +1. Analyze the test execution logs +2. Look for test result indicators: + - Test summary showing "passed" or "PASSED" + - Check for "FAILURES" or "FAILED" sections + - Check for error messages or exceptions + - Warning messages are acceptable and don't indicate failure +3. Determine overall success status + +Return: +- success: True if ALL tests passed successfully, False if ANY test failed + +Important rules: +- Even a single test failure means overall success is False +- If tests couldn't run due to errors (e.g., import errors, syntax errors), return False +- Warnings alone don't constitute failure +- Empty test runs or no tests found should return False +- Look for clear pass/fail indicators in the test framework output +""" + + HUMAN_PROMPT = """\ +We have run the existing tests on the codebase. + +Test Execution Logs: +--- BEGIN LOG --- +{test_log} +--- END LOG --- + +Please analyze the logs and determine if all tests passed successfully. +Return True only if ALL tests passed without any failures. +Return False if ANY test failed or if tests couldn't run properly. +""" + + def __init__(self, model: BaseChatModel): + prompt = ChatPromptTemplate.from_messages( + [("system", self.SYS_PROMPT), ("human", "{human_message}")] + ) + structured_llm = model.with_structured_output(RunExistingTestsStructureOutput) + self.model = prompt | structured_llm + self._logger = logging.getLogger( + f"thread-{threading.get_ident()}.prometheus.lang_graph.nodes.run_existing_tests_structure_node" + ) + + def __call__(self, state: RunExistingTestsState): + # Get human message from the state + human_message = self.HUMAN_PROMPT.format(test_log=state["test_log"]) + self._logger.debug(f"Human Message: {human_message}") + + # Invoke the model + response = self.model.invoke({"human_message": human_message}) + + # Log the full response for debugging + self._logger.debug(response) + + # return the response + return { + "success": response.success, + } diff --git a/prometheus/lang_graph/nodes/run_existing_tests_subgraph_node.py b/prometheus/lang_graph/nodes/run_existing_tests_subgraph_node.py new file mode 100644 index 00000000..ac4b0bd0 --- /dev/null +++ b/prometheus/lang_graph/nodes/run_existing_tests_subgraph_node.py @@ -0,0 +1,43 @@ +import logging +import threading +from typing import Dict + +from langchain_core.language_models.chat_models import BaseChatModel + +from prometheus.docker.base_container import BaseContainer +from prometheus.git.git_repository import GitRepository +from prometheus.lang_graph.subgraphs.run_existing_tests_subgraph import RunExistingTestsSubgraph + + +class RunExistingTestsSubgraphNode: + def __init__( + self, + model: BaseChatModel, + container: BaseContainer, + git_repo: GitRepository, + testing_patch_key: str, + existing_test_fail_log_key: str, + ): + self._logger = logging.getLogger( + f"thread-{threading.get_ident()}.prometheus.lang_graph.nodes.run_existing_tests_subgraph_node" + ) + self.subgraph = RunExistingTestsSubgraph( + base_model=model, container=container, git_repo=git_repo + ) + self.git_repo = git_repo + self.testing_patch_key = testing_patch_key + self.existing_test_fail_log_key = existing_test_fail_log_key + + def __call__(self, state: Dict): + self._logger.info("Enter run_existing_tests_subgraph_node") + + try: + output_state = self.subgraph.invoke(testing_patch=state[self.testing_patch_key]) + finally: + self.git_repo.reset_repository() + + self._logger.debug(output_state["test_fail_log"]) + + return { + self.existing_test_fail_log_key: output_state["test_fail_log"], + } diff --git a/prometheus/lang_graph/nodes/update_container_node.py b/prometheus/lang_graph/nodes/update_container_node.py index 76f73a90..41a42cf5 100644 --- a/prometheus/lang_graph/nodes/update_container_node.py +++ b/prometheus/lang_graph/nodes/update_container_node.py @@ -43,7 +43,10 @@ def __call__(self, _: Dict): if self.container.is_running(): self._logger.info("Copy over all updated files to the container") all_files_patch = self.git_repo.get_diff() - self.container.restart_container() + + # Reset the container to ensure a clean state before applying updates + self.container.reset_repository() + added_files, modified_file, removed_files = get_updated_files(all_files_patch) self.container.update_files( self.git_repo.get_working_directory(), added_files + modified_file, removed_files diff --git a/prometheus/lang_graph/subgraphs/issue_bug_state.py b/prometheus/lang_graph/subgraphs/issue_bug_state.py index 98a60c91..31c4c3f7 100644 --- a/prometheus/lang_graph/subgraphs/issue_bug_state.py +++ b/prometheus/lang_graph/subgraphs/issue_bug_state.py @@ -23,7 +23,7 @@ class IssueBugState(TypedDict): edit_patch: str passed_reproducing_test: bool - passed_build: bool + passed_regression_test: bool passed_existing_test: bool issue_response: str diff --git a/prometheus/lang_graph/subgraphs/issue_bug_subgraph.py b/prometheus/lang_graph/subgraphs/issue_bug_subgraph.py index ac509316..462f6f0c 100644 --- a/prometheus/lang_graph/subgraphs/issue_bug_subgraph.py +++ b/prometheus/lang_graph/subgraphs/issue_bug_subgraph.py @@ -28,7 +28,6 @@ def __init__( container: BaseContainer, kg: KnowledgeGraph, git_repo: GitRepository, - build_commands: Optional[Sequence[str]] = None, test_commands: Optional[Sequence[str]] = None, ): # Construct bug reproduction node @@ -56,8 +55,6 @@ def __init__( container=container, kg=kg, git_repo=git_repo, - build_commands=build_commands, - test_commands=test_commands, ) # Construct issue not verified bug subgraph node issue_not_verified_bug_subgraph_node = IssueNotVerifiedBugSubgraphNode( @@ -135,7 +132,6 @@ def invoke( issue_title: str, issue_body: str, issue_comments: Sequence[Mapping[str, str]], - run_build: bool, run_existing_test: bool, run_regression_test: bool, run_reproduce_test: bool, @@ -148,7 +144,6 @@ def invoke( "issue_title": issue_title, "issue_body": issue_body, "issue_comments": issue_comments, - "run_build": run_build, "run_existing_test": run_existing_test, "run_regression_test": run_regression_test, "run_reproduce_test": run_reproduce_test, @@ -159,9 +154,7 @@ def invoke( return { "edit_patch": output_state["edit_patch"], "passed_reproducing_test": output_state["passed_reproducing_test"], - "passed_build": output_state["passed_build"], "passed_existing_test": output_state["passed_existing_test"], - "passed_regression_test": bool(output_state.get("selected_regression_tests", [])) - and bool(output_state["edit_patch"]), + "passed_regression_test": output_state["passed_regression_test"], "issue_response": output_state["issue_response"], } diff --git a/prometheus/lang_graph/subgraphs/issue_question_subgraph.py b/prometheus/lang_graph/subgraphs/issue_question_subgraph.py index c4b2ab52..54b29f35 100644 --- a/prometheus/lang_graph/subgraphs/issue_question_subgraph.py +++ b/prometheus/lang_graph/subgraphs/issue_question_subgraph.py @@ -79,7 +79,6 @@ def invoke( return { "edit_patch": None, "passed_reproducing_test": False, - "passed_build": False, "passed_existing_test": False, "passed_regression_test": False, "issue_response": output_state["question_response"], diff --git a/prometheus/lang_graph/subgraphs/issue_verified_bug_state.py b/prometheus/lang_graph/subgraphs/issue_verified_bug_state.py index d65dbff1..a31f69ef 100644 --- a/prometheus/lang_graph/subgraphs/issue_verified_bug_state.py +++ b/prometheus/lang_graph/subgraphs/issue_verified_bug_state.py @@ -15,7 +15,6 @@ class IssueVerifiedBugState(TypedDict): max_refined_query_loop: int refined_query: str - run_build: bool run_existing_test: bool run_regression_test: bool @@ -36,10 +35,4 @@ class IssueVerifiedBugState(TypedDict): reproducing_test_fail_log: str - exist_build: bool - build_command_summary: str - build_fail_log: str - - exist_test: bool - test_command_summary: str existing_test_fail_log: str diff --git a/prometheus/lang_graph/subgraphs/issue_verified_bug_subgraph.py b/prometheus/lang_graph/subgraphs/issue_verified_bug_subgraph.py index bfa5f6b3..e6e4e463 100644 --- a/prometheus/lang_graph/subgraphs/issue_verified_bug_subgraph.py +++ b/prometheus/lang_graph/subgraphs/issue_verified_bug_subgraph.py @@ -1,5 +1,5 @@ import functools -from typing import Mapping, Optional, Sequence +from typing import Mapping, Sequence from langchain_core.language_models.chat_models import BaseChatModel from langgraph.graph import END, StateGraph @@ -11,7 +11,6 @@ from prometheus.lang_graph.nodes.bug_fix_verification_subgraph_node import ( BugFixVerificationSubgraphNode, ) -from prometheus.lang_graph.nodes.build_and_test_subgraph_node import BuildAndTestSubgraphNode from prometheus.lang_graph.nodes.context_retrieval_subgraph_node import ContextRetrievalSubgraphNode from prometheus.lang_graph.nodes.edit_message_node import EditMessageNode from prometheus.lang_graph.nodes.edit_node import EditNode @@ -24,6 +23,9 @@ from prometheus.lang_graph.nodes.issue_bug_analyzer_node import IssueBugAnalyzerNode from prometheus.lang_graph.nodes.issue_bug_context_message_node import IssueBugContextMessageNode from prometheus.lang_graph.nodes.noop_node import NoopNode +from prometheus.lang_graph.nodes.run_existing_tests_subgraph_node import ( + RunExistingTestsSubgraphNode, +) from prometheus.lang_graph.subgraphs.issue_verified_bug_state import IssueVerifiedBugState @@ -51,8 +53,6 @@ def __init__( container: BaseContainer, kg: KnowledgeGraph, git_repo: GitRepository, - build_commands: Optional[Sequence[str]] = None, - test_commands: Optional[Sequence[str]] = None, ): """ Initialize the verified bug fix subgraph. @@ -60,12 +60,10 @@ def __init__( Args: advanced_model (BaseChatModel): A strong LLM used for bug understanding and patch generation. base_model (BaseChatModel): A smaller, less expensive LLM used for context retrieval and test verification. - container (BaseContainer): A build/test container to run code validations. + container (BaseContainer): A test container to run code validations. kg (KnowledgeGraph): A knowledge graph used for context-aware retrieval of relevant code entities. git_repo (GitRepository): Git interface to apply patches and get diffs. neo4j_driver (neo4j.Driver): Neo4j driver for executing graph-based semantic queries. - build_commands (Optional[Sequence[str]]): Commands to build the project inside the container. - test_commands (Optional[Sequence[str]]): Commands to test the project inside the container. """ # Phase 1: Retrieve context related to the bug @@ -111,14 +109,14 @@ def __init__( base_model, container, git_repo ) - # Phase 7: Optionally run full build and test after fix - build_or_test_branch_node = NoopNode() - build_and_test_subgraph_node = BuildAndTestSubgraphNode( - container, - advanced_model, - kg, - build_commands, - test_commands, + # Phase 7: Optionally run existing tests + run_existing_tests_branch_node = NoopNode() + run_existing_tests_subgraph_node = RunExistingTestsSubgraphNode( + model=base_model, + container=container, + git_repo=git_repo, + testing_patch_key="edit_patch", + existing_test_fail_log_key="existing_test_fail_log", ) # Build the LangGraph workflow @@ -144,8 +142,8 @@ def __init__( ) workflow.add_node("bug_fix_verification_subgraph_node", bug_fix_verification_subgraph_node) - workflow.add_node("build_or_test_branch_node", build_or_test_branch_node) - workflow.add_node("build_and_test_subgraph_node", build_and_test_subgraph_node) + workflow.add_node("run_existing_tests_branch_node", run_existing_tests_branch_node) + workflow.add_node("run_existing_tests_subgraph_node", run_existing_tests_subgraph_node) # Define edges for full flow workflow.set_entry_point("issue_bug_context_message_node") @@ -192,20 +190,20 @@ def __init__( workflow.add_conditional_edges( "bug_fix_verification_subgraph_node", lambda state: bool(state["reproducing_test_fail_log"]), - {True: "issue_bug_analyzer_message_node", False: "build_or_test_branch_node"}, + {True: "issue_bug_analyzer_message_node", False: "run_existing_tests_branch_node"}, ) - # Optionally run full build/test suite + # Optionally run existing tests suite workflow.add_conditional_edges( - "build_or_test_branch_node", - lambda state: state["run_build"] or state["run_existing_test"], - {True: "build_and_test_subgraph_node", False: END}, + "run_existing_tests_branch_node", + lambda state: state["run_existing_test"], + {True: "run_existing_tests_subgraph_node", False: END}, ) - # If build/test fail, go back to reanalyze and patch + # If test fail, go back to reanalyze and patch workflow.add_conditional_edges( - "build_and_test_subgraph_node", - lambda state: bool(state["build_fail_log"]) or bool(state["existing_test_fail_log"]), + "run_existing_tests_subgraph_node", + lambda state: bool(state["existing_test_fail_log"]), {True: "issue_bug_analyzer_message_node", False: END}, ) @@ -217,7 +215,6 @@ def invoke( issue_title: str, issue_body: str, issue_comments: Sequence[Mapping[str, str]], - run_build: bool, run_regression_test: bool, run_existing_test: bool, reproduced_bug_file: str, @@ -232,7 +229,6 @@ def invoke( "issue_title": issue_title, "issue_body": issue_body, "issue_comments": issue_comments, - "run_build": run_build, "run_regression_test": run_regression_test, "run_existing_test": run_existing_test, "reproduced_bug_file": reproduced_bug_file, @@ -245,9 +241,4 @@ def invoke( output_state = self.subgraph.invoke(input_state, config) return { "edit_patch": output_state["edit_patch"], - "reproducing_test_fail_log": output_state["reproducing_test_fail_log"], - "exist_build": output_state.get("exist_build", False), - "build_fail_log": output_state.get("build_fail_log", ""), - "exist_test": output_state.get("exist_test", False), - "existing_test_fail_log": output_state.get("existing_test_fail_log", ""), } diff --git a/prometheus/lang_graph/subgraphs/run_existing_tests_state.py b/prometheus/lang_graph/subgraphs/run_existing_tests_state.py new file mode 100644 index 00000000..feece64d --- /dev/null +++ b/prometheus/lang_graph/subgraphs/run_existing_tests_state.py @@ -0,0 +1,9 @@ +from typing import TypedDict + + +class RunExistingTestsState(TypedDict): + testing_patch: str + + test_log: str + + success: bool diff --git a/prometheus/lang_graph/subgraphs/run_existing_tests_subgraph.py b/prometheus/lang_graph/subgraphs/run_existing_tests_subgraph.py new file mode 100644 index 00000000..2572a60b --- /dev/null +++ b/prometheus/lang_graph/subgraphs/run_existing_tests_subgraph.py @@ -0,0 +1,88 @@ +from langchain_core.language_models.chat_models import BaseChatModel +from langgraph.graph import END, StateGraph + +from prometheus.docker.base_container import BaseContainer +from prometheus.git.git_repository import GitRepository +from prometheus.lang_graph.nodes.git_apply_patch_node import GitApplyPatchNode +from prometheus.lang_graph.nodes.run_existing_tests_node import RunExistingTestsNode +from prometheus.lang_graph.nodes.run_existing_tests_structure_node import ( + RunExistingTestsStructuredNode, +) +from prometheus.lang_graph.nodes.update_container_node import UpdateContainerNode +from prometheus.lang_graph.subgraphs.run_existing_tests_state import RunExistingTestsState + + +class RunExistingTestsSubgraph: + """ + This class defines a LangGraph-based state machine that automatically runs existing tests + for GitHub issues. + """ + + def __init__( + self, + base_model: BaseChatModel, + container: BaseContainer, + git_repo: GitRepository, + ): + """ + Initialize the run existing tests pipeline with all necessary parts. + + Args: + base_model: Lighter LLM for simpler tasks (e.g., file selection). + container: Docker-based sandbox for running code. + """ + # Git apply patch node to apply the testing patch + edit_patch_apply_node = GitApplyPatchNode( + git_repo=git_repo, state_patch_name="testing_patch" + ) + + # Update the container with the current testing patch + update_container_node = UpdateContainerNode(container=container, git_repo=git_repo) + + # Run existing tests node + run_existing_tests_node = RunExistingTestsNode(container=container) + + # Get result node + run_existing_tests_structured_node = RunExistingTestsStructuredNode(model=base_model) + # Define the state machine + workflow = StateGraph(RunExistingTestsState) + + workflow.add_node("edit_patch_apply_node", edit_patch_apply_node) + workflow.add_node("update_container_node", update_container_node) + workflow.add_node("run_existing_tests_node", run_existing_tests_node) + + workflow.add_node("run_existing_tests_structured_node", run_existing_tests_structured_node) + workflow.set_entry_point("edit_patch_apply_node") + workflow.add_edge("edit_patch_apply_node", "update_container_node") + workflow.add_edge("update_container_node", "run_existing_tests_node") + workflow.add_edge("run_existing_tests_node", "run_existing_tests_structured_node") + workflow.add_edge("run_existing_tests_structured_node", END) + + # Compile the full LangGraph subgraph + self.subgraph = workflow.compile() + + def invoke( + self, + testing_patch: str, + recursion_limit: int = 50, + ): + """ + Run the bug existing subgraph. + + Args: + testing_patch: The code patch to test. + recursion_limit: Max steps before triggering recovery fallback. + Returns: + The result of the bug existing process + """ + config = {"recursion_limit": recursion_limit} + + input_state = { + "testing_patch": testing_patch, + "success": False, + } + + output_state = self.subgraph.invoke(input_state, config) + return { + "test_fail_log": output_state["test_fail_log"] if not output_state["success"] else "", + } diff --git a/prometheus/lang_graph/subgraphs/run_regression_tests_subgraph.py b/prometheus/lang_graph/subgraphs/run_regression_tests_subgraph.py index 5fd49ed3..967ccc90 100644 --- a/prometheus/lang_graph/subgraphs/run_regression_tests_subgraph.py +++ b/prometheus/lang_graph/subgraphs/run_regression_tests_subgraph.py @@ -15,8 +15,8 @@ class RunRegressionTestsSubgraph: """ - This class defines a LangGraph-based state machine that automatically selects and runs regression tests - for GitHub issues. It orchestrates context retrieval, tests selection, test execution, and feedback loops. + This class defines a LangGraph-based state machine that automatically runs regression tests + for GitHub issues. """ def __init__( diff --git a/prometheus/script/github_issue_debug.py b/prometheus/script/github_issue_debug.py index 40fe93b7..4e77924a 100644 --- a/prometheus/script/github_issue_debug.py +++ b/prometheus/script/github_issue_debug.py @@ -364,10 +364,6 @@ def main(): if prometheus_result.get("patch"): print("✅ Generated fix patch") - if prometheus_result.get("passed_build") is not None: - status = "✅ Passed" if prometheus_result["passed_build"] else "❌ Failed" - print(f" Build Validation: {status}") - if prometheus_result.get("passed_existing_test") is not None: status = "✅ Passed" if prometheus_result["passed_existing_test"] else "❌ Failed" print(f" Test Validation: {status}") diff --git a/prometheus/tools/graph_traversal.py b/prometheus/tools/graph_traversal.py index be823353..a472d86d 100644 --- a/prometheus/tools/graph_traversal.py +++ b/prometheus/tools/graph_traversal.py @@ -334,6 +334,9 @@ class FindTextNodeWithTextInput(BaseModel): same as python's check `'foo' in text`, ie. it is case sensitive and is looking for exact matches. Therefore the search text should be exact as well. +Text Node is a chunk of text extracted from a text file, such as comments or documentation. +Source code files are not split into TextNodes! + You can use this tool to find all text/documentation in codebase that contains this text.""" @@ -378,9 +381,12 @@ class FindTextNodeWithTextInFileInput(BaseModel): Find all TextNode in the graph that exactly contains this text in a file with this basename. The contains is same as python's check `'foo' in text`, ie. it is case sensitive and is looking for exact matches. Therefore the search text should be exact as well. -The basename must include the extension, like 'bar.py', 'baz.java' or 'foo' +The basename must include the extension, like 'README.md' or 'foo' (in this case foo is a file without extension). +Text Node is a chunk of text extracted from a text file, such as comments or documentation. +Source code files are not split into TextNodes! + You can use this tool to find text/documentation in a specific file that contains this text.""" @@ -429,6 +435,9 @@ class GetNextTextNodeWithNodeIdInput(BaseModel): GET_NEXT_TEXT_NODE_WITH_NODE_ID_DESCRIPTION = """\ Get the next TextNode of this given node_id. +Text Node is a chunk of text extracted from a text file, such as comments or documentation. +Source code files are not split into TextNodes! + You can use this tool to read the next section of text that you are interested in.""" diff --git a/tests/app/api/test_issue.py b/tests/app/api/test_issue.py index 5bf1869c..a7071ba9 100644 --- a/tests/app/api/test_issue.py +++ b/tests/app/api/test_issue.py @@ -44,7 +44,6 @@ def test_answer_issue(mock_service): mock_service["issue_service"].answer_issue.return_value = ( "test patch", # patch True, # passed_reproducing_test - True, # passed_build True, # passed_regression_test True, # passed_existing_test "Issue fixed", # issue_response @@ -67,7 +66,6 @@ def test_answer_issue(mock_service): "data": { "patch": "test patch", "passed_reproducing_test": True, - "passed_build": True, "passed_regression_test": True, "passed_existing_test": True, "issue_response": "Issue fixed", @@ -140,7 +138,6 @@ def test_answer_issue_with_container(mock_service): True, True, True, - True, "Issue fixed", IssueType.BUG, ) @@ -169,7 +166,6 @@ def test_answer_issue_with_container(mock_service): "data": { "patch": "test patch", "passed_reproducing_test": True, - "passed_build": True, "passed_regression_test": True, "passed_existing_test": True, "issue_response": "Issue fixed", diff --git a/tests/app/services/test_issue_service.py b/tests/app/services/test_issue_service.py index b9932caa..6500d3a6 100644 --- a/tests/app/services/test_issue_service.py +++ b/tests/app/services/test_issue_service.py @@ -55,7 +55,6 @@ async def test_answer_issue_with_general_container(issue_service, monkeypatch): "issue_type": IssueType.BUG, "edit_patch": "test_patch", "passed_reproducing_test": True, - "passed_build": True, "passed_regression_test": True, "passed_existing_test": True, "issue_response": "test_response", @@ -87,10 +86,9 @@ async def test_answer_issue_with_general_container(issue_service, monkeypatch): kg=knowledge_graph, git_repo=repository, container=mock_container, - build_commands=None, test_commands=None, ) - assert result == ("test_patch", True, True, True, True, "test_response", IssueType.BUG) + assert result == ("test_patch", True, True, True, "test_response", IssueType.BUG) async def test_answer_issue_with_user_defined_container(issue_service, monkeypatch): @@ -115,7 +113,6 @@ async def test_answer_issue_with_user_defined_container(issue_service, monkeypat "issue_type": IssueType.QUESTION, "edit_patch": None, "passed_reproducing_test": False, - "passed_build": False, "passed_regression_test": False, "passed_existing_test": False, "issue_response": "test_response", @@ -151,4 +148,4 @@ async def test_answer_issue_with_user_defined_container(issue_service, monkeypat "FROM python:3.8", "test-image", ) - assert result == (None, False, False, False, False, "test_response", IssueType.QUESTION) + assert result == (None, False, False, False, "test_response", IssueType.QUESTION) diff --git a/tests/docker/test_base_container.py b/tests/docker/test_base_container.py index 400293af..39a4b75c 100644 --- a/tests/docker/test_base_container.py +++ b/tests/docker/test_base_container.py @@ -143,25 +143,29 @@ def test_execute_command(container): # Verify mock_container.exec_run.assert_called_once_with( - '/bin/bash -l -c "timeout -k 5 120s test command"', workdir=container.workdir + ["timeout", "-k", "5", "120s", "/bin/bash", "-lc", "test command"], + workdir=container.workdir, ) assert result == "command output" -def test_restart_container(container): - """Test container restart""" - # Setup - mock_container = Mock() - container.container = mock_container - container.start_container = Mock() +def test_reset_repository(container): + """Test container reset repository""" + # Setup - Mock the execute_command method of the container itself + container.execute_command = Mock(return_value="Command output") + + # Also ensure the container has a valid container attribute (even if it's not used in this method) + container.container = Mock() # Execute - container.restart_container() + container.reset_repository() - # Verify - mock_container.stop.assert_called_once_with(timeout=10) - mock_container.remove.assert_called_once_with(force=True) - container.start_container.assert_called_once() + # Verify - Check that execute_command was called twice with the correct commands + assert container.execute_command.call_count == 2 + + # Check the specific calls + expected_calls = [call("git reset --hard"), call("git clean -fd")] + container.execute_command.assert_has_calls(expected_calls, any_order=False) def test_cleanup(container, mock_docker_client): diff --git a/tests/lang_graph/nodes/test_issue_bug_responder_node.py b/tests/lang_graph/nodes/test_issue_bug_responder_node.py index 061622c2..1ca72b99 100644 --- a/tests/lang_graph/nodes/test_issue_bug_responder_node.py +++ b/tests/lang_graph/nodes/test_issue_bug_responder_node.py @@ -1,5 +1,4 @@ import pytest -from langchain_core.messages import AIMessage from prometheus.lang_graph.nodes.issue_bug_responder_node import IssueBugResponderNode from prometheus.lang_graph.subgraphs.issue_bug_state import IssueBugState @@ -16,19 +15,27 @@ def fake_llm(): @pytest.fixture def basic_state(): return IssueBugState( - { - "issue_title": "Test Bug", - "issue_body": "Found a bug in the code", - "issue_comments": [ - {"username": "user1", "comment": "This affects my workflow"}, - {"username": "user2", "comment": "Same issue here"}, - ], - "edit_messages": [AIMessage("I have fixed the bug")], - "edit_patch": "Fixed array index calculation", - "passed_reproducing_test": True, - "passed_build": True, - "passed_existing_test": True, - } + issue_title="Test Bug", + issue_body="Found a bug in the code", + issue_comments=[ + {"username": "user1", "comment": "This affects my workflow"}, + {"username": "user2", "comment": "Same issue here"}, + ], + edit_patch="Fixed array index calculation", + passed_reproducing_test=True, + passed_regression_test=True, + passed_existing_test=True, + run_build=True, + run_existing_test=True, + run_regression_test=True, + run_reproduce_test=True, + number_of_candidate_patch=6, + reproduced_bug=True, + reproduced_bug_file="mock.py", + reproduced_bug_patch="mock patch to reproduce the bug", + reproduced_bug_commands="pytest test_bug.py", + selected_regression_tests=["tests:tests"], + issue_response="Mock Response", ) @@ -50,46 +57,40 @@ def test_format_human_message_verification(fake_llm, basic_state): message = node.format_human_message(basic_state) assert "✓ The bug reproducing test passed" in message.content - assert "✓ Build passes successfully" in message.content + assert "✓ All selected regression tests passes successfully" in message.content assert "✓ All existing tests pass successfully" in message.content def test_format_human_message_no_verification(fake_llm): """Test message formatting without verifications.""" state = IssueBugState( - { - "issue_title": "Test Bug", - "issue_body": "Bug description", - "issue_comments": [], - "edit_messages": [AIMessage("I have fixed the bug")], - "edit_patch": "Fixed array index calculation", - "passed_reproducing_test": False, - "passed_build": False, - "passed_existing_test": False, - } + issue_title="Test Bug", + issue_body="Bug description", + issue_comments=[], + edit_patch="Fixed array index calculation", + passed_reproducing_test=False, + passed_existing_test=False, + passed_regression_test=False, ) node = IssueBugResponderNode(fake_llm) message = node.format_human_message(state) assert "✓ The bug reproducing test passed" not in message.content - assert "✓ Build passes successfully" not in message.content + assert "✓ All selected regression tests passes successfully" not in message.content assert "✓ All existing tests pass successfully" not in message.content def test_format_human_message_partial_verification(fake_llm): """Test message formatting with partial verifications.""" state = IssueBugState( - { - "issue_title": "Test Bug", - "issue_body": "Bug description", - "issue_comments": [], - "edit_messages": [AIMessage("I have fixed the bug")], - "edit_patch": "Fixed array index calculation", - "passed_reproducing_test": True, - "passed_build": False, - "passed_existing_test": True, - } + issue_title="Test Bug", + issue_body="Bug description", + issue_comments=[], + edit_patch="Fixed array index calculation", + passed_reproducing_test=True, + passed_existing_test=True, + passed_regression_test=True, ) node = IssueBugResponderNode(fake_llm) diff --git a/tests/lang_graph/subgraphs/test_issue_bug_subgraph.py b/tests/lang_graph/subgraphs/test_issue_bug_subgraph.py index 41a08a86..1899e6d1 100644 --- a/tests/lang_graph/subgraphs/test_issue_bug_subgraph.py +++ b/tests/lang_graph/subgraphs/test_issue_bug_subgraph.py @@ -53,7 +53,6 @@ def test_issue_bug_subgraph_with_commands(mock_container, mock_kg, mock_git_repo """Test that IssueBugSubgraph initializes correctly with build and test commands.""" fake_advanced_model = FakeListChatWithToolsModel(responses=[]) fake_base_model = FakeListChatWithToolsModel(responses=[]) - build_commands = ["make build"] test_commands = ["make test"] subgraph = IssueBugSubgraph( @@ -62,7 +61,6 @@ def test_issue_bug_subgraph_with_commands(mock_container, mock_kg, mock_git_repo container=mock_container, kg=mock_kg, git_repo=mock_git_repo, - build_commands=build_commands, test_commands=test_commands, )