import contextlib import io import json import os import tempfile import unittest from pathlib import Path os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test") from scripts import run_script_static_checks class ScriptStaticChecksTests(unittest.TestCase): def test_detects_ambient_env_fallback(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: script_dir = Path(tmpdir) (script_dir / "bad.py").write_text("env = env or os.environ\n", encoding="utf-8") result = run_script_static_checks.run_script_static_checks(script_dir=script_dir) self.assertFalse(result["passed"]) self.assertEqual(result["findings"][0]["file"], "bad.py") self.assertEqual(result["findings"][0]["reason"], "empty_env_must_not_fall_back_to_ambient_environment") def test_passes_clean_scripts(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: script_dir = Path(tmpdir) (script_dir / "ok.py").write_text("env = os.environ if env is None else env\n", encoding="utf-8") result = run_script_static_checks.run_script_static_checks(script_dir=script_dir) self.assertTrue(result["passed"]) self.assertEqual(result["script_count"], 1) def test_detects_python_syntax_errors(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: script_dir = Path(tmpdir) (script_dir / "broken.py").write_text("def nope(:\n", encoding="utf-8") result = run_script_static_checks.run_script_static_checks(script_dir=script_dir) self.assertFalse(result["passed"]) self.assertEqual(result["findings"][0]["reason"], "script_python_syntax_error") def test_detects_missing_system_exit_for_automation_entrypoints(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: script_dir = Path(tmpdir) (script_dir / "run_pipeline.py").write_text( "def main() -> int:\n return 2\n\nif __name__ == '__main__':\n main()\n", encoding="utf-8", ) result = run_script_static_checks.run_script_static_checks(script_dir=script_dir) self.assertFalse(result["passed"]) self.assertEqual(result["findings"][0]["reason"], "automation_entrypoint_must_preserve_exit_code") def test_detects_missing_system_exit_for_any_script_with_main(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: script_dir = Path(tmpdir) (script_dir / "ad_hoc.py").write_text( "def main() -> int:\n return 0\n\nif __name__ == '__main__':\n main()\n", encoding="utf-8", ) result = run_script_static_checks.run_script_static_checks(script_dir=script_dir) self.assertFalse(result["passed"]) self.assertEqual(result["findings"][0]["file"], "ad_hoc.py") self.assertEqual(result["findings"][0]["reason"], "automation_entrypoint_must_preserve_exit_code") def test_detects_python39_incompatible_optional_annotation(self) -> None: with tempfile.TemporaryDirectory() as tmpdir: script_dir = Path(tmpdir) (script_dir / "bad.py").write_text("def f(value: str | None = None):\n return value\n", encoding="utf-8") result = run_script_static_checks.run_script_static_checks(script_dir=script_dir) self.assertFalse(result["passed"]) self.assertEqual(result["findings"][0]["reason"], "python39_incompatible_pep604_optional_annotation") def test_json_output_is_machine_readable(self) -> None: output = io.StringIO() with contextlib.redirect_stdout(output): exit_code = run_script_static_checks.main(env={"SCRIPT_CHECKS_OUTPUT_JSON": "1"}) summary = json.loads(output.getvalue()) self.assertEqual(exit_code, 0) self.assertTrue(summary["passed"]) self.assertGreaterEqual(summary["script_count"], 1) if __name__ == "__main__": unittest.main()