main
py 186 lines 5.35 KB
Raw
1 """Tests for topic config schema validation."""
2
3 import pytest
4 from pydantic import ValidationError
5
6 from scripts.validate_topic_config import TopicConfig, validate_file
7
8 # --- Valid configs pass ---
9
10
11 def test_valid_ai_ml_config():
12 config = validate_file("examples/topics/ai-ml.yml")
13 assert config.topic.id == "ai-ml"
14 assert config.topic.name == "AI & Machine Learning"
15 assert len(config.queries.primary) >= 1
16
17
18 def test_valid_rust_config():
19 config = validate_file("examples/topics/rust.yml")
20 assert config.topic.id == "rust"
21 assert config.scoring.min_stars == 30
22
23
24 def test_valid_root_config():
25 config = validate_file("squadscope.topic.yml")
26 assert config.topic.id == "ai-ml"
27
28
29 def test_minimal_config():
30 """Only required fields — scoring/quality/learning get defaults."""
31 data = {
32 "topic": {"id": "minimal", "name": "Minimal Topic"},
33 "queries": {"primary": ["stars:>100"]},
34 }
35 config = TopicConfig.model_validate(data)
36 assert config.scoring.min_stars == 20
37 assert config.quality.min_repos_per_week == 5
38 assert "{topic_id}" in config.learning.wisdom_file
39
40
41 # --- Invalid configs fail with correct errors ---
42
43
44 def test_invalid_topic_id_uppercase():
45 data = {
46 "topic": {"id": "AI-ML", "name": "Test"},
47 "queries": {"primary": ["stars:>10"]},
48 }
49 with pytest.raises(ValidationError, match="URL-safe"):
50 TopicConfig.model_validate(data)
51
52
53 def test_invalid_topic_id_spaces():
54 data = {
55 "topic": {"id": "ai ml", "name": "Test"},
56 "queries": {"primary": ["stars:>10"]},
57 }
58 with pytest.raises(ValidationError, match="URL-safe"):
59 TopicConfig.model_validate(data)
60
61
62 def test_invalid_topic_id_special_chars():
63 data = {
64 "topic": {"id": "ai_ml!", "name": "Test"},
65 "queries": {"primary": ["stars:>10"]},
66 }
67 with pytest.raises(ValidationError, match="URL-safe"):
68 TopicConfig.model_validate(data)
69
70
71 def test_missing_topic_name():
72 data = {
73 "topic": {"id": "test"},
74 "queries": {"primary": ["stars:>10"]},
75 }
76 with pytest.raises(ValidationError, match="name"):
77 TopicConfig.model_validate(data)
78
79
80 def test_empty_topic_name():
81 data = {
82 "topic": {"id": "test", "name": " "},
83 "queries": {"primary": ["stars:>10"]},
84 }
85 with pytest.raises(ValidationError, match="must not be empty"):
86 TopicConfig.model_validate(data)
87
88
89 def test_missing_primary_queries():
90 data = {
91 "topic": {"id": "test", "name": "Test"},
92 "queries": {"secondary": ["stars:>100"]},
93 }
94 with pytest.raises(ValidationError, match="primary"):
95 TopicConfig.model_validate(data)
96
97
98 def test_empty_primary_queries_list():
99 data = {
100 "topic": {"id": "test", "name": "Test"},
101 "queries": {"primary": []},
102 }
103 with pytest.raises(ValidationError, match="least"):
104 TopicConfig.model_validate(data)
105
106
107 def test_empty_string_in_primary():
108 data = {
109 "topic": {"id": "test", "name": "Test"},
110 "queries": {"primary": [""]},
111 }
112 with pytest.raises(ValidationError, match="empty string"):
113 TopicConfig.model_validate(data)
114
115
116 def test_invalid_language_boost_too_high():
117 data = {
118 "topic": {"id": "test", "name": "Test"},
119 "queries": {"primary": ["stars:>10"]},
120 "scoring": {"language_boost": {"Python": 50.0}},
121 }
122 with pytest.raises(ValidationError, match="between 0.1 and 10.0"):
123 TopicConfig.model_validate(data)
124
125
126 def test_invalid_language_boost_too_low():
127 data = {
128 "topic": {"id": "test", "name": "Test"},
129 "queries": {"primary": ["stars:>10"]},
130 "scoring": {"language_boost": {"Go": 0.0}},
131 }
132 with pytest.raises(ValidationError, match="between 0.1 and 10.0"):
133 TopicConfig.model_validate(data)
134
135
136 def test_invalid_quality_min_greater_than_max():
137 data = {
138 "topic": {"id": "test", "name": "Test"},
139 "queries": {"primary": ["stars:>10"]},
140 "quality": {"min_repos_per_week": 50, "max_repos_per_week": 10},
141 }
142 with pytest.raises(ValidationError, match="must be <="):
143 TopicConfig.model_validate(data)
144
145
146 def test_invalid_relevance_score_out_of_range():
147 data = {
148 "topic": {"id": "test", "name": "Test"},
149 "queries": {"primary": ["stars:>10"]},
150 "scoring": {"min_relevance_score": 150},
151 }
152 with pytest.raises(ValidationError, match="less than or equal to 100"):
153 TopicConfig.model_validate(data)
154
155
156 def test_missing_topic_section():
157 data = {"queries": {"primary": ["stars:>10"]}}
158 with pytest.raises(ValidationError, match="topic"):
159 TopicConfig.model_validate(data)
160
161
162 def test_missing_queries_section():
163 data = {"topic": {"id": "test", "name": "Test"}}
164 with pytest.raises(ValidationError, match="queries"):
165 TopicConfig.model_validate(data)
166
167
168 def test_file_not_found():
169 with pytest.raises(FileNotFoundError):
170 validate_file("nonexistent.yml")
171
172
173 def test_cli_exit_code_valid(monkeypatch):
174 """CLI returns 0 for valid config."""
175 from scripts.validate_topic_config import main
176
177 monkeypatch.setattr("sys.argv", ["validate", "examples/topics/ai-ml.yml"])
178 assert main() == 0
179
180
181 def test_cli_exit_code_invalid(monkeypatch):
182 """CLI returns 1 for missing file."""
183 from scripts.validate_topic_config import main
184
185 monkeypatch.setattr("sys.argv", ["validate", "nonexistent.yml"])
186 assert main() == 1