返回 last30days-skill
test_env_v3.py
根目录 / tests / test_env_v3.py
1 import os
2 import unittest
3 from pathlib import Path
4 from unittest import mock
5
6 from lib import bird_x, env
7
8
9 class EnvV3Tests(unittest.TestCase):
10 def setUp(self):
11 self._saved_credentials = dict(bird_x._credentials)
12
13 def tearDown(self):
14 bird_x._credentials.clear()
15 bird_x._credentials.update(self._saved_credentials)
16
17 def test_x_source_prefers_xai_without_bird_probe(self):
18 with mock.patch("lib.bird_x.is_bird_authenticated", side_effect=AssertionError("should not probe bird auth")):
19 source = env.get_x_source({"XAI_API_KEY": "test"})
20 self.assertEqual("xai", source)
21
22 def test_x_source_uses_bird_with_explicit_cookies(self):
23 with mock.patch("lib.bird_x.is_bird_installed", return_value=True):
24 source = env.get_x_source({"AUTH_TOKEN": "a", "CT0": "b"})
25 self.assertEqual("bird", source)
26 self.assertEqual("a", bird_x._credentials["AUTH_TOKEN"])
27 self.assertEqual("b", bird_x._credentials["CT0"])
28
29 def test_bird_auth_never_checks_browser_cookies(self):
30 # The guarantee: is_bird_authenticated() must not spawn any child
31 # process to probe for cookies. All subprocess paths in bird_x go
32 # through subproc.run_with_timeout, so patching that covers it.
33 with mock.patch("lib.bird_x.is_bird_installed", return_value=True), mock.patch(
34 "lib.bird_x.subproc.run_with_timeout",
35 side_effect=AssertionError("browser-cookie whoami should not run"),
36 ):
37 bird_x._credentials.clear()
38 with mock.patch.dict(os.environ, {}, clear=False):
39 self.assertIsNone(bird_x.is_bird_authenticated())
40
41 def test_file_permission_check_skips_windows_posix_mode_bits(self):
42 path = mock.Mock(spec=Path)
43 with mock.patch.object(env.os, "name", "nt"), mock.patch.object(env.sys.stderr, "write") as write:
44 env._check_file_permissions(path)
45
46 path.stat.assert_not_called()
47 write.assert_not_called()
48
49 def test_get_config_includes_perplexity_knobs(self):
50 overrides = {
51 "LAST30DAYS_PERPLEXITY_MODE": "search",
52 "LAST30DAYS_PERPLEXITY_MODEL": "sonar-reasoning-pro",
53 "LAST30DAYS_PERPLEXITY_AGENT_MODEL": "perplexity/sonar",
54 "LAST30DAYS_PERPLEXITY_AGENT_PRESET": "low",
55 "LAST30DAYS_PERPLEXITY_AGENT_MAX_STEPS": "4",
56 "LAST30DAYS_PERPLEXITY_AGENT_MAX_OUTPUT_TOKENS": "8192",
57 "LAST30DAYS_PERPLEXITY_AGENT_TIMEOUT_SECONDS": "120",
58 "LAST30DAYS_PERPLEXITY_MAX_RESULTS": "3",
59 "LAST30DAYS_PERPLEXITY_SEARCH_CONTEXT_SIZE": "low",
60 "LAST30DAYS_PERPLEXITY_SEARCH_TYPE": "fast",
61 "LAST30DAYS_PERPLEXITY_SEARCH_MODE": "academic",
62 "LAST30DAYS_PERPLEXITY_DOMAIN_FILTER": "example.com",
63 "LAST30DAYS_PERPLEXITY_LANGUAGE_FILTER": "en",
64 "LAST30DAYS_PERPLEXITY_COUNTRY": "US",
65 "LAST30DAYS_PERPLEXITY_RECENCY_FILTER": "week",
66 "LAST30DAYS_PERPLEXITY_REASONING_EFFORT": "high",
67 "LAST30DAYS_PERPLEXITY_DEEP_TIMEOUT_SECONDS": "600",
68 }
69 with mock.patch.object(env, "CONFIG_FILE", None), \
70 mock.patch.object(env, "_find_project_env", return_value=None), \
71 mock.patch("lib.env._load_keychain", return_value={}), \
72 mock.patch("lib.env._load_pass", return_value={}), \
73 mock.patch.dict(os.environ, overrides, clear=False):
74 config = env.get_config()
75
76 for key, value in overrides.items():
77 self.assertEqual(value, config[key])
78
79
80 class XurlSafePathGatingTests(unittest.TestCase):
81 """F1: the safe/diagnose path (probe=False — what doctor uses) never
82 runs xurl's live `whoami` network check; it keys on local evidence
83 (xurl_x.has_stored_auth) instead."""
84
85 BIRD_OFF = {
86 "installed": False,
87 "authenticated": False,
88 "username": None,
89 "can_install": True,
90 }
91
92 def _status(self, config, probe, stored_mock, live_mock):
93 with mock.patch("lib.bird_x.get_bird_status", return_value=dict(self.BIRD_OFF)), \
94 mock.patch("lib.bird_x.set_credentials", lambda *a, **k: None), \
95 mock.patch("lib.xurl_x.has_stored_auth", **stored_mock), \
96 mock.patch("lib.xurl_x.is_available", **live_mock):
97 return env.get_x_source_status(config, probe=probe)
98
99 _LIVE_FORBIDDEN = {
100 "side_effect": AssertionError(
101 "probe=False must not run the live `xurl whoami` network check"
102 )
103 }
104 _STORED_FORBIDDEN = {
105 "side_effect": AssertionError("probe=True should use the live check")
106 }
107
108 def test_probe_false_uses_local_evidence_only(self):
109 status = self._status(
110 {}, probe=False,
111 stored_mock={"return_value": True}, live_mock=self._LIVE_FORBIDDEN,
112 )
113 self.assertEqual("xurl", status["source"])
114 self.assertTrue(status["xurl_available"])
115
116 def test_probe_false_without_stored_auth_reports_unavailable(self):
117 status = self._status(
118 {}, probe=False,
119 stored_mock={"return_value": False}, live_mock=self._LIVE_FORBIDDEN,
120 )
121 self.assertIsNone(status["source"])
122 self.assertFalse(status["xurl_available"])
123
124 def test_probe_true_keeps_the_live_check(self):
125 status = self._status(
126 {}, probe=True,
127 stored_mock=self._STORED_FORBIDDEN, live_mock={"return_value": True},
128 )
129 self.assertEqual("xurl", status["source"])
130 self.assertTrue(status["xurl_available"])
131
132 def test_x_backend_chain_local_only_never_calls_live_check(self):
133 with mock.patch(
134 "lib.xurl_x.is_available",
135 side_effect=AssertionError("local_only chain must not run `xurl whoami`"),
136 ), mock.patch("lib.xurl_x.has_stored_auth", return_value=True):
137 chain = env.x_backend_chain({}, local_only=True)
138 self.assertEqual(["xurl"], chain)
139
140 def test_x_backend_chain_default_stays_live(self):
141 with mock.patch("lib.xurl_x.is_available", return_value=True), \
142 mock.patch(
143 "lib.xurl_x.has_stored_auth",
144 side_effect=AssertionError("default chain should use the live check"),
145 ):
146 chain = env.x_backend_chain({})
147 self.assertEqual(["xurl"], chain)
148
149
150 class ThreadsAvailabilityTests(unittest.TestCase):
151 """Threads is in the SC default-on family: same key, same per-call cost
152 shape as TikTok / Instagram, so the same default-on rule applies.
153 Suppression goes through EXCLUDE_SOURCES, not gated opt-in."""
154
155 def test_threads_available_with_sc_key_only(self):
156 self.assertTrue(env.is_threads_available({"SCRAPECREATORS_API_KEY": "k"}))
157
158 def test_threads_unavailable_without_sc_key(self):
159 self.assertFalse(env.is_threads_available({}))
160 self.assertFalse(env.is_threads_available({"INCLUDE_SOURCES": "threads"}))
161
162 def test_threads_availability_predicate_is_key_only(self):
163 """is_threads_available is availability-only (key present).
164
165 Scheduling is gated separately by INCLUDE_SOURCES in the pipeline's
166 available_sources (see TestScrapeCreatorsTierGating) — the predicate
167 itself only reports whether the credential exists.
168 """
169 self.assertTrue(env.is_threads_available({
170 "SCRAPECREATORS_API_KEY": "k",
171 "INCLUDE_SOURCES": "",
172 }))
173
174 if __name__ == "__main__":
175 unittest.main()
176
176 lines PYTHON