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.
150 lines
5.7 KiB
Python
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
|