main
py 312 lines 11.7 KB
Raw
1 """Integration tests: validate ai-ml and rust topic configs end-to-end.
2
3 Verifies schema validation passes and scoring pipeline produces reasonable
4 results with mock repo data for both topics, plus cross-topic isolation.
5 """
6
7 from __future__ import annotations
8
9 from datetime import UTC, datetime, timedelta
10
11 import pytest
12
13 from scripts.score_repos import (
14 compute_relevance_score,
15 get_scoring_config,
16 load_config,
17 score_repos,
18 )
19 from scripts.validate_topic_config import validate_file
20
21 # --- Helpers ---
22
23
24 def _make_repo(
25 name: str,
26 language: str,
27 stars: int,
28 stars_gained: int,
29 topics: list[str],
30 age_days: int = 30,
31 ) -> dict:
32 """Create a mock repo dict with given characteristics."""
33 return {
34 "name": name,
35 "owner": "mock-org",
36 "full_name": f"mock-org/{name}",
37 "description": f"Mock repo: {name}",
38 "language": language,
39 "stars": stars,
40 "forks": stars // 10,
41 "created_at": (datetime.now(UTC) - timedelta(days=age_days)).isoformat(),
42 "topics": topics,
43 "license": "MIT",
44 "url": f"https://github.com/mock-org/{name}",
45 "stars_gained": stars_gained,
46 }
47
48
49 # --- AI/ML Config Fixtures ---
50
51
52 @pytest.fixture
53 def aiml_root_config():
54 return validate_file("squadscope.topic.yml")
55
56
57 @pytest.fixture
58 def aiml_example_config():
59 return validate_file("examples/topics/ai-ml.yml")
60
61
62 @pytest.fixture
63 def aiml_scoring_config():
64 config = load_config("squadscope.topic.yml")
65 return get_scoring_config(config)
66
67
68 @pytest.fixture
69 def rust_config():
70 return validate_file("examples/topics/rust.yml")
71
72
73 @pytest.fixture
74 def rust_scoring_config():
75 config = load_config("examples/topics/rust.yml")
76 return get_scoring_config(config)
77
78
79 @pytest.fixture
80 def aiml_repos():
81 """Sample repos matching AI/ML profile."""
82 return [
83 _make_repo(
84 "transformer-lib",
85 "Python",
86 500,
87 120,
88 ["machine-learning", "transformers", "deep-learning"],
89 ),
90 _make_repo(
91 "llm-toolkit", "Python", 1200, 300, ["llm", "artificial-intelligence", "python"]
92 ),
93 _make_repo(
94 "ml-starter", "Jupyter Notebook", 150, 40, ["machine-learning", "neural-network"]
95 ),
96 _make_repo("data-pipeline", "Python", 80, 20, ["machine-learning"], age_days=60),
97 _make_repo(
98 "ai-research",
99 "Python",
100 3000,
101 500,
102 ["deep-learning", "llm", "transformers"],
103 age_days=10,
104 ),
105 _make_repo("small-ml", "Python", 50, 15, ["machine-learning"], age_days=90),
106 _make_repo("mid-ml", "Python", 200, 50, ["deep-learning", "neural-network"], age_days=45),
107 ]
108
109
110 @pytest.fixture
111 def rust_repos():
112 """Sample repos matching Rust profile."""
113 return [
114 _make_repo("fast-cli", "Rust", 400, 80, ["rust", "cli", "systems-programming"]),
115 _make_repo("async-runtime", "Rust", 2000, 200, ["rust", "async-rust", "cargo"]),
116 _make_repo("wasm-toolkit", "Rust", 600, 100, ["rust", "wasm", "cargo"]),
117 _make_repo("rust-game", "Rust", 150, 40, ["rust", "systems-programming"]),
118 _make_repo("tiny-crate", "Rust", 50, 20, ["rust", "cargo"], age_days=60),
119 ]
120
121
122 # =============================================================================
123 # 1. AI/ML Schema Validation
124 # =============================================================================
125
126
127 class TestAimlSchemaValidation:
128 """Issue #70: Validate ai-ml configs pass schema validation."""
129
130 def test_root_config_passes_validation(self, aiml_root_config):
131 assert aiml_root_config.topic.id == "ai-ml"
132 assert aiml_root_config.topic.name == "AI & Machine Learning"
133 assert len(aiml_root_config.queries.primary) >= 2
134
135 def test_example_config_passes_validation(self, aiml_example_config):
136 assert aiml_example_config.topic.id == "ai-ml"
137 assert aiml_example_config.scoring.min_stars == 20
138 assert aiml_example_config.scoring.min_relevance_score == 40
139
140 def test_root_config_scoring_section(self, aiml_root_config):
141 assert aiml_root_config.scoring.language_boost.get("Python") == 1.2
142 assert "machine-learning" in aiml_root_config.scoring.topic_relevance
143
144 def test_root_config_quality_section(self, aiml_root_config):
145 assert aiml_root_config.quality.min_repos_per_week == 5
146 assert aiml_root_config.quality.max_repos_per_week == 30
147
148 def test_root_config_learning_section(self, aiml_root_config):
149 assert "ai-ml" in aiml_root_config.learning.wisdom_file
150
151
152 # =============================================================================
153 # 2. AI/ML Scoring Pipeline
154 # =============================================================================
155
156
157 class TestAimlScoringPipeline:
158 """Issue #70: AI/ML repos get reasonable scores with mock data."""
159
160 def test_typical_aiml_repos_score_above_threshold(self, aiml_scoring_config, aiml_repos):
161 scored = score_repos(aiml_repos, aiml_scoring_config)
162 # All well-formed AI/ML repos should pass the min_relevance_score (40)
163 assert len(scored) >= 5
164
165 def test_high_quality_repo_scores_above_40(self, aiml_scoring_config):
166 repo = _make_repo(
167 "top-ml", "Python", 500, 100, ["machine-learning", "deep-learning", "llm"]
168 )
169 score = compute_relevance_score(repo, aiml_scoring_config)
170 assert score >= 40
171
172 def test_min_repos_per_week_achievable(self, aiml_scoring_config, aiml_repos):
173 """With typical data, at least min_repos_per_week pass threshold."""
174 min_required = 5 # from config quality.min_repos_per_week
175 scored = score_repos(aiml_repos, aiml_scoring_config)
176 assert len(scored) >= min_required
177
178 def test_python_language_boost_applies(self, aiml_scoring_config):
179 python_repo = _make_repo("py-ml", "Python", 200, 50, ["machine-learning"])
180 other_repo = _make_repo("go-ml", "Go", 200, 50, ["machine-learning"])
181
182 py_score = compute_relevance_score(python_repo, aiml_scoring_config)
183 go_score = compute_relevance_score(other_repo, aiml_scoring_config)
184 assert py_score > go_score
185
186 def test_topic_relevance_boosts_score(self, aiml_scoring_config):
187 relevant = _make_repo(
188 "relevant", "Python", 200, 50, ["machine-learning", "deep-learning", "llm"]
189 )
190 irrelevant = _make_repo("irrelevant", "Python", 200, 50, ["cooking", "recipes"])
191
192 rel_score = compute_relevance_score(relevant, aiml_scoring_config)
193 irr_score = compute_relevance_score(irrelevant, aiml_scoring_config)
194 assert rel_score > irr_score
195
196
197 # =============================================================================
198 # 3. Rust Schema Validation
199 # =============================================================================
200
201
202 class TestRustSchemaValidation:
203 """Issue #71: Validate rust config passes schema validation."""
204
205 def test_rust_config_passes_validation(self, rust_config):
206 assert rust_config.topic.id == "rust"
207 assert rust_config.topic.name == "Rust Ecosystem"
208 assert len(rust_config.queries.primary) >= 2
209
210 def test_rust_scoring_section(self, rust_config):
211 assert rust_config.scoring.language_boost.get("Rust") == 1.3
212 assert rust_config.scoring.min_stars == 30
213 assert rust_config.scoring.max_age_days == 730
214
215 def test_rust_topic_relevance(self, rust_config):
216 assert "rust" in rust_config.scoring.topic_relevance
217 assert "cargo" in rust_config.scoring.topic_relevance
218 assert "wasm" in rust_config.scoring.topic_relevance
219
220 def test_rust_quality_section(self, rust_config):
221 assert rust_config.quality.min_repos_per_week == 5
222 assert rust_config.quality.max_repos_per_week == 25
223 assert rust_config.quality.min_quality_score == 55
224
225
226 # =============================================================================
227 # 4. Rust Scoring Pipeline
228 # =============================================================================
229
230
231 class TestRustScoringPipeline:
232 """Issue #71: Rust repos score reasonably with language_boost."""
233
234 def test_typical_rust_repos_score_above_threshold(self, rust_scoring_config, rust_repos):
235 scored = score_repos(rust_repos, rust_scoring_config)
236 # Most Rust repos should pass the lower threshold (35)
237 assert len(scored) >= 3
238
239 def test_rust_language_boost_applies(self, rust_scoring_config):
240 rust_repo = _make_repo("cli-tool", "Rust", 300, 60, ["rust", "cli"])
241 go_repo = _make_repo("cli-tool-go", "Go", 300, 60, ["go", "cli"])
242
243 rust_score = compute_relevance_score(rust_repo, rust_scoring_config)
244 go_score = compute_relevance_score(go_repo, rust_scoring_config)
245 assert rust_score > go_score
246
247 def test_narrower_topic_still_produces_results(self, rust_scoring_config, rust_repos):
248 """Rust is narrower than AI/ML but should still yield min_repos_per_week."""
249 min_required = 5 # from config quality.min_repos_per_week
250 scored = score_repos(rust_repos, rust_scoring_config)
251 assert len(scored) >= min_required
252
253 def test_high_quality_rust_repo_scores_above_40(self, rust_scoring_config):
254 repo = _make_repo("blazing-fast", "Rust", 1000, 150, ["rust", "async-rust", "cargo"])
255 score = compute_relevance_score(repo, rust_scoring_config)
256 assert score >= 40
257
258
259 # =============================================================================
260 # 5. Cross-Topic Isolation
261 # =============================================================================
262
263
264 class TestCrossTopicIsolation:
265 """Verify configs don't score repos from other domains highly."""
266
267 def test_aiml_config_does_not_score_rust_repos_highly(self, aiml_scoring_config, rust_repos):
268 """Rust repos should score lower under ai-ml config due to no topic overlap."""
269 scored = score_repos(rust_repos, aiml_scoring_config)
270 # Rust repos lack AI/ML topics, so fewer should pass threshold
271 for repo in scored:
272 # No Rust repo should score as high as a good AI/ML repo would
273 assert repo["relevance_score"] < 70
274
275 def test_rust_config_does_not_score_python_ml_repos_highly(
276 self, rust_scoring_config, aiml_repos
277 ):
278 """Python ML repos should score lower under rust config due to topic/lang mismatch."""
279 scored = score_repos(aiml_repos, rust_scoring_config)
280 # Python repos don't get Rust language boost and lack rust topics
281 for repo in scored:
282 assert repo["relevance_score"] < 75
283
284 def test_aiml_repos_score_higher_with_own_config(
285 self, aiml_scoring_config, rust_scoring_config, aiml_repos
286 ):
287 """AI/ML repos should score higher with ai-ml config than rust config."""
288 aiml_scored = score_repos(aiml_repos, aiml_scoring_config)
289 rust_scored = score_repos(aiml_repos, rust_scoring_config)
290
291 avg_aiml = sum(r["relevance_score"] for r in aiml_scored) / max(len(aiml_scored), 1)
292 avg_rust = (
293 sum(r["relevance_score"] for r in rust_scored) / max(len(rust_scored), 1)
294 if rust_scored
295 else 0
296 )
297 assert avg_aiml > avg_rust
298
299 def test_rust_repos_score_higher_with_own_config(
300 self, aiml_scoring_config, rust_scoring_config, rust_repos
301 ):
302 """Rust repos should score higher with rust config than ai-ml config."""
303 rust_scored = score_repos(rust_repos, rust_scoring_config)
304 aiml_scored = score_repos(rust_repos, aiml_scoring_config)
305
306 avg_rust = sum(r["relevance_score"] for r in rust_scored) / max(len(rust_scored), 1)
307 avg_aiml = (
308 sum(r["relevance_score"] for r in aiml_scored) / max(len(aiml_scored), 1)
309 if aiml_scored
310 else 0
311 )
312 assert avg_rust > avg_aiml