805 lines
28 KiB
Python
805 lines
28 KiB
Python
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
APP_DIR = ROOT / "web" / "operator-gui"
|
||
INDEX = APP_DIR / "index.html"
|
||
STYLES = APP_DIR / "styles.css"
|
||
APP_JS = APP_DIR / "app.js"
|
||
OPERATOR_LABELS_JS = APP_DIR / "operator-labels.js"
|
||
SUBMISSION_IMPORT_JS = APP_DIR / "submission-import.js"
|
||
EVIDENCE_GUIDANCE_JS = APP_DIR / "evidence-guidance.js"
|
||
OPERATOR_SEARCH_JS = APP_DIR / "operator-search.js"
|
||
PITCH = APP_DIR / "pitch.html"
|
||
PITCH_STYLES = APP_DIR / "pitch.css"
|
||
PITCH_JS = APP_DIR / "pitch.js"
|
||
UI_OVERHAUL_FINAL_RESULTS = ROOT / "data" / "logs" / "ui-overhaul-final-results.json"
|
||
UI_OVERHAUL_FINAL_SCREENSHOTS = [
|
||
"ui-overhaul-desktop-final-queue.png",
|
||
"ui-overhaul-desktop-final-workbench-queries.png",
|
||
"ui-overhaul-desktop-final-knowledge-db.png",
|
||
"ui-overhaul-desktop-final-knowledge-corrections.png",
|
||
"ui-overhaul-mobile-final-queue.png",
|
||
"ui-overhaul-mobile-final-workbench-queries.png",
|
||
"ui-overhaul-mobile-final-knowledge-db.png",
|
||
"ui-overhaul-mobile-final-knowledge-corrections.png",
|
||
]
|
||
|
||
|
||
def _read(path: Path) -> str:
|
||
return path.read_text(encoding="utf-8")
|
||
|
||
|
||
def test_operator_workbench_files_exist():
|
||
assert INDEX.exists()
|
||
assert STYLES.exists()
|
||
assert APP_JS.exists()
|
||
assert OPERATOR_LABELS_JS.exists()
|
||
assert SUBMISSION_IMPORT_JS.exists()
|
||
assert EVIDENCE_GUIDANCE_JS.exists()
|
||
assert OPERATOR_SEARCH_JS.exists()
|
||
|
||
|
||
def test_operator_gui_text_does_not_include_mojibake_fragments():
|
||
suspect_fragments = [
|
||
"Â",
|
||
"Ã",
|
||
"<EFBFBD>",
|
||
"ê",
|
||
"ë",
|
||
"ì",
|
||
"í",
|
||
"?",
|
||
"?¤",
|
||
"?",
|
||
"?´",
|
||
"?¸",
|
||
"?",
|
||
"?¬",
|
||
"?",
|
||
"?",
|
||
]
|
||
|
||
for path in [INDEX, APP_JS, OPERATOR_LABELS_JS, SUBMISSION_IMPORT_JS, EVIDENCE_GUIDANCE_JS, OPERATOR_SEARCH_JS]:
|
||
text = _read(path)
|
||
for fragment in suspect_fragments:
|
||
assert fragment not in text, f"{path.name} contains mojibake fragment {fragment!r}"
|
||
|
||
|
||
def test_pitch_page_presents_copyright_review_flow_with_real_captures():
|
||
html = _read(PITCH)
|
||
styles = _read(PITCH_STYLES)
|
||
script = _read(PITCH_JS)
|
||
|
||
for path in [PITCH, PITCH_STYLES, PITCH_JS]:
|
||
assert path.exists()
|
||
|
||
for required_text in [
|
||
"이미지 저작권 심사를",
|
||
"판별 방식",
|
||
"운영 화면",
|
||
"DB 성장",
|
||
"거버넌스",
|
||
"Mermaid 원본 보기",
|
||
"실제 9500 운영 콘솔",
|
||
"Google",
|
||
"Naver",
|
||
"Ollama",
|
||
]:
|
||
assert required_text in html
|
||
|
||
for asset_name in [
|
||
"case-review.png",
|
||
"evidence-search.png",
|
||
"knowledge-db.png",
|
||
"provider-controls.png",
|
||
"risk-pipeline.svg",
|
||
"decision-loop.svg",
|
||
]:
|
||
assert (APP_DIR / "pitch-assets" / asset_name).exists()
|
||
assert f"pitch-assets/{asset_name}" in html
|
||
|
||
assert "hydrateMetrics" in script
|
||
assert "overflow-x: hidden" in styles
|
||
assert "@media (max-width: 640px)" in styles
|
||
|
||
|
||
def test_workbench_shell_exposes_all_internal_operator_views():
|
||
html = _read(INDEX)
|
||
styles = _read(STYLES)
|
||
|
||
assert "<nav" in html
|
||
assert "<main" in html
|
||
assert 'data-internal-only="true"' in html
|
||
assert 'class="product-purpose"' in html
|
||
assert 'aria-label="제품 목적"' in html
|
||
assert "이미지 저작권 위험 심사" in html
|
||
assert "제출 이미지, 외부 검색 근거, 내부 기준 DB를 한 화면에서 검토합니다." in html
|
||
assert ".product-purpose" in styles
|
||
assert 'class="operator-workflow"' in html
|
||
assert 'aria-label="운영 흐름"' in html
|
||
assert "심사 건 추가" in html
|
||
assert "근거 보강" in html
|
||
assert "운영 결정" in html
|
||
assert "사진 추가 또는 제출 폴더 불러오기" in html
|
||
assert "추천 쿼리로 외부 검색 결과를 보강합니다." in html
|
||
assert "기준 DB에 반영합니다." in html
|
||
assert ".operator-workflow" in styles
|
||
assert "bulk-approve" not in html.lower()
|
||
assert "bulk-reject" not in html.lower()
|
||
|
||
assert 'data-view="evidence"' not in html
|
||
assert 'id="evidence-view"' not in html
|
||
assert 'data-view="corrections"' not in html
|
||
assert 'id="corrections-view"' not in html
|
||
|
||
for view in [
|
||
"queue",
|
||
"workbench",
|
||
"knowledge",
|
||
"providers",
|
||
"audit",
|
||
]:
|
||
assert f'data-view="{view}"' in html
|
||
assert f'id="{view}-view"' in html
|
||
|
||
|
||
def test_static_app_models_core_review_operations():
|
||
script = _read(APP_JS)
|
||
|
||
for state_name in [
|
||
"submissions",
|
||
"providers",
|
||
"knowledgeEntries",
|
||
"collectionCandidates",
|
||
"corrections",
|
||
"auditEvents",
|
||
]:
|
||
assert state_name in script
|
||
|
||
for renderer in [
|
||
"renderQueue",
|
||
"renderCaseReview",
|
||
"renderEvidenceSearch",
|
||
"renderKnowledgeBase",
|
||
"renderCollectionCandidates",
|
||
"renderCorrections",
|
||
"renderProviderControls",
|
||
"renderAuditLog",
|
||
"renderCoverageTabs",
|
||
]:
|
||
assert f"function {renderer}" in script
|
||
|
||
for operation in [
|
||
"selectCase",
|
||
"setDecision",
|
||
"runManualSearch",
|
||
"rerunEnrichment",
|
||
"addKnowledgeEntry",
|
||
"runCandidateCollection",
|
||
"promoteCollectionCandidate",
|
||
"deactivateCorrectionEntry",
|
||
"toggleProvider",
|
||
]:
|
||
assert f"function {operation}" in script
|
||
|
||
|
||
def test_operator_gui_extracts_submission_import_helpers_from_main_app():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
labels = _read(OPERATOR_LABELS_JS)
|
||
helpers = _read(SUBMISSION_IMPORT_JS)
|
||
guidance = _read(EVIDENCE_GUIDANCE_JS)
|
||
search = _read(OPERATOR_SEARCH_JS)
|
||
|
||
assert html.index('src="operator-labels.js"') < html.index('src="submission-import.js"')
|
||
assert html.index('src="submission-import.js"') < html.index('src="app.js"')
|
||
assert html.index('src="evidence-guidance.js"') < html.index('src="app.js"')
|
||
assert html.index('src="operator-search.js"') < html.index('src="app.js"')
|
||
assert "OperatorLabels" in labels
|
||
assert "riskLabels" in labels
|
||
assert "providerLabels" in labels
|
||
assert "} = window.OperatorLabels;" in script
|
||
assert "OperatorSubmissionImport" in helpers
|
||
assert "fileToImagePayload" in helpers
|
||
assert "importedFolderStatusMessage" in helpers
|
||
assert "importedSubmissionStatusMessage" in helpers
|
||
assert "window.OperatorSubmissionImport.fileToImagePayload(file)" in script
|
||
assert "window.OperatorSubmissionImport.importedFolderStatusMessage" in script
|
||
assert "window.OperatorSubmissionImport.importedSubmissionStatusMessage" in script
|
||
assert "OperatorEvidenceGuidance" in guidance
|
||
assert "suggestedEvidenceQueries" in guidance
|
||
assert "evidenceFollowupReasons" in guidance
|
||
assert "window.OperatorEvidenceGuidance.suggestedEvidenceQueries" in script
|
||
assert "window.OperatorEvidenceGuidance.evidenceFollowupReasons" in script
|
||
assert "OperatorSearch" in search
|
||
assert "formatQueryStatus" in search
|
||
assert "formatQueryStrategy" in search
|
||
assert "normalizeManualSearchProvider" in search
|
||
assert "window.OperatorSearch.formatQueryStatus" in script
|
||
assert "window.OperatorSearch.formatQueryStrategy" in script
|
||
assert "window.OperatorSearch.normalizeManualSearchProvider" in script
|
||
|
||
|
||
def test_operator_gui_does_not_boot_with_demo_review_data():
|
||
script = _read(APP_JS)
|
||
|
||
for collection in [
|
||
"submissions",
|
||
"providers",
|
||
"knowledgeEntries",
|
||
"collectionCandidates",
|
||
"corrections",
|
||
"auditEvents",
|
||
]:
|
||
assert f"const {collection} = [];" in script
|
||
|
||
for demo_token in [
|
||
"SUB-1007",
|
||
"ev-1007",
|
||
"kb-iu",
|
||
"DEC-0992",
|
||
]:
|
||
assert demo_token not in script
|
||
|
||
assert "clearRuntimeData()" in script
|
||
assert "renderNoSelectedCase" in script
|
||
assert "state.apiError" in script
|
||
|
||
|
||
def test_design_contract_has_accessibility_risk_states_and_responsive_layouts():
|
||
styles = _read(STYLES)
|
||
|
||
assert ":focus-visible" in styles
|
||
assert "@media (max-width: 980px)" in styles
|
||
assert "@media (max-width: 680px)" in styles
|
||
assert "linear-gradient" not in styles.lower()
|
||
assert "orb" not in styles.lower()
|
||
|
||
for class_name in [
|
||
".risk-high",
|
||
".risk-medium",
|
||
".risk-low",
|
||
".risk-failed",
|
||
".source-naver",
|
||
".source-google",
|
||
".source-llm",
|
||
".source-internal",
|
||
]:
|
||
assert class_name in styles
|
||
|
||
|
||
@pytest.mark.skipif(
|
||
not UI_OVERHAUL_FINAL_RESULTS.exists(),
|
||
reason="local-only audit artifacts: data/ is gitignored, so fresh clones lack them; "
|
||
"regenerate with the live-server Playwright audit before release sign-off",
|
||
)
|
||
def test_ui_overhaul_final_audit_has_no_overflow_and_required_screenshots():
|
||
results = json.loads(_read(UI_OVERHAUL_FINAL_RESULTS))
|
||
|
||
for breakpoint_results in results.values():
|
||
for view_name, view_result in breakpoint_results.items():
|
||
if not isinstance(view_result, dict) or "overflow" not in view_result:
|
||
continue
|
||
|
||
assert view_result["docW"] <= view_result["vw"], view_name
|
||
assert view_result["overflow"] == [], view_name
|
||
|
||
for screenshot_name in UI_OVERHAUL_FINAL_SCREENSHOTS:
|
||
screenshot_path = UI_OVERHAUL_FINAL_RESULTS.parent / screenshot_name
|
||
assert screenshot_path.exists(), screenshot_name
|
||
assert screenshot_path.stat().st_size > 0, screenshot_name
|
||
|
||
|
||
def test_header_coverage_uses_horizontal_filter_tabs_not_scrolling_panel():
|
||
html = _read(INDEX)
|
||
styles = _read(STYLES)
|
||
script = _read(APP_JS)
|
||
|
||
assert 'id="coverage-tabs"' in html
|
||
assert 'id="search-coverage"' not in html
|
||
assert "function renderCoverageTabs" in script
|
||
assert "function applyCoverageFilter" in script
|
||
assert "data-coverage-filter" in script
|
||
assert ".coverage-tabs" in styles
|
||
assert ".coverage-tab" in styles
|
||
assert ".search-coverage" not in styles
|
||
|
||
|
||
def test_queue_uses_grid_row_contract_for_dense_evidence_and_provider_columns():
|
||
html = _read(INDEX)
|
||
styles = _read(STYLES)
|
||
script = _read(APP_JS)
|
||
|
||
assert "queue-grid" in html
|
||
assert "queue-row" in script
|
||
assert ".queue-grid" in styles
|
||
assert ".queue-row" in styles
|
||
assert "grid-template-columns: 28px 64px minmax(104px, 0.68fr) 72px minmax(126px, 0.58fr) minmax(360px, 1.5fr) 82px 76px 90px" in styles
|
||
assert "queue-submission-cell" in script
|
||
assert "queue-risk-cell" in script
|
||
assert "queue-time-cell" in script
|
||
assert "queue-title" not in script
|
||
|
||
|
||
def test_visual_overhaul_removes_obsolete_header_cards_and_fixed_table_widths():
|
||
styles = _read(STYLES)
|
||
|
||
for obsolete_selector in [
|
||
".coverage-main",
|
||
".coverage-badges",
|
||
".coverage-provider-grid",
|
||
".coverage-mini-line",
|
||
]:
|
||
assert obsolete_selector not in styles
|
||
|
||
assert ".queue-table th:nth-child" not in styles
|
||
assert ".queue-row td:nth-child(5)" in styles
|
||
assert ".queue-row td:nth-child(6)" in styles
|
||
|
||
|
||
def test_case_workbench_owns_evidence_and_query_history_as_internal_tabs():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
styles = _read(STYLES)
|
||
|
||
assert 'id="workbench-view"' in html
|
||
assert 'data-workbench-tab="summary"' not in html
|
||
assert 'data-workbench-tab="evidence"' in html
|
||
assert 'data-workbench-tab="queries"' in html
|
||
assert 'data-workbench-tab="decision"' not in html
|
||
assert 'data-workbench-panel="evidence"' in html
|
||
assert 'data-workbench-panel="queries"' in html
|
||
assert 'data-workbench-panel="decision"' not in html
|
||
assert "근거 및 판단" in html
|
||
assert 'id="query-history"' in html
|
||
assert 'id="search-results"' in html
|
||
assert "workbenchTab" in script
|
||
assert 'workbenchTab: "evidence"' in script
|
||
assert "function switchWorkbenchTab" in script
|
||
assert 'switchView("workbench")' in script
|
||
assert 'panelName === nextTab ||' not in script
|
||
assert ".workbench-tabs" in styles
|
||
assert ".workbench-panel" in styles
|
||
|
||
|
||
def test_case_workbench_tabs_do_not_share_the_same_review_layout():
|
||
html = _read(INDEX)
|
||
|
||
evidence_panel = html.split('data-workbench-panel="evidence"', 1)[1].split('data-workbench-panel="queries"', 1)[0]
|
||
query_panel = html.split('data-workbench-panel="queries"', 1)[1].split('id="knowledge-view"', 1)[0]
|
||
|
||
assert 'id="case-image"' in evidence_panel
|
||
assert 'id="evidence-groups"' in evidence_panel
|
||
assert evidence_panel.index('id="recommendation-box"') < evidence_panel.index('id="case-image"')
|
||
assert evidence_panel.index('id="decision-memo"') < evidence_panel.index('id="case-image"')
|
||
assert 'id="query-history"' in query_panel
|
||
|
||
|
||
def test_case_decision_controls_float_while_reviewing_evidence():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
styles = _read(STYLES)
|
||
|
||
assert "floating-decision-panel" in html
|
||
assert 'id="floating-case-score"' in html
|
||
assert "floating-case-score" in script
|
||
assert ".floating-decision-panel" in styles
|
||
assert "position: fixed" in styles
|
||
assert "bottom: 24px" in styles
|
||
assert 'data-workbench-panel="evidence"' in styles
|
||
assert "padding-right: 334px" in styles
|
||
assert ".evidence-layout" in styles
|
||
assert "padding-bottom: 260px" in styles
|
||
assert ".floating-decision-panel .decision-actions" in styles
|
||
|
||
|
||
def test_knowledge_base_rows_separate_title_metadata_chips_and_actions():
|
||
script = _read(APP_JS)
|
||
styles = _read(STYLES)
|
||
|
||
assert "knowledge-main" in script
|
||
assert "knowledge-chip-row" in script
|
||
assert "knowledge-detail-line" in script
|
||
assert "memoInline" in script
|
||
assert "memoBlock" in script
|
||
assert "knowledge-meta" in script
|
||
assert ".knowledge-main" in styles
|
||
assert ".knowledge-chip-row" in styles
|
||
assert ".knowledge-detail-line" in styles
|
||
assert ".knowledge-meta" in styles
|
||
assert ".knowledge-actions" in styles
|
||
assert "grid-template-areas" in styles
|
||
|
||
|
||
def test_correction_history_lives_inside_knowledge_database_panel():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
styles = _read(STYLES)
|
||
|
||
assert 'data-knowledge-tab="collect"' in html
|
||
assert 'data-knowledge-tab="registered"' in html
|
||
assert 'data-knowledge-tab="manual"' in html
|
||
assert 'data-knowledge-tab="corrections"' in html
|
||
assert 'data-knowledge-panel="collect"' in html
|
||
assert 'data-knowledge-panel="registered"' in html
|
||
assert 'data-knowledge-panel="manual"' in html
|
||
assert 'data-knowledge-panel="corrections"' in html
|
||
assert 'id="corrections-list"' in html
|
||
assert "knowledgeTab" in script
|
||
assert "function switchKnowledgeTab" in script
|
||
assert 'switchView("knowledge")' in script
|
||
assert 'switchKnowledgeTab("corrections")' in script
|
||
assert ".knowledge-tabs" in styles
|
||
assert ".knowledge-panel" in styles
|
||
|
||
|
||
def test_knowledge_collection_and_registered_entries_are_separate_tabs():
|
||
html = _read(INDEX)
|
||
|
||
collect_panel = html.split('data-knowledge-panel="collect"', 1)[1].split('data-knowledge-panel="registered"', 1)[0]
|
||
registered_panel = html.split('data-knowledge-panel="registered"', 1)[1].split('data-knowledge-panel="manual"', 1)[0]
|
||
manual_panel = html.split('data-knowledge-panel="manual"', 1)[1].split('data-knowledge-panel="corrections"', 1)[0]
|
||
|
||
assert 'id="candidate-collection-form"' in collect_panel
|
||
assert 'id="collection-candidates"' in collect_panel
|
||
assert 'id="knowledge-list"' not in collect_panel
|
||
assert 'id="knowledge-list"' in registered_panel
|
||
assert 'id="candidate-collection-form"' not in registered_panel
|
||
assert 'id="knowledge-form"' in manual_panel
|
||
|
||
|
||
def test_safety_rules_are_visible_in_ui_contract():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
|
||
assert 'id="decision-memo"' in html
|
||
assert 'id="manual-query-provider"' in html
|
||
assert 'option value="google_search"' not in html
|
||
assert "네이버" in html
|
||
assert "reverse search" not in html.lower()
|
||
assert "sourceEvidenceIds" in script
|
||
assert "requiresMemo" in script
|
||
assert "automatic" in script
|
||
|
||
|
||
def test_provider_quota_zero_does_not_render_nan_meter():
|
||
script = _read(APP_JS)
|
||
|
||
assert "provider.quota ? Math.min" in script
|
||
assert "configuredEnv" in script
|
||
assert "requiredEnv" in script
|
||
|
||
|
||
def test_query_history_rerun_preserves_original_search_provider():
|
||
script = _read(APP_JS)
|
||
search = _read(OPERATOR_SEARCH_JS)
|
||
|
||
assert "data-rerun-provider" in script
|
||
assert "rerunHistoricalQuery" in script
|
||
assert "normalizeManualSearchProvider" in search
|
||
assert "window.OperatorSearch.normalizeManualSearchProvider" in script
|
||
assert 'providerSelect.value = provider' in script
|
||
assert "NaN" not in script
|
||
|
||
|
||
def test_operator_gui_exposes_submission_reload_action():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
|
||
assert 'id="submission-folder"' in html
|
||
assert 'id="submission-image"' in html
|
||
assert 'id="submission-image-name"' in html
|
||
assert 'id="queue-upload-guidance"' in html
|
||
assert 'id="upload-submission-image"' in html
|
||
assert 'id="reload-submissions"' in html
|
||
assert 'id="submission-import-status"' in html
|
||
assert "사진을 추가하면 현재 큐에 새 심사 건으로 들어가고" in html
|
||
assert "/api/submissions/reload" in script
|
||
assert "/api/submissions/import-folder" in script
|
||
assert "/api/submissions/upload-image" in script
|
||
assert "uploadSubmissionImage" in script
|
||
assert "updateSubmissionImageName" in script
|
||
assert 'switchView("workbench")' in script
|
||
assert 'switchWorkbenchTab("evidence")' in script
|
||
assert "새 심사 건으로 바로 선택했습니다." in script
|
||
assert "reloadSubmissions" in script
|
||
|
||
|
||
def test_operator_gui_uses_clear_korean_status_copy_for_queue_imports():
|
||
script = _read(APP_JS) + _read(SUBMISSION_IMPORT_JS)
|
||
|
||
for required_copy in [
|
||
"제출 폴더를 읽는 중입니다.",
|
||
"추가된 제출 없음",
|
||
"건 가져옴",
|
||
"건 추가됨",
|
||
"선택됨",
|
||
"재분석할 제출이 없습니다.",
|
||
"건 재분석을 시작합니다.",
|
||
"재분석 중",
|
||
"건 재분석 완료",
|
||
"사진을 현재 제출 폴더에 넣는 중입니다.",
|
||
"사진이 추가되었습니다.",
|
||
]:
|
||
assert required_copy in script
|
||
|
||
|
||
def test_operator_gui_bulk_rerun_uses_checked_queue_rows():
|
||
script = _read(APP_JS)
|
||
|
||
assert "function selectedBulkSubmissionIds" in script
|
||
assert 'input[data-bulk-id]:checked' in script
|
||
assert "function rerunSelectedEnrichment" in script
|
||
assert 'document.getElementById("bulk-rerun").addEventListener("click", rerunSelectedEnrichment)' in script
|
||
|
||
|
||
def test_operator_gui_manual_knowledge_entry_accepts_reference_image():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
|
||
assert 'id="knowledge-image"' in html
|
||
assert 'id="knowledge-image-name"' in html
|
||
assert 'type="file"' in html
|
||
assert 'accept="image/*"' in html
|
||
assert "updateKnowledgeImageName" in script
|
||
assert "readKnowledgeImage" in script
|
||
assert "/api/knowledge/manual" in script
|
||
assert "imageAsset" in script
|
||
assert "sampleFingerprints" in script
|
||
|
||
|
||
def test_operator_gui_exposes_keyword_candidate_collection_workflow():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
search = _read(OPERATOR_SEARCH_JS)
|
||
|
||
assert 'id="candidate-collection-form"' in html
|
||
assert 'id="collection-query"' in html
|
||
assert 'id="collection-provider"' in html
|
||
assert 'option value="google_search"' not in html
|
||
assert 'id="collection-candidates"' in html
|
||
assert 'id="collection-status"' in html
|
||
assert 'id="select-all-candidates"' in html
|
||
assert 'id="clear-selected-candidates"' in html
|
||
assert "/api/collections/keyword" in script
|
||
assert "/api/collections/candidates/" in script
|
||
assert "/api/collections/candidates/promote-batch" in script
|
||
assert "바로 편입" in script
|
||
assert "선택 후보 묶어서 편입" in html
|
||
assert 'id="collection-promotion-form"' in html
|
||
assert 'id="collection-promotion-name"' in html
|
||
assert 'id="collection-promotion-type"' in html
|
||
assert 'id="collection-promotion-aliases"' in html
|
||
assert 'id="collection-promotion-keywords"' in html
|
||
assert 'id="collection-promotion-memo"' in html
|
||
assert 'id="promote-selected-candidates"' in html
|
||
assert "selectedCollectionCandidateIds" in script
|
||
assert "setAllCollectionCandidateSelection" in script
|
||
assert "promoteSelectedCollectionCandidates" in script
|
||
assert "clearCollectionCandidatesForSearch" in script
|
||
assert "clearCollectionCandidatesAfterAction" in script
|
||
assert "dismissCollectionCandidate" in script
|
||
assert "currentCollectionQuery" in script
|
||
assert "visibleCollectionCandidates" in script
|
||
assert "data-collection-candidate-id" in script
|
||
assert "data-promote-candidate" in script
|
||
assert "data-dismiss-candidate" in script
|
||
assert "candidate-actions" in script
|
||
assert ".candidate-actions" in _read(STYLES)
|
||
assert "candidate-card" in script
|
||
assert "sourceClass(candidate.provider)" in script
|
||
assert "formatCandidateSourceType" in script
|
||
assert "검색 이미지 결과" in script
|
||
assert "페이지 대표 이미지" in script
|
||
assert "formatQueryStrategy" in search
|
||
assert "window.OperatorSearch.formatQueryStrategy" in script
|
||
assert "구글 페이지 제목 기반" in search
|
||
assert "제출 제목/파일명 기반" in search
|
||
|
||
|
||
def test_google_custom_search_is_not_exposed_as_operator_choice():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
|
||
assert "구글 맞춤 검색" not in html
|
||
assert "구글 맞춤 검색" not in script
|
||
assert 'value="google_search"' not in html
|
||
assert "operatorSearchProviders" in script
|
||
assert "visibleProviderControls" in script
|
||
|
||
|
||
def test_audit_target_and_change_columns_have_equal_widths():
|
||
styles = _read(STYLES)
|
||
|
||
assert "--audit-object-width: 24%" in styles
|
||
assert ".audit-table th:nth-child(4)" in styles
|
||
assert ".audit-table th:nth-child(5)" in styles
|
||
assert "width: var(--audit-object-width)" in styles
|
||
|
||
|
||
def test_queue_provider_judgments_render_on_one_line_with_narrower_reason_column():
|
||
script = _read(APP_JS)
|
||
styles = _read(STYLES)
|
||
|
||
assert "provider-strip" in script
|
||
assert "queue-provider-chip" in script
|
||
assert "formatQueueProviderStatus" in script
|
||
assert "근거 있음" in script
|
||
assert "결과 없음" in script
|
||
assert "미실행" in script
|
||
assert ".queue-provider-strip" in styles
|
||
assert "flex-wrap: nowrap" in styles
|
||
assert ".queue-row td:nth-child(6)" in styles
|
||
assert "white-space: nowrap" in styles
|
||
assert "grid-template-columns: 28px 64px minmax(104px, 0.68fr) 72px minmax(126px, 0.58fr) minmax(360px, 1.5fr) 82px 76px 90px" in styles
|
||
|
||
|
||
def test_evidence_operator_status_actions_are_binary_use_or_ignore():
|
||
script = _read(APP_JS)
|
||
|
||
assert '["used_for_judgment", "사용"]' in script
|
||
assert '["ignored", "미사용"]' in script
|
||
assert '["irrelevant",' not in script
|
||
assert '["false_positive",' not in script
|
||
assert '["pending",' not in script
|
||
assert "normalizeEvidenceOperatorStatus" in script
|
||
|
||
|
||
def test_operator_gui_uses_korean_operator_copy_for_visible_labels():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
|
||
for english_label in [
|
||
"Image Rights Operator Console",
|
||
"Review Queue",
|
||
"Case Review",
|
||
"Evidence Search",
|
||
"Knowledge Base",
|
||
"Provider Controls",
|
||
"Audit Log",
|
||
"Automated Recommendation",
|
||
"Fingerprint match",
|
||
"Face/person",
|
||
"LLM summary",
|
||
"Provider failure",
|
||
"Google Web Detection</option>",
|
||
]:
|
||
assert english_label not in html
|
||
|
||
assert "권리 검수 콘솔" in html
|
||
assert "심사 대기열" in html
|
||
assert "얼굴/인물 감지" in html
|
||
assert "공급자" not in html
|
||
assert "외부 검색 tool 활용" in html
|
||
assert "공급자" not in script
|
||
assert "외부 검색 tool" in script
|
||
assert "formatEvidenceTitle" in script
|
||
assert "formatReason" in script
|
||
assert "동일인 판정이 아닙니다" in script
|
||
assert "신뢰도" in script
|
||
assert "근거 ID" in script
|
||
assert "샘플 지문" in script
|
||
|
||
|
||
def test_operator_gui_renders_search_result_links_and_thumbnails():
|
||
script = _read(APP_JS)
|
||
|
||
assert "renderEvidenceLink" in script
|
||
assert "thumbnailUrl" in script
|
||
assert "matchType" in script
|
||
assert "evidence-preview" in script
|
||
assert "search_result_image" in script
|
||
assert "search_result_page_image" in script
|
||
assert "Naver blog search result found" in script
|
||
assert "naver_blog" in script
|
||
assert "Naver web search result found" in script
|
||
assert "naver_web" in script
|
||
assert "Google custom image search result found" in script
|
||
assert "Google custom web search result found" in script
|
||
assert "google_search" in script
|
||
assert "google_best_guess" in script
|
||
assert "google_face_crop_page" in script
|
||
assert "google_face_crop_entity" in script
|
||
assert "searchType" in script
|
||
assert "이미지 유사도" in script
|
||
|
||
|
||
def test_operator_gui_prioritizes_evidence_and_hides_overflow_details():
|
||
script = _read(APP_JS)
|
||
styles = _read(STYLES)
|
||
|
||
assert "renderEvidenceSummary" in script
|
||
assert "renderEvidenceGroup" in script
|
||
assert "topItems" in script
|
||
assert "자세히 보기" in script
|
||
assert "renderEvidenceGroups({ ...submission, evidence: searchableEvidence })" in script
|
||
assert "evidence-summary-board" in script
|
||
assert ".evidence-card-grid" in styles
|
||
assert ".evidence-details" in styles
|
||
|
||
|
||
def test_operator_gui_suggests_followup_queries_for_insufficient_evidence():
|
||
html = _read(INDEX)
|
||
script = _read(APP_JS)
|
||
guidance = _read(EVIDENCE_GUIDANCE_JS)
|
||
combined_script = script + guidance
|
||
styles = _read(STYLES)
|
||
|
||
assert 'id="evidence-next-actions"' in html
|
||
assert "function evidenceNeedsFollowup" in combined_script
|
||
assert "function evidenceFollowupReasons" in combined_script
|
||
assert "function suggestedEvidenceQueries" in combined_script
|
||
assert "function renderEvidenceNextActions" in script
|
||
assert "function applySuggestedQuery" in script
|
||
assert "data-suggested-query" in script
|
||
assert "evidence-followup-reasons" in script
|
||
assert "직접 매칭 또는 원문 페이지 근거가 없습니다." in guidance
|
||
assert "검색 근거가 2건 미만입니다." in guidance
|
||
assert "외부 검색 tool이 빈 결과를 반환했습니다." in guidance
|
||
assert "외부 검색 tool 실패 이력이 있습니다." in guidance
|
||
assert "switchWorkbenchTab(\"queries\")" in script
|
||
assert "/api/search/manual" in script
|
||
assert "추천 쿼리를 입력했습니다" in script
|
||
assert ".evidence-next-action-panel" in styles
|
||
assert ".evidence-followup-reasons" in styles
|
||
assert ".suggested-query-list" in styles
|
||
|
||
|
||
def test_operator_gui_separates_face_crop_web_evidence_from_identity_matching():
|
||
script = _read(APP_JS)
|
||
|
||
assert "faceCropSearch" in script
|
||
assert "face_web" in script
|
||
assert "얼굴 영역 웹 근거" in script
|
||
assert "동일인 판정이 아닙니다" in script
|
||
|
||
|
||
def test_operator_gui_exposes_evidence_status_and_watchlist_controls():
|
||
script = _read(APP_JS)
|
||
styles = _read(STYLES)
|
||
|
||
assert "/api/evidence/" in script
|
||
assert "data-evidence-status" in script
|
||
assert "판단에 사용" in script
|
||
assert "오탐" in script
|
||
assert "주의 후보 근거" in script
|
||
assert "knowledgeEntryStatus" in script
|
||
assert "/promote-watchlist" in script
|
||
assert "/exclude-watchlist" in script
|
||
assert "data-promote-watchlist" in script
|
||
assert "data-exclude-watchlist" in script
|
||
assert ".watchlist-chip" in styles
|
||
|
||
|
||
def test_visual_assets_are_referenced_for_review_images():
|
||
html = _read(INDEX)
|
||
assets_dir = APP_DIR / "assets"
|
||
|
||
assert (assets_dir / "case-portrait.svg").exists()
|
||
assert "assets/case-portrait.svg" in html or "assets/case-portrait.svg" in _read(APP_JS)
|
||
|
||
for orphan_asset in [
|
||
"case-character.svg",
|
||
"case-emblem.svg",
|
||
"match-web.svg",
|
||
]:
|
||
assert not (assets_dir / orphan_asset).exists(), f"{orphan_asset} is unused; do not reintroduce it"
|
||
|
||
|
||
def test_suggested_queries_support_one_click_and_batch_execution():
|
||
script = _read(APP_JS)
|
||
styles = _read(STYLES)
|
||
|
||
assert "executeSuggestedQueries" in script
|
||
assert "data-run-suggested-query" in script
|
||
assert 'id="run-all-suggested-queries"' in script
|
||
assert "모두 실행" in script
|
||
assert "바로 실행" in script
|
||
assert "suggested-query-run-status" in script
|
||
# 기존 "입력칸 채우기" 버튼은 수정용으로 유지된다.
|
||
assert "data-suggested-query" in script
|
||
assert ".suggested-query-item" in styles
|