diff --git a/docs/memory/claude_inventory_validation_20260513.md b/docs/memory/claude_inventory_validation_20260513.md index edabf46..fcb6f47 100644 --- a/docs/memory/claude_inventory_validation_20260513.md +++ b/docs/memory/claude_inventory_validation_20260513.md @@ -38,6 +38,7 @@ - `cache_service.py` 已成為 `cache_manager.py` 的相容 shim,`_SALES_CACHE_TTL` 單一來源有測試鎖住。 - `aiops-core/requirements.txt` 已不存在,`aiops-core/README.md` 已標記此目錄只保留歷史 stub,不應安裝或部署。 - V2 提到的「死依賴」不可整批刪:`beautifulsoup4` 用於多個 crawler、`google-api-python-client` 用於 Google Drive、`google-generativeai` 用於 Gemini paths、`python-pptx` 用於 PPT generator、`matplotlib` 用於 Telegram/圖表/PPT。 +- `tests/test_requirements_pinning.py` 已鎖住上述被 V2 誤列的 runtime dependencies:套件需留在 `requirements.txt`,且至少一個 runtime import 證據仍存在。 - Telegram `momo:eig:` callback 已在 `routes/openclaw_bot_routes.py` 與 `services/telegram_bot_service.py` 實作並有 webhook 測試覆蓋,不是未實作缺口。 - Telegram `date_*` / `goal_*` 不是死 callback handler:按鈕先送 `await:*` 進入輸入等待狀態,使用者下一則文字才由 pending action 消費;`tests/test_openclaw_bot_menu_keyboards.py` 與 `tests/test_openclaw_bot_routes_webhook.py` 已覆蓋。 - `services/ai_automation_smoke_service.py` 不是死 service:`run_scheduler.py` 每日 09:10 掛 `run_ai_smoke_daily_summary_task()`,該 task 會呼叫 `send_smoke_daily_summary()`;`tests/test_ai_automation_smoke_service.py` 與 `tests/test_ai_automation_metrics.py` 已覆蓋。 diff --git a/tests/test_requirements_pinning.py b/tests/test_requirements_pinning.py index 54e42e7..50f0af5 100644 --- a/tests/test_requirements_pinning.py +++ b/tests/test_requirements_pinning.py @@ -8,6 +8,24 @@ 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(): @@ -22,3 +40,39 @@ def test_requirements_have_version_specifiers(): 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 == []