111
112
113
class AnalysisGateTests(unittest.TestCase):
114
+ def test_objective_quality_press_included_never_scores_below_press_less(self) -> None:
115
+ body = make_body().replace(
116
+ "No press data was provided this week.",
117
+ "- [Industry report](https://example.com/industry-report) — confirms the trend.",
118
+ )
119
+ analysis = make_analysis(VALID_FRONTMATTER, body)
120
+
121
+ press_score, press_breakdown = analysis_gate.compute_objective_quality(
122
+ analysis, RAW_PAYLOAD_WITH_REPOS, press_context_available=True
123
+ )
124
+ press_less_score, press_less_breakdown = analysis_gate.compute_objective_quality(
125
+ analysis, RAW_PAYLOAD_WITH_REPOS, press_context_available=False
126
+ )
127
+
128
+ # jmservera/SquadScope#583: prevent the W30 paradox where adding real press
129
+ # evidence made an otherwise identical summary score lower.
130
+ self.assertGreaterEqual(press_score, press_less_score)
131
+ self.assertGreater(press_score, press_less_score)
132
+ self.assertEqual(press_breakdown["words"], press_less_breakdown["words"])
133
+ self.assertEqual(press_breakdown["repo_citations"], press_less_breakdown["repo_citations"])
134
+ self.assertEqual(press_breakdown["press_citations"], 1)
135
+ self.assertEqual(press_less_breakdown["press"], 0)
136
+
137
+ def test_objective_quality_no_press_week_retains_publishable_base_score(self) -> None:
138
+ score, breakdown = analysis_gate.compute_objective_quality(
139
+ make_analysis("week: 2026-W23", "A short press-less summary."),
140
+ RAW_PAYLOAD,
141
+ press_context_available=False,
142
+ )
143
+
144
+ self.assertGreaterEqual(score, 60)
145
+ self.assertEqual(breakdown["base"], 60)
146
+ self.assertEqual(breakdown["press"], 0)
147
+ self.assertFalse(breakdown["press_available"])
148
+
149
+ def test_objective_quality_depth_and_evidence_scale_and_cap(self) -> None:
150
+ short_text = make_analysis("week: 2026-W23", "word " * 200)
151
+ medium_text = make_analysis("week: 2026-W23", "word " * 700)
152
+ long_text = make_analysis("week: 2026-W23", "word " * 1400)
153
+
154
+ _, short = analysis_gate.compute_objective_quality(short_text, {}, False)
155
+ _, medium = analysis_gate.compute_objective_quality(medium_text, {}, False)
156
+ _, long = analysis_gate.compute_objective_quality(long_text, {}, False)
157
+
158
+ self.assertEqual(short["depth"], 0)
159
+ self.assertGreater(medium["depth"], short["depth"])
160
+ self.assertEqual(long["depth"], 15)
161
+
162
+ repos = [f"owner/repo-{index}" for index in range(12)]
163
+ raw_payload = {
164
+ "new_repos": [{"full_name": repo} for repo in repos[:6]],
165
+ "trending_repos": [{"full_name": repo} for repo in repos[6:]],
166
+ }
167
+
168
+ def cited_analysis(count: int) -> str:
169
+ links = "\n".join(f"- [{repo}](https://github.com/{repo})" for repo in repos[:count])
170
+ return make_analysis("week: 2026-W23", links)
171
+
172
+ _, none = analysis_gate.compute_objective_quality(cited_analysis(0), raw_payload, False)
173
+ _, half = analysis_gate.compute_objective_quality(cited_analysis(5), raw_payload, False)
174
+ _, full = analysis_gate.compute_objective_quality(cited_analysis(12), raw_payload, False)
175
+ _, no_inventory = analysis_gate.compute_objective_quality(cited_analysis(1), {}, False)
176
+
177
+ self.assertEqual(none["evidence"], 0)
178
+ self.assertEqual(half["evidence"], 5)
179
+ self.assertEqual(full["evidence"], 10)
180
+ self.assertEqual(full["repo_citations"], 12)
181
+ self.assertEqual(no_inventory["evidence"], 0)
182
+ self.assertEqual(no_inventory["repo_citations"], 0)
183
+
184
+ def test_objective_quality_press_bonus_caps_and_requires_press_context(self) -> None:
185
+ body = """## Key References
186
+
187
+### Notable Projects
188
+
189
+- [owner/repo-a](https://github.com/owner/repo-a)
190
+
191
+### Press & Industry
192
+
193
+- [One](https://one.example/article)
194
+- [Two](https://two.example/article)
195
+- [Three](https://three.example/article)
196
+- [Four](https://four.example/article)
197
+- [GitHub](https://github.com/owner/repo-a)
198
+"""
199
+ analysis = make_analysis("week: 2026-W23", body)
200
+
201
+ _, available = analysis_gate.compute_objective_quality(
202
+ analysis, RAW_PAYLOAD_WITH_REPOS, True
203
+ )
204
+ _, unavailable = analysis_gate.compute_objective_quality(
205
+ analysis, RAW_PAYLOAD_WITH_REPOS, False
206
+ )
207
+
208
+ self.assertEqual(available["press"], 15)
209
+ self.assertEqual(available["press_citations"], 4)
210
+ self.assertEqual(unavailable["press"], 0)
211
+ self.assertEqual(unavailable["press_citations"], 0)
212
+
213
+ def test_objective_quality_excludes_github_owned_hosts_from_press(self) -> None:
214
+ body = """## Key References
215
+
216
+### Notable Projects
217
+
218
+- [owner/repo-a](https://github.com/owner/repo-a)
219
+
220
+### Press & Industry
221
+
222
+- [Real press](https://press.example/article)
223
+- [Gist](https://gist.github.com/owner/abc123)
224
+- [Raw](https://raw.githubusercontent.com/owner/repo-a/main/README.md)
225
+- [Sub](https://api.github.com/repos/owner/repo-a)
226
+"""
227
+ analysis = make_analysis("week: 2026-W23", body)
228
+
229
+ _, breakdown = analysis_gate.compute_objective_quality(
230
+ analysis, RAW_PAYLOAD_WITH_REPOS, True
231
+ )
232
+
233
+ self.assertEqual(breakdown["press_citations"], 1)
234
+
235
+ def test_objective_quality_ignores_urls_after_press_subsection(self) -> None:
236
+ body = """## Key References
237
+
238
+### Press & Industry
239
+
240
+- [Real press](https://press.example/article)
241
+
242
+### Further Reading
243
+
244
+- [Not press](https://blog.example/post)
245
+- [Also not](https://docs.example/guide)
246
+"""
247
+ analysis = make_analysis("week: 2026-W23", body)
248
+
249
+ _, breakdown = analysis_gate.compute_objective_quality(
250
+ analysis, RAW_PAYLOAD_WITH_REPOS, True
251
+ )
252
+
253
+ self.assertEqual(breakdown["press_citations"], 1)
254
+
255
+ def test_set_frontmatter_quality_score_replaces_inserts_and_preserves_body(self) -> None:
256
+ body = "Body with quality_score: 999 that must remain untouched.\n"
257
+ existing = make_analysis("week: 2026-W23\nquality_score: 99", body)
258
+ missing = make_analysis("week: 2026-W23", body)
259
+
260
+ replaced = analysis_gate.set_frontmatter_quality_score(existing, 72)
261
+ inserted = analysis_gate.set_frontmatter_quality_score(missing, 68)
262
+
263
+ self.assertIn("\nquality_score: 72\n---", replaced)
264
+ self.assertEqual(replaced.split("---\n", 2)[2], f"\n{body}\n")
265
+ self.assertIn("\nquality_score: 68\n---", inserted)
266
+ self.assertEqual(inserted.split("---\n", 2)[2], f"\n{body}\n")
267
+ with self.assertRaisesRegex(ValueError, "missing YAML frontmatter"):
268
+ analysis_gate.set_frontmatter_quality_score(body, 70)
269
+
270
+ def test_main_overwrites_llm_quality_score_and_reports_objective_breakdown(self) -> None:
271
+ tests_root = Path(__file__).resolve().parent
272
+ with tempfile.TemporaryDirectory(dir=tests_root) as tmpdir:
273
+ workspace = Path(tmpdir)
274
+ analysis_path = workspace / "candidate.md"
275
+ raw_path = workspace / "raw.json"
276
+ report_path = workspace / "report.json"
277
+ original = make_analysis(
278
+ VALID_FRONTMATTER.replace("quality_score: 82", "quality_score: 99"),
279
+ make_body(),
280
+ )
281
+ expected_score, expected_breakdown = analysis_gate.compute_objective_quality(
282
+ original, RAW_PAYLOAD_WITH_REPOS, False
283
+ )
284
+ analysis_path.write_text(original, encoding="utf-8")
285
+ raw_path.write_text(json.dumps(RAW_PAYLOAD_WITH_REPOS), encoding="utf-8")
286
+
287
+ self.assertEqual(
288
+ analysis_gate.main(
289
+ [
290
+ "--analysis-file",
291
+ str(analysis_path),
292
+ "--raw-json",
293
+ str(raw_path),
294
+ "--current-datetime",
295
+ CURRENT_DATETIME,
296
+ "--source",
297
+ "copilot-cli",
298
+ "--model",
299
+ "copilot-default",
300
+ "--report-json",
301
+ str(report_path),
302
+ ]
303
+ ),
304
+ 0,
305
+ )
306
+
307
+ frontmatter, _ = analysis_gate.extract_frontmatter(
308
+ analysis_path.read_text(encoding="utf-8")
309
+ )
310
+ report = json.loads(report_path.read_text(encoding="utf-8"))
311
+ self.assertEqual(frontmatter["quality_score"], expected_score)
312
+ self.assertNotEqual(frontmatter["quality_score"], 99)
313
+ self.assertEqual(report["quality_breakdown"], expected_breakdown)
314
+
315
def test_validate_analysis_accepts_block_style_lists(self) -> None:
316
errors, word_count = analysis_gate.validate_analysis(
317
make_analysis(VALID_FRONTMATTER, make_body()),