| """ |
| 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, |
| ) |
|
|
|
|
| |
|
|
| 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" |
|
|
|
|
| |
|
|
| 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, |
| ) |
|
|
| |
| n1 = insert_word_neuron(db, enc, "shakespeare", confidence=0.7) |
|
|
| conv = fake_convergence([n1]) |
| result = gen.generate(conv) |
|
|
| |
| assert result.text |
| 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 |
|
|
|
|
| |
|
|
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
| |
|
|
| conv = fake_convergence([n1]) |
| result = gen.generate(conv) |
|
|
| assert result.strategy == "concept_list" |
|
|
|
|
| |
|
|
| 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] |
|
|
|
|
| |
|
|
| 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 |
|
|
|
|
| |
|
|
| 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"} |
|
|
|
|
| |
|
|
| 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"]) |
|
|