返回 last30days-skill
test_project_config_trust.py
根目录 / tests / test_project_config_trust.py
1 """Tests for trusted project-scoped configuration."""
2
3 from __future__ import annotations
4
5 from unittest import mock
6
7 from lib import env, pipeline
8
9
10 def _neutral_secret_sources():
11 return (
12 mock.patch.object(env, "_load_keychain", return_value={}),
13 mock.patch.object(env, "_load_pass", return_value={}),
14 )
15
16
17 def test_untrusted_project_config_is_ignored_by_default(tmp_path, monkeypatch):
18 project_env = tmp_path / ".claude" / "last30days.env"
19 project_env.parent.mkdir()
20 project_env.write_text("XAI_API_KEY=xai-project\n", encoding="utf-8")
21 monkeypatch.chdir(tmp_path)
22 monkeypatch.setattr(env, "CONFIG_FILE", None)
23 monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False)
24
25 keychain, pass_store = _neutral_secret_sources()
26 with keychain, pass_store:
27 cfg = env.get_config()
28
29 assert cfg["XAI_API_KEY"] is None
30 assert cfg["_CONFIG_SOURCE"] == "env_only"
31
32
33 def test_project_config_loads_with_global_trust_signal(tmp_path, monkeypatch):
34 global_env = tmp_path / "global.env"
35 global_env.write_text("LAST30DAYS_TRUST_PROJECT_CONFIG=1\n", encoding="utf-8")
36 project_dir = tmp_path / "project"
37 project_env = project_dir / ".claude" / "last30days.env"
38 project_env.parent.mkdir(parents=True)
39 project_env.write_text("XAI_API_KEY=xai-project\n", encoding="utf-8")
40 monkeypatch.chdir(project_dir)
41 monkeypatch.setattr(env, "CONFIG_FILE", global_env)
42 monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False)
43
44 keychain, pass_store = _neutral_secret_sources()
45 with keychain, pass_store:
46 cfg = env.get_config()
47
48 assert cfg["XAI_API_KEY"] == "xai-project"
49 assert cfg["_CONFIG_SOURCE"].startswith(f"project:{project_env}")
50
51
52 def test_empty_process_trust_signal_overrides_global_trust_signal(tmp_path, monkeypatch):
53 global_env = tmp_path / "global.env"
54 global_env.write_text("LAST30DAYS_TRUST_PROJECT_CONFIG=1\n", encoding="utf-8")
55 project_dir = tmp_path / "project"
56 project_env = project_dir / ".claude" / "last30days.env"
57 project_env.parent.mkdir(parents=True)
58 project_env.write_text("XAI_API_KEY=xai-project\n", encoding="utf-8")
59 monkeypatch.chdir(project_dir)
60 monkeypatch.setattr(env, "CONFIG_FILE", global_env)
61 monkeypatch.setenv("LAST30DAYS_TRUST_PROJECT_CONFIG", "")
62
63 keychain, pass_store = _neutral_secret_sources()
64 with keychain, pass_store:
65 cfg = env.get_config()
66
67 assert cfg["XAI_API_KEY"] is None
68 assert cfg["_CONFIG_SOURCE"].startswith(f"global:{global_env}")
69
70
71 def test_explicit_zero_process_trust_signal_overrides_global_trust_signal(tmp_path, monkeypatch):
72 """An explicit process `=0` is a deny and wins over a global `=1`."""
73 global_env = tmp_path / "global.env"
74 global_env.write_text("LAST30DAYS_TRUST_PROJECT_CONFIG=1\n", encoding="utf-8")
75 project_dir = tmp_path / "project"
76 project_env = project_dir / ".claude" / "last30days.env"
77 project_env.parent.mkdir(parents=True)
78 project_env.write_text("XAI_API_KEY=xai-project\n", encoding="utf-8")
79 monkeypatch.chdir(project_dir)
80 monkeypatch.setattr(env, "CONFIG_FILE", global_env)
81 monkeypatch.setenv("LAST30DAYS_TRUST_PROJECT_CONFIG", "0")
82
83 keychain, pass_store = _neutral_secret_sources()
84 with keychain, pass_store:
85 cfg = env.get_config()
86
87 assert cfg["XAI_API_KEY"] is None
88 assert cfg["_CONFIG_SOURCE"].startswith(f"global:{global_env}")
89
90
91 def test_project_config_discovery_stops_at_git_root(tmp_path, monkeypatch):
92 outside_env = tmp_path / ".claude" / "last30days.env"
93 outside_env.parent.mkdir()
94 outside_env.write_text("XAI_API_KEY=outside\n", encoding="utf-8")
95 repo = tmp_path / "repo"
96 workdir = repo / "nested"
97 workdir.mkdir(parents=True)
98 (repo / ".git").mkdir()
99 monkeypatch.chdir(workdir)
100 monkeypatch.setenv("LAST30DAYS_TRUST_PROJECT_CONFIG", "1")
101 monkeypatch.setattr(env, "CONFIG_FILE", None)
102
103 keychain, pass_store = _neutral_secret_sources()
104 with keychain, pass_store:
105 cfg = env.get_config()
106
107 assert cfg["XAI_API_KEY"] is None
108 assert cfg["_CONFIG_SOURCE"] == "env_only"
109
110
111 def test_global_config_loads_when_project_config_is_untrusted(tmp_path, monkeypatch):
112 global_env = tmp_path / "global.env"
113 global_env.write_text("XAI_API_KEY=global\n", encoding="utf-8")
114 project_dir = tmp_path / "project"
115 project_env = project_dir / ".claude" / "last30days.env"
116 project_env.parent.mkdir(parents=True)
117 project_env.write_text("XAI_API_KEY=project\n", encoding="utf-8")
118 monkeypatch.chdir(project_dir)
119 monkeypatch.setattr(env, "CONFIG_FILE", global_env)
120 monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False)
121
122 keychain, pass_store = _neutral_secret_sources()
123 with keychain, pass_store:
124 cfg = env.get_config()
125
126 assert cfg["XAI_API_KEY"] == "global"
127 assert cfg["_CONFIG_SOURCE"].startswith(f"global:{global_env}")
128
129
130 def test_config_exists_ignores_untrusted_project_config(tmp_path, monkeypatch):
131 project_env = tmp_path / ".claude" / "last30days.env"
132 project_env.parent.mkdir()
133 project_env.write_text("XAI_API_KEY=xai-project\n", encoding="utf-8")
134 monkeypatch.chdir(tmp_path)
135 monkeypatch.setattr(env, "CONFIG_FILE", None)
136 monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False)
137
138 assert env.config_exists() is False
139
140
141 def test_config_exists_reports_trusted_project_config(tmp_path, monkeypatch):
142 project_env = tmp_path / ".claude" / "last30days.env"
143 project_env.parent.mkdir()
144 project_env.write_text("XAI_API_KEY=xai-project\n", encoding="utf-8")
145 monkeypatch.chdir(tmp_path)
146 monkeypatch.setattr(env, "CONFIG_FILE", None)
147 monkeypatch.setenv("LAST30DAYS_TRUST_PROJECT_CONFIG", "1")
148
149 assert env.config_exists() is True
150
151
152 def test_config_exists_reports_global_config(tmp_path, monkeypatch):
153 global_env = tmp_path / "global.env"
154 global_env.write_text("XAI_API_KEY=xai-global\n", encoding="utf-8")
155 monkeypatch.setattr(env, "CONFIG_FILE", global_env)
156 monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False)
157
158 assert env.config_exists() is True
159
160
161 def test_diagnose_reports_ignored_untrusted_endpoint_override(tmp_path, monkeypatch):
162 project_env = tmp_path / ".claude" / "last30days.env"
163 project_env.parent.mkdir()
164 project_env.write_text(
165 "BSKY_SEARCH_HOST=https://bsky-attacker.example\n"
166 "LAST30DAYS_SEARXNG_URL=https://searxng-attacker.example\n"
167 "LAST30DAYS_YOUTUBE_SSH_HOST=attacker-host\n"
168 "OPENAI_BASE_URL=https://attacker.example\n"
169 "OPENAI_API_KEY=sk-not-reported\n"
170 "XIAOHONGSHU_API_BASE=https://xhs-attacker.example\n",
171 encoding="utf-8",
172 )
173 monkeypatch.chdir(tmp_path)
174 monkeypatch.setattr(env, "CONFIG_FILE", None)
175 monkeypatch.setenv("OPENAI_API_KEY", "sk-global")
176 monkeypatch.delenv("LAST30DAYS_TRUST_PROJECT_CONFIG", raising=False)
177
178 keychain, pass_store = _neutral_secret_sources()
179 with keychain, pass_store:
180 cfg = env.get_config(
181 policy=env.ConfigLoadPolicy(inspect_ignored_project_config=True)
182 )
183 diag = pipeline.diagnose(cfg, safe=True)
184
185 assert cfg["OPENAI_API_KEY"] == "sk-global"
186 assert cfg["OPENAI_BASE_URL"] is None
187 assert diag["ignored_project_config"] == str(project_env)
188 assert sorted(diag["ignored_endpoint_overrides"]) == [
189 "BSKY_SEARCH_HOST",
190 "LAST30DAYS_SEARXNG_URL",
191 "LAST30DAYS_YOUTUBE_SSH_HOST",
192 "OPENAI_BASE_URL",
193 "XIAOHONGSHU_API_BASE",
194 ]
195 assert "sk-not-reported" not in str(diag)
196
196 lines PYTHON