| #!/usr/bin/env fuchsia-vendored-python | |
| # Copyright 2026 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| import os | |
| import subprocess | |
| import unittest | |
| from pathlib import Path | |
| from typing import Any | |
| from unittest import mock | |
| from agents.lib.githooks import adapters | |
| from agents.lib.githooks.reporters import ConsoleReporter | |
| from agents_testing.base import BaseTestCase | |
| class AdaptersTest(unittest.TestCase): | |
| """Unit tests for githooks.adapters dataclasses and definitions.""" | |
| def test_hook_action_definition(self) -> None: | |
| action = adapters.HookAction( | |
| name="test_action", | |
| action_fn=lambda ctx, files: True, | |
| is_mutating=True, | |
| extensions=[".py"], | |
| remediation_cmd_fn=lambda files: [f"fix {','.join(files)}"], | |
| ) | |
| self.assertEqual(action.name, "test_action") | |
| self.assertTrue(action.is_mutating) | |
| self.assertEqual(action.extensions, [".py"]) | |
| self.assertIsNotNone(action.remediation_cmd_fn) | |
| if action.remediation_cmd_fn: | |
| self.assertEqual(action.remediation_cmd_fn(["a.py"]), ["fix a.py"]) | |
| self.assertTrue(len(adapters.DEFAULT_PRE_COMMIT_ACTIONS) >= 2) | |
| default_cmd_fn = adapters.DEFAULT_PRE_COMMIT_ACTIONS[ | |
| 0 | |
| ].remediation_cmd_fn | |
| self.assertIsNotNone(default_cmd_fn) | |
| if default_cmd_fn: | |
| self.assertEqual( | |
| default_cmd_fn(["a.py"]), | |
| ["fx format-code --files=a.py"], | |
| ) | |
| lint_action = adapters.DEFAULT_PRE_COMMIT_ACTIONS[1] | |
| self.assertEqual(lint_action.name, "fx_lint") | |
| self.assertFalse(lint_action.is_mutating) | |
| self.assertIsNotNone(lint_action.remediation_cmd_fn) | |
| if lint_action.remediation_cmd_fn: | |
| self.assertEqual( | |
| lint_action.remediation_cmd_fn(["a.py"]), | |
| ["fx lint --files=a.py"], | |
| ) | |
| class FormatCodeActionTest(BaseTestCase): | |
| """Unit tests for format_code_action using HookContext.reporter.""" | |
| def make_mock_reporter( | |
| self, | |
| spec: Any = None, | |
| ) -> mock.MagicMock: | |
| """Factory helper to construct a mock ConsoleReporter.""" | |
| return mock.MagicMock(spec=spec) if spec else mock.MagicMock() | |
| def setUp(self) -> None: | |
| super().setUp() | |
| (self.test_dir / ".fx-root").touch() | |
| def test_format_code_action_empty(self) -> None: | |
| ctx = adapters.HookContext(repo_root=self.test_dir) | |
| self.assertTrue(adapters.format_code_action(ctx, [])) | |
| def test_format_code_action_runs_fx_format_code(self) -> None: | |
| ctx = adapters.HookContext(repo_root=self.test_dir) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process() | |
| ok = adapters.format_code_action(ctx, ["a.py", "b.cc"]) | |
| self.assertTrue(ok) | |
| mock_cmd.assert_called_once_with( | |
| ["fx", "format-code", "--files=a.py,b.cc"], | |
| cwd=self.test_dir, | |
| ) | |
| def test_format_code_action_error_reporting(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| reporter=mock_reporter, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process( | |
| returncode=1, stderr="Error formatting code" | |
| ) | |
| ok = adapters.format_code_action(ctx, ["foo.py"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once_with( | |
| "Error formatting code" | |
| ) | |
| def test_format_code_action_check_only_modified_non_destructive_returns_false( | |
| self, | |
| ) -> None: | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| check_only=True, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.side_effect = [ | |
| # 1. fx format-code | |
| self.make_completed_process(), | |
| # 2. git diff --quiet -- foo.py | |
| self.make_completed_process(returncode=1), | |
| ] | |
| ok = adapters.format_code_action(ctx, ["foo.py"]) | |
| self.assertFalse(ok) | |
| self.assertEqual( | |
| mock_cmd.call_args_list, | |
| [ | |
| mock.call( | |
| ["fx", "format-code", "--files=foo.py"], | |
| cwd=self.test_dir, | |
| ), | |
| mock.call( | |
| ["git", "diff", "--quiet", "--", "foo.py"], | |
| cwd=self.test_dir, | |
| ), | |
| ], | |
| ) | |
| def test_format_code_action_check_only_unmodified_returns_true( | |
| self, | |
| ) -> None: | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| check_only=True, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.side_effect = [ | |
| # 1. fx format-code | |
| self.make_completed_process(), | |
| # 2. git diff --quiet -- foo.py | |
| self.make_completed_process(returncode=0), | |
| ] | |
| ok = adapters.format_code_action(ctx, ["foo.py"]) | |
| self.assertTrue(ok) | |
| self.assertEqual( | |
| mock_cmd.call_args_list, | |
| [ | |
| mock.call( | |
| ["fx", "format-code", "--files=foo.py"], | |
| cwd=self.test_dir, | |
| ), | |
| mock.call( | |
| ["git", "diff", "--quiet", "--", "foo.py"], | |
| cwd=self.test_dir, | |
| ), | |
| ], | |
| ) | |
| def test_format_code_action_resolves_fx_from_scripts_when_not_in_path( | |
| self, | |
| ) -> None: | |
| scripts_dir = self.test_dir / "tools" / "devshell" | |
| scripts_dir.mkdir(parents=True, exist_ok=True) | |
| fx_script = scripts_dir / "format-code" | |
| fx_script.touch() | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value=None), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process() | |
| ok = adapters.format_code_action(ctx, ["foo.py"]) | |
| self.assertTrue(ok) | |
| mock_cmd.assert_called_once_with( | |
| [str(fx_script), "--files=foo.py"], | |
| cwd=self.test_dir, | |
| ) | |
| def test_format_code_action_prioritizes_scripts_fx_over_ambient_path( | |
| self, | |
| ) -> None: | |
| scripts_dir = self.test_dir / "tools" / "devshell" | |
| scripts_dir.mkdir(parents=True, exist_ok=True) | |
| fx_script = scripts_dir / "format-code" | |
| fx_script.touch() | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value="/ambient/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process() | |
| ok = adapters.format_code_action(ctx, ["foo.py"]) | |
| self.assertTrue(ok) | |
| mock_cmd.assert_called_once_with( | |
| [str(fx_script), "--files=foo.py"], | |
| cwd=self.test_dir, | |
| ) | |
| def test_format_code_action_missing_fx_not_in_path_or_scripts_human( | |
| self, | |
| ) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| reporter=mock_reporter, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value=None), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| ok = adapters.format_code_action(ctx, ["foo.py"]) | |
| self.assertTrue(ok) | |
| mock_reporter.on_warning.assert_called_once() | |
| mock_cmd.assert_not_called() | |
| def test_format_code_action_missing_fx_not_in_path_or_scripts_agent( | |
| self, | |
| ) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value=None), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| ok = adapters.format_code_action(ctx, ["foo.py"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once() | |
| mock_cmd.assert_not_called() | |
| def test_format_code_action_combined_stdout_and_stderr_error(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = subprocess.CompletedProcess( | |
| args=[], | |
| returncode=1, | |
| stdout="Formatter stdout output", | |
| stderr="Formatter stderr output", | |
| ) | |
| ok = adapters.format_code_action(ctx, ["foo.py"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once_with( | |
| "Formatter stdout output\nFormatter stderr output" | |
| ) | |
| def test_format_code_action_empty_output_fallback_error(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process( | |
| returncode=2, stderr="" | |
| ) | |
| ok = adapters.format_code_action(ctx, ["foo.py"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once() | |
| err_msg = mock_reporter.on_error.call_args[0][0] | |
| self.assertIn("failed with exit code 2", err_msg) | |
| class LintCodeActionTest(BaseTestCase): | |
| """Unit tests for lint_code_action using HookContext.reporter.""" | |
| def make_mock_reporter( | |
| self, | |
| spec: Any = None, | |
| ) -> mock.MagicMock: | |
| """Factory helper to construct a mock ConsoleReporter.""" | |
| return mock.MagicMock(spec=spec) if spec else mock.MagicMock() | |
| def setUp(self) -> None: | |
| super().setUp() | |
| (self.test_dir / ".fx-root").touch() | |
| def test_lint_code_action_empty(self) -> None: | |
| ctx = adapters.HookContext(repo_root=self.test_dir, is_agent=True) | |
| self.assertTrue(adapters.lint_code_action(ctx, [])) | |
| def test_lint_code_action_human_skips_slow_linters(self) -> None: | |
| ctx = adapters.HookContext(repo_root=self.test_dir, is_agent=False) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process() | |
| self.assertTrue(adapters.lint_code_action(ctx, ["foo.py"])) | |
| base = [ | |
| "fx", | |
| "lint", | |
| "--skip=check_licenses,clang_tidy,python_format", | |
| ] | |
| self.assertEqual( | |
| mock_cmd.call_args_list, | |
| [ | |
| mock.call([*base, "--files=foo.py"], cwd=self.test_dir), | |
| ], | |
| ) | |
| def test_lint_code_action_runs_fx_lint_for_agent(self) -> None: | |
| ctx = adapters.HookContext(repo_root=self.test_dir, is_agent=True) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process() | |
| ok = adapters.lint_code_action(ctx, ["a.py", "b.cc"]) | |
| self.assertTrue(ok) | |
| base = ["fx", "lint", "--skip=clang_tidy,python_format"] | |
| self.assertEqual( | |
| mock_cmd.call_args_list, | |
| [ | |
| mock.call([*base, "--files=a.py,b.cc"], cwd=self.test_dir), | |
| ], | |
| ) | |
| def test_lint_code_action_error_reporting(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process( | |
| returncode=1, stderr="Linter error found" | |
| ) | |
| ok = adapters.lint_code_action(ctx, ["foo.py"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once_with("Linter error found") | |
| def test_lint_code_action_missing_fx_agent(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value=None), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| ok = adapters.lint_code_action(ctx, ["foo.py"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once() | |
| mock_cmd.assert_not_called() | |
| def test_lint_code_action_missing_fx_human(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| reporter=mock_reporter, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value=None), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| ok = adapters.lint_code_action(ctx, ["foo.py"]) | |
| self.assertTrue(ok) | |
| mock_reporter.on_warning.assert_called_once() | |
| mock_cmd.assert_not_called() | |
| def test_lint_code_action_prioritizes_scripts_fx_over_ambient_path( | |
| self, | |
| ) -> None: | |
| scripts_dir = self.test_dir / "tools" / "devshell" | |
| scripts_dir.mkdir(parents=True, exist_ok=True) | |
| fx_script = scripts_dir / "lint" | |
| fx_script.touch() | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value="/ambient/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process() | |
| ok = adapters.lint_code_action(ctx, ["foo.py"]) | |
| self.assertTrue(ok) | |
| self.assertEqual( | |
| mock_cmd.call_args_list, | |
| [ | |
| mock.call( | |
| [ | |
| str(fx_script), | |
| "--skip=clang_tidy,python_format", | |
| "--files=foo.py", | |
| ], | |
| cwd=self.test_dir, | |
| ), | |
| ], | |
| ) | |
| def test_lint_code_action_empty_output_fallback_error(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with ( | |
| mock.patch("shutil.which", return_value="/bin/fx"), | |
| mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd, | |
| ): | |
| mock_cmd.return_value = self.make_completed_process( | |
| returncode=2, stderr="" | |
| ) | |
| ok = adapters.lint_code_action(ctx, ["foo.py"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once() | |
| err_msg = mock_reporter.on_error.call_args[0][0] | |
| self.assertIn("failed with exit code 2", err_msg) | |
| class CommitMsgActionTest(BaseTestCase): | |
| """Unit tests for commit_msg_action using HookContext.reporter.""" | |
| def make_mock_reporter( | |
| self, | |
| spec: Any = None, | |
| ) -> mock.MagicMock: | |
| """Factory helper to construct a mock ConsoleReporter.""" | |
| return mock.MagicMock(spec=spec) if spec else mock.MagicMock() | |
| def setUp(self) -> None: | |
| super().setUp() | |
| (self.test_dir / ".fx-root").touch() | |
| self.checker_script = ( | |
| self.test_dir / "scripts" / "shac" / "commit_msg_checker.py" | |
| ) | |
| self.checker_script.parent.mkdir(parents=True, exist_ok=True) | |
| self.checker_script.touch() | |
| def test_commit_msg_action_empty(self) -> None: | |
| ctx = adapters.HookContext(repo_root=self.test_dir) | |
| self.assertTrue(adapters.commit_msg_action(ctx, [])) | |
| def test_commit_msg_action_runs_checker_strict_for_agent(self) -> None: | |
| ctx = adapters.HookContext(repo_root=self.test_dir, is_agent=True) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = self.make_completed_process() | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertTrue(ok) | |
| mock_cmd.assert_called_once() | |
| cmd_args = mock_cmd.call_args[0][0] | |
| self.assertIn("--strict", cmd_args) | |
| def test_commit_msg_action_error_reporting(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = self.make_completed_process( | |
| returncode=1, stderr="Title too long" | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once_with("Title too long") | |
| def test_commit_msg_action_warning_human(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| reporter=mock_reporter, | |
| ) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = subprocess.CompletedProcess( | |
| args=[], | |
| returncode=0, | |
| stdout="Warning: check format", | |
| stderr="", | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertTrue(ok) | |
| mock_reporter.on_warning.assert_called_once_with( | |
| "Warning: check format" | |
| ) | |
| def test_commit_msg_action_warning_human_with_stderr_noise(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| reporter=mock_reporter, | |
| ) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = subprocess.CompletedProcess( | |
| args=[], | |
| returncode=0, | |
| stdout="Warning: subject line exceeds 50 chars", | |
| stderr="Some python warning\nDeprecationWarning: ...\n", | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertTrue(ok) | |
| mock_reporter.on_warning.assert_called_once_with( | |
| "Warning: subject line exceeds 50 chars" | |
| ) | |
| def test_commit_msg_action_success_with_stderr_noise_no_warning( | |
| self, | |
| ) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| reporter=mock_reporter, | |
| ) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = self.make_completed_process( | |
| returncode=0, | |
| stderr="Some python warning\n", | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertTrue(ok) | |
| mock_reporter.on_warning.assert_not_called() | |
| def test_commit_msg_action_error_combines_stdout_and_stderr(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = subprocess.CompletedProcess( | |
| args=[], | |
| returncode=1, | |
| stdout="[ERROR] Missing Change-Id line", | |
| stderr="Traceback (most recent call last):\n File 'test.py', line 1\n", | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once_with( | |
| "[ERROR] Missing Change-Id line\nTraceback (most recent call last):\n File 'test.py', line 1" | |
| ) | |
| def test_commit_msg_action_empty_output_fallback_error(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = self.make_completed_process( | |
| returncode=3, stderr="" | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once() | |
| err_msg = mock_reporter.on_error.call_args[0][0] | |
| self.assertIn("failed with exit code 3", err_msg) | |
| def test_commit_msg_action_checker_script_missing_human(self) -> None: | |
| self.checker_script.unlink() | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| reporter=mock_reporter, | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertTrue(ok) | |
| mock_reporter.on_warning.assert_called_once() | |
| mock_reporter.on_error.assert_not_called() | |
| def test_commit_msg_action_checker_script_missing_agent(self) -> None: | |
| self.checker_script.unlink() | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once() | |
| mock_reporter.on_warning.assert_not_called() | |
| def test_commit_msg_action_multiline_warnings_human(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=False, | |
| reporter=mock_reporter, | |
| ) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = subprocess.CompletedProcess( | |
| args=[], | |
| returncode=0, | |
| stdout="Warning: line 1\n\n Warning: line 2 \n", | |
| stderr="", | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertTrue(ok) | |
| mock_reporter.on_warning.assert_called_once_with( | |
| "Warning: line 1\n\n Warning: line 2" | |
| ) | |
| def test_commit_msg_action_multiline_errors_agent(self) -> None: | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = subprocess.CompletedProcess( | |
| args=[], | |
| returncode=1, | |
| stdout="Error: line 1\n\n Error: line 2 \n", | |
| stderr="", | |
| ) | |
| ok = adapters.commit_msg_action(ctx, ["COMMIT_EDITMSG"]) | |
| self.assertFalse(ok) | |
| mock_reporter.on_error.assert_called_once_with( | |
| "Error: line 1\n\n Error: line 2" | |
| ) | |
| def test_commit_msg_action_relative_path_from_subdirectory(self) -> None: | |
| subdir = self.test_dir / "nested" / "sub" | |
| subdir.mkdir(parents=True, exist_ok=True) | |
| mock_reporter = self.make_mock_reporter(ConsoleReporter) | |
| ctx = adapters.HookContext( | |
| repo_root=self.test_dir, | |
| is_agent=True, | |
| reporter=mock_reporter, | |
| ) | |
| with mock.patch("agents.lib.githooks.adapters.run_cmd") as mock_cmd: | |
| mock_cmd.return_value = self.make_completed_process() | |
| self.addCleanup(os.chdir, Path.cwd()) | |
| os.chdir(subdir) | |
| ok = adapters.commit_msg_action(ctx, [".git/COMMIT_EDITMSG"]) | |
| self.assertTrue(ok) | |
| mock_cmd.assert_called_once() | |
| cmd_args = mock_cmd.call_args[0][0] | |
| self.assertIn(".git/COMMIT_EDITMSG", cmd_args) | |
| if __name__ == "__main__": | |
| unittest.main() |