"""Tests for src/cmos/runtime_validator.py. Runtime validator runs at format time on a single candidate string with no exemplar context. It checks type-independent structural sanity rules so the formatter can self-retry when GPT-5's reasoning drift produces a malformed entry. It is deliberately weaker than the scoring linter (which has access to canonical fields and source type) but it catches the most common nondeterministic failures: missing terminal period, stray Ibid., dropped italic markers, unbalanced quotes/asterisks. """ from cmos.runtime_validator import validate def test_clean_book_entry_passes(): candidate = "Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020." result = validate(candidate) assert result.passed is True assert result.failures == [] def test_clean_journal_entry_passes(): candidate = ( 'Kwon, Hyeyoung. "Inclusion Work: Children of Immigrants Claiming Membership in Everyday Life." ' "*American Journal of Sociology* 127, no. 6 (2022): 1818–59. " "https://doi.org/10.1086/720277." ) assert validate(candidate).passed def test_missing_terminal_period_fails(): result = validate("Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020") assert result.passed is False assert any("period" in f.lower() for f in result.failures) def test_ibid_fails(): result = validate("Ibid., 42.") assert result.passed is False assert any("ibid" in f.lower() for f in result.failures) def test_missing_italics_anywhere_fails(): # The "New Yorker" magazine regression we saw in iter 7 variance: italic # markers around the magazine name disappeared. This rule should catch # that — almost every CMOS bibliography entry italicizes something. candidate = 'Mead, Rebecca. "Terms of Aggrievement." New Yorker, December 18, 2023.' result = validate(candidate) assert result.passed is False assert any("italic" in f.lower() for f in result.failures) # --- Shortened-form note carve-out (chunk 3 fix #3, Option D) ------------- def test_shortened_form_author_only_passes_without_italics(): """CMOS shortened note form for an author-only reference (no page, no title) is just the author's last name with a period: 'Ettarh.', 'Murch.', 'O'Mara.'. These contain no italics by definition. Without a carve-out, the validator's "must have italics" rule rejects them and the formatter's retry loop fires its full budget on valid output. Surfaced by the Anti-Communist Formations of LIS real-draft run; ~30 of 107 notes were correct shortened-forms that wasted ~60 API calls retrying. The carve-out: if a candidate has no italics AND looks like a shortened-form note (short, name-token shape, optional page, terminal period), the missing-italics check is skipped. """ for candidate in ["Ettarh.", "Murch.", "O'Mara.", "Burden-Stelly."]: result = validate(candidate) assert result.passed is True, ( f"shortened-form author-only note {candidate!r} should pass " f"validation; got failures {result.failures}" ) def test_shortened_form_author_plus_page_passes_without_italics(): """The other common CMOS shortened form is author last name + page: 'Rosen, 7.', 'Mitchell, 197.', 'CBS, 47.'. Same carve-out applies.""" for candidate in [ "Rosen, 7.", "Mitchell, 197.", "CBS, 47.", "Seybold, 282.", "Lawrence Powell, 45.", # multi-word name "Mitchell, 137-39.", # page range with hyphen ]: result = validate(candidate) assert result.passed is True, ( f"shortened-form note {candidate!r} should pass validation; " f"got failures {result.failures}" ) def test_long_no_italic_output_still_fails(): """The carve-out must NOT be a free pass for any short string. A long output with no italics is still likely a malformed full-form citation (model dropped the italic title), and the retry loop should still fire on it. """ candidate = ( "Steve Batterson, The Prosecution of Professor Chandler Davis: " "McCarthyism, Communism, and the Myth of Academic Freedom (NYU Press, 2023)." ) result = validate(candidate) assert result.passed is False assert any("italic" in f.lower() for f in result.failures) def test_short_but_unstructured_no_italic_output_still_fails(): """A short but non-name-shaped string (e.g., a stray sentence fragment with no name structure) should still fail. The carve-out is NOT just "short and ends with period" — it requires the candidate to look like a shortened-form note specifically. """ candidate = "this is some prose without italics." result = validate(candidate) assert result.passed is False, ( "lowercase prose fragment must not be accepted as a shortened-form " "note; the carve-out should require name-token shape" ) def test_unbalanced_italic_markers_fails(): candidate = "Yu, Charles. *Interior Chinatown. Pantheon Books, 2020." result = validate(candidate) assert result.passed is False assert any("italic" in f.lower() or "*" in f for f in result.failures) def test_unbalanced_double_quotes_fails(): candidate = 'Mead, Rebecca. "Terms of Aggrievement. *New Yorker*, December 18, 2023.' result = validate(candidate) assert result.passed is False assert any("quote" in f.lower() for f in result.failures) def test_passed_property_is_negation_of_failures(): # Sanity: passed should be True iff there are no failures. result = validate("Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020.") assert result.passed == (result.failures == [])