from datetime import datetime, timezone from pathlib import Path from fastapi import FastAPI from fastapi.testclient import TestClient from app.api.schemas import (BatchStatusResponse, CaseCandidate, DetectResponse, EvidenceSpan, ExtractedElements, LegalRiskSignal, MatchResult) from app.engine.taxonomy import load_taxonomy def response(audience): return DetectResponse( audience=audience, doc_id="test", is_infringement=True, confidence=.9, extracted_elements=ExtractedElements(), engine_version="test-engine", taxonomy_version="cases_1.3", analyzed_at=datetime.now(timezone.utc), ccl_basis="A6 판례 2006나16757", legal_risk=LegalRiskSignal( status="review_required", similarity_evidence="A6", protected_expression="not_reviewed", access_evidence="not_provided", precedent_ids=["2006나16757"], judgment_summary="A6 2006나16757", supporting_reasons=["A6 2006나16757"], disclaimer="검토 필요"), matches=[MatchResult(source_doc="source", similarity=.9, case_id="A6", case_title="case", evidence_spans=[EvidenceSpan(start=0,end=3,matched="일치문")], case_candidates=[CaseCandidate(case_id="A6",title="case", handling="technical_detection", representative_precedents=["2006나16757"])])], ) def test_author_http_and_batch_serialization(): app = FastAPI() @app.get('/detect', response_model=DetectResponse) def detect(): return response('author') @app.get('/batch', response_model=BatchStatusResponse) def batch(): return BatchStatusResponse(job_id='job',status='completed',total=1,processed=1, created_at=datetime.now(timezone.utc),results=[response('author')]) with TestClient(app) as client: direct=client.get('/detect').json() nested=client.get('/batch').json()['results'][0] for body in (direct,nested): assert 'A6' not in str(body) and '2006나16757' not in str(body) assert 'case_id' not in body['matches'][0] assert 'legal_risk' not in body assert body['matches'][0]['evidence_spans'][0]['start']==0 assert body['request_id'] and body['taxonomy_version']=='cases_1.3' assert body['ccl_basis']=='확인이 필요한 부분이 있습니다.' def test_admin_preserves_candidates_and_unknown_verdict(): body=response('admin').model_dump() assert body['matches'][0]['case_id']=='A6' assert body['matches'][0]['case_candidates'][0]['representative_precedents']==['2006나16757'] assert body['matches'][0]['publication_verdict'] is None assert body['matches'][0]['publication_verdict_status']=='source_pending' assert response('admin').request_id != response('admin').request_id def test_taxonomy_does_not_invent_missing_verdicts(): tax=load_taxonomy(Path('data/taxonomy')) assert len(tax.cases)==39 assert all(c.publication_verdict is None for c in tax.cases) def test_real_detector_forwards_audience_for_direct_batch_and_review(tmp_path): from app.main import app text = Path('data/reference/ref-0001__어린왕자.txt').read_text() from scripts.run_precision_eval import build_corpus from app.engine.persistent_index import PersistentCorpusIndex from app.engine.detector import PlagiarismDetector from app.core.config import get_settings db = tmp_path / 'corpus.sqlite3' index = tmp_path / 'index' build_corpus([{'author':'reference','segment_id':'ref-1','text':text}], db) PersistentCorpusIndex(db, index).sync() settings = get_settings().model_copy(update={ 'use_persistent_index':True,'corpus_db_path':str(db), 'persistent_index_dir':str(index)}) with TestClient(app) as client: app.state.detector = PlagiarismDetector(settings) payload={'doc_id':'audience-test','text':text,'options':{'audience':'author','threshold':0}} direct=client.post('/v1/plagiarism/detect',json=payload) assert direct.status_code==200 body=direct.json() assert body['audience']=='author' and body['matches'] assert all('case_candidates' not in m for m in body['matches']) batch=client.post('/v1/plagiarism/batch',json={ 'items':[{'doc_id':'batch-author','text':text}], 'options':{'audience':'author','threshold':0}}) result=client.get('/v1/plagiarism/batch/'+batch.json()['job_id']).json() assert result['status']=='completed' assert result['results'][0]['audience']=='author' assert 'legal_risk' not in result['results'][0] review=client.post('/v1/plagiarism/review',json=payload).json() assert review['legal_judgment']['precedent_ids']==[] assert review['legal_judgment']['label']=='확인이 필요한 부분' admin=client.post('/v1/plagiarism/detect',json={ 'doc_id':'admin-test','text':text,'options':{'threshold':0}}).json() assert admin['audience']=='admin' and 'legal_risk' in admin assert any(m['case_candidates'] for m in admin['matches']) assert all(c['publication_verdict'] is None for m in admin['matches'] for c in m['case_candidates']) def test_author_no_match_message_and_invalid_audience(): import pytest from pydantic import ValidationError from app.api.schemas import DetectOptions result = response('author').model_copy(update={'matches': []}) assert result.model_dump()['ccl_basis'] == '등록된 비교 자료에서 일치 구간을 찾지 못했습니다.' with pytest.raises(ValidationError): DetectOptions(audience='public')