204 lines
6.3 KiB
Python
204 lines
6.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Focused Security / Contract / E2E Regression Gate - Local Validator
|
|
|
|
Runs the supplemental focused gate locally before pushing. This lane is useful
|
|
for plugin security, metadata, dependency-policy, outbound-payload, and E2E
|
|
regression checks, but it is not a replacement for tests/TEST_SOP.md.
|
|
Mirrors: .github/workflows/focused-regression-gate.yml
|
|
|
|
Usage:
|
|
python scripts/focused_gate.py # Run all focused checks
|
|
python scripts/focused_gate.py --fast # Python tests only
|
|
python scripts/focused_gate.py --e2e # E2E tests only
|
|
|
|
Exit codes:
|
|
0 = All checks passed
|
|
1 = Python tests failed
|
|
2 = E2E tests failed
|
|
3 = Both failed
|
|
"""
|
|
|
|
import argparse
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Sequence, Union
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
GREEN = "\033[92m"
|
|
RED = "\033[91m"
|
|
YELLOW = "\033[93m"
|
|
RESET = "\033[0m"
|
|
BOLD = "\033[1m"
|
|
|
|
Command = Union[str, Sequence[str]]
|
|
|
|
|
|
def run_command(cmd: Command, cwd=REPO_ROOT, timeout=300):
|
|
"""Run a command and return (success, stdout, stderr)."""
|
|
try:
|
|
use_shell = isinstance(cmd, str)
|
|
result = subprocess.run(
|
|
cmd,
|
|
cwd=cwd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
timeout=timeout,
|
|
shell=use_shell,
|
|
)
|
|
return result.returncode == 0, result.stdout, result.stderr
|
|
except subprocess.TimeoutExpired:
|
|
return False, "", f"Command timed out after {timeout}s"
|
|
|
|
|
|
def print_header(text):
|
|
"""Print colored section header."""
|
|
print(f"\n{BOLD}{text}{RESET}")
|
|
print("=" * len(text))
|
|
|
|
|
|
def _detect_pytest(python_exe: str) -> bool:
|
|
success, _, _ = run_command([python_exe, "-c", "import pytest"], timeout=20)
|
|
return success
|
|
|
|
|
|
def _select_python_for_pytest() -> str:
|
|
"""
|
|
Prefer the current interpreter if it has pytest installed; otherwise fall back
|
|
to the repo-local .venv interpreter when present.
|
|
"""
|
|
current = sys.executable
|
|
if _detect_pytest(current):
|
|
return current
|
|
|
|
venv_python = (
|
|
REPO_ROOT / ".venv" / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python")
|
|
)
|
|
if venv_python.exists() and _detect_pytest(str(venv_python)):
|
|
return str(venv_python)
|
|
|
|
return current
|
|
|
|
|
|
def run_python_gate():
|
|
"""Run focused Python security and contract test suites."""
|
|
print_header("Focused Python Security & Contract Tests")
|
|
|
|
python_exe = _select_python_for_pytest()
|
|
suites = [
|
|
("Plugin Security", "tests/test_plugins_security.py", 10),
|
|
("Metadata Contract", "tests/test_metadata_contract.py", 1),
|
|
("Dependency Policy", "tests/test_pipeline_dependency_policy.py", 2),
|
|
("Outbound Payload Safety", "tests/test_outbound_payload_safety.py", 4),
|
|
]
|
|
|
|
all_passed = True
|
|
for name, path, test_count in suites:
|
|
print(f"\n{YELLOW}Running {name} ({test_count} tests)...{RESET}")
|
|
cmd = [python_exe, "-m", "pytest", "-q", path, "--tb=short"]
|
|
success, stdout, stderr = run_command(cmd, timeout=120)
|
|
|
|
if success:
|
|
print(f"{GREEN}PASS {name}: PASS{RESET}")
|
|
print(stdout)
|
|
else:
|
|
print(f"{RED}FAIL {name}: FAIL{RESET}")
|
|
print(stdout)
|
|
print(stderr)
|
|
all_passed = False
|
|
|
|
return all_passed
|
|
|
|
|
|
def run_e2e_gate():
|
|
"""Run focused E2E regression tests."""
|
|
print_header("Focused E2E Regression Tests")
|
|
|
|
if not (REPO_ROOT / "node_modules").exists():
|
|
print(f"{YELLOW}Installing npm dependencies...{RESET}")
|
|
success, stdout, stderr = run_command(["npm", "ci"], timeout=300)
|
|
if not success:
|
|
print(f"{RED}FAIL npm ci failed{RESET}")
|
|
print(stderr)
|
|
return False
|
|
|
|
print(f"{YELLOW}Checking Playwright browsers...{RESET}")
|
|
success, stdout, stderr = run_command(
|
|
["npx", "playwright", "install", "chromium", "--with-deps"],
|
|
timeout=300,
|
|
)
|
|
if not success:
|
|
print(f"{RED}FAIL Playwright browser installation failed{RESET}")
|
|
print(stderr)
|
|
return False
|
|
|
|
print(f"\n{YELLOW}Running E2E tests...{RESET}")
|
|
success, stdout, stderr = run_command(["npm", "test"], timeout=600)
|
|
|
|
if success:
|
|
print(f"{GREEN}PASS E2E Tests: PASS{RESET}")
|
|
print(stdout)
|
|
else:
|
|
print(f"{RED}FAIL E2E Tests: FAIL{RESET}")
|
|
print(stdout)
|
|
print(stderr)
|
|
|
|
return success
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Focused Security / Contract / E2E Regression Gate - Local Validator",
|
|
epilog="Supplemental lane only; final acceptance still requires tests/TEST_SOP.md.",
|
|
)
|
|
parser.add_argument("--fast", action="store_true", help="Run Python tests only (skip E2E)")
|
|
parser.add_argument("--e2e", action="store_true", help="Run E2E tests only (skip Python)")
|
|
args = parser.parse_args()
|
|
|
|
print(f"{BOLD}Focused Security / Contract / E2E Regression Gate{RESET}")
|
|
print("Mirrors: .github/workflows/focused-regression-gate.yml")
|
|
print("Note: this supplemental lane does not replace tests/TEST_SOP.md.\n")
|
|
|
|
python_passed = True
|
|
e2e_passed = True
|
|
|
|
if not args.e2e:
|
|
python_passed = run_python_gate()
|
|
|
|
if not args.fast:
|
|
e2e_passed = run_e2e_gate()
|
|
|
|
print_header("Focused Gate Summary")
|
|
if python_passed and e2e_passed:
|
|
print(f"{GREEN}{BOLD}PASS ALL FOCUSED CHECKS PASSED{RESET}")
|
|
print(f"\n{GREEN}Security & Governance: PASS{RESET}")
|
|
print(" Plugin security: PASS")
|
|
print(" Metadata contract: PASS")
|
|
print(" Dependency policy: PASS")
|
|
print(" Outbound payload safety: PASS")
|
|
print(f"\n{GREEN}Frontend Regression: PASS{RESET}")
|
|
print(" E2E tests: PASS")
|
|
print(f"\n{GREEN}Focused gate is green; run the full TEST_SOP gate before acceptance.{RESET}")
|
|
return 0
|
|
|
|
print(f"{RED}{BOLD}FAIL FOCUSED GATE FAILED{RESET}")
|
|
if not python_passed:
|
|
print(f"{RED} Python tests: FAIL{RESET}")
|
|
if not e2e_passed:
|
|
print(f"{RED} E2E tests: FAIL{RESET}")
|
|
print(f"\n{RED}Please fix the failing checks before pushing.{RESET}")
|
|
|
|
if not python_passed and not e2e_passed:
|
|
return 3
|
|
if not python_passed:
|
|
return 1
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|