|
#!/usr/bin/env python3 |
|
from __future__ import annotations |
|
|
|
import argparse |
|
import json |
|
import subprocess |
|
import sys |
|
from pathlib import Path |
|
|
|
|
|
TEST_HINTS = ( |
|
".test.", |
|
".spec.", |
|
"_test.py", |
|
"test_", |
|
) |
|
|
|
VERIFY_SCRIPT_KEYS = ("verify", "build", "lint", "check") |
|
TEST_SCRIPT_KEYS = ("test", "test:e2e", "test:unit", "test:ui", "test:fullstack", "test:mocked") |
|
|
|
|
|
def run(cmd: list[str], cwd: Path) -> str: |
|
try: |
|
return subprocess.check_output(cmd, cwd=str(cwd), stderr=subprocess.DEVNULL, text=True).strip() |
|
except Exception: |
|
return "" |
|
|
|
|
|
def git_root(path: Path) -> Path: |
|
root = run(["git", "rev-parse", "--show-toplevel"], path) |
|
return Path(root) if root else path |
|
|
|
|
|
def load_package_json(path: Path) -> dict: |
|
try: |
|
return json.loads(path.read_text()) |
|
except Exception: |
|
return {} |
|
|
|
|
|
def tracked_tests(repo_root: Path) -> list[str]: |
|
output = run(["git", "ls-files"], repo_root) |
|
if not output: |
|
return [] |
|
files = output.splitlines() |
|
return [f for f in files if any(hint in f for hint in TEST_HINTS) or "/tests/" in f or f.startswith("tests/")] |
|
|
|
|
|
def gather_commands(pkg_path: Path, label: str) -> tuple[list[str], list[str]]: |
|
data = load_package_json(pkg_path) |
|
scripts = data.get("scripts", {}) |
|
test_cmds = [] |
|
verify_cmds = [] |
|
for key, value in scripts.items(): |
|
rendered = f"{label}: {key} -> {value}" |
|
if key in TEST_SCRIPT_KEYS or "test" in key: |
|
test_cmds.append(rendered) |
|
elif key in VERIFY_SCRIPT_KEYS or any(token in key for token in VERIFY_SCRIPT_KEYS): |
|
verify_cmds.append(rendered) |
|
return test_cmds, verify_cmds |
|
|
|
|
|
def pyproject_markers(path: Path) -> list[str]: |
|
if not path.exists(): |
|
return [] |
|
text = path.read_text(errors="ignore") |
|
markers = [] |
|
for token in ("pytest", "streamlit", "playwright", "vitest"): |
|
if token in text: |
|
markers.append(token) |
|
return markers |
|
|
|
|
|
def main() -> int: |
|
parser = argparse.ArgumentParser(description="Inspect repo review/test surfaces quickly.") |
|
parser.add_argument("repo", nargs="?", default=".", help="Repo path to inspect") |
|
parser.add_argument("--json", action="store_true", help="Emit JSON instead of markdown") |
|
args = parser.parse_args() |
|
|
|
start = Path(args.repo).expanduser().resolve() |
|
repo_root = git_root(start) |
|
|
|
package_candidates = [ |
|
repo_root / "package.json", |
|
repo_root / "frontend" / "package.json", |
|
repo_root / "web" / "package.json", |
|
repo_root / "apps" / "web" / "package.json", |
|
] |
|
package_candidates = [p for p in package_candidates if p.exists()] |
|
|
|
test_commands: list[str] = [] |
|
verify_commands: list[str] = [] |
|
deps: set[str] = set() |
|
for pkg in package_candidates: |
|
label = str(pkg.relative_to(repo_root)) |
|
data = load_package_json(pkg) |
|
deps.update(data.get("dependencies", {}).keys()) |
|
deps.update(data.get("devDependencies", {}).keys()) |
|
tests, verify = gather_commands(pkg, label) |
|
test_commands.extend(tests) |
|
verify_commands.extend(verify) |
|
|
|
pyproject = repo_root / "pyproject.toml" |
|
pytest_ini = repo_root / "pytest.ini" |
|
py_markers = pyproject_markers(pyproject) |
|
if pytest_ini.exists() and "pytest" not in py_markers: |
|
py_markers.append("pytest") |
|
|
|
has_netlify = (repo_root / "netlify.toml").exists() or (repo_root / "netlify" / "functions").exists() |
|
has_playwright = any((repo_root / name).exists() for name in ("playwright.config.ts", "playwright.config.js", "playwright.config.mjs")) or "@playwright/test" in deps or "playwright" in py_markers |
|
has_vitest = any((repo_root / name).exists() for name in ("vitest.config.ts", "vitest.config.js", "vitest.config.mjs")) or "vitest" in deps |
|
has_jest = any((repo_root / name).exists() for name in ("jest.config.ts", "jest.config.js", "jest.config.mjs")) or "jest" in deps |
|
has_pytest = "pytest" in py_markers |
|
has_streamlit = "streamlit" in py_markers |
|
|
|
tracked = tracked_tests(repo_root) |
|
|
|
detected_surfaces = [] |
|
if has_playwright: |
|
detected_surfaces.append("playwright") |
|
if has_vitest: |
|
detected_surfaces.append("vitest") |
|
if has_jest: |
|
detected_surfaces.append("jest") |
|
if has_pytest: |
|
detected_surfaces.append("pytest") |
|
if has_streamlit: |
|
detected_surfaces.append("streamlit") |
|
|
|
notes = [] |
|
if has_netlify: |
|
notes.append("Netlify markers detected; deployed preview/prod proof may be required for functions, redirects, auth, or cookie behavior.") |
|
if has_playwright: |
|
notes.append("Repo Playwright CLI is available for durable browser regression coverage.") |
|
if not tracked: |
|
notes.append("Little or no tracked test coverage detected; prefer the thinnest honest addition in the repo's existing stack.") |
|
if not detected_surfaces: |
|
notes.append("No obvious automated test harness detected from common manifests/configs.") |
|
|
|
result = { |
|
"repo_root": str(repo_root), |
|
"has_netlify": has_netlify, |
|
"detected_test_surfaces": detected_surfaces, |
|
"test_commands": test_commands, |
|
"verify_commands": verify_commands, |
|
"tracked_test_file_count": len(tracked), |
|
"tracked_test_examples": tracked[:12], |
|
"notes": notes, |
|
} |
|
|
|
if args.json: |
|
print(json.dumps(result, indent=2)) |
|
return 0 |
|
|
|
print(f"Repo root: {result['repo_root']}") |
|
print(f"Netlify: {'yes' if has_netlify else 'no'}") |
|
print(f"Detected test surfaces: {', '.join(detected_surfaces) if detected_surfaces else 'none-obvious'}") |
|
print(f"Tracked test files: {len(tracked)}") |
|
if tracked: |
|
print("Tracked test examples:") |
|
for item in tracked[:12]: |
|
print(f"- {item}") |
|
if test_commands: |
|
print("Test commands:") |
|
for item in test_commands: |
|
print(f"- {item}") |
|
if verify_commands: |
|
print("Verify/build commands:") |
|
for item in verify_commands: |
|
print(f"- {item}") |
|
if notes: |
|
print("Notes:") |
|
for item in notes: |
|
print(f"- {item}") |
|
return 0 |
|
|
|
|
|
if __name__ == "__main__": |
|
sys.exit(main()) |
|
|