"""Static guard: every ``read_text`` / ``write_text`` call in the gateway and bundled update-response adapters must pass an explicit ``encoding=`` keyword argument so non-UTF-8 Windows locales don't corrupt file IPC. Mirrors the AST-based guard pattern in ``tests/tools/test_windows_compat.py``. """ import ast import pathlib import pytest REPO_ROOT = pathlib.Path(__file__).resolve().parents[2] GATEWAY_DIR = REPO_ROOT / "gateway" UPDATE_RESPONSE_FILES = ( REPO_ROOT / "plugins/platforms/discord/adapter.py", REPO_ROOT / "plugins/platforms/telegram/adapter.py", REPO_ROOT / "plugins/platforms/feishu/adapter.py", REPO_ROOT / "plugins/platforms/whatsapp/adapter.py", REPO_ROOT / "plugins/platforms/google_chat/adapter.py", REPO_ROOT / "plugins/platforms/google_chat/oauth.py", ) METHODS = {"read_text", "write_text"} SUPPRESSION = "# gateway-utf8: ok" def _find_violations(): violations = [] py_files = list(GATEWAY_DIR.rglob("*.py")) + list(UPDATE_RESPONSE_FILES) for py_file in sorted(py_files): source = py_file.read_text(encoding="utf-8") source_lines = source.splitlines() try: tree = ast.parse(source, filename=str(py_file)) except SyntaxError: continue for node in ast.walk(tree): if not isinstance(node, ast.Call): continue func = node.func if not isinstance(func, ast.Attribute): continue if func.attr not in METHODS: continue if any(kw.arg == "encoding" for kw in node.keywords): continue lineno = node.lineno if lineno <= len(source_lines) and SUPPRESSION in source_lines[lineno - 1]: continue rel = py_file.relative_to(REPO_ROOT) violations.append(f"{rel}:{lineno}") return violations def test_all_read_write_text_pass_encoding(): violations = _find_violations() assert not violations, ( "Bare read_text()/write_text() calls found (missing encoding= kwarg).\n" "Add encoding=\"utf-8\" or suppress with '# gateway-utf8: ok':\n" + "\n".join(f" {v}" for v in violations) )