Files
cmos/tests/test_note_formatter.py
T
Mark Eaton d061f78b7f v2 chunk 2a iter 1: rules 6+18 — ed. invariant, expand publishers
Two prompt edits to SYSTEM_PROMPT_NOTES driven by chunk 1 smoke test
gaps on chapter_first_doyle.toml.

Rule 6 (chapter form) expanded with an explicit invariance statement:
"ed." is the canonical abbreviation regardless of editor count — do
NOT pluralize to "eds." for multiple editors. CMOS NB treats it as
an invariant abbreviation, not a number-agreeing word.

New rule 18 (publisher expansion) mirrors v1 formatter.py rule 14:
publisher names must be in full canonical form, with note-form-
specific examples ("U of Chicago Press" → "University of Chicago
Press"). Includes the same MIT Press / ALA Editions / MLA carve-out
for publishers whose canonical self-presentation legitimately uses
initials.

Two new prompt-content unit tests added to tests/test_note_formatter.py
following the v1 test_formatter.py discipline. TDD cycle: red-green-
verified end-to-end.

Real-API smoke test, 3 v2 exemplars × 2 runs each: 6/6 byte-perfect
matches (was 4/6 in chunk 1; chapter_first_doyle went 0/2 → 2/2,
book and journal still 2/2). v1 untouched, 96/96 v1 tests still
passing. Total suite: 116/116.
2026-04-11 18:27:55 -04:00

150 lines
5.7 KiB
Python

"""Tests for src/cmos/note_formatter.py — the v2 inner-loop iterable artifact.
Mirrors tests/test_formatter.py in shape and discipline. The note formatter
is iterated independently of the bibliography formatter (Path B); these
tests pin the stable seams (caller injection, retry loop, prompt smoke
checks) and leave LLM output shape testing to the v2 exemplar canary.
No real API calls in this file. Tests use injected fake callers.
"""
from cmos.note_formatter import (
MODEL,
SYSTEM_PROMPT_NOTES,
build_user_message,
format_note_entry,
)
def test_default_model_is_gpt5():
# Same default as v1. Override with OPENAI_MODEL=... in .env.
assert MODEL == "gpt-5"
def test_system_prompt_mentions_cmos_18_note_form():
assert "CMOS" in SYSTEM_PROMPT_NOTES
assert "18" in SYSTEM_PROMPT_NOTES
assert "NOTE" in SYSTEM_PROMPT_NOTES
def test_system_prompt_mentions_no_inversion():
# The single most distinctive difference from bibliography form: notes
# use normal-order author names ("First Last"), not inverted.
lower = SYSTEM_PROMPT_NOTES.lower()
assert "normal" in lower or "not inverted" in lower
def test_system_prompt_mentions_comma_separation():
# The second most distinctive difference: commas between elements,
# not periods.
lower = SYSTEM_PROMPT_NOTES.lower()
assert "comma" in lower
def test_system_prompt_excludes_leading_number():
# The note number prefix ("1. ", "2. ") is supplied externally; the
# formatter must not include it. The prompt must say so.
lower = SYSTEM_PROMPT_NOTES.lower()
assert "leading number" in lower or "do not include" in lower
def test_system_prompt_mentions_no_ibid():
# CMOS 18 deprecates Ibid., same as v1.
assert "Ibid" in SYSTEM_PROMPT_NOTES or "ibid" in SYSTEM_PROMPT_NOTES.lower()
def test_system_prompt_mentions_publisher_full_form():
"""Surfaced by chunk 1 smoke test on chapter_first_doyle: the formatter
preserved 'U of Chicago Press' from the messy input instead of
expanding it to 'University of Chicago Press'. v1 formatter.py rule 14
covers this; the v2 prompt must too. The rule is "publisher names must
be in full canonical form, not abbreviated."
"""
lower = SYSTEM_PROMPT_NOTES.lower()
assert "publisher" in lower
# Either "do not abbreviate" or "full canonical" or "full form" — any
# phrasing that conveys the prohibition.
assert (
"do not abbreviate" in lower
or "not abbreviated" in lower
or "full canonical" in lower
or "full form" in lower
)
def test_system_prompt_mentions_ed_invariant_for_multiple_editors():
"""Surfaced by chunk 1 smoke test on chapter_first_doyle: the formatter
pluralized 'ed.' to 'eds.' when there were two editors. CMOS NB form
uses 'ed.' invariantly regardless of editor count. The prompt must
explicitly state this so the model does not grammatically pluralize
by default.
"""
# Search for an explicit mention of "eds." being wrong, OR an explicit
# statement that "ed." is invariant / not pluralized.
lower = SYSTEM_PROMPT_NOTES.lower()
assert "eds." in lower or "invariant" in lower or "do not pluralize" in lower or "not pluralize" in lower
def test_build_user_message_contains_the_messy_input():
msg = build_user_message("yu, charles. interior chinatown. 2020, 45")
assert "yu, charles. interior chinatown. 2020, 45" in msg
def test_note_formatter_uses_injected_caller():
"""Same caller-injection seam as v1. Tests can substitute a fake without
touching the network."""
recorded: dict = {}
def fake_caller(system: str, user: str) -> str:
recorded["system"] = system
recorded["user"] = user
return "Charles Yu, *Interior Chinatown* (Pantheon Books, 2020), 45."
output = format_note_entry(
"yu, charles. interior chinatown. New York: Pantheon Books, 2020, p 45.",
caller=fake_caller,
)
assert output == "Charles Yu, *Interior Chinatown* (Pantheon Books, 2020), 45."
assert recorded["system"] == SYSTEM_PROMPT_NOTES
assert "yu, charles" in recorded["user"]
def test_note_formatter_strips_whitespace_from_caller_output():
def fake_caller(system: str, user: str) -> str:
return " Charles Yu, *Interior Chinatown* (Pantheon Books, 2020), 45. \n"
output = format_note_entry("anything", caller=fake_caller)
assert output == "Charles Yu, *Interior Chinatown* (Pantheon Books, 2020), 45."
def test_note_formatter_retries_on_validator_failure():
"""If the runtime validator rejects the first attempt (e.g., italics
dropped), the formatter retries. Same retry semantics as v1."""
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 "Charles Yu, Interior Chinatown (Pantheon Books, 2020), 45."
return "Charles Yu, *Interior Chinatown* (Pantheon Books, 2020), 45."
output = format_note_entry("messy yu", caller=flaky_caller)
assert "*Interior Chinatown*" in output
assert len(attempts) == 2
def test_note_formatter_returns_last_attempt_after_max_retries():
"""If every attempt fails validation, return the last attempt and don't
loop forever. Same as v1 — the failure surfaces via scoring or human
review, not via a runtime exception."""
attempts = []
def always_bad(system: str, user: str) -> str:
attempts.append(1)
return "Charles Yu, Interior Chinatown (Pantheon Books, 2020), 45."
output = format_note_entry("messy", caller=always_bad, max_retries=2)
assert len(attempts) == 3 # 1 initial + 2 retries
assert "*" not in output