Files
cmos/tests/test_formatter.py
Mark Eaton 553240c153 v2 chunk 3: bug fixes from real-draft triage + polish
Six independent fixes addressing the Anti-Communist Formations of LIS
real-draft test findings (chunk 2c). Four are bug fixes for issues
that destroyed user data or wasted API calls; two are polish quality
improvements revisited from the deferred list.

src/cmos/note_formatter.py — empty input guard (fix #1)
  Empty / whitespace-only input now short-circuits the API entirely
  and returns the input verbatim. Surfaced by note [66] in the
  Anti-Communist draft, where GPT-5 broke character on empty input
  and returned a conversational meta-reply ("Please paste the
  citation entry...") that then got substituted into the document.
  Whitespace preserved so cli.reformat_notes is byte-exact on empty
  notes. v2 only.

src/cmos/note_formatter.py — deprecated Latin guard (fix #2)
  New Python guard catches the full CMOS-18-deprecated Latin
  citation set (ibid, idem, id., op. cit., loc. cit.) and returns
  input verbatim before the API call. Rule 10 in the prompt also
  rewritten: explicitly says "return verbatim" instead of the old
  "return cleanest possible full-form note", which the model
  interpreted as "return empty when there's no information",
  silently destroying the marker. Surfaced by note [61] = "Ibid.".
  v2 only.

src/cmos/runtime_validator.py — shortened-form carve-out (fix #3)
  The "must contain italics" check is now skipped when the
  candidate looks like a CMOS shortened-form note (author last
  name, optional page, no italic content). Surfaced by ~30 of 107
  notes in the Anti-Communist draft that were correctly returned
  as shortened forms ("Rosen, 7.", "Mitchell, 197.") but rejected
  by the validator's strict italic check, firing the full retry
  budget on valid output (~60 wasted API calls per run). The
  carve-out is conservative: name-token regex + terminal period;
  doesn't match unstructured prose. Shared infrastructure — affects
  v1 and v2, no-op in v1's normal workflow because v1 outputs
  always have italics.

src/cmos/note_formatter.py — substantive prose pass-through (fix #4)
  Long discursive prose with no citation skeleton markers now
  short-circuits the API and returns the input verbatim. Surfaced
  by notes [8] (689 chars), [9] (893 chars), [75] (163 chars) in
  the Anti-Communist draft — substantive notes that the formatter
  was extracting a single citation from and silently discarding
  the surrounding commentary. CMOS 14.39 explicitly allows
  substantive notes; preserving them is the user's explicit policy
  ("preserve all free text as long as that does not break other
  formatting"). Detection: length > 150 chars AND no citation
  skeleton markers (parenthesized year, URL, vol./no./pp., DOI,
  terminal page or terminal year). Conservative — does not
  false-positive on legitimate first-occurrence notes. v2 only.

src/cmos/formatter.py + note_formatter.py — month/season preservation
  (polish #5)
  Both v1 rule 10 (journal article format) and v2 rule 5 (journal
  article note form) now explicitly instruct the model to preserve
  (Month YEAR) and (Season YEAR) parentheticals when the source
  provides them. Surfaced by entries in both Reading_Disrepair and
  Anti-Communist where (Spring, 1993) and August 1952 were silently
  collapsed to (1993) and 1952. Both forms are valid CMOS but
  month/season is more informative when the source has it. Affects
  v1 and v2 in mirror.

src/cmos/note_formatter.py — government documents rule 19 (polish #7)
  v2 now has an explicit rule mirroring v1's rule 27: government
  bodies, institutional reports, and similar standalone documents
  get italicized titles and book-form treatment, NOT quoted-article
  treatment. Includes the Anti-Communist Senate of California
  Tenth Report and a hypothetical GAO example. Implicit handling
  worked in chunk 2c, but explicit rule provides regression
  protection. v2 only.

Tests: +14 net new (across test_note_formatter.py, test_formatter.py,
test_runtime_validator.py). Total suite 150/150 (was 136 before this
chunk). All v1 tests still passing.

Path B status: formatter.py and runtime_validator.py edits cross
the v1/v2 boundary, but only by user-explicit approval per the
relevant fix discussions. The shared validator was always shared;
the v1 formatter rule 10 mirror is a small additive edit that
doesn't affect the v1 prompt's existing behavior on its existing
inputs.
2026-04-11 20:06:59 -04:00

137 lines
5.4 KiB
Python

"""Tests for src/cmos/formatter.py — the inner-loop iterable artifact.
Because `formatter.py` is edited every iteration of the dev-time autoresearch
loop, these tests intentionally test STABLE pieces: the prompt skeleton, the
caller-injection seam, and a smoke check that the system prompt mentions key
CMOS 18 rules. The actual LLM output shape is tested via the harness (running
all exemplars through `score`) rather than pinned here — exact string
matches on LLM output belong in the canary exact-match rate, not in unit
tests.
No real API calls in this file. Tests that hit the OpenAI API live in
`tests/test_formatter_integration.py` (not yet created) and are gated on an
OPENAI_API_KEY being set.
"""
from cmos.formatter import (
MODEL,
SYSTEM_PROMPT,
build_user_message,
format_bibliography_entry,
)
def test_default_model_is_gpt5():
# The user specified "GPT-5 / frontier reasoning" as the default. Override
# via the OPENAI_MODEL env var if gpt-5 is unavailable in your account.
assert MODEL == "gpt-5"
def test_system_prompt_mentions_cmos_18():
assert "CMOS" in SYSTEM_PROMPT or "Chicago Manual of Style" in SYSTEM_PROMPT
assert "18" in SYSTEM_PROMPT
def test_system_prompt_mentions_no_place_of_publication():
# CMOS 14.30 / 18th ed. change. The formatter MUST know this.
lower = SYSTEM_PROMPT.lower()
assert "place of publication" in lower
def test_system_prompt_mentions_doi_preference():
assert "doi" in SYSTEM_PROMPT.lower()
def test_system_prompt_mentions_italic_markers():
# Plan v1 uses Markdown `*Title*` for italics.
assert "*" in SYSTEM_PROMPT and "italic" in SYSTEM_PROMPT.lower()
def test_system_prompt_mentions_month_or_season_preservation():
"""Polish fix #5: when the source provides month or season information
in a scholarly journal date, the formatter should preserve it in CMOS
form `(Month YEAR)` or `(Season YEAR)` rather than collapsing to
year-only. Surfaced by Reading_Disrepair (v1) and Anti-Communist (v2)
real-draft runs where dates like `(Spring, 1993)` and `August 1952`
were silently collapsed to `(1993)` and `1952`. Both forms are valid
CMOS but month/season is more informative when the source has it.
"""
lower = SYSTEM_PROMPT.lower()
assert "month" in lower
assert "season" in lower
# And some indication of preservation, not collapse.
assert "preserve" in lower
def test_build_user_message_contains_the_messy_input():
msg = build_user_message("yu, charles. interior chinatown. 2020")
assert "yu, charles. interior chinatown. 2020" in msg
def test_formatter_uses_injected_caller():
"""The formatter accepts a caller shim so tests (and the harness) can
substitute a fake OpenAI call. This is how the whole pipeline can run
without an API key during tests."""
recorded: dict = {}
def fake_caller(system: str, user: str) -> str:
recorded["system"] = system
recorded["user"] = user
return "Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020."
output = format_bibliography_entry(
"yu, charles. interior chinatown. New York: Pantheon Books, 2020.",
caller=fake_caller,
)
assert output == "Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020."
assert recorded["system"] == SYSTEM_PROMPT
assert "yu, charles" in recorded["user"]
def test_formatter_strips_whitespace_from_caller_output():
# Language-model output often has leading/trailing whitespace or
# surrounding code fences. v0 only strips whitespace; code-fence
# stripping can be added when an exemplar forces it.
def fake_caller(system: str, user: str) -> str:
return " Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020. \n"
output = format_bibliography_entry("anything", caller=fake_caller)
assert output == "Yu, Charles. *Interior Chinatown*. Pantheon Books, 2020."
def test_formatter_retries_on_validator_failure():
"""If the runtime validator rejects the first attempt (e.g., italics
missing), the formatter should retry and return a passing later attempt.
Mirrors the iter 7 magazine_mead variance: GPT-5 sometimes drops italics
around a magazine name, and a re-call typically succeeds."""
attempts = []
def flaky_caller(system: str, user: str) -> str:
attempts.append(len(attempts) + 1)
if len(attempts) == 1:
# First attempt: italics dropped (validator should fail).
return 'Mead, Rebecca. "Terms of Aggrievement." New Yorker, December 18, 2023.'
# Retry: clean.
return 'Mead, Rebecca. "Terms of Aggrievement." *New Yorker*, December 18, 2023.'
output = format_bibliography_entry("messy mead", caller=flaky_caller)
assert "*New Yorker*" in output
assert len(attempts) == 2
def test_formatter_returns_last_attempt_after_max_retries():
"""If every attempt fails validation, return the last attempt and don't
loop forever. We surface the failure later via scoring/canary, not by
raising at runtime — the user should still get *something* back."""
attempts = []
def always_bad(system: str, user: str) -> str:
attempts.append(1)
return 'Mead, Rebecca. "Terms of Aggrievement." New Yorker, December 18, 2023.'
output = format_bibliography_entry("messy", caller=always_bad, max_retries=2)
# 1 initial + 2 retries = 3 calls total.
assert len(attempts) == 3
assert "*" not in output # the bad output is what we got