100 lines
3.5 KiB
Python
100 lines
3.5 KiB
Python
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _strip_inline_comment(line: str) -> str:
|
|
return line.split("#", 1)[0].strip()
|
|
|
|
|
|
def _requirement_name(line: str) -> str:
|
|
line = _strip_inline_comment(line)
|
|
for operator in ("==", ">=", "<=", "~=", "!=", ">"):
|
|
if operator in line:
|
|
return line.split(operator, 1)[0].strip().lower()
|
|
return line.lower()
|
|
|
|
|
|
def _requirement_names() -> set[str]:
|
|
names = set()
|
|
for raw_line in (ROOT / "requirements.txt").read_text(encoding="utf-8").splitlines():
|
|
line = _strip_inline_comment(raw_line)
|
|
if not line or line.startswith(("-", "--")):
|
|
continue
|
|
names.add(_requirement_name(line))
|
|
return names
|
|
|
|
|
|
def test_requirements_have_version_specifiers():
|
|
unpinned = []
|
|
for raw_line in (ROOT / "requirements.txt").read_text(encoding="utf-8").splitlines():
|
|
line = _strip_inline_comment(raw_line)
|
|
if not line:
|
|
continue
|
|
if line.startswith(("-", "--")):
|
|
continue
|
|
if "@" in line:
|
|
continue
|
|
if not any(op in line for op in ("==", ">=", "<=", "~=", "!=", ">")):
|
|
unpinned.append(line)
|
|
|
|
assert unpinned == []
|
|
|
|
|
|
def test_audit_flagged_runtime_dependencies_have_import_evidence():
|
|
requirements = _requirement_names()
|
|
runtime_evidence = {
|
|
"beautifulsoup4": [
|
|
("services/cosme_crawler.py", "from bs4 import BeautifulSoup"),
|
|
("services/momo_crawler.py", "from bs4 import BeautifulSoup"),
|
|
("services/trend_crawler.py", "from bs4 import BeautifulSoup"),
|
|
],
|
|
"google-api-python-client": [
|
|
("services/google_drive_service.py", "from googleapiclient.discovery import build"),
|
|
],
|
|
"google-generativeai": [
|
|
("services/gemini_service.py", "import google.generativeai as genai"),
|
|
("services/openclaw_strategist_service.py", "import google.generativeai as genai"),
|
|
],
|
|
"python-pptx": [
|
|
("services/ppt_generator.py", "from pptx import Presentation"),
|
|
],
|
|
"matplotlib": [
|
|
("routes/openclaw_bot_routes.py", "import matplotlib"),
|
|
("services/chart_generator_service.py", "import matplotlib"),
|
|
("services/ppt_generator.py", "import matplotlib"),
|
|
],
|
|
}
|
|
|
|
missing_packages = sorted(set(runtime_evidence) - requirements)
|
|
assert missing_packages == []
|
|
|
|
missing_evidence = []
|
|
for package, locations in runtime_evidence.items():
|
|
if not any(pattern in (ROOT / path).read_text(encoding="utf-8") for path, pattern in locations):
|
|
missing_evidence.append(package)
|
|
|
|
assert missing_evidence == []
|
|
|
|
|
|
def test_cli_ssh_helper_replaces_legacy_paramiko_dependency():
|
|
requirements = _requirement_names()
|
|
helper_source = (ROOT / "utils" / "ssh_helper.py").read_text(encoding="utf-8")
|
|
|
|
assert "paramiko" not in requirements
|
|
assert "subprocess.run" in helper_source
|
|
assert '"ssh"' in helper_source
|
|
assert "paramiko" not in helper_source
|
|
|
|
|
|
def test_pgvector_extension_uses_local_sqlalchemy_type_not_python_package():
|
|
requirements = _requirement_names()
|
|
ai_models_source = (ROOT / "database" / "ai_models.py").read_text(encoding="utf-8")
|
|
migration_source = (ROOT / "migrations" / "009_pgvector_embedding.sql").read_text(encoding="utf-8")
|
|
|
|
assert "pgvector" not in requirements
|
|
assert "matplotlib-inline" not in requirements
|
|
assert "class Vector(UserDefinedType)" in ai_models_source
|
|
assert "CREATE EXTENSION IF NOT EXISTS vector" in migration_source
|