Implement the core scripture module for local-first ESV text storage, verse retrieval, reference parsing, original language support, cross-referencing, topical mapping, and automated meditation workflows. Architecture: - scripture/constants.py: 66-book Protestant canon with aliases and metadata - scripture/models.py: Pydantic models with integer-encoded verse IDs - scripture/parser.py: Regex-based reference extraction and formatting - scripture/store.py: SQLite-backed verse/xref/topic/Strong's storage - scripture/memory.py: Tripartite memory (working/long-term/associative) - scripture/meditation.py: Sequential/thematic/lectionary meditation scheduler - dashboard/routes/scripture.py: REST endpoints for all scripture operations - config.py: scripture_enabled, translation, meditation settings - 95 comprehensive tests covering all modules and routes https://claude.ai/code/session_015wv7FM6BFsgZ35Us6WeY7H
902 lines
34 KiB
Python
902 lines
34 KiB
Python
"""Tests for the scripture module — sovereign biblical text integration.
|
|
|
|
Covers: constants, models, parser, store, memory, meditation, and routes.
|
|
All tests use in-memory or temp-file SQLite — no external services needed.
|
|
"""
|
|
|
|
import json
|
|
import sqlite3
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Constants
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestConstants:
|
|
def test_total_books(self):
|
|
from scripture.constants import BOOKS, TOTAL_BOOKS
|
|
assert len(BOOKS) == TOTAL_BOOKS == 66
|
|
|
|
def test_ot_nt_split(self):
|
|
from scripture.constants import BOOKS, OT_BOOKS, NT_BOOKS
|
|
ot = [b for b in BOOKS if b.testament == "OT"]
|
|
nt = [b for b in BOOKS if b.testament == "NT"]
|
|
assert len(ot) == OT_BOOKS == 39
|
|
assert len(nt) == NT_BOOKS == 27
|
|
|
|
def test_book_ids_sequential(self):
|
|
from scripture.constants import BOOKS
|
|
for i, book in enumerate(BOOKS, start=1):
|
|
assert book.id == i, f"Book {book.name} has id {book.id}, expected {i}"
|
|
|
|
def test_book_by_name_full(self):
|
|
from scripture.constants import book_by_name
|
|
info = book_by_name("Genesis")
|
|
assert info is not None
|
|
assert info.id == 1
|
|
assert info.testament == "OT"
|
|
|
|
def test_book_by_name_abbreviation(self):
|
|
from scripture.constants import book_by_name
|
|
info = book_by_name("Rev")
|
|
assert info is not None
|
|
assert info.id == 66
|
|
assert info.name == "Revelation"
|
|
|
|
def test_book_by_name_case_insensitive(self):
|
|
from scripture.constants import book_by_name
|
|
assert book_by_name("JOHN") is not None
|
|
assert book_by_name("john") is not None
|
|
assert book_by_name("John") is not None
|
|
|
|
def test_book_by_name_alias(self):
|
|
from scripture.constants import book_by_name
|
|
assert book_by_name("1 Cor").id == 46
|
|
assert book_by_name("Phil").id == 50
|
|
assert book_by_name("Ps").id == 19
|
|
|
|
def test_book_by_name_unknown(self):
|
|
from scripture.constants import book_by_name
|
|
assert book_by_name("Nonexistent") is None
|
|
|
|
def test_book_by_id(self):
|
|
from scripture.constants import book_by_id
|
|
info = book_by_id(43)
|
|
assert info is not None
|
|
assert info.name == "John"
|
|
|
|
def test_book_by_id_invalid(self):
|
|
from scripture.constants import book_by_id
|
|
assert book_by_id(0) is None
|
|
assert book_by_id(67) is None
|
|
|
|
def test_genres_present(self):
|
|
from scripture.constants import GENRES
|
|
assert "gospel" in GENRES
|
|
assert "epistle" in GENRES
|
|
assert "wisdom" in GENRES
|
|
assert "prophecy" in GENRES
|
|
|
|
def test_first_and_last_books(self):
|
|
from scripture.constants import BOOKS
|
|
assert BOOKS[0].name == "Genesis"
|
|
assert BOOKS[-1].name == "Revelation"
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Models
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestModels:
|
|
def test_encode_verse_id(self):
|
|
from scripture.models import encode_verse_id
|
|
assert encode_verse_id(43, 3, 16) == 43003016
|
|
assert encode_verse_id(1, 1, 1) == 1001001
|
|
assert encode_verse_id(66, 22, 21) == 66022021
|
|
|
|
def test_decode_verse_id(self):
|
|
from scripture.models import decode_verse_id
|
|
assert decode_verse_id(43003016) == (43, 3, 16)
|
|
assert decode_verse_id(1001001) == (1, 1, 1)
|
|
assert decode_verse_id(66022021) == (66, 22, 21)
|
|
|
|
def test_encode_decode_roundtrip(self):
|
|
from scripture.models import decode_verse_id, encode_verse_id
|
|
for book in (1, 19, 43, 66):
|
|
for chapter in (1, 10, 50):
|
|
for verse in (1, 5, 31):
|
|
vid = encode_verse_id(book, chapter, verse)
|
|
assert decode_verse_id(vid) == (book, chapter, verse)
|
|
|
|
def test_verse_ref_id(self):
|
|
from scripture.models import VerseRef
|
|
ref = VerseRef(book=43, chapter=3, verse=16)
|
|
assert ref.verse_id == 43003016
|
|
|
|
def test_verse_range_ids(self):
|
|
from scripture.models import VerseRange, VerseRef
|
|
vr = VerseRange(
|
|
start=VerseRef(book=1, chapter=1, verse=1),
|
|
end=VerseRef(book=1, chapter=1, verse=3),
|
|
)
|
|
ids = vr.verse_ids()
|
|
assert 1001001 in ids
|
|
assert 1001002 in ids
|
|
assert 1001003 in ids
|
|
|
|
def test_verse_model(self):
|
|
from scripture.models import Verse
|
|
v = Verse(
|
|
verse_id=43003016,
|
|
book=43,
|
|
chapter=3,
|
|
verse_num=16,
|
|
text="For God so loved the world...",
|
|
translation="ESV",
|
|
testament="NT",
|
|
genre="gospel",
|
|
)
|
|
assert v.text.startswith("For God")
|
|
assert v.testament == "NT"
|
|
|
|
def test_meditation_state_advance(self):
|
|
from scripture.models import MeditationState
|
|
state = MeditationState()
|
|
assert state.verses_meditated == 0
|
|
state.advance(1, 1, 2)
|
|
assert state.current_verse == 2
|
|
assert state.verses_meditated == 1
|
|
assert state.last_meditation is not None
|
|
|
|
def test_scripture_query_defaults(self):
|
|
from scripture.models import ScriptureQuery
|
|
q = ScriptureQuery(raw_text="test")
|
|
assert q.intent == "lookup"
|
|
assert q.references == []
|
|
assert q.keywords == []
|
|
|
|
def test_cross_reference_model(self):
|
|
from scripture.models import CrossReference
|
|
xref = CrossReference(
|
|
source_verse_id=43003016,
|
|
target_verse_id=45005008,
|
|
reference_type="thematic",
|
|
confidence=0.9,
|
|
)
|
|
assert xref.reference_type == "thematic"
|
|
assert xref.confidence == 0.9
|
|
|
|
def test_strongs_entry(self):
|
|
from scripture.models import StrongsEntry
|
|
entry = StrongsEntry(
|
|
strongs_number="H7225",
|
|
language="hebrew",
|
|
lemma="רֵאשִׁית",
|
|
transliteration="reshith",
|
|
gloss="beginning",
|
|
)
|
|
assert entry.language == "hebrew"
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Parser
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestParser:
|
|
def test_parse_single_verse(self):
|
|
from scripture.parser import parse_reference
|
|
result = parse_reference("John 3:16")
|
|
assert result is not None
|
|
assert result.start.book == 43
|
|
assert result.start.chapter == 3
|
|
assert result.start.verse == 16
|
|
assert result.end.verse == 16
|
|
|
|
def test_parse_range(self):
|
|
from scripture.parser import parse_reference
|
|
result = parse_reference("Romans 5:1-11")
|
|
assert result is not None
|
|
assert result.start.verse == 1
|
|
assert result.end.verse == 11
|
|
|
|
def test_parse_whole_chapter(self):
|
|
from scripture.parser import parse_reference
|
|
result = parse_reference("Genesis 1")
|
|
assert result is not None
|
|
assert result.start.verse == 1
|
|
assert result.end.verse == 999 # sentinel for whole chapter
|
|
|
|
def test_parse_multi_chapter_range(self):
|
|
from scripture.parser import parse_reference
|
|
result = parse_reference("Genesis 1:1-2:3")
|
|
assert result is not None
|
|
assert result.start.chapter == 1
|
|
assert result.start.verse == 1
|
|
assert result.end.chapter == 2
|
|
assert result.end.verse == 3
|
|
|
|
def test_parse_abbreviation(self):
|
|
from scripture.parser import parse_reference
|
|
result = parse_reference("Rom 8:28")
|
|
assert result is not None
|
|
assert result.start.book == 45
|
|
|
|
def test_parse_numbered_book(self):
|
|
from scripture.parser import parse_reference
|
|
result = parse_reference("1 Cor 13:4")
|
|
assert result is not None
|
|
assert result.start.book == 46
|
|
|
|
def test_parse_invalid(self):
|
|
from scripture.parser import parse_reference
|
|
assert parse_reference("not a reference") is None
|
|
|
|
def test_parse_unknown_book(self):
|
|
from scripture.parser import parse_reference
|
|
assert parse_reference("Hezekiah 1:1") is None
|
|
|
|
def test_extract_multiple_references(self):
|
|
from scripture.parser import extract_references
|
|
text = "See John 3:16 and Romans 5:8 for the gospel message."
|
|
refs = extract_references(text)
|
|
assert len(refs) == 2
|
|
assert refs[0].start.book == 43
|
|
assert refs[1].start.book == 45
|
|
|
|
def test_extract_no_references(self):
|
|
from scripture.parser import extract_references
|
|
assert extract_references("No references here.") == []
|
|
|
|
def test_format_reference(self):
|
|
from scripture.models import VerseRef
|
|
from scripture.parser import format_reference
|
|
ref = VerseRef(book=43, chapter=3, verse=16)
|
|
assert format_reference(ref) == "John 3:16"
|
|
|
|
def test_format_reference_chapter_only(self):
|
|
from scripture.models import VerseRef
|
|
from scripture.parser import format_reference
|
|
ref = VerseRef(book=1, chapter=1, verse=0)
|
|
assert format_reference(ref) == "Genesis 1"
|
|
|
|
def test_format_range_single(self):
|
|
from scripture.models import VerseRange, VerseRef
|
|
from scripture.parser import format_range
|
|
vr = VerseRange(
|
|
start=VerseRef(book=43, chapter=3, verse=16),
|
|
end=VerseRef(book=43, chapter=3, verse=16),
|
|
)
|
|
assert format_range(vr) == "John 3:16"
|
|
|
|
def test_format_range_same_chapter(self):
|
|
from scripture.models import VerseRange, VerseRef
|
|
from scripture.parser import format_range
|
|
vr = VerseRange(
|
|
start=VerseRef(book=45, chapter=5, verse=1),
|
|
end=VerseRef(book=45, chapter=5, verse=11),
|
|
)
|
|
assert format_range(vr) == "Romans 5:1-11"
|
|
|
|
def test_format_range_multi_chapter(self):
|
|
from scripture.models import VerseRange, VerseRef
|
|
from scripture.parser import format_range
|
|
vr = VerseRange(
|
|
start=VerseRef(book=1, chapter=1, verse=1),
|
|
end=VerseRef(book=1, chapter=2, verse=3),
|
|
)
|
|
assert format_range(vr) == "Genesis 1:1-2:3"
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Store
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_store():
|
|
"""Create a ScriptureStore backed by a temp file."""
|
|
from scripture.store import ScriptureStore
|
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
db_path = f.name
|
|
store = ScriptureStore(db_path=db_path)
|
|
yield store
|
|
store.close()
|
|
Path(db_path).unlink(missing_ok=True)
|
|
|
|
|
|
def _sample_verse(book=43, chapter=3, verse_num=16, text="For God so loved the world"):
|
|
from scripture.models import Verse, encode_verse_id
|
|
return Verse(
|
|
verse_id=encode_verse_id(book, chapter, verse_num),
|
|
book=book,
|
|
chapter=chapter,
|
|
verse_num=verse_num,
|
|
text=text,
|
|
translation="ESV",
|
|
testament="NT",
|
|
genre="gospel",
|
|
)
|
|
|
|
|
|
class TestStore:
|
|
def test_insert_and_get(self, temp_store):
|
|
verse = _sample_verse()
|
|
temp_store.insert_verse(verse)
|
|
result = temp_store.get_verse(43, 3, 16)
|
|
assert result is not None
|
|
assert result.text == "For God so loved the world"
|
|
|
|
def test_get_nonexistent(self, temp_store):
|
|
assert temp_store.get_verse(99, 1, 1) is None
|
|
|
|
def test_get_verse_by_id(self, temp_store):
|
|
verse = _sample_verse()
|
|
temp_store.insert_verse(verse)
|
|
result = temp_store.get_verse_by_id(43003016)
|
|
assert result is not None
|
|
assert result.book == 43
|
|
|
|
def test_bulk_insert(self, temp_store):
|
|
verses = [
|
|
_sample_verse(1, 1, 1, "In the beginning God created the heavens and the earth."),
|
|
_sample_verse(1, 1, 2, "The earth was without form and void."),
|
|
_sample_verse(1, 1, 3, "And God said, Let there be light."),
|
|
]
|
|
temp_store.insert_verses(verses)
|
|
assert temp_store.count_verses() == 3
|
|
|
|
def test_get_chapter(self, temp_store):
|
|
verses = [
|
|
_sample_verse(1, 1, i, f"Verse {i}")
|
|
for i in range(1, 6)
|
|
]
|
|
temp_store.insert_verses(verses)
|
|
chapter = temp_store.get_chapter(1, 1)
|
|
assert len(chapter) == 5
|
|
assert chapter[0].verse_num == 1
|
|
|
|
def test_get_range(self, temp_store):
|
|
from scripture.models import encode_verse_id
|
|
verses = [
|
|
_sample_verse(1, 1, i, f"Verse {i}")
|
|
for i in range(1, 11)
|
|
]
|
|
temp_store.insert_verses(verses)
|
|
result = temp_store.get_range(
|
|
encode_verse_id(1, 1, 3),
|
|
encode_verse_id(1, 1, 7),
|
|
)
|
|
assert len(result) == 5
|
|
|
|
def test_search_text(self, temp_store):
|
|
verses = [
|
|
_sample_verse(43, 3, 16, "For God so loved the world"),
|
|
_sample_verse(43, 3, 17, "For God did not send his Son"),
|
|
_sample_verse(45, 5, 8, "God shows his love for us"),
|
|
]
|
|
temp_store.insert_verses(verses)
|
|
results = temp_store.search_text("God")
|
|
assert len(results) == 3
|
|
results = temp_store.search_text("loved")
|
|
assert len(results) == 1
|
|
|
|
def test_count_verses(self, temp_store):
|
|
assert temp_store.count_verses() == 0
|
|
temp_store.insert_verse(_sample_verse())
|
|
assert temp_store.count_verses() == 1
|
|
|
|
def test_get_books(self, temp_store):
|
|
verses = [
|
|
_sample_verse(1, 1, 1, "Genesis verse"),
|
|
_sample_verse(43, 1, 1, "John verse"),
|
|
]
|
|
temp_store.insert_verses(verses)
|
|
books = temp_store.get_books()
|
|
assert len(books) == 2
|
|
assert books[0]["name"] == "Genesis"
|
|
assert books[1]["name"] == "John"
|
|
|
|
def test_cross_references(self, temp_store):
|
|
from scripture.models import CrossReference
|
|
xref = CrossReference(
|
|
source_verse_id=43003016,
|
|
target_verse_id=45005008,
|
|
reference_type="thematic",
|
|
confidence=0.9,
|
|
)
|
|
temp_store.insert_cross_reference(xref)
|
|
results = temp_store.get_cross_references(43003016)
|
|
assert len(results) == 1
|
|
assert results[0].target_verse_id == 45005008
|
|
|
|
def test_cross_references_bidirectional(self, temp_store):
|
|
from scripture.models import CrossReference
|
|
xref = CrossReference(
|
|
source_verse_id=43003016,
|
|
target_verse_id=45005008,
|
|
)
|
|
temp_store.insert_cross_reference(xref)
|
|
# Query from target side
|
|
results = temp_store.get_cross_references(45005008)
|
|
assert len(results) == 1
|
|
|
|
def test_topics(self, temp_store):
|
|
from scripture.models import Topic
|
|
topic = Topic(
|
|
topic_id="love",
|
|
name="Love",
|
|
description="Biblical concept of love",
|
|
verse_ids=[43003016, 45005008],
|
|
)
|
|
temp_store.insert_topic(topic)
|
|
result = temp_store.get_topic("love")
|
|
assert result is not None
|
|
assert result.name == "Love"
|
|
assert len(result.verse_ids) == 2
|
|
|
|
def test_search_topics(self, temp_store):
|
|
from scripture.models import Topic
|
|
temp_store.insert_topic(Topic(topic_id="love", name="Love"))
|
|
temp_store.insert_topic(Topic(topic_id="faith", name="Faith"))
|
|
results = temp_store.search_topics("lov")
|
|
assert len(results) == 1
|
|
assert results[0].name == "Love"
|
|
|
|
def test_get_verses_for_topic(self, temp_store):
|
|
from scripture.models import Topic
|
|
verse = _sample_verse()
|
|
temp_store.insert_verse(verse)
|
|
topic = Topic(
|
|
topic_id="love",
|
|
name="Love",
|
|
verse_ids=[verse.verse_id],
|
|
)
|
|
temp_store.insert_topic(topic)
|
|
verses = temp_store.get_verses_for_topic("love")
|
|
assert len(verses) == 1
|
|
assert verses[0].verse_id == verse.verse_id
|
|
|
|
def test_strongs(self, temp_store):
|
|
from scripture.models import StrongsEntry
|
|
entry = StrongsEntry(
|
|
strongs_number="G26",
|
|
language="greek",
|
|
lemma="ἀγάπη",
|
|
transliteration="agape",
|
|
gloss="love",
|
|
)
|
|
temp_store.insert_strongs(entry)
|
|
result = temp_store.get_strongs("G26")
|
|
assert result is not None
|
|
assert result.gloss == "love"
|
|
assert temp_store.get_strongs("G9999") is None
|
|
|
|
def test_stats(self, temp_store):
|
|
stats = temp_store.stats()
|
|
assert stats["verses"] == 0
|
|
assert stats["cross_references"] == 0
|
|
assert stats["topics"] == 0
|
|
assert stats["strongs_entries"] == 0
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Memory
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_memory():
|
|
"""Create a ScriptureMemory backed by a temp file."""
|
|
from scripture.memory import ScriptureMemory
|
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
db_path = f.name
|
|
mem = ScriptureMemory(db_path=db_path)
|
|
yield mem
|
|
mem.close()
|
|
Path(db_path).unlink(missing_ok=True)
|
|
|
|
|
|
class TestWorkingMemory:
|
|
def test_focus_and_retrieve(self):
|
|
from scripture.memory import WorkingMemory
|
|
wm = WorkingMemory(capacity=3)
|
|
v1 = _sample_verse(1, 1, 1, "Verse 1")
|
|
v2 = _sample_verse(1, 1, 2, "Verse 2")
|
|
wm.focus(v1)
|
|
wm.focus(v2)
|
|
assert len(wm) == 2
|
|
focused = wm.get_focused()
|
|
assert focused[0].verse_id == v1.verse_id
|
|
assert focused[1].verse_id == v2.verse_id
|
|
|
|
def test_capacity_eviction(self):
|
|
from scripture.memory import WorkingMemory
|
|
wm = WorkingMemory(capacity=2)
|
|
v1 = _sample_verse(1, 1, 1, "Verse 1")
|
|
v2 = _sample_verse(1, 1, 2, "Verse 2")
|
|
v3 = _sample_verse(1, 1, 3, "Verse 3")
|
|
wm.focus(v1)
|
|
wm.focus(v2)
|
|
wm.focus(v3)
|
|
assert len(wm) == 2
|
|
assert not wm.is_focused(v1.verse_id)
|
|
assert wm.is_focused(v2.verse_id)
|
|
assert wm.is_focused(v3.verse_id)
|
|
|
|
def test_refocus_moves_to_end(self):
|
|
from scripture.memory import WorkingMemory
|
|
wm = WorkingMemory(capacity=3)
|
|
v1 = _sample_verse(1, 1, 1, "Verse 1")
|
|
v2 = _sample_verse(1, 1, 2, "Verse 2")
|
|
wm.focus(v1)
|
|
wm.focus(v2)
|
|
wm.focus(v1) # Re-focus v1
|
|
focused = wm.get_focused()
|
|
assert focused[-1].verse_id == v1.verse_id
|
|
|
|
def test_clear(self):
|
|
from scripture.memory import WorkingMemory
|
|
wm = WorkingMemory()
|
|
wm.focus(_sample_verse())
|
|
wm.clear()
|
|
assert len(wm) == 0
|
|
|
|
|
|
class TestAssociativeMemory:
|
|
def test_meditation_state_persistence(self, temp_memory):
|
|
from scripture.models import MeditationState
|
|
state = temp_memory.associative.get_meditation_state()
|
|
assert state.current_book == 1
|
|
assert state.mode == "sequential"
|
|
|
|
state.advance(43, 3, 16)
|
|
state.mode = "thematic"
|
|
temp_memory.associative.save_meditation_state(state)
|
|
|
|
loaded = temp_memory.associative.get_meditation_state()
|
|
assert loaded.current_book == 43
|
|
assert loaded.current_chapter == 3
|
|
assert loaded.current_verse == 16
|
|
assert loaded.mode == "thematic"
|
|
assert loaded.verses_meditated == 1
|
|
|
|
def test_meditation_log(self, temp_memory):
|
|
temp_memory.associative.log_meditation(43003016, notes="Great verse")
|
|
temp_memory.associative.log_meditation(45005008, notes="Also good")
|
|
history = temp_memory.associative.get_meditation_history(limit=10)
|
|
assert len(history) == 2
|
|
assert history[0]["verse_id"] == 45005008 # most recent first
|
|
|
|
def test_meditation_count(self, temp_memory):
|
|
assert temp_memory.associative.meditation_count() == 0
|
|
temp_memory.associative.log_meditation(1001001)
|
|
assert temp_memory.associative.meditation_count() == 1
|
|
|
|
def test_insights(self, temp_memory):
|
|
temp_memory.associative.add_insight(
|
|
43003016, "God's love is unconditional", category="theology"
|
|
)
|
|
insights = temp_memory.associative.get_insights(43003016)
|
|
assert len(insights) == 1
|
|
assert insights[0]["category"] == "theology"
|
|
|
|
def test_recent_insights(self, temp_memory):
|
|
temp_memory.associative.add_insight(1001001, "Creation narrative")
|
|
temp_memory.associative.add_insight(43003016, "Gospel core")
|
|
recent = temp_memory.associative.get_recent_insights(limit=5)
|
|
assert len(recent) == 2
|
|
|
|
def test_duplicate_insight_ignored(self, temp_memory):
|
|
temp_memory.associative.add_insight(43003016, "Same insight")
|
|
temp_memory.associative.add_insight(43003016, "Same insight")
|
|
insights = temp_memory.associative.get_insights(43003016)
|
|
assert len(insights) == 1
|
|
|
|
|
|
class TestScriptureMemory:
|
|
def test_status(self, temp_memory):
|
|
status = temp_memory.status()
|
|
assert "working_memory_items" in status
|
|
assert "meditation_mode" in status
|
|
assert status["working_memory_items"] == 0
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Meditation Scheduler
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_scheduler():
|
|
"""Create a MeditationScheduler backed by temp stores."""
|
|
from scripture.meditation import MeditationScheduler
|
|
from scripture.memory import ScriptureMemory
|
|
from scripture.store import ScriptureStore
|
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
store_path = f.name
|
|
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
|
|
mem_path = f.name
|
|
store = ScriptureStore(db_path=store_path)
|
|
memory = ScriptureMemory(db_path=mem_path)
|
|
scheduler = MeditationScheduler(store=store, memory=memory)
|
|
yield scheduler, store, memory
|
|
store.close()
|
|
memory.close()
|
|
Path(store_path).unlink(missing_ok=True)
|
|
Path(mem_path).unlink(missing_ok=True)
|
|
|
|
|
|
class TestMeditationScheduler:
|
|
def test_initial_state(self, temp_scheduler):
|
|
scheduler, _, _ = temp_scheduler
|
|
state = scheduler.state
|
|
assert state.mode == "sequential"
|
|
assert state.current_book == 1
|
|
|
|
def test_set_mode(self, temp_scheduler):
|
|
scheduler, _, _ = temp_scheduler
|
|
state = scheduler.set_mode("thematic", theme="love")
|
|
assert state.mode == "thematic"
|
|
assert state.theme == "love"
|
|
|
|
def test_set_invalid_mode(self, temp_scheduler):
|
|
scheduler, _, _ = temp_scheduler
|
|
with pytest.raises(ValueError, match="Unknown mode"):
|
|
scheduler.set_mode("invalid")
|
|
|
|
def test_next_sequential(self, temp_scheduler):
|
|
scheduler, store, _ = temp_scheduler
|
|
verses = [
|
|
_sample_verse(1, 1, i, f"Genesis 1:{i}")
|
|
for i in range(1, 6)
|
|
]
|
|
store.insert_verses(verses)
|
|
# State starts at 1:1:1, next should be 1:1:2
|
|
result = scheduler.next_meditation()
|
|
assert result is not None
|
|
assert result.verse_num == 2
|
|
|
|
def test_sequential_chapter_advance(self, temp_scheduler):
|
|
scheduler, store, _ = temp_scheduler
|
|
# Only two verses in chapter 1, plus verse 1 of chapter 2
|
|
store.insert_verses([
|
|
_sample_verse(1, 1, 1, "Gen 1:1"),
|
|
_sample_verse(1, 1, 2, "Gen 1:2"),
|
|
_sample_verse(1, 2, 1, "Gen 2:1"),
|
|
])
|
|
# Start at 1:1:1 → next is 1:1:2
|
|
v = scheduler.next_meditation()
|
|
assert v.verse_num == 2
|
|
# Next should advance to 1:2:1
|
|
v = scheduler.next_meditation()
|
|
assert v is not None
|
|
assert v.chapter == 2
|
|
assert v.verse_num == 1
|
|
|
|
def test_current_focus_empty(self, temp_scheduler):
|
|
scheduler, _, _ = temp_scheduler
|
|
assert scheduler.current_focus() is None
|
|
|
|
def test_meditate_on(self, temp_scheduler):
|
|
scheduler, store, memory = temp_scheduler
|
|
verse = _sample_verse()
|
|
store.insert_verse(verse)
|
|
scheduler.meditate_on(verse, notes="Reflecting on love")
|
|
assert memory.working.is_focused(verse.verse_id)
|
|
state = scheduler.state
|
|
assert state.verses_meditated == 1
|
|
|
|
def test_status(self, temp_scheduler):
|
|
scheduler, _, _ = temp_scheduler
|
|
status = scheduler.status()
|
|
assert "mode" in status
|
|
assert "current_book" in status
|
|
assert "verses_meditated" in status
|
|
|
|
def test_history(self, temp_scheduler):
|
|
scheduler, store, _ = temp_scheduler
|
|
verse = _sample_verse()
|
|
store.insert_verse(verse)
|
|
scheduler.meditate_on(verse)
|
|
history = scheduler.history(limit=5)
|
|
assert len(history) == 1
|
|
|
|
def test_get_context(self, temp_scheduler):
|
|
scheduler, store, _ = temp_scheduler
|
|
verses = [_sample_verse(1, 1, i, f"Gen 1:{i}") for i in range(1, 6)]
|
|
store.insert_verses(verses)
|
|
ctx = scheduler.get_context(verses[2], before=1, after=1)
|
|
assert len(ctx) == 3
|
|
|
|
def test_get_cross_references(self, temp_scheduler):
|
|
from scripture.models import CrossReference
|
|
scheduler, store, _ = temp_scheduler
|
|
v1 = _sample_verse(43, 3, 16, "For God so loved")
|
|
v2 = _sample_verse(45, 5, 8, "God shows his love")
|
|
store.insert_verse(v1)
|
|
store.insert_verse(v2)
|
|
store.insert_cross_reference(CrossReference(
|
|
source_verse_id=v1.verse_id,
|
|
target_verse_id=v2.verse_id,
|
|
))
|
|
xrefs = scheduler.get_cross_references(v1)
|
|
assert len(xrefs) == 1
|
|
assert xrefs[0].verse_id == v2.verse_id
|
|
|
|
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
# Routes
|
|
# ════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.fixture
|
|
def scripture_client(tmp_path):
|
|
"""TestClient with isolated scripture stores."""
|
|
from scripture.meditation import MeditationScheduler
|
|
from scripture.memory import ScriptureMemory
|
|
from scripture.store import ScriptureStore
|
|
|
|
store = ScriptureStore(db_path=tmp_path / "scripture.db")
|
|
memory = ScriptureMemory(db_path=tmp_path / "memory.db")
|
|
scheduler = MeditationScheduler(store=store, memory=memory)
|
|
|
|
# Seed with some verses for route testing
|
|
store.insert_verses([
|
|
_sample_verse(43, 3, 16, "For God so loved the world, that he gave his only Son, that whoever believes in him should not perish but have eternal life."),
|
|
_sample_verse(43, 3, 17, "For God did not send his Son into the world to condemn the world, but in order that the world might be saved through him."),
|
|
_sample_verse(45, 5, 8, "but God shows his love for us in that while we were still sinners, Christ died for us."),
|
|
_sample_verse(1, 1, 1, "In the beginning, God created the heavens and the earth."),
|
|
_sample_verse(1, 1, 2, "The earth was without form and void, and darkness was over the face of the deep."),
|
|
_sample_verse(1, 1, 3, "And God said, Let there be light, and there was light."),
|
|
])
|
|
|
|
with patch("dashboard.routes.scripture.scripture_store", store), \
|
|
patch("dashboard.routes.scripture.scripture_memory", memory), \
|
|
patch("dashboard.routes.scripture.meditation_scheduler", scheduler):
|
|
from dashboard.app import app
|
|
with TestClient(app) as c:
|
|
yield c
|
|
|
|
store.close()
|
|
memory.close()
|
|
|
|
|
|
class TestScriptureRoutes:
|
|
def test_scripture_status(self, scripture_client):
|
|
resp = scripture_client.get("/scripture")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "store" in data
|
|
assert "memory" in data
|
|
assert "meditation" in data
|
|
|
|
def test_get_verse(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/verse", params={"ref": "John 3:16"})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["reference"] == "John 3:16"
|
|
assert "loved" in data["text"]
|
|
|
|
def test_get_verse_range(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/verse", params={"ref": "John 3:16-17"})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "verses" in data
|
|
assert len(data["verses"]) == 2
|
|
|
|
def test_get_verse_bad_ref(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/verse", params={"ref": "not a ref"})
|
|
assert resp.status_code == 400
|
|
|
|
def test_get_verse_not_found(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/verse", params={"ref": "Jude 1:25"})
|
|
assert resp.status_code == 404
|
|
|
|
def test_get_chapter(self, scripture_client):
|
|
resp = scripture_client.get(
|
|
"/scripture/chapter", params={"book": "Genesis", "chapter": 1}
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["book"] == "Genesis"
|
|
assert len(data["verses"]) == 3
|
|
|
|
def test_get_chapter_bad_book(self, scripture_client):
|
|
resp = scripture_client.get(
|
|
"/scripture/chapter", params={"book": "FakeBook", "chapter": 1}
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_search(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/search", params={"q": "God"})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["count"] > 0
|
|
|
|
def test_search_empty(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/search", params={"q": "xyznonexistent"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["count"] == 0
|
|
|
|
def test_meditate_get(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/meditate")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "status" in data
|
|
|
|
def test_meditate_post(self, scripture_client):
|
|
resp = scripture_client.post("/scripture/meditate")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "verse" in data
|
|
assert "status" in data
|
|
|
|
def test_set_meditation_mode(self, scripture_client):
|
|
resp = scripture_client.post(
|
|
"/scripture/meditate/mode", params={"mode": "thematic", "theme": "love"}
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["mode"] == "thematic"
|
|
|
|
def test_set_meditation_mode_invalid(self, scripture_client):
|
|
resp = scripture_client.post(
|
|
"/scripture/meditate/mode", params={"mode": "invalid"}
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_memory_status(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/memory")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "working_memory_items" in data
|
|
|
|
def test_stats(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/stats")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "verses" in data
|
|
|
|
def test_ingest(self, scripture_client):
|
|
payload = {
|
|
"verses": [
|
|
{"book": 19, "chapter": 23, "verse_num": 1, "text": "The LORD is my shepherd; I shall not want."},
|
|
{"book": 19, "chapter": 23, "verse_num": 2, "text": "He makes me lie down in green pastures."},
|
|
]
|
|
}
|
|
resp = scripture_client.post("/scripture/ingest", json=payload)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["ingested"] == 2
|
|
|
|
def test_ingest_invalid(self, scripture_client):
|
|
resp = scripture_client.post("/scripture/ingest", json={"verses": []})
|
|
assert resp.status_code == 400
|
|
|
|
def test_ingest_bad_json(self, scripture_client):
|
|
resp = scripture_client.post(
|
|
"/scripture/ingest",
|
|
content=b"not json",
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_xref(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/xref", params={"ref": "John 3:16"})
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "source" in data
|
|
assert "cross_references" in data
|
|
|
|
def test_xref_not_found(self, scripture_client):
|
|
resp = scripture_client.get("/scripture/xref", params={"ref": "Jude 1:25"})
|
|
assert resp.status_code == 404
|