Vocabulary study with FSRS spaced repetition, AI tutoring (Ollama/Claude), essay marking, idioms browser, Anki export, and dashboard. 918 vocabulary entries across 39 categories. 41 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
152 lines
4.7 KiB
Python
152 lines
4.7 KiB
Python
"""Tests for db.py — SQLite database layer with FSRS integration."""
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
# Add project root to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
import fsrs
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def temp_db(tmp_path):
|
|
"""Use a temporary database for each test."""
|
|
import db as db_mod
|
|
|
|
db_mod._conn = None
|
|
db_mod.DB_PATH = tmp_path / "test.db"
|
|
db_mod.init_db()
|
|
yield db_mod
|
|
db_mod.close()
|
|
|
|
|
|
def test_init_db_creates_tables(temp_db):
|
|
"""init_db should create all required tables."""
|
|
conn = temp_db.get_connection()
|
|
tables = conn.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
).fetchall()
|
|
table_names = {row["name"] for row in tables}
|
|
assert "word_progress" in table_names
|
|
assert "quiz_sessions" in table_names
|
|
assert "essays" in table_names
|
|
assert "tutor_sessions" in table_names
|
|
|
|
|
|
def test_get_word_progress_nonexistent(temp_db):
|
|
"""Should return None for a word that hasn't been reviewed."""
|
|
assert temp_db.get_word_progress("nonexistent") is None
|
|
|
|
|
|
def test_update_and_get_word_progress(temp_db):
|
|
"""update_word_progress should create and update progress."""
|
|
card = temp_db.update_word_progress("verb_go", fsrs.Rating.Good)
|
|
assert card is not None
|
|
assert card.stability is not None
|
|
|
|
progress = temp_db.get_word_progress("verb_go")
|
|
assert progress is not None
|
|
assert progress["word_id"] == "verb_go"
|
|
assert progress["reps"] == 1
|
|
assert progress["fsrs_state"] is not None
|
|
|
|
|
|
def test_update_word_progress_increments_reps(temp_db):
|
|
"""Reviewing the same word multiple times should increment reps."""
|
|
temp_db.update_word_progress("verb_go", fsrs.Rating.Good)
|
|
temp_db.update_word_progress("verb_go", fsrs.Rating.Easy)
|
|
progress = temp_db.get_word_progress("verb_go")
|
|
assert progress["reps"] == 2
|
|
|
|
|
|
def test_get_due_words(temp_db):
|
|
"""get_due_words should return words that are due for review."""
|
|
# A newly reviewed word with Rating.Again should be due soon
|
|
temp_db.update_word_progress("verb_go", fsrs.Rating.Again)
|
|
# An easy word should have a later due date
|
|
temp_db.update_word_progress("verb_eat", fsrs.Rating.Easy)
|
|
|
|
# Due words depend on timing; at minimum both should be in the system
|
|
all_progress = temp_db.get_connection().execute(
|
|
"SELECT word_id FROM word_progress"
|
|
).fetchall()
|
|
assert len(all_progress) == 2
|
|
|
|
|
|
def test_get_word_counts(temp_db):
|
|
"""get_word_counts should return correct counts."""
|
|
counts = temp_db.get_word_counts(total_vocab_size=100)
|
|
assert counts["total"] == 100
|
|
assert counts["seen"] == 0
|
|
assert counts["mastered"] == 0
|
|
assert counts["due"] == 0
|
|
|
|
temp_db.update_word_progress("verb_go", fsrs.Rating.Good)
|
|
counts = temp_db.get_word_counts(total_vocab_size=100)
|
|
assert counts["seen"] == 1
|
|
|
|
|
|
def test_record_quiz_session(temp_db):
|
|
"""record_quiz_session should insert a quiz record."""
|
|
temp_db.record_quiz_session("Common verbs", 10, 7, 120)
|
|
rows = temp_db.get_connection().execute(
|
|
"SELECT * FROM quiz_sessions"
|
|
).fetchall()
|
|
assert len(rows) == 1
|
|
assert rows[0]["correct"] == 7
|
|
assert rows[0]["total_questions"] == 10
|
|
|
|
|
|
def test_save_essay(temp_db):
|
|
"""save_essay should store the essay and feedback."""
|
|
temp_db.save_essay("متن آزمایشی", "B1", "Good effort!", "Identity and culture")
|
|
essays = temp_db.get_recent_essays()
|
|
assert len(essays) == 1
|
|
assert essays[0]["grade"] == "B1"
|
|
|
|
|
|
def test_save_tutor_session(temp_db):
|
|
"""save_tutor_session should store the conversation."""
|
|
messages = [
|
|
{"role": "user", "content": "سلام"},
|
|
{"role": "assistant", "content": "سلام! حالت چطوره؟"},
|
|
]
|
|
temp_db.save_tutor_session("Identity and culture", messages, 300)
|
|
rows = temp_db.get_connection().execute(
|
|
"SELECT * FROM tutor_sessions"
|
|
).fetchall()
|
|
assert len(rows) == 1
|
|
assert rows[0]["theme"] == "Identity and culture"
|
|
|
|
|
|
def test_get_stats(temp_db):
|
|
"""get_stats should return aggregated stats."""
|
|
stats = temp_db.get_stats()
|
|
assert stats["total_reviews"] == 0
|
|
assert stats["total_quizzes"] == 0
|
|
assert stats["streak"] == 0
|
|
assert isinstance(stats["recent_quizzes"], list)
|
|
|
|
|
|
def test_close_and_reopen(temp_db):
|
|
"""Closing and reopening should preserve data."""
|
|
temp_db.update_word_progress("verb_go", fsrs.Rating.Good)
|
|
db_path = temp_db.DB_PATH
|
|
|
|
temp_db.close()
|
|
|
|
# Reopen
|
|
temp_db._conn = None
|
|
temp_db.DB_PATH = db_path
|
|
temp_db.init_db()
|
|
|
|
progress = temp_db.get_word_progress("verb_go")
|
|
assert progress is not None
|
|
assert progress["reps"] == 1
|