core/tests/test_surface.py

272 lines
10 KiB
Python

"""tests/test_surface.py — Unit tests for generate.surface.SentenceAssembler.
Covers:
- All four DialogueRole templates in English (assert, elaborate, question, refute)
- Empty-slot guard (both subject and predicate empty)
- Elaboration token weaving (elaborate role with walk tokens)
- Stop-surface filtering (function words not woven into elaboration)
- Deduplication of elaboration tokens
- Hebrew VSO word order
- Ancient Greek SOV word order
- SentencePlan dataclass fields populated correctly
- Module-level assemble() convenience function
- Determinism: identical inputs produce identical outputs
"""
from __future__ import annotations
import pytest
from generate.articulation import ArticulationPlan
from generate.surface import SentenceAssembler, SentencePlan, assemble
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _plan(
subject: str = "truth",
predicate: str = "speaks",
object_: str | None = "light",
lang: str = "en",
frame_id: str = "test-frame",
) -> ArticulationPlan:
surface = " ".join(p for p in [subject, predicate, object_] if p)
return ArticulationPlan(
subject=subject,
predicate=predicate,
object=object_,
surface=surface,
output_language=lang,
frame_id=frame_id,
)
ASSEMBLER = SentenceAssembler()
# ---------------------------------------------------------------------------
# English SVO — four roles
# ---------------------------------------------------------------------------
class TestEnglishRoles:
def test_assert_with_object(self):
plan = _plan("truth", "speaks", "light")
sp = ASSEMBLER.assemble(plan, [], role="assert")
assert sp.surface == "Truth speaks light."
assert sp.dialogue_role == "assert"
assert sp.elaboration is None
def test_assert_no_object(self):
plan = _plan("truth", "speaks", None)
sp = ASSEMBLER.assemble(plan, [], role="assert")
assert sp.surface == "Truth speaks."
def test_elaborate_no_walk_tokens(self):
plan = _plan("truth", "speaks", "light")
sp = ASSEMBLER.assemble(plan, [], role="elaborate")
# No walk tokens — falls back to plain assert-style with period.
assert sp.surface == "Truth speaks light."
assert sp.elaboration is None
def test_elaborate_with_walk_tokens(self):
plan = _plan("truth", "speaks", "light")
tokens = ["covenant", "wisdom", "glory", "word"]
sp = ASSEMBLER.assemble(plan, tokens, role="elaborate")
assert "" in sp.surface
assert sp.surface.endswith(".")
assert sp.elaboration is not None
assert "covenant" in sp.elaboration
def test_question_with_object(self):
plan = _plan("truth", "speak", "light")
sp = ASSEMBLER.assemble(plan, [], role="question")
assert sp.surface == "Does truth speak light?"
assert sp.surface.endswith("?")
def test_question_no_object(self):
plan = _plan("truth", "speak", None)
sp = ASSEMBLER.assemble(plan, [], role="question")
assert sp.surface == "Does truth speak?"
def test_refute_with_object(self):
plan = _plan("truth", "deny", "light")
sp = ASSEMBLER.assemble(plan, [], role="refute")
assert sp.surface == "Truth does not deny light."
def test_refute_no_object(self):
plan = _plan("truth", "deny", None)
sp = ASSEMBLER.assemble(plan, [], role="refute")
assert sp.surface == "Truth does not deny."
# ---------------------------------------------------------------------------
# Empty-slot guard
# ---------------------------------------------------------------------------
class TestEmptySlotGuard:
def test_both_empty_falls_back_to_plan_surface(self):
plan = _plan("", "", None)
# Override surface on the plan to a non-trivial fallback.
plan_with_surface = ArticulationPlan(
subject="",
predicate="",
object=None,
surface="covenant light",
output_language="en",
frame_id="test",
)
sp = ASSEMBLER.assemble(plan_with_surface, [], role="assert")
assert sp.surface == "covenant light"
def test_both_empty_falls_back_to_tokens(self):
plan = ArticulationPlan(
subject="",
predicate="",
object=None,
surface="",
output_language="en",
frame_id="test",
)
sp = ASSEMBLER.assemble(plan, ["covenant", "light"], role="assert")
assert "covenant" in sp.surface or "light" in sp.surface
def test_subject_only_empty_still_assembles(self):
"""Only the both-empty case triggers the guard."""
plan = _plan("", "speaks", "light")
sp = ASSEMBLER.assemble(plan, [], role="assert")
# Subject is empty string — capitalised empty + predicate + object.
assert sp.surface.endswith(".")
assert "speaks" in sp.surface
# ---------------------------------------------------------------------------
# Elaboration filtering
# ---------------------------------------------------------------------------
class TestElaborationFiltering:
def test_stop_surfaces_excluded(self):
plan = _plan("truth", "speaks", "light")
# All tokens are stop surfaces — elaboration should be None.
tokens = ["the", "a", "is", "are", "and", "or", "to"]
sp = ASSEMBLER.assemble(plan, tokens, role="elaborate")
assert sp.elaboration is None
def test_slot_words_excluded_from_elaboration(self):
plan = _plan("truth", "speaks", "light")
# Slot words themselves should not appear in elaboration.
tokens = ["truth", "speaks", "light", "covenant"]
sp = ASSEMBLER.assemble(plan, tokens, role="elaborate")
if sp.elaboration:
assert "truth" not in sp.elaboration
assert "speaks" not in sp.elaboration
assert "light" not in sp.elaboration
assert "covenant" in sp.elaboration
def test_deduplication_in_elaboration(self):
plan = _plan("truth", "speaks", "light")
# Repeated token should appear only once.
tokens = ["covenant", "covenant", "covenant", "wisdom"]
sp = ASSEMBLER.assemble(plan, tokens, role="elaborate")
if sp.elaboration:
elab_list = sp.elaboration.split(", ")
assert len(set(elab_list)) == len(elab_list)
def test_max_elaboration_tokens_capped(self):
plan = _plan("truth", "speaks", "light")
# Feed more unique tokens than _MAX_ELAB_TOKENS (4).
tokens = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta"]
sp = ASSEMBLER.assemble(plan, tokens, role="elaborate")
if sp.elaboration:
elab_list = sp.elaboration.split(", ")
assert len(elab_list) <= 4
# ---------------------------------------------------------------------------
# Language routing
# ---------------------------------------------------------------------------
class TestLanguageRouting:
def test_hebrew_vso_assert(self):
plan = _plan("emet", "medaber", "or", lang="he")
sp = ASSEMBLER.assemble(plan, [], role="assert")
# VSO: predicate first, then subject, then object.
parts = sp.surface.rstrip(".").split()
assert parts[0] == "medaber"
assert parts[1] == "emet"
assert parts[2] == "or"
def test_hebrew_question_prefix(self):
plan = _plan("emet", "medaber", "or", lang="he")
sp = ASSEMBLER.assemble(plan, [], role="question")
# Hebrew question: האם prefix.
assert sp.surface.startswith("\u05d4\u05d0\u05dd")
assert sp.surface.endswith("?")
def test_greek_sov_assert(self):
plan = _plan("logos", "esti", "phos", lang="grc")
sp = ASSEMBLER.assemble(plan, [], role="assert")
# SOV: subject, object, verb.
parts = sp.surface.rstrip(".").split()
assert parts[0] == "Logos"
assert parts[1] == "phos"
assert parts[2] == "esti"
def test_greek_question_semicolon(self):
plan = _plan("logos", "esti", "phos", lang="grc")
sp = ASSEMBLER.assemble(plan, [], role="question")
assert sp.surface.endswith(";")
def test_unknown_language_falls_back_to_english(self):
plan = _plan("truth", "speaks", "light", lang="fr")
sp = ASSEMBLER.assemble(plan, [], role="assert")
assert sp.surface == "Truth speaks light."
# ---------------------------------------------------------------------------
# SentencePlan field invariants
# ---------------------------------------------------------------------------
class TestSentencePlanFields:
def test_all_fields_populated(self):
plan = _plan("truth", "speaks", "light")
sp = ASSEMBLER.assemble(plan, ["covenant"], role="elaborate")
assert isinstance(sp, SentencePlan)
assert sp.subject == "truth"
assert sp.predicate_phrase == "speaks"
assert sp.object_phrase == "light"
assert sp.output_language == "en"
assert sp.dialogue_role == "elaborate"
assert sp.surface # non-empty
def test_surface_is_string(self):
plan = _plan()
sp = ASSEMBLER.assemble(plan, [], role="assert")
assert isinstance(sp.surface, str)
# ---------------------------------------------------------------------------
# Module-level assemble() wrapper
# ---------------------------------------------------------------------------
class TestAssembleWrapper:
def test_returns_string(self):
plan = _plan()
result = assemble(plan, [], role="assert")
assert isinstance(result, str)
assert result == "Truth speaks light."
# ---------------------------------------------------------------------------
# Determinism guarantee
# ---------------------------------------------------------------------------
class TestDeterminism:
def test_identical_inputs_identical_outputs(self):
plan = _plan("truth", "speaks", "light")
tokens = ["covenant", "wisdom", "glory"]
results = [
ASSEMBLER.assemble(plan, tokens, role="elaborate").surface
for _ in range(10)
]
assert len(set(results)) == 1, "Non-deterministic output detected"