OPEN SOURCE
software/tests/test_alignment.py
このサイトで読み、必要なファイルを保存できます。
日常訳はAIを用いて作成された翻訳です。人間の聖書学・原語の専門家による全訳の検証は確認されていません。既存の翻訳や原語資料と照らし合わせてお読みください。
1import json2import pytest3from src.output import alignment as a456def review(text,targets,decision='mapped',**extra):7return dict(text_sha256=a.digest(text),mapped_targets=targets,decision=decision,basis='fixture',reviewer='fixture',**extra)8910def test_lossless_routes():11assert a.route_record('b:1:1','abc',None)==[('b:1:1','abc')]12assert a.route_record('b:1:1','abc',review('abc',[],'same'))==[('b:1:1','abc')]13assert a.route_record('b:1:1','abc',review('abc',['b:1:2']))==[('b:1:2','abc')]14assert a.route_record('b:1:1','abc',review('abc',['b:1:2','b:1:3']))==[]15assert a.route_record('b:1:1','abc',review('abc',[],'mixed'))==[]16for record,message in [(review('abd',[]),'text changed'),(review('abc',[],'guess'),'Unknown review')]:17with pytest.raises(ValueError,match=message):a.route_record('b:1:1','abc',record)18routes=[dict(target='b:1:1',start=0,end=1),dict(target='b:1:2',start=1,end=3)]19assert a.route_record('b:1:1','abc',review('abc',[],routes=routes))==[('b:1:1','a'),('b:1:2','bc')]20with pytest.raises(ValueError,match='Unassigned'):a.route_record('b:1:1','abc',review('abc',[],routes=routes[:1]))21with pytest.raises(ValueError,match='Character loss'):a.route_record('b:1:1','abc',review('abc',[],routes=routes[1:]))222324def test_conflicts_missing_and_held_extras():25records={'b:1:1':'first','b:1:2':'other','b:1:3':'unknown'}26r={'records':{'b:1:2':review('other',['b:1:1']),'b:1:3':review('unknown',[],'unclear')}}27result=a.align(records,{'b:1:1','b:1:2'},r)28assert not result['numbering_ready'] and result['missing_passages']==['b:1:2']29assert len(result['conflicts']['b:1:1'])==230assert result['extra_references']['b:1:3']['status']=='held'31r['records']['b:1:3']=review('unknown',['b:1:1'])32assert a.align(records,{'b:1:1','b:1:2'},r)['extra_references']['b:1:3']['status']=='conflict'333435def test_selections_joins_headings_and_identical_text():36records={'b:1:1':'title','b:1:2':'same','b:1:3':'same','b:1:4':'end','b:1:5':'wrong'}37r={'records':{k:review(v,[target]) for (k,v),target in zip(records.items(),['b:1:0','b:1:1','b:1:1','b:1:2','b:1:2'])},'resolutions':{'b:1:2':dict(expected_candidates=['b:1:4','b:1:5'],sources=['b:1:4'],reason='Accurate closing')}}38result=a.align(records,{'b:1:1','b:1:2'},r)39assert result['numbering_ready']40assert result['aligned_changes']['b:1:0']['text']=='title'41assert result['aligned_changes']['b:1:1']['text']=='same'42assert result['aligned_changes']['b:1:2']['text']=='end'43assert len(result['resolutions']['b:1:2']['all_candidates'])==244assert result['extra_references']['b:1:5']['destinations']['b:1:2']['selected']['sources']==['b:1:4']45r['resolutions']['b:1:2']['sources']=['b:1:4','b:1:5']46assert a.align(records,{'b:1:1','b:1:2'},r)['aligned_changes']['b:1:2']['text']=='endwrong'474849def test_invalid_review_rejected():50with pytest.raises(ValueError,match='unknown source'):a.align({},set(),{'records':{'b:1:1':{}}})51with pytest.raises(ValueError,match='outside site'):a.align({'b:1:1':'a'},set(),{'records':{}})52with pytest.raises(ValueError,match='no candidates'):a.align({},set(),{'records':{},'resolutions':{'b:1:1':{}}})53r={'records':{},'resolutions':{'b:1:1':dict(expected_candidates=['b:1:2'],sources=['b:1:2'],reason='fixture')}}54with pytest.raises(ValueError,match='candidates changed'):a.align({'b:1:1':'a'},{'b:1:1'},r)55r['resolutions']['b:1:1']['expected_candidates']=['b:1:1']56with pytest.raises(ValueError,match='Invalid selection'):a.align({'b:1:1':'a'},{'b:1:1'},r)575859def test_snapshot_and_input_protection(tmp_path,monkeypatch):60source=tmp_path/'source';chapter=source/'b/chapter_01.json';chapter.parent.mkdir(parents=True)61chapter.write_text(json.dumps({'chapter':1,'verses':{'1':{'text':'a'}}}))62records,files=a.read_output(source);assert records=={'b:1:1':'a'}63rp=tmp_path/'review.json';out=tmp_path/'result.json'64monkeypatch.setattr(a,'load_site',lambda:({('b',1,1):'reference'},{'b':'b'}))65r=dict(records={},input_files=files,site_sha256=a.digest(json.dumps({'b:1:1':'reference'},ensure_ascii=False,sort_keys=True)),usage={})66rp.write_text(json.dumps(r));assert a.build_audit(source,rp,out)['numbering_ready']67assert json.loads(out.read_text())['summary']['resolved_site_references']==168for dest in [source/'audit.json',rp]:69with pytest.raises(ValueError,match='overwrite'):a.build_audit(source,rp,dest)70r['site_sha256']='changed';rp.write_text(json.dumps(r))71with pytest.raises(ValueError,match='reference text changed'):a.build_audit(source,rp,out)72chapter.write_text(json.dumps({'chapter':1,'verses':{'1':{'text':'b'}}}))73with pytest.raises(ValueError,match='snapshot changed'):a.build_audit(source,rp,out)74chapter.write_text(json.dumps({'chapter':1,'verses':{'1':{'text':''}}}))75with pytest.raises(ValueError,match='empty translation'):a.read_output(source)76chapter.unlink()77with pytest.raises(ValueError,match='No translations'):a.read_output(source)787980def repair(text, candidates):81return dict(text=text, expected_candidates=a.candidate_hashes(candidates),82reason='Reviewed against mapped source', evidence_sha256='a'*64, verified_by='independent review')838485def test_reviewed_repairs_fill_gaps_and_replace_incomplete_candidates():86records={'b:1:1':'partial'}87r={'records':{},'target_repairs':{'b:1:1':repair('complete',[{'source':'b:1:1','text':'partial'}]),'b:1:2':repair('restored',[])}}88result=a.align(records,{'b:1:1','b:1:2'},r)89assert result['numbering_ready'] and result['summary']['resolved_site_references']==290assert result['aligned_changes']['b:1:2']['restored']91assert result['aligned_changes']['b:1:1']['text']=='complete'92assert result['resolutions']['b:1:1']['all_candidates']==[{'source':'b:1:1','text':'partial'}]93r['target_repairs']['b:1:3']=repair('outside',[])94with pytest.raises(ValueError,match='Repair outside'):a.align(records,{'b:1:1','b:1:2'},r)959697def test_repair_requires_unchanged_candidates_clean_text_and_review_evidence():98item=repair('fixed',[])99with pytest.raises(ValueError,match='candidates changed'):a.validate_repair('b:1:1',item,[{'source':'b:1:1','text':'different'}])100for text in ['', ' ', 'bad\ntext', '<span>', '```', None]:101with pytest.raises(ValueError,match='Invalid repaired text'):a.validate_repair('b:1:1',{**item,'text':text},[])102for field in ['reason','evidence_sha256','verified_by']:103with pytest.raises(ValueError,match='lacks review evidence'):a.validate_repair('b:1:1',{**item,field:''},[])104105106def export_fixture(tmp_path,monkeypatch):107source=tmp_path/'data/output';chapter=source/'b/chapter_01.json';chapter.parent.mkdir(parents=True)108chapter.write_text(json.dumps({'chapter':1,'verses':{'1':{'text':'original'},'2':{'text':'unchanged'}}}))109monkeypatch.setattr(a,'load_site',lambda:({('b',1,1):'first',('b',1,2):'second'},{'b':'bb'}))110_,files=a.read_output(source)111rp=tmp_path/'review.json';audit=tmp_path/'audit.json'112r=dict(records={},input_files=files,site_sha256=a.digest(json.dumps({'b:1:1':'first','b:1:2':'second'},ensure_ascii=False,sort_keys=True)),usage={})113rp.write_text(json.dumps(r));a.build_audit(source,rp,audit)114return source,rp,audit,r115116117def test_repair_source_files_are_bound_to_audit(tmp_path,monkeypatch):118source,rp,audit,r=export_fixture(tmp_path,monkeypatch)119text=source.parent/'sources/hebrew/b.json';text.parent.mkdir(parents=True);text.write_text('source')120r['repair_source_files']={'sources/hebrew/b.json':a.digest('source')};rp.write_text(json.dumps(r))121assert a.build_audit(source,rp,audit)['numbering_ready']122text.write_text('changed')123with pytest.raises(ValueError,match='Repair source text changed'):a.build_audit(source,rp,audit)124r['repair_source_files']={'../other':'hash'};rp.write_text(json.dumps(r))125with pytest.raises(ValueError,match='path outside'):a.build_audit(source,rp,audit)126127128def test_export_complete_release_and_heading(tmp_path,monkeypatch):129source,rp,audit,r=export_fixture(tmp_path,monkeypatch)130result=json.loads(audit.read_text());result['aligned_changes']={'b:1:1':{'text':'fixed'},'b:1:0':{'text':'heading'}};audit.write_text(json.dumps(result))131out=tmp_path/'release';manifest=a.export_aligned(source,audit,out)132assert manifest['verses']==2 and manifest['chapters']==1 and manifest['books']==1133book=json.loads((out/'bb.json').read_text())134assert book['translation']=='日常訳第1版'135assert book['chapters']['1']=={'heading':'heading','verses':{'1':'fixed','2':'unchanged'}}136assert manifest['files']['bb.json']==a.digest((out/'bb.json').read_text())137for dest in [source/'release',source.parent]:138with pytest.raises(ValueError,match='separate'):a.export_aligned(source,audit,dest)139with pytest.raises(ValueError,match='empty'):a.export_aligned(source,audit,out)140result['numbering_ready']=False;audit.write_text(json.dumps(result))141with pytest.raises(ValueError,match='incomplete'):a.export_aligned(source,audit,tmp_path/'bad')142result['numbering_ready']=True;result['site_sha256']='changed';audit.write_text(json.dumps(result))143with pytest.raises(ValueError,match='reference text changed'):a.export_aligned(source,audit,tmp_path/'bad')144result['site_sha256']=r['site_sha256'];result['aligned_changes']['b:1:1']['text']='';audit.write_text(json.dumps(result))145with pytest.raises(ValueError,match='Missing release verse'):a.export_aligned(source,audit,tmp_path/'bad')146