diff --git a/src/migrations/versions/ee9136e6ff84_migration.py b/src/migrations/versions/ee9136e6ff84_migration.py new file mode 100644 index 000000000..fe7b800e0 --- /dev/null +++ b/src/migrations/versions/ee9136e6ff84_migration.py @@ -0,0 +1,70 @@ +"""Migration + +Revision ID: ee9136e6ff84 +Revises: e0fcdc14251c +Create Date: 2025-03-06 14:31:33.084873 + +""" + +import sqlalchemy as sa +from alembic import op +from pgvector.sqlalchemy import Vector # type: ignore +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "ee9136e6ff84" +down_revision = "e0fcdc14251c" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "review_comments_embedding", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("provider", sa.String(), nullable=False), + sa.Column("owner", sa.String(), nullable=False), + sa.Column("repo", sa.String(), nullable=False), + sa.Column("pr_id", sa.BigInteger(), nullable=False), + sa.Column("body", sa.String(), nullable=False), + sa.Column("is_good_pattern", sa.Boolean(), nullable=False), + sa.Column("embedding", Vector(dim=768), nullable=False), + sa.Column("comment_metadata", postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("provider", "pr_id", "repo", "owner"), + ) + with op.batch_alter_table("review_comments_embedding", schema=None) as batch_op: + batch_op.create_index( + "ix_review_comments_embedding_hnsw", + ["embedding"], + unique=False, + postgresql_using="hnsw", + postgresql_with={"m": 16, "ef_construction": 200}, + postgresql_ops={"embedding": "vector_cosine_ops"}, + ) + batch_op.create_index( + "ix_review_comments_is_good_pattern", ["is_good_pattern"], unique=False + ) + batch_op.create_index( + "ix_review_comments_repo_owner_pr", ["owner", "repo", "pr_id"], unique=False + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("review_comments_embedding", schema=None) as batch_op: + batch_op.drop_index("ix_review_comments_repo_owner_pr") + batch_op.drop_index("ix_review_comments_is_good_pattern") + batch_op.drop_index( + "ix_review_comments_embedding_hnsw", + postgresql_using="hnsw", + postgresql_with={"m": 16, "ef_construction": 200}, + postgresql_ops={"embedding": "vector_cosine_ops"}, + ) + + op.drop_table("review_comments_embedding") + # ### end Alembic commands ### diff --git a/src/seer/app.py b/src/seer/app.py index 1ee60a411..6b20c0dfe 100644 --- a/src/seer/app.py +++ b/src/seer/app.py @@ -50,6 +50,7 @@ CodecovTaskRequest, CodegenBaseRequest, CodegenBaseResponse, + CodegenPrClosedResponse, CodegenPrReviewResponse, CodegenPrReviewStateRequest, CodegenPrReviewStateResponse, @@ -60,6 +61,7 @@ CodegenUnitTestsStateResponse, ) from seer.automation.codegen.tasks import ( + codegen_pr_closed, codegen_pr_review, codegen_relevant_warnings, codegen_unittest, @@ -257,6 +259,11 @@ def codegen_unit_tests_endpoint(data: CodegenBaseRequest) -> CodegenUnitTestsRes return codegen_unittest(data) +@json_api(blueprint, "/v1/automation/codegen/pr-closed") +def codegen_pr_closed_endpoint(data: CodegenBaseRequest) -> CodegenPrClosedResponse: + return codegen_pr_closed(data) + + @json_api(blueprint, "/v1/automation/codegen/unit-tests/state") def codegen_unit_tests_state_endpoint( data: CodegenUnitTestsStateRequest, @@ -307,6 +314,8 @@ def codecov_request_endpoint( return codegen_pr_review_endpoint(data.data) elif data.request_type == "unit-tests": return codegen_unit_tests_endpoint(data.data) + elif data.request_type == "pr-closed": + return codegen_pr_closed_endpoint(data.data) raise ValueError(f"Unsupported request_type: {data.request_type}") diff --git a/src/seer/automation/codebase/repo_client.py b/src/seer/automation/codebase/repo_client.py index 86b78cff4..66f757d3b 100644 --- a/src/seer/automation/codebase/repo_client.py +++ b/src/seer/automation/codebase/repo_client.py @@ -122,11 +122,31 @@ def get_codecov_pr_review_app_credentials( return app_id, private_key +@inject +def get_codecov_pr_closed_app_credentials( + config: AppConfig = injected, +) -> tuple[int | str | None, str | None]: + app_id = config.GITHUB_CODECOV_PR_CLOSED_APP_ID + private_key = config.GITHUB_CODECOV_PR_CLOSED_PRIVATE_KEY + + if not app_id: + logger.warning("No key set GITHUB_CODECOV_PR_CLOSED_APP_ID") + if not private_key: + logger.warning("No key set GITHUB_CODECOV_PR_CLOSED_PRIVATE_KEY") + + if not app_id or not private_key: + sentry_sdk.capture_message("Invalid credentials for codecov pr closed app.") + return get_write_app_credentials() + + return app_id, private_key + + class RepoClientType(str, Enum): READ = "read" WRITE = "write" CODECOV_UNIT_TEST = "codecov_unit_test" CODECOV_PR_REVIEW = "codecov_pr_review" + CODECOV_PR_CLOSED = "codecov_pr_closed" class RepoClient: @@ -225,6 +245,8 @@ def from_repo_definition(cls, repo_def: RepoDefinition, type: RepoClientType): return cls(*get_codecov_unit_test_app_credentials(), repo_def) elif type == RepoClientType.CODECOV_PR_REVIEW: return cls(*get_codecov_pr_review_app_credentials(), repo_def) + elif type == RepoClientType.CODECOV_PR_CLOSED: + return cls(*get_codecov_pr_closed_app_credentials(), repo_def) return cls(*get_read_app_credentials(), repo_def) diff --git a/src/seer/automation/codegen/models.py b/src/seer/automation/codegen/models.py index 108e04167..ef5f31161 100644 --- a/src/seer/automation/codegen/models.py +++ b/src/seer/automation/codegen/models.py @@ -55,6 +55,10 @@ class CodegenPrReviewRequest(CodegenBaseRequest): pass +class CodegenPrClosedRequest(CodegenBaseRequest): + pass + + class CodegenContinuation(CodegenState): request: CodegenBaseRequest @@ -82,6 +86,10 @@ class CodegenUnitTestsResponse(CodegenBaseResponse): pass +class CodegenPrClosedResponse(CodegenBaseResponse): + pass + + class CodegenUnitTestsStateRequest(BaseModel): run_id: int @@ -207,6 +215,11 @@ class CodePredictRelevantWarningsOutput(BaseComponentOutput): class CodecovTaskRequest(BaseModel): - data: CodegenUnitTestsRequest | CodegenPrReviewRequest | CodegenRelevantWarningsRequest + data: ( + CodegenUnitTestsRequest + | CodegenPrReviewRequest + | CodegenRelevantWarningsRequest + | CodegenPrClosedRequest + ) external_owner_id: str - request_type: Literal["unit-tests", "pr-review", "relevant-warnings"] + request_type: Literal["unit-tests", "pr-review", "relevant-warnings", "pr-closed"] diff --git a/src/seer/automation/codegen/pr_closed_step.py b/src/seer/automation/codegen/pr_closed_step.py new file mode 100644 index 000000000..137e94c81 --- /dev/null +++ b/src/seer/automation/codegen/pr_closed_step.py @@ -0,0 +1,154 @@ +from asyncio.log import logger +from typing import Any + +from github.PullRequestComment import PullRequestComment +from langfuse.decorators import observe +from sentry_sdk.ai.monitoring import ai_track +from sqlalchemy.dialects.postgresql import insert + +from celery_app.app import celery_app +from seer.automation.agent.embeddings import GoogleProviderEmbeddings +from seer.automation.autofix.config import ( + AUTOFIX_EXECUTION_HARD_TIME_LIMIT_SECS, + AUTOFIX_EXECUTION_SOFT_TIME_LIMIT_SECS, +) +from seer.automation.codebase.repo_client import RepoClientType +from seer.automation.codegen.step import CodegenStep +from seer.automation.models import RepoDefinition +from seer.automation.pipeline import PipelineStepTaskRequest +from seer.automation.state import DbStateRunTypes +from seer.db import DbReviewCommentEmbedding, Session + + +class PrClosedStepRequest(PipelineStepTaskRequest): + pr_id: int + repo_definition: RepoDefinition + + +class CommentAnalyzer: + """ + Handles comment analysis logic + """ + + def __init__(self, bot_username: str = "codecov-ai-reviewer[bot]"): + self.bot_username = bot_username + + def is_bot_comment(self, comment: PullRequestComment) -> bool: + """Check if comment is authored by bot""" + return comment.user.login == self.bot_username + + def analyze_reactions(self, comment: PullRequestComment) -> tuple[bool, bool]: + """ + Analyze reactions on a comment + Returns: (is_good_pattern, is_bad_pattern) + """ + reactions = comment.get_reactions() + upvotes = sum(1 for r in reactions if r.content == "+1") + downvotes = sum(1 for r in reactions if r.content == "-1") + + is_good_pattern = upvotes >= downvotes + is_bad_pattern = downvotes > upvotes + return is_good_pattern, is_bad_pattern + + +@celery_app.task( + time_limit=AUTOFIX_EXECUTION_HARD_TIME_LIMIT_SECS, + soft_time_limit=AUTOFIX_EXECUTION_SOFT_TIME_LIMIT_SECS, +) +def pr_closed_task(*args, request: dict[str, Any]): + PrClosedStep(request, DbStateRunTypes.PR_CLOSED).invoke() + + +class PrClosedStep(CodegenStep): + """ + This class represents the PR Closed step in the codegen pipeline. It is responsible for + processing a closed or merged PR, including gathering and analyzing comment reactions. + """ + + name = "PrClosedStep" + max_retries = 2 + + @staticmethod + def _instantiate_request(request: dict[str, Any]) -> PrClosedStepRequest: + return PrClosedStepRequest.model_validate(request) + + @staticmethod + def get_task(): + return pr_closed_task + + def __init__(self, request: dict[str, Any], type: DbStateRunTypes): + super().__init__(request, type) + self.analyzer = CommentAnalyzer() + + def _process_comment(self, comment: PullRequestComment, pr): + try: + is_good_pattern, is_bad_pattern = self.analyzer.analyze_reactions(comment) + + logger.info( + f"Processing bot comment id {comment.id} on PR {pr.url}: " + f"good_pattern={is_good_pattern}, " + f"bad_pattern={is_bad_pattern}" + ) + + model = GoogleProviderEmbeddings.model( + "text-embedding-005", task_type="CODE_RETRIEVAL_QUERY" + ) + # encode() expects list[str], returns 2D array + embedding = model.encode([comment.body])[0] + + with Session() as session: + insert_stmt = insert(DbReviewCommentEmbedding).values( + provider="github", + owner=pr.base.repo.owner.login, + repo=pr.base.repo.name, + pr_id=pr.number, + body=comment.body, + is_good_pattern=is_good_pattern, + comment_metadata={ + "url": comment.html_url, + "comment_id": comment.id, + "location": ( + {"file_path": comment.path, "line_number": comment.position} + if hasattr(comment, "path") + else None + ), + "timestamps": { + "created_at": comment.created_at.isoformat(), + "updated_at": comment.updated_at.isoformat(), + }, + }, + embedding=embedding, + ) + + session.execute( + insert_stmt.on_conflict_do_nothing( + index_elements=["provider", "pr_id", "repo", "owner"] + ) + ) + session.commit() + + except Exception as e: + self.logger.error(f"Error processing comment {comment.id} on PR {pr.url}: {e}") + raise + + @observe(name="Codegen - PR Closed") + @ai_track(description="Codegen - PR Closed Step") + def _invoke(self, **kwargs): + self.logger.info("Executing Codegen - PR Closed Step") + self.context.event_manager.mark_running() + + repo_client = self.context.get_repo_client(type=RepoClientType.CODECOV_PR_CLOSED) + pr = repo_client.repo.get_pull(self.request.pr_id) + + try: + review_comments = pr.get_review_comments() + + for comment in review_comments: + if self.analyzer.is_bot_comment(comment): + self._process_comment(comment, pr) + + self.context.event_manager.mark_completed() + + except Exception as e: + self.logger.error(f"Error processing closed PR {pr.url}: {e}") + raise diff --git a/src/seer/automation/codegen/tasks.py b/src/seer/automation/codegen/tasks.py index 276d7745a..ad2d144be 100644 --- a/src/seer/automation/codegen/tasks.py +++ b/src/seer/automation/codegen/tasks.py @@ -1,6 +1,7 @@ from seer.automation.codegen.models import ( CodegenBaseRequest, CodegenContinuation, + CodegenPrClosedResponse, CodegenPrReviewResponse, CodegenRelevantWarningsRequest, CodegenRelevantWarningsResponse, @@ -8,6 +9,7 @@ CodegenUnitTestsResponse, CodegenUnitTestsStateRequest, ) +from seer.automation.codegen.pr_closed_step import PrClosedStep, PrClosedStepRequest from seer.automation.codegen.pr_review_step import PrReviewStep, PrReviewStepRequest from seer.automation.codegen.relevant_warnings_step import ( RelevantWarningsStep, @@ -46,6 +48,19 @@ def create_initial_pr_review_run(request: CodegenBaseRequest) -> DbState[Codegen return state +def create_initial_pr_closed_run(request: CodegenBaseRequest) -> DbState[CodegenContinuation]: + state = CodegenContinuationState.new( + CodegenContinuation(request=request), group_id=request.pr_id, t=DbStateRunTypes.PR_CLOSED + ) + + with state.update() as cur: + cur.status = CodegenStatus.PENDING + cur.signals = [] + cur.mark_triggered() + + return state + + def create_initial_relevant_warnings_run( request: CodegenRelevantWarningsRequest, ) -> DbState[CodegenContinuation]: @@ -83,6 +98,25 @@ def codegen_unittest(request: CodegenBaseRequest, app_config: AppConfig = inject return CodegenUnitTestsResponse(run_id=cur_state.run_id) +@inject +def codegen_pr_closed(request: CodegenBaseRequest, app_config: AppConfig = injected): + state = create_initial_pr_closed_run(request) + + cur_state = state.get() + + pr_closed_request = PrClosedStepRequest( + run_id=cur_state.run_id, + pr_id=request.pr_id, + repo_definition=request.repo, + ) + + PrClosedStep.get_signature( + pr_closed_request, queue=app_config.CELERY_WORKER_QUEUE + ).apply_async() + + return CodegenPrClosedResponse(run_id=cur_state.run_id) + + def get_unittest_state(request: CodegenUnitTestsStateRequest): state = CodegenContinuationState(request.run_id) return state.get() diff --git a/src/seer/automation/state.py b/src/seer/automation/state.py index 090b242c9..83d71046a 100644 --- a/src/seer/automation/state.py +++ b/src/seer/automation/state.py @@ -20,6 +20,7 @@ class DbStateRunTypes(str, Enum): UNIT_TEST = "unit-test" PR_REVIEW = "pr-review" RELEVANT_WARNINGS = "relevant-warnings" + PR_CLOSED = "pr-closed" class State(abc.ABC, Generic[_State]): diff --git a/src/seer/configuration.py b/src/seer/configuration.py index e7cd78060..38a01bac5 100644 --- a/src/seer/configuration.py +++ b/src/seer/configuration.py @@ -58,6 +58,8 @@ class AppConfig(BaseModel): GITHUB_CODECOV_UNIT_TEST_PRIVATE_KEY: str | None = None GITHUB_CODECOV_PR_REVIEW_APP_ID: str | None = None GITHUB_CODECOV_PR_REVIEW_PRIVATE_KEY: str | None = None + GITHUB_CODECOV_PR_CLOSED_APP_ID: str | None = None + GITHUB_CODECOV_PR_CLOSED_PRIVATE_KEY: str | None = None LANGFUSE_PUBLIC_KEY: str = "" LANGFUSE_SECRET_KEY: str = "" LANGFUSE_HOST: str = "" diff --git a/src/seer/db.py b/src/seer/db.py index fcf5bab89..2629949f6 100644 --- a/src/seer/db.py +++ b/src/seer/db.py @@ -14,6 +14,7 @@ JSON, TIMESTAMP, BigInteger, + Boolean, Connection, DateTime, Enum, @@ -29,7 +30,7 @@ select, text, ) -from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.dialects.postgresql import JSONB, insert from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, sessionmaker from seer.configuration import AppConfig @@ -439,3 +440,35 @@ class DbProphetAlertTimeSeriesHistory(Base): saved_at: Mapped[datetime.datetime] = mapped_column( DateTime, nullable=False, default=datetime.datetime.now(datetime.UTC) ) + + +class DbReviewCommentEmbedding(Base): + """Store PR review comments with their embeddings for pattern matching per organization""" + + __tablename__ = "review_comments_embedding" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + provider: Mapped[str] = mapped_column(String, nullable=False) + owner: Mapped[str] = mapped_column(String, nullable=False) + repo: Mapped[str] = mapped_column(String, nullable=False) + pr_id: Mapped[int] = mapped_column(BigInteger, nullable=False) + body: Mapped[str] = mapped_column(String, nullable=False) + is_good_pattern: Mapped[bool] = mapped_column(Boolean, nullable=False) + embedding: Mapped[Vector] = mapped_column(Vector(768), nullable=False) + comment_metadata: Mapped[dict] = mapped_column(JSONB) + created_at: Mapped[datetime.datetime] = mapped_column( + DateTime, nullable=False, default=datetime.datetime.now(datetime.UTC) + ) + + __table_args__ = ( + UniqueConstraint("provider", "pr_id", "repo", "owner"), + Index("ix_review_comments_repo_owner_pr", "owner", "repo", "pr_id"), + Index( + "ix_review_comments_embedding_hnsw", + "embedding", + postgresql_using="hnsw", + postgresql_with={"m": 16, "ef_construction": 200}, + postgresql_ops={"embedding": "vector_cosine_ops"}, + ), + Index("ix_review_comments_is_good_pattern", "is_good_pattern"), + ) diff --git a/tests/automation/codegen/test_pr_closed_step.py b/tests/automation/codegen/test_pr_closed_step.py new file mode 100644 index 000000000..795841828 --- /dev/null +++ b/tests/automation/codegen/test_pr_closed_step.py @@ -0,0 +1,211 @@ +import unittest +from datetime import datetime, timezone +from unittest.mock import MagicMock, patch + +import pytest + +from seer.automation.codegen.models import CodegenContinuation, CodegenStatus +from seer.automation.codegen.pr_closed_step import ( + CommentAnalyzer, + PrClosedStep, + PrClosedStepRequest, + pr_closed_task, +) +from seer.automation.codegen.state import CodegenContinuationState +from seer.automation.models import RepoDefinition +from seer.automation.state import DbStateRunTypes +from seer.db import DbReviewCommentEmbedding, Session, db + + +@pytest.fixture +def repo_definition(): + """Create test repo definition""" + return RepoDefinition( + name="test-repo", + owner="test-org", + provider="github", + external_id="12345", + base_commit_sha="test_sha", + provider_raw="github", + ) + + +@pytest.fixture +def mock_pr(): + """Create mock PR""" + pr = MagicMock() + pr.number = 123 + pr.base.repo.name = "test-repo" + pr.base.repo.owner.login = "test-org" + pr.get_review_comments.return_value = [] + return pr + + +@pytest.fixture +def mock_repo_client(mock_pr): + """Create mock repo client""" + client = MagicMock() + client.repo = mock_pr.base.repo + client.repo.get_pull.return_value = mock_pr + return client + + +@pytest.fixture +def pr_closed_request(repo_definition): + """Create PR closed request""" + return PrClosedStepRequest(run_id=789, step_id=123, pr_id=123, repo_definition=repo_definition) + + +@pytest.fixture +def state(pr_closed_request): + """Create test state""" + now = datetime.now(timezone.utc) + + state = CodegenContinuationState.new( + CodegenContinuation( + request={ + "repo": pr_closed_request.repo_definition.model_dump(), + "pr_id": pr_closed_request.pr_id, + }, + status=CodegenStatus.PENDING, + run_id=pr_closed_request.run_id, + last_triggered_at=now, + updated_at=now, + completed_at=None, + file_changes=[], + signals=[], + relevant_warning_results=[], + ), + group_id=pr_closed_request.pr_id, + t=DbStateRunTypes.PR_CLOSED, + ) + + with state.update() as cur: + cur.status = CodegenStatus.PENDING + cur.run_id = pr_closed_request.run_id + cur.mark_triggered() + + return state + + +class TestPrClosedStep(unittest.TestCase): + def setUp(self): + self.request_data = { + "run_id": 789, + "step_id": 123, + "pr_id": 123, + "repo_definition": RepoDefinition( + name="test-repo", + owner="test-org", + provider="github", + external_id="12345", + base_commit_sha="test_sha", + provider_raw="github", + ).model_dump(), + } + self.request = PrClosedStepRequest(**self.request_data) + + @patch("seer.automation.codegen.pr_closed_step.PrClosedStep") + def test_pr_closed_task(self, mock_step_class): + pr_closed_task(request=self.request_data) + + mock_step_class.assert_called_once_with(self.request_data, DbStateRunTypes.PR_CLOSED) + mock_step_class.return_value.invoke.assert_called_once() + + @patch("seer.automation.pipeline.PipelineStep", new_callable=MagicMock) + @patch("seer.automation.codegen.step.CodegenStep._instantiate_context") + def test_invoke_no_comments(self, mock_instantiate_context, _): + mock_repo_client = MagicMock() + mock_pr = MagicMock() + mock_context = MagicMock() + mock_context.get_repo_client.return_value = mock_repo_client + mock_repo_client.repo.get_pull.return_value = mock_pr + mock_pr.get_review_comments.return_value = [] + + step = PrClosedStep(request=self.request_data, type=DbStateRunTypes.PR_CLOSED) + step.context = mock_context + step.invoke() + + mock_context.get_repo_client.assert_called_once() + mock_repo_client.repo.get_pull.assert_called_once_with(self.request.pr_id) + mock_pr.get_review_comments.assert_called_once() + mock_context.event_manager.mark_completed.assert_called_once() + + @patch("seer.automation.pipeline.PipelineStep", new_callable=MagicMock) + @patch("seer.automation.codegen.step.CodegenStep._instantiate_context") + @patch("seer.automation.agent.embeddings.GoogleProviderEmbeddings.model") + def test_invoke_with_bot_comment(self, mock_model, mock_instantiate_context, _): + mock_model.return_value.encode.return_value = [[0.1] * 768] + + mock_pr = MagicMock() + mock_pr.number = 123 + mock_pr.base.repo.name = "test-repo" + mock_pr.base.repo.owner.login = "test-org" + + mock_repo_client = MagicMock() + mock_repo_client.repo = mock_pr.base.repo + mock_repo_client.repo.get_pull.return_value = mock_pr + + mock_context = MagicMock() + mock_context.get_repo_client.return_value = mock_repo_client + + now = datetime.now(timezone.utc) + comment = MagicMock(spec=[]) + comment.id = 456 + comment.user = MagicMock(spec=[]) + comment.user.login = "codecov-ai-reviewer[bot]" + comment.body = "wrap this in try-catch" + + reaction = MagicMock(spec=[]) + reaction.content = "+1" + comment.get_reactions = MagicMock(return_value=[reaction]) + + comment.html_url = "https://github.com/org/repo/pull/123#discussion_r456" + comment.path = "src/file.py" + comment.position = 42 + comment.created_at = now + comment.updated_at = now + + comment.raw_data = { + "id": 456, + "url": "https://github.com/org/repo/pull/123#discussion_r456", + "html_url": "https://github.com/org/repo/pull/123#discussion_r456", + "path": "src/file.py", + "position": 42, + "user": {"login": "codecov-ai-reviewer[bot]"}, + "created_at": now.isoformat(), + "updated_at": now.isoformat(), + "body": "wrap this in try-catch", + "reactions": [{"content": "+1"}], + } + mock_pr.get_review_comments.return_value = [comment] + + step = PrClosedStep(request=self.request_data, type=DbStateRunTypes.PR_CLOSED) + step.context = mock_context + step.invoke() + + with Session() as session: + record = session.query(DbReviewCommentEmbedding).first() + self.assertIsNotNone(record) + self.assertEqual(record.provider, "github") + self.assertEqual(record.owner, "test-org") + self.assertEqual(record.repo, "test-repo") + self.assertEqual(record.pr_id, 123) + self.assertEqual(record.body, "wrap this in try-catch") + self.assertTrue(record.is_good_pattern) + + step.context.event_manager.mark_completed.assert_called_once() + + def test_comment_analyzer(self): + analyzer = CommentAnalyzer(bot_username="codecov-ai-reviewer[bot]") + + comment = MagicMock() + comment.get_reactions.return_value = [ + MagicMock(content="+1"), + MagicMock(content="+1"), + MagicMock(content="-1"), + ] + + is_good, is_bad = analyzer.analyze_reactions(comment) + self.assertTrue(is_good) + self.assertFalse(is_bad) diff --git a/tests/test_celery.py b/tests/test_celery.py index b0f324177..5dc89637a 100644 --- a/tests/test_celery.py +++ b/tests/test_celery.py @@ -29,6 +29,7 @@ def test_detected_celery_jobs(): "seer.automation.codegen.relevant_warnings_step.relevant_warnings_task", "seer.automation.tasks.delete_data_for_ttl", "seer.smoke_test.smoke_test", + "seer.automation.codegen.pr_closed_step.pr_closed_task", ] )