返回 last30days-skill
test_permission_preflight.py
根目录 / tests / test_permission_preflight.py
1 """Tests for the safe permission preflight contract."""
2
3 from __future__ import annotations
4
5 import io
6 import json
7 import sys
8 from contextlib import redirect_stderr, redirect_stdout
9 from unittest import mock
10
11 import last30days as cli
12 from lib import env, permission_preflight, pipeline
13
14 DEFAULT_SAVE_DIR = "~" + "/Documents/Last30Days"
15
16
17 def _diag(**overrides):
18 base = {
19 "providers": {
20 "google": False,
21 "openai": False,
22 "xai": False,
23 "openrouter": False,
24 "perplexity": False,
25 },
26 "has_scrapecreators": False,
27 "has_github": False,
28 "available_sources": ["reddit", "hackernews", "polymarket"],
29 "safe": True,
30 "config_source": "env_only",
31 "ignored_project_config": None,
32 "ignored_project_config_keys": [],
33 "ignored_endpoint_overrides": [],
34 "browser_cookies": {"mode": "plan_only", "browsers": [], "reads_values": False},
35 "external_commands": {"yt-dlp": False, "digg-pp-cli": False, "gh": False},
36 "credential_destinations": {"global_env": None},
37 "local_writes": [],
38 "native_search": False,
39 }
40 base.update(overrides)
41 return base
42
43
44 def _diag_with_preflight(config=None, **overrides):
45 config = config or {}
46 diag = _diag(**overrides)
47 diag["permission_preflight"] = permission_preflight.build(config, diag)
48 return diag
49
50
51 def test_preflight_reports_safe_browser_default_without_cookie_values():
52 diag = _diag()
53 preflight = permission_preflight.build({}, diag)
54
55 assert preflight["status"] == "ready"
56 assert preflight["action_items"] == []
57 browser = preflight["local_reads"]["browser_cookies"]
58 assert browser == {
59 "status": "off",
60 "mode": "plan_only",
61 "browsers": [],
62 "reads_values": False,
63 }
64 text = permission_preflight.render_text(preflight)
65 assert "Browser cookies: off" in text
66 assert "cookie values" not in text
67
68
69 def test_preflight_reports_configured_browser_without_reading_values():
70 diag = _diag(
71 browser_cookies={"mode": "plan_only", "browsers": ["firefox"], "reads_values": False}
72 )
73 preflight = permission_preflight.build({"FROM_BROWSER": "firefox"}, diag)
74
75 browser = preflight["local_reads"]["browser_cookies"]
76 assert browser["status"] == "enabled_by_config"
77 assert browser["browsers"] == ["firefox"]
78 assert browser["reads_values"] is False
79 assert "preflight did not read cookie values" in permission_preflight.render_text(preflight)
80
81
82 def test_preflight_reports_conditional_report_on_save_without_definite_write():
83 preflight = permission_preflight.build(
84 {},
85 _diag(),
86 report_on_save_dir=DEFAULT_SAVE_DIR,
87 )
88
89 assert preflight["local_writes"] == []
90 assert preflight["conditional_writes"] == [
91 {"kind": "report_on_save", "path": DEFAULT_SAVE_DIR}
92 ]
93 rendered = permission_preflight.render_text(preflight)
94 assert "none planned" in rendered
95 assert f"Report (if saved): {DEFAULT_SAVE_DIR}" in rendered
96
97
98 def test_preflight_prefers_definite_save_dir_over_conditional_report_on_save(tmp_path):
99 save_dir = tmp_path / "reports"
100 preflight = permission_preflight.build(
101 {},
102 _diag(),
103 planned_save_dir=str(save_dir),
104 report_on_save_dir=DEFAULT_SAVE_DIR,
105 )
106
107 assert preflight["local_writes"] == [{"kind": "report", "path": str(save_dir)}]
108 assert preflight["conditional_writes"] == []
109
110
111 def test_preflight_dedupes_env_save_dir_against_conditional_report_on_save():
112 preflight = permission_preflight.build(
113 {"LAST30DAYS_MEMORY_DIR": DEFAULT_SAVE_DIR},
114 _diag(local_writes=[{"kind": "report", "path": DEFAULT_SAVE_DIR}]),
115 report_on_save_dir=DEFAULT_SAVE_DIR,
116 )
117
118 assert preflight["local_writes"] == [{"kind": "report", "path": DEFAULT_SAVE_DIR}]
119 assert preflight["conditional_writes"] == []
120
121
122 def test_preflight_project_config_not_active_is_not_trusted_with_trust_env():
123 preflight = permission_preflight.build(
124 {"LAST30DAYS_TRUST_PROJECT_CONFIG": "1"},
125 _diag(config_source="env_only", ignored_project_config=None),
126 )
127
128 project = preflight["local_reads"]["project_config"]
129 assert project["status"] == "not_active"
130 assert project["trusted"] is False
131
132
133 def test_preflight_reports_ignored_project_config_without_secret_values(tmp_path, monkeypatch):
134 project_env = tmp_path / ".claude" / "last30days.env"
135 project_env.parent.mkdir()
136 project_env.write_text(
137 "OPENAI_BASE_URL=https://attacker.example\nOPENAI_API_KEY=sk-not-reported\n",
138 encoding="utf-8",
139 )
140 monkeypatch.chdir(tmp_path)
141 monkeypatch.setattr(env, "CONFIG_FILE", None)
142 monkeypatch.setenv("OPENAI_API_KEY", "sk-global")
143 monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False)
144
145 with mock.patch.object(env, "_load_keychain", return_value={}), \
146 mock.patch.object(env, "_load_pass", return_value={}):
147 config = env.get_config(
148 policy=env.ConfigLoadPolicy(
149 browser_cookies="plan_only",
150 inspect_ignored_project_config=True,
151 )
152 )
153 diag = pipeline.diagnose(config, safe=True)
154
155 preflight = diag["permission_preflight"]
156 assert preflight["local_reads"]["project_config"]["status"] == "ignored_untrusted"
157 assert preflight["network"]["ignored_endpoint_overrides"] == ["OPENAI_BASE_URL"]
158 rendered = permission_preflight.render_text(preflight)
159 assert "Project config: ignored untrusted file" in rendered
160 assert "OPENAI_API_KEY" in rendered
161 assert "sk-not-reported" not in str(preflight)
162 assert "sk-global" not in str(preflight)
163 assert "sk-not-reported" not in rendered
164 assert "sk-global" not in rendered
165
166
167 def test_diagnose_uses_preflight_endpoint_override_key_set():
168 ignored_keys = sorted(permission_preflight.ENDPOINT_OVERRIDE_KEYS) + ["UNRELATED_KEY"]
169 diag = pipeline.diagnose(
170 {
171 "_IGNORED_PROJECT_CONFIG_KEYS": ignored_keys,
172 "_BROWSER_COOKIE_MODE": "off",
173 "_BROWSER_COOKIE_BROWSERS": [],
174 },
175 safe=True,
176 )
177
178 assert sorted(diag["ignored_endpoint_overrides"]) == sorted(
179 permission_preflight.ENDPOINT_OVERRIDE_KEYS
180 )
181
182
183 def test_cli_preflight_uses_plan_only_policy_and_does_not_run_research(monkeypatch):
184 seen: dict[str, object] = {}
185
186 def fake_get_config(*, policy):
187 seen["policy"] = policy
188 return {"_BROWSER_COOKIE_MODE": policy.browser_cookies, "_BROWSER_COOKIE_BROWSERS": []}
189
190 with mock.patch.object(cli.env, "get_config", side_effect=fake_get_config), \
191 mock.patch.object(cli.pipeline, "diagnose", return_value=_diag_with_preflight()) as diagnose, \
192 mock.patch.object(cli.pipeline, "run", side_effect=AssertionError("research should not run")), \
193 mock.patch.object(sys, "argv", ["last30days.py", "--preflight"]):
194 stdout = io.StringIO()
195 stderr = io.StringIO()
196 with redirect_stdout(stdout), redirect_stderr(stderr):
197 assert cli.main() == 0
198
199 assert seen["policy"].browser_cookies == "plan_only"
200 assert seen["policy"].inspect_ignored_project_config is True
201 diagnose.assert_called_once()
202 assert "last30days preflight" in stdout.getvalue()
203 assert "Local writes:" in stdout.getvalue()
204
205
206 def test_cli_preflight_reuses_embedded_preflight_without_save_overrides(monkeypatch):
207 embedded = permission_preflight.build({}, _diag())
208 diag = _diag(permission_preflight=embedded)
209 with mock.patch.object(cli.env, "get_config", return_value={}), \
210 mock.patch.object(cli.pipeline, "diagnose", return_value=diag), \
211 mock.patch.object(cli.permission_preflight, "build", side_effect=AssertionError("should reuse embedded preflight")), \
212 mock.patch.object(sys, "argv", ["last30days.py", "--preflight", "--emit=json"]):
213 stdout = io.StringIO()
214 stderr = io.StringIO()
215 with redirect_stdout(stdout), redirect_stderr(stderr):
216 assert cli.main() == 0
217
218 payload = json.loads(stdout.getvalue())
219 assert payload == embedded
220
221
222 def test_cli_preflight_reports_explicit_save_dir(monkeypatch, tmp_path):
223 save_dir = tmp_path / "reports"
224 with mock.patch.object(cli.env, "get_config", return_value={}), \
225 mock.patch.object(cli.pipeline, "diagnose", return_value=_diag_with_preflight()), \
226 mock.patch.object(sys, "argv", ["last30days.py", "--preflight", "--save-dir", str(save_dir)]):
227 stdout = io.StringIO()
228 stderr = io.StringIO()
229 with redirect_stdout(stdout), redirect_stderr(stderr):
230 assert cli.main() == 0
231
232 assert str(save_dir) in stdout.getvalue()
233 assert "report" in stdout.getvalue()
234
235
236 def test_cli_preflight_reports_conditional_save_dir(monkeypatch):
237 with mock.patch.object(cli.env, "get_config", return_value={}), \
238 mock.patch.object(cli.pipeline, "diagnose", return_value=_diag_with_preflight()), \
239 mock.patch.object(
240 sys,
241 "argv",
242 [
243 "last30days.py",
244 "--preflight",
245 "--preflight-report-on-save-dir",
246 DEFAULT_SAVE_DIR,
247 ],
248 ):
249 stdout = io.StringIO()
250 stderr = io.StringIO()
251 with redirect_stdout(stdout), redirect_stderr(stderr):
252 assert cli.main() == 0
253
254 assert f"Report (if saved): {DEFAULT_SAVE_DIR}" in stdout.getvalue()
255
256
257 def test_cli_preflight_json_returns_structured_contract(monkeypatch):
258 with mock.patch.object(cli.env, "get_config", return_value={}), \
259 mock.patch.object(cli.pipeline, "diagnose", return_value=_diag_with_preflight()), \
260 mock.patch.object(sys, "argv", ["last30days.py", "--preflight", "--emit=json"]):
261 stdout = io.StringIO()
262 stderr = io.StringIO()
263 with redirect_stdout(stdout), redirect_stderr(stderr):
264 assert cli.main() == 0
265
266 payload = json.loads(stdout.getvalue())
267 assert payload["local_reads"]["browser_cookies"]["status"] == "off"
268 assert payload["conditional_writes"] == []
269 assert payload["safe"] is True
270
270 lines PYTHON