Merge pull request #573 from AIOSAI/dev

fix: Windows CI + drone status/diff --all flag
This commit is contained in:
AIPass
2026-05-14 20:37:41 -07:00
committed by GitHub
8 changed files with 541 additions and 29 deletions
+46
View File
@@ -37,6 +37,52 @@ jobs:
pip install -e ".[dev]"
- run: coverage run -m pytest -v --tb=short --rootdir=.
standards:
name: seedgo-audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
- name: Run seedgo standards audit
run: python -c "
import sys
from pathlib import Path
from aipass.seedgo.apps.handlers.audit.branch_audit import audit_branch
from aipass.seedgo.apps.handlers.bypass.bypass_handler import load_bypass_rules
src = Path('src/aipass')
pack = src / 'seedgo/apps/handlers/aipass_standards'
branches = []
for d in sorted(src.iterdir()):
if d.is_dir() and (d / 'apps').is_dir():
entry = d / 'apps' / f'{d.name}.py'
branches.append({'name': d.name, 'path': str(d),
'entry_file': str(entry) if entry.exists() else ''})
failed = []
for branch in branches:
bypass_rules = load_bypass_rules(branch['path'])
result = audit_branch(branch, bypass_rules, pack_path=pack)
avg = result.get('average', 0)
print(f\" {branch['name']:>12}: {avg:.0f}%\")
if avg < 80:
failed.append((branch['name'], avg))
if failed:
print(f\"\nFAILED: {len(failed)} branch(es) below 80%\")
for name, score in failed:
print(f\" {name}: {score:.0f}%\")
sys.exit(1)
else:
print(f\"\nAll {len(branches)} branches pass (>=80%)\")
"
coverage:
name: coverage
needs: [test]
+5
View File
@@ -17,9 +17,12 @@ from __future__ import annotations
import os
import stat
import sys
from pathlib import Path
from unittest.mock import patch
import pytest
from aipass.api.apps.handlers.auth.env import create_env_template
@@ -60,6 +63,7 @@ class TestCreateEnvTemplate:
assert result is True
assert target.read_text(encoding="utf-8") == "EXISTING=content\n"
@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not support Unix file permissions")
def test_file_permissions(self, tmp_path: Path) -> None:
"""Created file has owner-only read/write (0o600)."""
target = tmp_path / "secrets" / ".env"
@@ -68,6 +72,7 @@ class TestCreateEnvTemplate:
file_mode = stat.S_IMODE(os.stat(target).st_mode)
assert file_mode == 0o600
@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not support Unix file permissions")
def test_directory_permissions(self, tmp_path: Path) -> None:
"""Parent directory has owner-only access (0o700)."""
secrets_dir = tmp_path / "secrets"
@@ -43,8 +43,8 @@ Three rules:
3. Local files = source of truth.
```
drone @git status # changes (all branches)
drone @git diff # diff (all branches)
drone @git status --all # changes (full repo)
drone @git diff --all # diff (full repo)
drone @git log # commits (all branches)
drone @git branches # remote branches (all branches)
drone @git commit "msg" --all # commit all (devpulse only)
@@ -55,13 +55,14 @@ def get_branch_diff(branch_dir: Path, staged: bool = False) -> dict:
rel_dir = branch_dir
rel_prefix = rel_dir.as_posix() + "/"
show_all = rel_dir == Path(".")
filtered_lines: list[str] = []
include_block = False
files_changed = 0
for line in result.stdout.splitlines():
if line.startswith("diff --git"):
include_block = rel_prefix in line
include_block = show_all or rel_prefix in line
if include_block:
files_changed += 1
if include_block:
@@ -62,6 +62,7 @@ def get_branch_status(branch_dir: Path) -> dict:
rel_dir = branch_dir
rel_prefix = rel_dir.as_posix() + "/"
show_all = rel_dir == Path(".")
files = []
for line in result.stdout.splitlines():
@@ -69,7 +70,7 @@ def get_branch_status(branch_dir: Path) -> dict:
continue
status_code = line[:2].strip()
file_path = line[3:]
if file_path.startswith(rel_prefix) or file_path == str(rel_dir):
if show_all or file_path.startswith(rel_prefix) or file_path == str(rel_dir):
files.append({"status": status_code, "path": file_path})
total = len(files)
+21 -8
View File
@@ -132,7 +132,7 @@ def handle_command(command: str | None = None, args: list[str] | None = None) ->
if command in _GH_PASSTHROUGH_COMMANDS:
return _handle_gh_passthrough(command, args)
if command == "status":
return _handle_status()
return _handle_status(args)
if command == "diff":
return _handle_diff(args)
if command == "log":
@@ -344,8 +344,11 @@ def _handle_fix(args: list[str], caller: str) -> dict:
}
def _handle_status() -> dict:
"""Handle the status subcommand (global tier)."""
def _handle_status(args: list[str] | None = None) -> dict:
"""Handle the status subcommand (global tier). --all for repo-wide."""
args = args or []
show_all = "--all" in args
detected = _detect_branch_dir()
if detected is None:
return {
@@ -355,7 +358,13 @@ def _handle_status() -> dict:
}
_, branch_dir = detected
result = status_handler.get_branch_status(branch_dir)
if show_all:
repo_root = lock_handler.find_repo_root()
result = status_handler.get_branch_status(repo_root)
result["message"] = f"{result['total']} file(s) changed in repo"
else:
result = status_handler.get_branch_status(branch_dir)
lines = [result["message"]]
for f in result["files"]:
@@ -369,7 +378,7 @@ def _handle_status() -> dict:
def _handle_diff(args: list[str]) -> dict:
"""Handle the diff subcommand (global tier)."""
"""Handle the diff subcommand (global tier). --staged, --all supported."""
detected = _detect_branch_dir()
if detected is None:
return {
@@ -380,7 +389,10 @@ def _handle_diff(args: list[str]) -> dict:
_, branch_dir = detected
staged = "--staged" in args
result = diff_handler.get_branch_diff(branch_dir, staged=staged)
show_all = "--all" in args
target_dir = lock_handler.find_repo_root() if show_all else branch_dir
result = diff_handler.get_branch_diff(target_dir, staged=staged)
return {
"stdout": result["diff"] if result["diff"] else result["message"],
@@ -532,12 +544,13 @@ def get_help(command: str | None = None) -> str:
if command == "pr":
return "git pr — DEPRECATED. Agent PRs are no longer supported. Devpulse handles git.\n"
if command == "status":
return "git status — Show git status filtered to your branch directory [global]\n"
return "git status [--all] — Show git status filtered to your branch (--all for repo-wide) [global]\n"
if command == "diff":
return (
"git diff [--staged] — Show git diff filtered to your branch directory [global]\n"
"git diff [--staged] [--all] — Show git diff filtered to your branch [global]\n"
" Options:\n"
" --staged Show staged changes only.\n"
" --all Show all repo changes (not just your branch).\n"
)
if command == "log":
return "git log [count] — Show recent git log entries (default: 10) [global]\n"
@@ -1,26 +1,12 @@
# =================== AIPass ====================
# Name: windows_compat_check.py
# Description: Windows Compatibility Standards Checker Handler
# Version: 1.0.0
# Version: 1.1.0
# Created: 2026-05-10
# Modified: 2026-05-10
# Modified: 2026-05-14
# =============================================
"""
Windows Compatibility Standards Checker Handler
Detects POSIX-only patterns that will crash or behave incorrectly on
Windows. Scans for:
1. Unguarded POSIX-only imports (fcntl, pwd, grp, termios, resource)
2. Unguarded POSIX-only constants (os.WNOHANG, signal.SIGPIPE)
3. Unguarded POSIX-only calls (os.fork, os.setpgid, os.killpg, os.waitpid)
4. os.kill() without try/except catching OSError
Detection uses AST parsing to identify violations and check whether they
sit inside a platform guard (if sys.platform / if os.name / try-except
ImportError).
"""
"""Windows Compatibility Standards Checker Handler."""
import ast
from pathlib import Path
@@ -48,6 +34,15 @@ _GUARDED_EXCEPT_TYPES = frozenset(
}
)
_TEST_POSIX_CALLS: dict[tuple[str, str], str] = {
("os", "chmod"): "os.chmod() — Windows ignores permission bits",
("os", "symlink"): "os.symlink() — Windows needs privileges",
("os", "getuid"): "os.getuid() — not available on Windows",
("os", "getgid"): "os.getgid() — not available on Windows",
("os", "chown"): "os.chown() — not available on Windows",
("stat", "S_IMODE"): "stat.S_IMODE() — Unix permission assertion",
}
def _is_platform_guard(node: ast.expr) -> bool:
"""Return True if the test expression is a sys.platform or os.name comparison."""
@@ -172,6 +167,111 @@ def _find_os_kill_violations(tree: ast.Module, guarded: set[int]) -> list[tuple[
return violations
def _is_test_file(path: Path) -> bool:
return "tests" in path.parts or path.name.startswith("test_") or path.name.endswith("_test.py")
def _is_pytest_mark(node: ast.expr) -> bool:
return (
isinstance(node, ast.Attribute)
and node.attr == "mark"
and isinstance(node.value, ast.Name)
and node.value.id == "pytest"
)
def _references_platform(node: ast.expr) -> bool:
for child in ast.walk(node):
if isinstance(child, ast.Attribute) and isinstance(child.value, ast.Name):
if (child.value.id == "sys" and child.attr == "platform") or (
child.value.id == "os" and child.attr == "name"
):
return True
return False
def _has_platform_skipif(decorators: list[ast.expr]) -> bool:
for dec in decorators:
if isinstance(dec, ast.Attribute) and dec.attr == "skip" and _is_pytest_mark(dec.value):
return True
if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
continue
func = dec.func
if func.attr == "skip" and _is_pytest_mark(func.value):
return True
if func.attr == "skipif" and _is_pytest_mark(func.value):
if any(_references_platform(arg) for arg in dec.args):
return True
return False
def _match_posix_call(node: ast.AST) -> str | None:
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
return None
if not isinstance(node.func.value, ast.Name):
return None
key = (node.func.value.id, node.func.attr)
return _TEST_POSIX_CALLS.get(key)
def _match_stat_constant(node: ast.AST) -> str | None:
if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name):
return None
if node.value.id == "stat" and node.attr.startswith("S_I") and node.attr != "S_IMODE":
return f"stat.{node.attr} — Unix permission constant"
return None
def _scan_test_body(
func_node: ast.FunctionDef | ast.AsyncFunctionDef,
guarded: set[int],
violations: list[tuple[int, str]],
) -> None:
seen_lines: set[int] = set()
for node in ast.walk(func_node):
lineno = getattr(node, "lineno", None)
if lineno is None or lineno in guarded or lineno in seen_lines:
continue
desc = _match_posix_call(node) or _match_stat_constant(node)
if desc:
violations.append((lineno, desc))
seen_lines.add(lineno)
def _collect_unguarded_tests(
parent: ast.Module | ast.ClassDef,
class_skipif: bool = False,
) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
results: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
for node in ast.iter_child_nodes(parent):
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
if not node.name.startswith("test_"):
continue
if not class_skipif and not _has_platform_skipif(node.decorator_list):
results.append(node)
return results
def _find_test_platform_violations(
tree: ast.Module,
guarded: set[int],
) -> list[tuple[int, str]]:
violations: list[tuple[int, str]] = []
targets: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
targets.extend(_collect_unguarded_tests(tree))
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.ClassDef):
targets.extend(_collect_unguarded_tests(node, _has_platform_skipif(node.decorator_list)))
for func in targets:
_scan_test_body(func, guarded, violations)
return violations
def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
"""Check a Python file for Windows-incompatible patterns."""
path = Path(module_path)
@@ -260,6 +360,9 @@ def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
all_violations.extend(_find_posix_call_violations(tree, guarded))
all_violations.extend(_find_os_kill_violations(tree, guarded))
if _is_test_file(path):
all_violations.extend(_find_test_platform_violations(tree, guarded))
non_bypassed = [
(ln, desc) for ln, desc in all_violations if not is_bypassed(module_path, "windows_compat", ln, bypass_rules)
]
@@ -0,0 +1,343 @@
# =================== AIPass ====================
# Name: test_windows_compat.py
# Description: Tests for windows_compat_check.py
# Version: 1.0.0
# Created: 2026-05-14
# Modified: 2026-05-14
# =============================================
"""Tests for windows_compat_check — both POSIX-import detection and test-file skipif enforcement."""
import pytest
from unittest.mock import MagicMock
@pytest.fixture(autouse=True)
def _mock_infrastructure(monkeypatch):
import sys
mock_logger = MagicMock()
mock_json_handler = MagicMock()
mock_json_handler.log_operation = MagicMock(return_value=True)
prax_mod = MagicMock()
prax_mod.logger = mock_logger
monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod)
json_pkg = MagicMock()
json_pkg.json_handler = mock_json_handler
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json", json_pkg)
json_mod = MagicMock()
json_mod.log_operation = mock_json_handler.log_operation
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json.json_handler", json_mod)
bypass_pkg = MagicMock()
bypass_ignore = MagicMock()
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed as real_is_bypassed
bypass_utils = MagicMock()
bypass_utils.is_bypassed = real_is_bypassed
bypass_pkg.utils = bypass_utils
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg)
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.ignore_handler", bypass_ignore)
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.utils", bypass_utils)
for mod_name in ["aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check"]:
monkeypatch.delitem(sys.modules, mod_name, raising=False)
# ===========================================================================
# Existing POSIX-import detection
# ===========================================================================
def test_clean_file_passes(tmp_path):
f = tmp_path / "clean.py"
f.write_text("import os\nx = os.path.join('a', 'b')\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
assert result["score"] == 100
def test_unguarded_fcntl_import_fails(tmp_path):
f = tmp_path / "bad.py"
f.write_text("import fcntl\nfcntl.flock(0, 0)\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "import fcntl" in result["checks"][0]["message"]
def test_guarded_fcntl_import_passes(tmp_path):
f = tmp_path / "guarded.py"
f.write_text("import sys\ntry:\n import fcntl\nexcept ImportError:\n fcntl = None\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
def test_platform_guarded_os_kill_passes(tmp_path):
f = tmp_path / "guarded_kill.py"
f.write_text("import os, sys\nif sys.platform != 'win32':\n os.kill(1, 9)\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
def test_unguarded_os_kill_fails(tmp_path):
f = tmp_path / "bad_kill.py"
f.write_text("import os\nos.kill(1, 9)\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "os.kill()" in result["checks"][0]["message"]
def test_init_file_skipped(tmp_path):
f = tmp_path / "__init__.py"
f.write_text("import fcntl\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
assert result["checks"][0]["message"] == "File skipped (non-target)"
def test_posix_only_call_fork_fails(tmp_path):
f = tmp_path / "forker.py"
f.write_text("import os\npid = os.fork()\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "os.fork()" in result["checks"][0]["message"]
def test_posix_constant_wnohang_fails(tmp_path):
f = tmp_path / "waiter.py"
f.write_text("import os\nflags = os.WNOHANG\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "os.WNOHANG" in result["checks"][0]["message"]
# ===========================================================================
# Test-file skipif enforcement (new)
# ===========================================================================
def test_unguarded_chmod_in_test_file_fails(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_perms.py"
f.write_text("import os\n\ndef test_permissions():\n os.chmod('file', 0o600)\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "os.chmod()" in result["checks"][0]["message"]
def test_skipif_guarded_chmod_passes(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_perms.py"
f.write_text(
"import os, sys, pytest\n\n"
'@pytest.mark.skipif(sys.platform == "win32", reason="no perms")\n'
"def test_permissions():\n"
" os.chmod('file', 0o600)\n"
)
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
def test_unguarded_stat_imode_in_test_fails(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_perms.py"
f.write_text(
"import os, stat\n\n"
"def test_file_mode():\n"
" mode = stat.S_IMODE(os.stat('file').st_mode)\n"
" assert mode == 0o600\n"
)
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "stat.S_IMODE()" in result["checks"][0]["message"]
def test_unguarded_stat_constant_in_test_fails(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_perms.py"
f.write_text("import stat\n\ndef test_check_bits():\n expected = stat.S_IRUSR | stat.S_IWUSR\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "stat.S_IRUSR" in result["checks"][0]["message"]
def test_unguarded_symlink_in_test_fails(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_links.py"
f.write_text("import os\n\ndef test_symlink():\n os.symlink('a', 'b')\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "os.symlink()" in result["checks"][0]["message"]
def test_unguarded_getuid_in_test_fails(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_user.py"
f.write_text("import os\n\ndef test_uid():\n uid = os.getuid()\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "os.getuid()" in result["checks"][0]["message"]
def test_class_level_skipif_guards_all_methods(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_perms.py"
f.write_text(
"import os, sys, pytest\n\n"
'@pytest.mark.skipif(sys.platform == "win32", reason="unix only")\n'
"class TestPermissions:\n"
" def test_chmod(self):\n"
" os.chmod('file', 0o600)\n"
" def test_getuid(self):\n"
" os.getuid()\n"
)
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
def test_method_level_skipif_in_unguarded_class(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_mixed.py"
f.write_text(
"import os, sys, pytest\n\n"
"class TestMixed:\n"
' @pytest.mark.skipif(sys.platform == "win32", reason="unix")\n'
" def test_guarded(self):\n"
" os.chmod('file', 0o600)\n"
" def test_unguarded(self):\n"
" os.chmod('file', 0o700)\n"
)
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "os.chmod()" in result["checks"][0]["message"]
def test_pytest_mark_skip_unconditional_passes(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_skipped.py"
f.write_text(
"import os, pytest\n\n@pytest.mark.skip(reason=\"not yet\")\ndef test_perms():\n os.chmod('file', 0o600)\n"
)
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
def test_non_test_file_ignores_chmod(tmp_path):
f = tmp_path / "handler.py"
f.write_text("import os\n\ndef set_perms():\n os.chmod('file', 0o600)\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
def test_inline_platform_guard_in_test_passes(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_guarded_inline.py"
f.write_text(
"import os, sys\n\ndef test_perms():\n if sys.platform != \"win32\":\n os.chmod('file', 0o600)\n"
)
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
def test_os_name_skipif_passes(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_osname.py"
f.write_text(
"import os, pytest\n\n"
'@pytest.mark.skipif(os.name != "posix", reason="posix only")\n'
"def test_chmod():\n"
" os.chmod('file', 0o600)\n"
)
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
def test_non_test_function_in_test_file_ignored(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_helpers.py"
f.write_text(
"import os\n\ndef helper_setup():\n os.chmod('file', 0o600)\n\ndef test_clean():\n assert True\n"
)
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is True
def test_chown_in_test_fails(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_owner.py"
f.write_text("import os\n\ndef test_ownership():\n os.chown('file', 1000, 1000)\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "os.chown()" in result["checks"][0]["message"]
def test_getgid_in_test_fails(tmp_path):
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
f = tests_dir / "test_gid.py"
f.write_text("import os\n\ndef test_group():\n gid = os.getgid()\n")
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
result = check_module(str(f))
assert result["passed"] is False
assert "os.getgid()" in result["checks"][0]["message"]