diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index e66324d200e..d6cc9bf8c72 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -66,6 +66,7 @@ jobs: - 'scripts/lint_controller_sqlalchemy.py' - 'scripts/ast_grep_rules/no_new_getattr.yml' - 'scripts/ast_grep_rules/no_new_controller_sqlalchemy.yml' + - 'scripts/ast_grep_rules/no_new_session_mock.yml' - '.github/workflows/style.yml' - '.github/workflows/main-ci.yml' - '.github/workflows/api-tests.yml' @@ -160,9 +161,11 @@ jobs: - 'scripts/ast_grep_guard.py' - 'scripts/check_no_new_getattr.py' - 'scripts/check_no_new_controller_sqlalchemy.py' + - 'scripts/check_no_new_session_mock.py' - 'scripts/lint_controller_sqlalchemy.py' - 'scripts/ast_grep_rules/no_new_getattr.yml' - 'scripts/ast_grep_rules/no_new_controller_sqlalchemy.yml' + - 'scripts/ast_grep_rules/no_new_session_mock.yml' - '.github/workflows/style.yml' - '.github/workflows/main-ci.yml' dify-agent-style: diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 232480785e1..7f17e22f48d 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -89,6 +89,10 @@ jobs: if: inputs.run-python-style run: uv run --project api python scripts/check_no_new_controller_sqlalchemy.py --base-rev "${{ inputs.base-rev }}" + - name: Run No New Session Mock Guard + if: inputs.run-python-style + run: uv run --project api python scripts/check_no_new_session_mock.py --base-rev "${{ inputs.base-rev }}" + - name: Run Type Checks if: inputs.run-python-style env: diff --git a/scripts/ast_grep_rules/no_new_session_mock.yml b/scripts/ast_grep_rules/no_new_session_mock.yml new file mode 100644 index 00000000000..6237766034c --- /dev/null +++ b/scripts/ast_grep_rules/no_new_session_mock.yml @@ -0,0 +1,11 @@ +id: no-new-session-mock +language: python +rule: + any: + - pattern: $SESSION = MagicMock($$$ARGS) + - pattern: $SESSION = Mock($$$ARGS) +constraints: + SESSION: + regex: ^(session|db_session|mock_session|read_session|write_session)$ +message: net-new session mock assignment in test code +severity: error diff --git a/scripts/check_no_new_session_mock.py b/scripts/check_no_new_session_mock.py new file mode 100644 index 00000000000..941f576b487 --- /dev/null +++ b/scripts/check_no_new_session_mock.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Block net-new session mock assignments in unit tests. + +This catches net-new top-level session mocks like +`session = MagicMock()` or `db_session = Mock()` added to +`tests/unit_tests/**`. Pre-existing matches are ignored via the +shared baseline diff (`scripts/ast_grep_guard.py`), so historical +mocks that haven't been cleaned up yet do not block the gate. + +A `# guard-ignore: no-new-session-mock -- ` line on the same +source line suppresses a violation with a human-readable reason. +""" + +from __future__ import annotations + +from pathlib import Path + +from ast_grep_guard import Match, has_reasoned_guard_ignore, is_python_source_path, rule_path, run_guard + + +RULE_ID = "no-new-session-mock" +RULE_PATH = rule_path("no_new_session_mock.yml") +TEST_ROOT = Path("api/tests/unit_tests") +VIOLATION_MESSAGE = "no-new-session-mock net-new session mock assignment in test code" + + +def is_test_source_path(path: str) -> bool: + if not is_python_source_path(path): + return False + return Path(path).is_relative_to(TEST_ROOT) + + +def is_reportable_match(match: Match) -> bool: + return not has_reasoned_guard_ignore(match.source_line, RULE_ID) + + +def main() -> int: + return run_guard( + description=__doc__, + rule=RULE_PATH, + is_scanned_path=is_test_source_path, + is_reportable_match=is_reportable_match, + violation_message=VIOLATION_MESSAGE, + ) + + +if __name__ == "__main__": + raise SystemExit(main())