OPEN SOURCE
software/tests/test_orchestrator.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1"""Tests for the job orchestrator."""23from __future__ import annotations45import tempfile6from datetime import datetime7from pathlib import Path8from unittest.mock import MagicMock, patch910import pytest1112from src.orchestration.models import (13ALL_BOOKS,14NEW_TESTAMENT_BOOKS,15OLD_TESTAMENT_BOOKS,16JobStatus,17)18from src.orchestration.orchestrator import JobOrchestrator19from src.orchestration.state import JobStateStore202122@pytest.fixture23def temp_data_dir() -> Path:24"""Create a temporary data directory."""25with tempfile.TemporaryDirectory() as tmpdir:26data_dir = Path(tmpdir)27# Create required subdirectories28(data_dir / "sources" / "greek").mkdir(parents=True)29(data_dir / "output").mkdir(parents=True)30(data_dir / "progress").mkdir(parents=True)31(data_dir / "memory").mkdir(parents=True)32(data_dir / "metadata").mkdir(parents=True)33(data_dir / "config").mkdir(parents=True)34(data_dir / "jobs").mkdir(parents=True)3536# Create minimal config37import json38with (data_dir / "config" / "style.json").open("w") as f:39json.dump({40"equivalence_approach": "optimal",41"readability_level": "general_public",42}, f)4344yield data_dir454647@pytest.fixture48def orchestrator(temp_data_dir: Path) -> JobOrchestrator:49"""Create an orchestrator with temporary directory."""50return JobOrchestrator(51data_dir=temp_data_dir,52max_parallel_books=1,53max_concurrent_api_calls=2,54)555657class TestJobOrchestratorInit:58"""Tests for JobOrchestrator initialization."""5960def test_init(self, temp_data_dir: Path) -> None:61"""Test basic initialization."""62orch = JobOrchestrator(data_dir=temp_data_dir)63assert orch.data_dir == temp_data_dir64assert orch.max_parallel_books == 4 # Default65assert orch.max_concurrent_api_calls == 5 # Default6667def test_custom_parallelism(self, temp_data_dir: Path) -> None:68"""Test initialization with custom parallelism."""69orch = JobOrchestrator(70data_dir=temp_data_dir,71max_parallel_books=8,72max_concurrent_api_calls=10,73)74assert orch.max_parallel_books == 875assert orch.max_concurrent_api_calls == 10767778class TestCreateJob:79"""Tests for job creation."""8081def test_create_job_all_books(self, orchestrator: JobOrchestrator) -> None:82"""Test creating a job for all books."""83job_id = orchestrator.create_job()8485job = orchestrator.state_store.get_bible_job(job_id)86assert job is not None87assert len(job.books) == 6688assert job.status == JobStatus.PENDING8990def test_create_job_specific_books(91self, orchestrator: JobOrchestrator92) -> None:93"""Test creating a job for specific books."""94job_id = orchestrator.create_job(books=["john", "matthew"])9596job = orchestrator.state_store.get_bible_job(job_id)97assert job is not None98assert job.books == ["john", "matthew"]99100def test_create_job_nt_only(self, orchestrator: JobOrchestrator) -> None:101"""Test creating a job for NT only."""102job_id = orchestrator.create_job(nt_only=True)103104job = orchestrator.state_store.get_bible_job(job_id)105assert job is not None106assert len(job.books) == 27107assert job.books == list(NEW_TESTAMENT_BOOKS)108109def test_create_job_ot_only(self, orchestrator: JobOrchestrator) -> None:110"""Test creating a job for OT only."""111job_id = orchestrator.create_job(ot_only=True)112113job = orchestrator.state_store.get_bible_job(job_id)114assert job is not None115assert len(job.books) == 39116assert job.books == list(OLD_TESTAMENT_BOOKS)117118def test_create_job_with_custom_id(119self, orchestrator: JobOrchestrator120) -> None:121"""Test creating a job with a custom ID."""122job_id = orchestrator.create_job(123books=["john"],124job_id="my-custom-job-id",125)126assert job_id == "my-custom-job-id"127128def test_create_job_invalid_book(129self, orchestrator: JobOrchestrator130) -> None:131"""Test that invalid book names raise an error."""132with pytest.raises(ValueError, match="Invalid book names"):133orchestrator.create_job(books=["invalid_book", "john"])134135def test_create_job_config_snapshot(136self, orchestrator: JobOrchestrator, temp_data_dir: Path137) -> None:138"""Test that job captures config snapshot."""139job_id = orchestrator.create_job(books=["john"])140141job = orchestrator.state_store.get_bible_job(job_id)142assert job is not None143assert "style" in job.config_snapshot or "model" in job.config_snapshot144145146class TestGetStatus:147"""Tests for job status retrieval."""148149def test_get_status_existing_job(150self, orchestrator: JobOrchestrator151) -> None:152"""Test getting status of an existing job."""153job_id = orchestrator.create_job(books=["john", "matthew"])154155status = orchestrator.get_status(job_id)156157assert status["job_id"] == job_id158assert status["status"] == "pending"159assert status["total_books"] == 2160assert len(status["books"]) == 2161162def test_get_status_nonexistent_job(163self, orchestrator: JobOrchestrator164) -> None:165"""Test getting status of non-existent job."""166with pytest.raises(ValueError, match="Job not found"):167orchestrator.get_status("nonexistent-id")168169170class TestRunJob:171"""Tests for running jobs."""172173def test_run_nonexistent_job(self, orchestrator: JobOrchestrator) -> None:174"""Test running a non-existent job raises error."""175with pytest.raises(ValueError, match="Job not found"):176orchestrator.run("nonexistent-job")177178def test_run_completed_job(self, orchestrator: JobOrchestrator) -> None:179"""Test running an already completed job raises error."""180job_id = orchestrator.create_job(books=["john"])181orchestrator.state_store.update_job_status(job_id, JobStatus.COMPLETED)182183with pytest.raises(ValueError, match="already completed"):184orchestrator.run(job_id)185186187class TestResumeJob:188"""Tests for job resumption."""189190def test_resume_nonexistent_job(191self, orchestrator: JobOrchestrator192) -> None:193"""Test resuming a non-existent job by ID."""194with pytest.raises(ValueError, match="Job not found"):195orchestrator.resume(job_id="nonexistent-id")196197def test_resume_no_incomplete_jobs(198self, orchestrator: JobOrchestrator199) -> None:200"""Test resuming when no incomplete jobs exist."""201with pytest.raises(ValueError, match="No incomplete jobs"):202orchestrator.resume()203204205class TestStateStoreIntegration:206"""Tests for state store integration."""207208def test_state_persists_across_instances(209self, temp_data_dir: Path210) -> None:211"""Test that state persists across orchestrator instances."""212# Create job with first orchestrator213orch1 = JobOrchestrator(data_dir=temp_data_dir)214job_id = orch1.create_job(books=["john"])215216# Load with second orchestrator217orch2 = JobOrchestrator(data_dir=temp_data_dir)218job = orch2.state_store.get_bible_job(job_id)219220assert job is not None221assert job.books == ["john"]222223224class TestOrphanedVerseRecovery:225"""Tests for orphaned verse recovery."""226227def test_reset_orphaned_on_resume(228self, orchestrator: JobOrchestrator229) -> None:230"""Test that orphaned verses are reset on resume."""231# Create job and initialize verses232job_id = orchestrator.create_job(books=["john"])233orchestrator.state_store.initialize_verse_jobs(234job_id, "john", [(1, 1), (1, 2)]235)236237# Claim verses (simulating in-progress state)238orchestrator.state_store.atomic_claim_verse(239job_id, "john", 1, 1, worker_pid=12345240)241242# Verify verses are in_progress243pending_before = orchestrator.state_store.get_pending_verses(244job_id, "john"245)246assert len(pending_before) == 1 # Only unclaimed verse247248# Reset orphaned verses249reset_count = orchestrator.state_store.reset_orphaned_verses(job_id)250assert reset_count == 1251252# Verify verses are pending again253pending_after = orchestrator.state_store.get_pending_verses(254job_id, "john"255)256assert len(pending_after) == 2257258259class TestConfigSnapshot:260"""Tests for configuration snapshotting."""261262def test_snapshot_includes_model(263self, orchestrator: JobOrchestrator264) -> None:265"""Test that config snapshot includes model info."""266job_id = orchestrator.create_job(books=["john"])267268job = orchestrator.state_store.get_bible_job(job_id)269assert job is not None270assert "model" in job.config_snapshot271272def test_snapshot_includes_style(273self, orchestrator: JobOrchestrator, temp_data_dir: Path274) -> None:275"""Test that config snapshot includes style if available."""276job_id = orchestrator.create_job(books=["john"])277278job = orchestrator.state_store.get_bible_job(job_id)279assert job is not None280assert "style" in job.config_snapshot281282283class TestBookJobCreation:284"""Tests for book job creation."""285286def test_book_jobs_created_with_bible_job(287self, orchestrator: JobOrchestrator288) -> None:289"""Test that book jobs are created when Bible job is created."""290job_id = orchestrator.create_job(books=["john", "matthew", "luke"])291292book_jobs = orchestrator.state_store.get_book_jobs(job_id)293assert len(book_jobs) == 3294295book_names = {bj.book for bj in book_jobs}296assert book_names == {"john", "matthew", "luke"}297298def test_all_book_jobs_pending(299self, orchestrator: JobOrchestrator300) -> None:301"""Test that all book jobs start as pending."""302job_id = orchestrator.create_job(books=["john", "matthew"])303304book_jobs = orchestrator.state_store.get_book_jobs(job_id)305for bj in book_jobs:306assert bj.status == JobStatus.PENDING307