guru / tests /test_generator.py
tejadabheja's picture
Upload folder using huggingface_hub
a5ae1ac verified
"""
Tests for the Generator.
Verifies against HLD spec:
- Strategy A (Template): concepts → closest template → fill slots → text
- Strategy B (Successor walk): walk successor lists, emit tokens
- Strategy C (Concept list): raw concepts as fallback (always works)
- Non-convergence → "I don't know" (invariant #4)
- Each strategy has an explanation trace (invariant #2)
- Template delete = gone (invariant #3)
- Two-speed successor walk (grammar > 0.8 = fast)
"""
import sys
from pathlib import Path
import numpy as np
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
from neuron import NeuronDB
from encoder import Encoder
from convergence import ConvergenceResult, Hop
from generator import Generator, Template, TemplateStore
DIM = 300
def make_vocab():
"""Deterministic test vocabulary."""
rng = np.random.RandomState(42)
words = {}
for w in ["shakespeare", "hamlet", "wrote", "playwright", "english",
"cat", "dog", "sat", "mat", "the", "on", "a",
"who", "is", "was", "in", "1600"]:
words[w] = rng.randn(DIM).astype(np.float32)
return words
def make_encoder():
enc = Encoder(data_dir="/tmp/test_gen", dim=DIM)
enc.load_from_dict(make_vocab())
return enc
def make_db():
return NeuronDB(dim=DIM)
def make_components():
"""Create db, encoder, template_store, generator."""
db = make_db()
enc = make_encoder()
ts = TemplateStore(enc)
gen = Generator(db, enc, ts)
return db, enc, ts, gen
def insert_word_neuron(db, encoder, word, confidence=0.5):
"""Insert a neuron for a known word."""
vec = encoder.encode_word(word)
return db.insert(vec, confidence=confidence)
def fake_convergence(concepts, converged=True, confidence=0.7):
"""Create a ConvergenceResult without running the loop."""
vec = np.mean([c.vector for c in concepts], axis=0) if concepts else np.zeros(DIM)
norm = np.linalg.norm(vec)
if norm > 0:
vec = vec / norm
return ConvergenceResult(
converged=converged,
vector=vec,
concepts=concepts,
confidence=confidence,
)
# --- Abstention tests ---
class TestGeneratorAbstention:
def test_non_convergence_says_i_dont_know(self):
"""Invariant #4: honest about failure."""
db, enc, ts, gen = make_components()
result = gen.generate(ConvergenceResult(
converged=False, vector=np.zeros(DIM),
concepts=[], confidence=0.0,
))
assert result.text == "I don't know."
assert result.strategy == "abstain"
assert result.confidence == 0.0
def test_no_concepts_says_i_dont_know(self):
db, enc, ts, gen = make_components()
result = gen.generate(ConvergenceResult(
converged=True, vector=np.zeros(DIM),
concepts=[], confidence=0.5,
))
assert result.text == "I don't know."
assert result.strategy == "abstain"
# --- Template tests ---
class TestGeneratorTemplate:
def test_template_fills_all_slots(self):
db, enc, ts, gen = make_components()
ts.add(
pattern="[PERSON] wrote [WORK]",
slots={"PERSON": "noun", "WORK": "noun"},
confidence=0.8,
)
n1 = insert_word_neuron(db, enc, "shakespeare", confidence=0.7)
n2 = insert_word_neuron(db, enc, "hamlet", confidence=0.6)
conv = fake_convergence([n1, n2])
result = gen.generate(conv)
assert result.strategy == "template"
assert "shakespeare" in result.text.lower() or "hamlet" in result.text.lower()
assert result.template_used is not None
assert result.confidence > 0
def test_template_partial_fill(self):
"""Some slots filled, others show '...'."""
db, enc, ts, gen = make_components()
ts.add(
pattern="[PERSON] wrote [WORK] in [YEAR]",
slots={"PERSON": "noun", "WORK": "noun", "YEAR": "number"},
confidence=0.8,
)
# Only provide one concept — not enough for all slots
n1 = insert_word_neuron(db, enc, "shakespeare", confidence=0.7)
conv = fake_convergence([n1])
result = gen.generate(conv)
# Should either be template (partial) or concept_list
assert result.text # something is returned
assert result.strategy in ("template", "concept_list")
def test_no_templates_falls_through(self):
"""No templates → should fall to successor or concept_list."""
db, enc, ts, gen = make_components()
n1 = insert_word_neuron(db, enc, "cat", confidence=0.5)
conv = fake_convergence([n1])
result = gen.generate(conv)
assert result.strategy in ("successor", "concept_list")
def test_template_has_explanation(self):
"""Invariant #2: every answer has a source."""
db, enc, ts, gen = make_components()
ts.add(
pattern="[SUBJECT] is [ATTRIBUTE]",
slots={"SUBJECT": "noun", "ATTRIBUTE": "noun"},
confidence=0.7,
)
n1 = insert_word_neuron(db, enc, "cat", confidence=0.5)
n2 = insert_word_neuron(db, enc, "english", confidence=0.5)
conv = fake_convergence([n1, n2])
result = gen.generate(conv)
explanation = result.explain()
assert "Strategy:" in explanation
assert len(result.trace) > 0
def test_template_delete_removes_it(self):
"""Invariant #3: delete = gone."""
db, enc, ts, gen = make_components()
t = ts.add(
pattern="[X] wrote [Y]",
slots={"X": "noun", "Y": "noun"},
confidence=0.8,
)
assert ts.count() == 1
ts.delete(t.id)
assert ts.count() == 0
# --- Successor walk tests ---
class TestGeneratorSuccessor:
def test_successor_walk_produces_text(self):
db, enc, ts, gen = make_components()
n_the = insert_word_neuron(db, enc, "the", confidence=0.6)
n_cat = insert_word_neuron(db, enc, "cat", confidence=0.7)
n_sat = insert_word_neuron(db, enc, "sat", confidence=0.6)
n_on = insert_word_neuron(db, enc, "on", confidence=0.5)
n_mat = insert_word_neuron(db, enc, "mat", confidence=0.5)
# Build successor chain: the → cat → sat → on → mat
db.update_successors(n_the.id, n_cat.id, 0.9)
db.update_successors(n_cat.id, n_sat.id, 0.85)
db.update_successors(n_sat.id, n_on.id, 0.8)
db.update_successors(n_on.id, n_mat.id, 0.75)
conv = fake_convergence([n_the])
result = gen.generate(conv)
assert result.strategy == "successor"
words = result.text.split()
assert len(words) >= 2
assert result.confidence > 0
def test_successor_walk_avoids_loops(self):
"""Should not revisit neurons."""
db, enc, ts, gen = make_components()
n1 = insert_word_neuron(db, enc, "cat", confidence=0.7)
n2 = insert_word_neuron(db, enc, "dog", confidence=0.6)
# Create a cycle: cat → dog → cat
db.update_successors(n1.id, n2.id, 0.9)
db.update_successors(n2.id, n1.id, 0.9)
conv = fake_convergence([n1])
result = gen.generate(conv)
# Should stop at 2 tokens, not loop forever
words = result.text.split()
assert len(words) <= 3
def test_successor_walk_has_trace(self):
"""Each step in the walk should be traced."""
db, enc, ts, gen = make_components()
n1 = insert_word_neuron(db, enc, "the", confidence=0.6)
n2 = insert_word_neuron(db, enc, "cat", confidence=0.7)
db.update_successors(n1.id, n2.id, 0.9)
conv = fake_convergence([n1])
result = gen.generate(conv)
if result.strategy == "successor":
assert len(result.trace) > 0
assert any("Step" in t or "Start" in t for t in result.trace)
def test_no_successors_falls_to_concept_list(self):
"""Neurons without successors → can't walk → concept_list."""
db, enc, ts, gen = make_components()
n1 = insert_word_neuron(db, enc, "hamlet", confidence=0.7)
# No successors set
conv = fake_convergence([n1])
result = gen.generate(conv)
assert result.strategy == "concept_list"
# --- Concept list tests ---
class TestGeneratorConceptList:
def test_concept_list_returns_words(self):
db, enc, ts, gen = make_components()
n1 = insert_word_neuron(db, enc, "shakespeare", confidence=0.7)
n2 = insert_word_neuron(db, enc, "hamlet", confidence=0.6)
conv = fake_convergence([n1, n2])
result = gen.generate(conv)
assert result.strategy == "concept_list"
assert "shakespeare" in result.text.lower() or "hamlet" in result.text.lower()
def test_concept_list_always_works(self):
"""Concept list is the guaranteed fallback."""
db, enc, ts, gen = make_components()
n = insert_word_neuron(db, enc, "cat", confidence=0.3)
conv = fake_convergence([n], confidence=0.3)
result = gen.generate(conv)
assert result.text != ""
assert result.text != "I don't know."
assert result.strategy == "concept_list"
def test_concept_list_has_trace(self):
db, enc, ts, gen = make_components()
n = insert_word_neuron(db, enc, "dog", confidence=0.5)
conv = fake_convergence([n])
result = gen.generate(conv)
assert len(result.trace) > 0
assert "Concept list fallback" in result.trace[0]
# --- Template store tests ---
class TestTemplateStore:
def test_add_and_search(self):
enc = make_encoder()
ts = TemplateStore(enc)
ts.add("[PERSON] wrote [WORK]", {"PERSON": "noun", "WORK": "noun"})
ts.add("[ANIMAL] sat on [SURFACE]", {"ANIMAL": "noun", "SURFACE": "noun"})
assert ts.count() == 2
query = enc.encode_sentence("who wrote hamlet")
results = ts.search(query, k=1)
assert len(results) == 1
def test_search_returns_closest(self):
enc = make_encoder()
ts = TemplateStore(enc)
t_write = ts.add("[PERSON] wrote [WORK]",
{"PERSON": "noun", "WORK": "noun"}, confidence=0.8)
t_sat = ts.add("[ANIMAL] sat on [SURFACE]",
{"ANIMAL": "noun", "SURFACE": "noun"}, confidence=0.8)
query = enc.encode_sentence("wrote hamlet")
results = ts.search(query, k=1)
assert results[0].id == t_write.id
def test_empty_store_returns_nothing(self):
enc = make_encoder()
ts = TemplateStore(enc)
results = ts.search(np.zeros(DIM, dtype=np.float32))
assert results == []
def test_delete_template(self):
enc = make_encoder()
ts = TemplateStore(enc)
t = ts.add("[X] is [Y]", {"X": "noun", "Y": "noun"})
assert ts.count() == 1
assert ts.delete(t.id) is True
assert ts.count() == 0
def test_delete_nonexistent(self):
enc = make_encoder()
ts = TemplateStore(enc)
assert ts.delete(999) is False
# --- Template dataclass tests ---
class TestTemplate:
def test_fill_all_slots(self):
t = Template(
id=0,
pattern="[PERSON] wrote [WORK] in [YEAR]",
slots={"PERSON": "noun", "WORK": "noun", "YEAR": "number"},
vector=np.zeros(DIM),
)
result = t.fill({"PERSON": "Shakespeare", "WORK": "Hamlet", "YEAR": "1600"})
assert result == "Shakespeare wrote Hamlet in 1600"
def test_fill_partial(self):
t = Template(
id=0,
pattern="[PERSON] wrote [WORK]",
slots={"PERSON": "noun", "WORK": "noun"},
vector=np.zeros(DIM),
)
result = t.fill({"PERSON": "Shakespeare"})
assert result == "Shakespeare wrote [WORK]"
def test_unfilled_slots(self):
t = Template(
id=0,
pattern="[A] [B] [C]",
slots={"A": "noun", "B": "verb", "C": "noun"},
vector=np.zeros(DIM),
)
assert t.unfilled_slots({"A": "x"}) == ["B", "C"]
assert t.unfilled_slots({"A": "x", "B": "y", "C": "z"}) == []
def test_slot_names(self):
t = Template(
id=0,
pattern="[X] [Y]",
slots={"X": "noun", "Y": "verb"},
vector=np.zeros(DIM),
)
assert set(t.slot_names) == {"X", "Y"}
# --- Generation result tests ---
class TestGenerationResult:
def test_explain_contains_strategy(self):
from generator import GenerationResult
r = GenerationResult(
text="hello",
strategy="template",
confidence=0.8,
trace=["matched template X"],
)
exp = r.explain()
assert "template" in exp
assert "0.8" in exp
def test_explain_with_template(self):
from generator import GenerationResult
t = Template(
id=0,
pattern="[X] wrote [Y]",
slots={"X": "noun", "Y": "noun"},
vector=np.zeros(DIM),
)
r = GenerationResult(
text="Shakespeare wrote Hamlet",
strategy="template",
confidence=0.7,
template_used=t,
slot_fills={"X": "Shakespeare", "Y": "Hamlet"},
)
exp = r.explain()
assert "[X] wrote [Y]" in exp
assert "Shakespeare" in exp
if __name__ == "__main__":
import pytest
pytest.main([__file__, "-v"])