返回 last30days-skill
test_env_doc_contract.py
根目录 / tests / test_env_doc_contract.py
1 from __future__ import annotations
2
3 import re
4 from pathlib import Path
5 from unittest import mock
6
7 from lib import env
8
9
10 ROOT = Path(__file__).resolve().parents[1]
11 DOC_PATHS = [
12 ROOT / "skills" / "last30days" / "SKILL.md",
13 ROOT / "README.md",
14 ROOT / "CONFIGURATION.md",
15 ]
16 CONFIG_ENV_KEY_RE = re.compile(
17 r"(?<![A-Z0-9_])(?:"
18 r"LAST30DAYS_[A-Z0-9_]+|"
19 r"(?:XAI|GOOGLE|GEMINI|GOOGLE_GENAI|XIAOHONGSHU|SCRAPECREATORS|APIFY|"
20 r"BSKY|TRUTHSOCIAL|BRAVE|EXA|SERPER|OPENROUTER|PERPLEXITY|PARALLEL|"
21 r"XQUIK|GROQ)_[A-Z0-9_]+|"
22 r"OPENAI_API_KEY|AUTH_TOKEN|CT0|FROM_BROWSER|INCLUDE_SOURCES|"
23 r"EXCLUDE_SOURCES|SETUP_COMPLETE|FUN_LEVEL|X_BEARER_TOKEN"
24 r")(?![A-Z0-9_])"
25 )
26 DOC_ONLY_KEYS = {
27 "LAST30DAYS_API_BASE",
28 "LAST30DAYS_API_KEY",
29 "LAST30DAYS_CACHE_DIR",
30 "LAST30DAYS_MCP_ALLOW_BROWSER_COOKIES",
31 "LAST30DAYS_MCP_TIMEOUT",
32 "LAST30DAYS_PYTHON",
33 # Read from the process environment before or outside get_config
34 # (config-dir override, Keychain/pass source switches), so they are
35 # documented in CONFIGURATION.md without being config keys.
36 "LAST30DAYS_CONFIG_DIR",
37 "LAST30DAYS_PASS_PREFIX",
38 "LAST30DAYS_SKIP_KEYCHAIN",
39 }
40
41
42 def _neutral_secret_sources():
43 return (
44 mock.patch.object(env, "_load_keychain", return_value={}),
45 mock.patch.object(env, "_load_pass", return_value={}),
46 )
47
48
49 def _documented_env_keys() -> set[str]:
50 keys: set[str] = set()
51 for path in DOC_PATHS:
52 keys.update(CONFIG_ENV_KEY_RE.findall(path.read_text(encoding="utf-8")))
53 return keys
54
55
56 def test_registered_user_config_keys_round_trip_from_env_file(tmp_path, monkeypatch):
57 config_file = tmp_path / ".env"
58 config_file.write_text(
59 "FUN_LEVEL=high\nLAST30DAYS_REPORT_CACHE_TTL_SECONDS=120\n",
60 encoding="utf-8",
61 )
62 config_file.chmod(0o600)
63 monkeypatch.setenv("LAST30DAYS_CONFIG_DIR", str(tmp_path))
64 monkeypatch.delenv("FUN_LEVEL", raising=False)
65 monkeypatch.delenv("LAST30DAYS_REPORT_CACHE_TTL_SECONDS", raising=False)
66 monkeypatch.setattr(env, "CONFIG_DIR", tmp_path)
67 monkeypatch.setattr(env, "CONFIG_FILE", config_file)
68 monkeypatch.chdir(tmp_path)
69
70 keychain, pass_store = _neutral_secret_sources()
71 with keychain, pass_store:
72 config = env.get_config()
73
74 assert config["FUN_LEVEL"] == "high"
75 assert config["LAST30DAYS_REPORT_CACHE_TTL_SECONDS"] == "120"
76
77
78 def test_documented_env_keys_are_registered_in_get_config(tmp_path, monkeypatch):
79 monkeypatch.setenv("LAST30DAYS_CONFIG_DIR", str(tmp_path))
80 monkeypatch.setattr(env, "CONFIG_DIR", tmp_path)
81 monkeypatch.setattr(env, "CONFIG_FILE", tmp_path / "does-not-exist.env")
82 monkeypatch.chdir(tmp_path)
83
84 keychain, pass_store = _neutral_secret_sources()
85 with keychain, pass_store:
86 registered_keys = set(env.get_config())
87
88 missing = sorted(_documented_env_keys() - DOC_ONLY_KEYS - registered_keys)
89
90 assert missing == []
91
91 lines PYTHON