OPEN SOURCE
software/tests/test_job_state.py
Read on this site and download the files you need.
Nichijoyaku was created using AI. A complete review by human biblical-language and biblical-studies specialists has not been established. Read alongside established translations and original-language resources.
← Back to the open-source guide
1"""Tests for SQLite job state persistence."""23from __future__ import annotations45import sqlite36import tempfile7from datetime import datetime8from pathlib import Path910import pytest1112from src.orchestration.models import JobStatus13from src.orchestration.state import JobStateStore141516@pytest.fixture17def temp_db() -> Path:18"""Create a temporary database path."""19with tempfile.TemporaryDirectory() as tmpdir:20yield Path(tmpdir) / "jobs" / "test.db"212223@pytest.fixture24def state_store(temp_db: Path) -> JobStateStore:25"""Create a JobStateStore with a temporary database."""26return JobStateStore(temp_db)272829class TestJobStateStoreInit:30"""Tests for JobStateStore initialization."""3132def test_creates_directory(self, temp_db: Path) -> None:33"""Test that state store creates parent directories."""34store = JobStateStore(temp_db)35assert temp_db.parent.exists()3637def test_creates_tables(self, temp_db: Path) -> None:38"""Test that state store creates required tables."""39store = JobStateStore(temp_db)4041# Check tables exist42conn = sqlite3.connect(str(temp_db))43cursor = conn.execute(44"SELECT name FROM sqlite_master WHERE type='table'"45)46tables = {row[0] for row in cursor.fetchall()}47conn.close()4849assert "bible_jobs" in tables50assert "book_jobs" in tables51assert "verse_jobs" in tables5253def test_idempotent_init(self, temp_db: Path) -> None:54"""Test that multiple initializations don't fail."""55store1 = JobStateStore(temp_db)56store2 = JobStateStore(temp_db) # Should not raise575859class TestBibleJobManagement:60"""Tests for Bible job management."""6162def test_create_bible_job(self, state_store: JobStateStore) -> None:63"""Test creating a Bible job."""64job = state_store.create_bible_job(65books=["john", "matthew"],66config={"model": "test"},67)6869assert job.job_id is not None70assert job.books == ["john", "matthew"]71assert job.status == JobStatus.PENDING72assert job.config_snapshot == {"model": "test"}7374def test_create_job_with_specific_id(self, state_store: JobStateStore) -> None:75"""Test creating a job with a specific ID."""76job = state_store.create_bible_job(77books=["john"],78config={},79job_id="my-custom-id",80)81assert job.job_id == "my-custom-id"8283def test_get_bible_job(self, state_store: JobStateStore) -> None:84"""Test getting a Bible job by ID."""85created = state_store.create_bible_job(books=["john"], config={})86retrieved = state_store.get_bible_job(created.job_id)8788assert retrieved is not None89assert retrieved.job_id == created.job_id90assert retrieved.books == ["john"]9192def test_get_nonexistent_job(self, state_store: JobStateStore) -> None:93"""Test getting a non-existent job returns None."""94result = state_store.get_bible_job("nonexistent-id")95assert result is None9697def test_get_latest_incomplete_job(self, state_store: JobStateStore) -> None:98"""Test getting the latest incomplete job."""99# Create a completed job100job1 = state_store.create_bible_job(books=["john"], config={})101state_store.update_job_status(job1.job_id, JobStatus.COMPLETED)102103# Create an incomplete job104job2 = state_store.create_bible_job(books=["matthew"], config={})105106latest = state_store.get_latest_incomplete_job()107assert latest is not None108assert latest.job_id == job2.job_id109110def test_get_latest_incomplete_no_jobs(self, state_store: JobStateStore) -> None:111"""Test getting latest incomplete when no jobs exist."""112result = state_store.get_latest_incomplete_job()113assert result is None114115def test_update_job_status(self, state_store: JobStateStore) -> None:116"""Test updating job status."""117job = state_store.create_bible_job(books=["john"], config={})118119started = datetime.utcnow()120state_store.update_job_status(121job.job_id,122JobStatus.IN_PROGRESS,123started_at=started,124)125126updated = state_store.get_bible_job(job.job_id)127assert updated is not None128assert updated.status == JobStatus.IN_PROGRESS129assert updated.started_at is not None130131def test_update_job_verse_counts(self, state_store: JobStateStore) -> None:132"""Test updating job verse counts."""133job = state_store.create_bible_job(books=["john"], config={})134135state_store.update_job_verse_counts(136job.job_id,137total=100,138completed=50,139failed=5,140)141142updated = state_store.get_bible_job(job.job_id)143assert updated is not None144assert updated.total_verses == 100145assert updated.completed_verses == 50146assert updated.failed_verses == 5147148149class TestBookJobManagement:150"""Tests for book job management."""151152def test_book_jobs_created_with_bible_job(153self, state_store: JobStateStore154) -> None:155"""Test that book jobs are created when Bible job is created."""156job = state_store.create_bible_job(157books=["john", "matthew", "luke"],158config={},159)160161book_jobs = state_store.get_book_jobs(job.job_id)162assert len(book_jobs) == 3163book_names = [bj.book for bj in book_jobs]164assert "john" in book_names165assert "matthew" in book_names166assert "luke" in book_names167168def test_get_book_job(self, state_store: JobStateStore) -> None:169"""Test getting a specific book job."""170job = state_store.create_bible_job(books=["john"], config={})171book_job = state_store.get_book_job(job.job_id, "john")172173assert book_job is not None174assert book_job.book == "john"175assert book_job.status == JobStatus.PENDING176177def test_get_nonexistent_book_job(self, state_store: JobStateStore) -> None:178"""Test getting a non-existent book job."""179job = state_store.create_bible_job(books=["john"], config={})180result = state_store.get_book_job(job.job_id, "matthew")181assert result is None182183def test_update_book_status(self, state_store: JobStateStore) -> None:184"""Test updating book status."""185job = state_store.create_bible_job(books=["john"], config={})186187state_store.update_book_status(188job.job_id,189"john",190JobStatus.IN_PROGRESS,191worker_pid=12345,192started_at=datetime.utcnow(),193)194195book_job = state_store.get_book_job(job.job_id, "john")196assert book_job is not None197assert book_job.status == JobStatus.IN_PROGRESS198assert book_job.worker_pid == 12345199200def test_update_book_verse_counts(self, state_store: JobStateStore) -> None:201"""Test updating book verse counts."""202job = state_store.create_bible_job(books=["john"], config={})203204state_store.update_book_verse_counts(205job.job_id,206"john",207total=879,208completed=100,209failed=2,210)211212book_job = state_store.get_book_job(job.job_id, "john")213assert book_job is not None214assert book_job.total_verses == 879215assert book_job.completed_verses == 100216assert book_job.failed_verses == 2217218219class TestVerseJobManagement:220"""Tests for verse job management."""221222def test_initialize_verse_jobs(self, state_store: JobStateStore) -> None:223"""Test initializing verse jobs for a book."""224job = state_store.create_bible_job(books=["john"], config={})225226verses = [(1, 1), (1, 2), (1, 3), (2, 1)]227state_store.initialize_verse_jobs(job.job_id, "john", verses)228229pending = state_store.get_pending_verses(job.job_id, "john")230assert len(pending) == 4231232def test_atomic_claim_verse_success(self, state_store: JobStateStore) -> None:233"""Test successfully claiming a verse."""234job = state_store.create_bible_job(books=["john"], config={})235state_store.initialize_verse_jobs(job.job_id, "john", [(1, 1)])236237claimed = state_store.atomic_claim_verse(238job.job_id, "john", 1, 1, worker_pid=12345239)240assert claimed is True241242# Verify verse is now in_progress243pending = state_store.get_pending_verses(job.job_id, "john")244assert len(pending) == 0245246def test_atomic_claim_verse_already_claimed(247self, state_store: JobStateStore248) -> None:249"""Test that claiming an already claimed verse fails."""250job = state_store.create_bible_job(books=["john"], config={})251state_store.initialize_verse_jobs(job.job_id, "john", [(1, 1)])252253# First claim succeeds254claim1 = state_store.atomic_claim_verse(255job.job_id, "john", 1, 1, worker_pid=12345256)257assert claim1 is True258259# Second claim fails260claim2 = state_store.atomic_claim_verse(261job.job_id, "john", 1, 1, worker_pid=54321262)263assert claim2 is False264265def test_update_verse_status_completed(266self, state_store: JobStateStore267) -> None:268"""Test marking a verse as completed."""269job = state_store.create_bible_job(books=["john"], config={})270state_store.initialize_verse_jobs(job.job_id, "john", [(1, 1)])271state_store.atomic_claim_verse(job.job_id, "john", 1, 1, 12345)272273state_store.update_verse_status(274job.job_id, "john", 1, 1, JobStatus.COMPLETED275)276277# Verify book counts updated278book_job = state_store.get_book_job(job.job_id, "john")279assert book_job is not None280assert book_job.completed_verses == 1281282def test_update_verse_status_failed(self, state_store: JobStateStore) -> None:283"""Test marking a verse as failed."""284job = state_store.create_bible_job(books=["john"], config={})285state_store.initialize_verse_jobs(job.job_id, "john", [(1, 1)])286state_store.atomic_claim_verse(job.job_id, "john", 1, 1, 12345)287288state_store.update_verse_status(289job.job_id,290"john",2911,2921,293JobStatus.FAILED,294error="Test error",295)296297# Verify book counts updated298book_job = state_store.get_book_job(job.job_id, "john")299assert book_job is not None300assert book_job.failed_verses == 1301302# Verify failed verses can be retrieved303failed = state_store.get_failed_verses(job.job_id, "john")304assert len(failed) == 1305assert failed[0].last_error == "Test error"306307def test_get_book_progress(self, state_store: JobStateStore) -> None:308"""Test getting book progress."""309job = state_store.create_bible_job(books=["john"], config={})310state_store.initialize_verse_jobs(311job.job_id, "john", [(1, 1), (1, 2), (1, 3), (1, 4)]312)313314# Complete 2 verses, fail 1315for i in range(3):316state_store.atomic_claim_verse(job.job_id, "john", 1, i + 1, 12345)317318state_store.update_verse_status(319job.job_id, "john", 1, 1, JobStatus.COMPLETED320)321state_store.update_verse_status(322job.job_id, "john", 1, 2, JobStatus.COMPLETED323)324state_store.update_verse_status(325job.job_id, "john", 1, 3, JobStatus.FAILED326)327328progress = state_store.get_book_progress(job.job_id, "john")329assert progress["total_verses"] == 4330assert progress["completed_verses"] == 2331assert progress["failed_verses"] == 1332assert progress["pending_verses"] == 1333assert progress["progress_percentage"] == 50.0334335def test_reset_orphaned_verses(self, state_store: JobStateStore) -> None:336"""Test resetting orphaned (in_progress) verses."""337job = state_store.create_bible_job(books=["john"], config={})338state_store.initialize_verse_jobs(job.job_id, "john", [(1, 1), (1, 2)])339340# Claim both verses341state_store.atomic_claim_verse(job.job_id, "john", 1, 1, 12345)342state_store.atomic_claim_verse(job.job_id, "john", 1, 2, 12345)343344# Reset orphaned345reset_count = state_store.reset_orphaned_verses(job.job_id)346assert reset_count == 2347348# Verify verses are pending again349pending = state_store.get_pending_verses(job.job_id, "john")350assert len(pending) == 2351352def test_reset_failed_verses(self, state_store: JobStateStore) -> None:353"""Test resetting failed verses for retry."""354job = state_store.create_bible_job(books=["john"], config={})355state_store.initialize_verse_jobs(job.job_id, "john", [(1, 1)])356state_store.atomic_claim_verse(job.job_id, "john", 1, 1, 12345)357state_store.update_verse_status(358job.job_id, "john", 1, 1, JobStatus.FAILED, error="Error"359)360361reset_count = state_store.reset_failed_verses(job.job_id, "john")362assert reset_count == 1363364# Verify verse is pending again365pending = state_store.get_pending_verses(job.job_id, "john")366assert len(pending) == 1367368# Verify book counts updated369book_job = state_store.get_book_job(job.job_id, "john")370assert book_job is not None371assert book_job.failed_verses == 0372373374class TestConcurrency:375"""Tests for concurrent access safety."""376377def test_wal_mode_enabled(self, temp_db: Path) -> None:378"""Test that WAL mode is enabled for better concurrency."""379store = JobStateStore(temp_db)380381conn = sqlite3.connect(str(temp_db))382cursor = conn.execute("PRAGMA journal_mode")383mode = cursor.fetchone()[0]384conn.close()385386assert mode.lower() == "wal"387388389class TestCleanupMethods:390"""Tests for job cleanup methods."""391392def test_delete_job(self, state_store: JobStateStore) -> None:393"""Test deleting a specific job."""394job = state_store.create_bible_job(books=["john"], config={})395state_store.initialize_verse_jobs(job.job_id, "john", [(1, 1), (1, 2)])396397result = state_store.delete_job(job.job_id)398assert result is True399400# Verify job is gone401assert state_store.get_bible_job(job.job_id) is None402assert state_store.get_book_job(job.job_id, "john") is None403assert state_store.get_pending_verses(job.job_id, "john") == []404405def test_delete_nonexistent_job(self, state_store: JobStateStore) -> None:406"""Test deleting a job that doesn't exist."""407result = state_store.delete_job("nonexistent-id")408assert result is False409410def test_delete_completed_jobs_keeps_latest(411self, state_store: JobStateStore412) -> None:413"""Test deleting completed jobs while keeping latest."""414# Create 3 completed jobs415job1 = state_store.create_bible_job(books=["john"], config={})416state_store.update_job_status(417job1.job_id,418JobStatus.COMPLETED,419completed_at=datetime(2024, 1, 1),420)421422job2 = state_store.create_bible_job(books=["matthew"], config={})423state_store.update_job_status(424job2.job_id,425JobStatus.COMPLETED,426completed_at=datetime(2024, 1, 2),427)428429job3 = state_store.create_bible_job(books=["luke"], config={})430state_store.update_job_status(431job3.job_id,432JobStatus.COMPLETED,433completed_at=datetime(2024, 1, 3),434)435436# Delete all but 1 latest437deleted = state_store.delete_completed_jobs(keep_latest=1)438assert deleted == 2439440# Only most recent should remain441assert state_store.get_bible_job(job1.job_id) is None442assert state_store.get_bible_job(job2.job_id) is None443assert state_store.get_bible_job(job3.job_id) is not None444445def test_delete_completed_jobs_keeps_multiple(446self, state_store: JobStateStore447) -> None:448"""Test keeping multiple recent completed jobs."""449jobs = []450for i in range(5):451job = state_store.create_bible_job(books=["john"], config={})452state_store.update_job_status(453job.job_id,454JobStatus.COMPLETED,455completed_at=datetime(2024, 1, i + 1),456)457jobs.append(job)458459deleted = state_store.delete_completed_jobs(keep_latest=2)460assert deleted == 3461462# Verify only last 2 remain463remaining = state_store.list_jobs()464assert len(remaining) == 2465466def test_delete_completed_jobs_no_completed(467self, state_store: JobStateStore468) -> None:469"""Test deleting completed jobs when none are completed."""470job = state_store.create_bible_job(books=["john"], config={})471# Job stays pending472473deleted = state_store.delete_completed_jobs(keep_latest=1)474assert deleted == 0475assert state_store.get_bible_job(job.job_id) is not None476477def test_delete_jobs_older_than(self, state_store: JobStateStore) -> None:478"""Test deleting jobs older than N days."""479from datetime import timedelta480481# Create an old job (manually set created_at)482job1 = state_store.create_bible_job(books=["john"], config={})483484# Manually update the created_at to be old485old_date = (datetime.utcnow() - timedelta(days=10)).isoformat()486with state_store._connection() as conn:487conn.execute(488"UPDATE bible_jobs SET created_at = ? WHERE job_id = ?",489(old_date, job1.job_id),490)491492# Create a recent job493job2 = state_store.create_bible_job(books=["matthew"], config={})494495# Delete jobs older than 5 days496deleted = state_store.delete_jobs_older_than(5)497assert deleted == 1498499assert state_store.get_bible_job(job1.job_id) is None500assert state_store.get_bible_job(job2.job_id) is not None501502def test_delete_jobs_older_than_none_old(503self, state_store: JobStateStore504) -> None:505"""Test deleting old jobs when none are old enough."""506job = state_store.create_bible_job(books=["john"], config={})507508deleted = state_store.delete_jobs_older_than(30)509assert deleted == 0510assert state_store.get_bible_job(job.job_id) is not None511512def test_list_jobs(self, state_store: JobStateStore) -> None:513"""Test listing all jobs."""514job1 = state_store.create_bible_job(books=["john"], config={})515job2 = state_store.create_bible_job(books=["matthew"], config={})516job3 = state_store.create_bible_job(books=["luke"], config={})517518jobs = state_store.list_jobs()519assert len(jobs) == 3520521# Should be ordered by created_at desc522job_ids = [j.job_id for j in jobs]523assert job3.job_id == job_ids[0]524assert job2.job_id == job_ids[1]525assert job1.job_id == job_ids[2]526527def test_list_jobs_with_limit(self, state_store: JobStateStore) -> None:528"""Test listing jobs with a limit."""529for i in range(10):530state_store.create_bible_job(books=["john"], config={})531532jobs = state_store.list_jobs(limit=5)533assert len(jobs) == 5534535def test_get_database_stats(self, state_store: JobStateStore) -> None:536"""Test getting database statistics."""537# Create jobs in various states538job1 = state_store.create_bible_job(books=["john"], config={})539state_store.initialize_verse_jobs(job1.job_id, "john", [(1, 1), (1, 2)])540541job2 = state_store.create_bible_job(books=["matthew"], config={})542state_store.update_job_status(job2.job_id, JobStatus.IN_PROGRESS)543544job3 = state_store.create_bible_job(books=["luke"], config={})545state_store.update_job_status(job3.job_id, JobStatus.COMPLETED)546547job4 = state_store.create_bible_job(books=["mark"], config={})548state_store.update_job_status(job4.job_id, JobStatus.FAILED)549550stats = state_store.get_database_stats()551552assert stats["total_jobs"] == 4553assert stats["pending_jobs"] == 1554assert stats["in_progress_jobs"] == 1555assert stats["completed_jobs"] == 1556assert stats["failed_jobs"] == 1557assert stats["total_verse_records"] == 2558assert stats["database_size_mb"] >= 0559