101 lines
4.8 KiB
Python
101 lines
4.8 KiB
Python
"""A character's species comes only from its OWN species assertion.
|
|
|
|
Regression for the dwarf-trait leak found on the real library (2026-10-03):
|
|
extraction notes in ``race_species`` that merely mention another character's
|
|
species ("Mensch (oder Zwerg, je nach Kontext ...)", "Mensch (... 'Der Zwerg'
|
|
wird separat erwähnt ...)") switched on the dwarf body lock (short legs, thick
|
|
full beard) for human characters, and the note's text about the other
|
|
character reached the Subject clause.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
|
|
os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-species-test-config-"))
|
|
|
|
import routes.conversation as conv
|
|
|
|
# Shapes copied from real records (book 02): a thin human boy and a human whose
|
|
# species note reasons about a separate dwarf.
|
|
HUMAN_BOY_HEDGED = {
|
|
"race_species": "Mensch (oder Zwerg, je nach Kontext 'Amboßzwerg', hier als junger Krieger beschrieben)",
|
|
"gender": "male",
|
|
"physical": "Klein, dünn, schwach; hat dünne Arme.",
|
|
"clothing": "Ein roter Umhang aus Wolle.",
|
|
}
|
|
HUMAN_WITH_DWARF_NOTE = {
|
|
"race_species": (
|
|
"Mensch (oder Ork-Verbündeter, Kontext unklar, aber 'Der Zwerg' wird separat erwähnt. "
|
|
"Warte: 'Der Zwerg lächelte grimmig ... Es wäre allein sein Verdienst ...'. "
|
|
"Der Text identifiziert den Zwerg als den, der den Erfolg für sich reklamiert.)"
|
|
),
|
|
"gender": "male",
|
|
"physical": "Schlanker Mann mit kurzem Haar.",
|
|
"clothing": "Ein grauer Wollmantel.",
|
|
}
|
|
DWARF = {
|
|
"race_species": "Zwerg (Amboßzwerg)",
|
|
"gender": "male",
|
|
"physical": "Breit gebaut, mit langem rotem Bart.",
|
|
"clothing": "Ein Kettenhemd über einer Wolltunika.",
|
|
}
|
|
# A human may have a beard of its own (face profile); what must not leak is the dwarf lock.
|
|
DWARF_TERMS = ("dwarf", "zwerg", "thick, full beard", "short strong legs")
|
|
|
|
|
|
def _render(name: str, sheet: dict) -> list[str]:
|
|
sheet = conv._character_form_sheet(sheet)
|
|
portrait = conv._build_character_portrait_prompt(name, "Book", sheet, {}, "cinematic")
|
|
turnaround = conv._build_character_sheet_prompt(name, "Book", sheet, {}, "cinematic")
|
|
out = conv._build_compact_character_render_prompts(name, "Book", sheet, {}, "cinematic", portrait, turnaround)
|
|
return [*out[:5], *out[5]]
|
|
|
|
|
|
class SpeciesAssertionTests(unittest.TestCase):
|
|
def test_hedged_mention_of_dwarf_is_not_a_dwarf(self):
|
|
self.assertFalse(conv._character_is_dwarf(HUMAN_BOY_HEDGED))
|
|
self.assertFalse(conv._character_is_dwarf(HUMAN_WITH_DWARF_NOTE))
|
|
self.assertNotEqual(conv._age_body_lock(HUMAN_BOY_HEDGED), conv._DWARF_BODY_LOCK)
|
|
|
|
def test_real_dwarf_still_gets_dwarf_lock(self):
|
|
self.assertTrue(conv._character_is_dwarf(DWARF))
|
|
self.assertTrue(conv._character_is_dwarf({"race_species": "Erzzwerg"}))
|
|
self.assertTrue(conv._character_is_dwarf({"race_species": "Mensch", "visual_species": "Zwerg"}))
|
|
self.assertEqual(conv._age_body_lock(DWARF), conv._DWARF_BODY_LOCK)
|
|
|
|
def test_alternatives_are_not_asserted(self):
|
|
self.assertFalse(conv._character_is_dwarf({"race_species": "Mensch oder Zwerg"}))
|
|
self.assertFalse(conv._character_is_dwarf({"race_species": "Mensch (oder Zwerg)"}))
|
|
|
|
def test_species_reasoning_about_others_is_dropped(self):
|
|
self.assertEqual(conv._character_species_text(HUMAN_WITH_DWARF_NOTE), "Mensch")
|
|
self.assertEqual(conv._character_species_text(HUMAN_BOY_HEDGED), "Mensch")
|
|
# short, factual notes survive unchanged
|
|
self.assertEqual(conv._character_species_text({"race_species": "Mensch (Thorwaler)"}), "Mensch (Thorwaler)")
|
|
self.assertEqual(conv._character_species_text({"race_species": "Zwerg (Amboßzwerg)"}), "Zwerg (Amboßzwerg)")
|
|
facts = conv._character_visual_facts("Arthag", HUMAN_WITH_DWARF_NOTE)
|
|
self.assertNotIn("Zwerg", facts)
|
|
self.assertIn("species: Mensch", facts)
|
|
|
|
|
|
class ConsecutiveCharacterPromptTests(unittest.TestCase):
|
|
"""A dwarf and a human built back to back: no dwarf trait in the human's prompts."""
|
|
|
|
def test_no_dwarf_traits_in_human_prompts(self):
|
|
for order in ((("Garbaz", DWARF), ("Marrad", HUMAN_BOY_HEDGED), ("Arthag", HUMAN_WITH_DWARF_NOTE)),
|
|
(("Marrad", HUMAN_BOY_HEDGED), ("Garbaz", DWARF), ("Arthag", HUMAN_WITH_DWARF_NOTE))):
|
|
prompts = {name: _render(name, sheet) for name, sheet in order}
|
|
for name in ("Marrad", "Arthag"):
|
|
text = " ".join(prompts[name]).lower()
|
|
for term in DWARF_TERMS:
|
|
self.assertNotIn(term, text, f"{term!r} leaked into {name}")
|
|
dwarf_text = " ".join(prompts["Garbaz"]).lower()
|
|
self.assertIn("adult dwarf", dwarf_text)
|
|
self.assertIn("beard", dwarf_text) # the dwarf's own beard now comes from its face clause
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|