a6db6c999b
적대 리뷰(10에이전트) 확정 반영: - license_filter.py 신설 — restricted_exclude_sql(raw)/restricted_exclude_orm(ORM) 단일 정의. retrieval _license_sql·digest·briefing·study 풀이가 공유(드리프트 방지). - major: explanation_rag(study 문제 AI 풀이 RAG)에 술어 누락 → doc_meta 쿼리에 ORM 적용(valid_doc_ids 경유로 청크도 차단). briefing/loader 2쿼리에 누락 → digest 와 동일 술어 추가(news restricted 부재=방어적·경로 일관성). - blocker(low-impact): file_watcher changed-doc 경로 material/license 보정(merge 주입· license 부재 시만 — extract_meta clobber 회피, pre-B-4 적재분 동기화). - 테스트: 단일-source 검증 + ORM 구성 스모크 2건 추가. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
3.6 KiB
Python
81 lines
3.6 KiB
Python
"""B-4 — licensed_restricted 차단 술어 + watch 타깃 (material/jurisdiction/license) 매핑 순수 테스트.
|
|
|
|
차단 술어(_license_sql)는 retrieval 3-leg + digest 가 공유하는 단일 술어. 실제 제외 동작은
|
|
GPU 라이브(합성 restricted doc 검색 제외)로 검증 — 여기선 술어 형태 + 매핑 표 계약만.
|
|
[[feedback_external_api_fixture_first]] / [[feedback_structural_integrity_over_path_discipline]]
|
|
"""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "app"))
|
|
|
|
from services.search.license_filter import ( # noqa: E402
|
|
restricted_exclude_orm,
|
|
restricted_exclude_sql,
|
|
)
|
|
from services.search.retrieval_service import _license_sql # noqa: E402
|
|
from workers.file_watcher import _TARGET_AXIS # noqa: E402
|
|
|
|
|
|
def test_shared_predicate_single_source():
|
|
# retrieval/digest/briefing 가 같은 술어 정의를 공유 — drift 방지(단일 source 계약)
|
|
assert _license_sql("d") == " AND " + restricted_exclude_sql("d")
|
|
assert _license_sql("") == " AND " + restricted_exclude_sql("")
|
|
assert restricted_exclude_sql("d").startswith("COALESCE(d.extract_meta")
|
|
|
|
|
|
def test_restricted_exclude_orm_constructs():
|
|
# study 풀이(explanation_rag)용 ORM 표현 — 컴파일 SQL 이 raw 술어와 동일 구조인지
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
clause = restricted_exclude_orm()
|
|
sql = str(clause.compile(dialect=postgresql.dialect(),
|
|
compile_kwargs={"literal_binds": True}))
|
|
assert "extract_meta" in sql
|
|
assert "'license'" in sql and "'restricted'" in sql # JSONB 경로 키
|
|
assert "'false'" in sql and "'true'" in sql # COALESCE 기본 + 비교값
|
|
|
|
|
|
def test_license_sql_shape_with_alias():
|
|
sql = _license_sql("d")
|
|
assert sql.startswith(" AND ") # 항상 ' AND ...' (WHERE 합성용)
|
|
assert "COALESCE(d.extract_meta -> 'license' ->> 'restricted', 'false')" in sql
|
|
assert "<> 'true'" in sql # restricted=true 만 제외
|
|
|
|
|
|
def test_license_sql_shape_no_alias():
|
|
# alias='' = 단일 FROM documents (컬럼 직접 참조)
|
|
sql = _license_sql("")
|
|
assert "COALESCE(extract_meta -> 'license' ->> 'restricted', 'false')" in sql
|
|
assert ".extract_meta" not in sql # 점 없는 컬럼 직접
|
|
|
|
|
|
def test_axis_books_papers_are_restricted():
|
|
for folder, mt in (("Books", "book"), ("Papers_Purchased", "paper")):
|
|
material, jur, lic = _TARGET_AXIS[folder]
|
|
assert material == mt
|
|
assert jur is None # 책/논문 = 관할 없음(A-2 paper NULL 강제와 정합)
|
|
assert lic["scheme"] == "proprietary"
|
|
assert lic["restricted"] is True # RAG/digest 차단 대상
|
|
assert lic["redistribute"] is False
|
|
|
|
|
|
def test_axis_manuals_proprietary_but_not_restricted():
|
|
material, jur, lic = _TARGET_AXIS["Manuals"]
|
|
assert material == "manual"
|
|
assert lic["scheme"] == "proprietary"
|
|
assert lic["restricted"] is False # 사용자 결정 2026-06-13 (검색·RAG 활용)
|
|
|
|
|
|
def test_axis_kgs_law_kr_public_not_restricted():
|
|
material, jur, lic = _TARGET_AXIS["KGS_Code"]
|
|
assert (material, jur) == ("law", "KR")
|
|
assert lic["scheme"] == "kogl"
|
|
assert lic["restricted"] is False # 법정 위임 공공 → 차단 아님
|
|
|
|
|
|
def test_axis_non_target_folder_yields_none():
|
|
# Inbox/Recordings 등 비대상 = (None, None, None) → material/license 미주입
|
|
assert _TARGET_AXIS.get("Inbox", (None, None, None)) == (None, None, None)
|