test(pdf): integration smoke (w/ search + cancel) + vector regression + alnum e2e (#[ignore]) for v0.20 sub-item 1
Step 9 (Group I) of v0.20.0 sub-item 1 (scanned PDF OCR) plan.
I3 — crates/kebab-app/tests/ingest_pdf_ocr_smoke.rs (신규):
- ingest_with_mock_ocr_yields_pdf_ocr_summary — `#[ignore]` real Ollama,
ingest_with_config production path + IngestItem.pdf_ocr_pages verify.
- ocr_text_indexed_and_searchable — `#[ignore]` real Ollama, app.search
의 OCR text indexed verify (§ Acceptance #2).
- ingest_with_cancel_aborts_mid_pdf — production cancel chain (pre-set
cancel=true + dummy endpoint, no panic/deadlock verify).
I4 — crates/kebab-parse-pdf/tests/text_extractor_regression.rs (신규):
- vector_pdf_extract_byte_identical_to_baseline — F4 mojibake.pdf 의 vector
PDF path canonical 의 byte-identical 보존 (Step 1-8 모든 변경 전후 invariant).
- baseline 신규 = tests/snapshots/vector_pdf_canonical.json (first run create).
- normalize_provenance_timestamps inline helper (R-3 mitigation, workspace
전체 부재 — 신규 12-line).
I5 — crates/kebab-parse-pdf/tests/ocr_e2e.rs (신규):
- f1_alnum_accuracy_ge_85 / f2_alnum_accuracy_ge_70 — `#[ignore]` real
Ollama qwen2.5vl:3b, § Acceptance §9 #3 의 implementation.
- alnum metric = strsim::levenshtein (dev-dep 추가).
- truth file copy from PoC scratch (page1.txt + page2-batchim.txt) →
scanned_page1_truth.txt + scanned_page2_truth.txt.
- kebab-parse-image dev-dep 추가 (OllamaVisionOcr::from_parts 호출용).
parser isolation invariant 의 dev-dep exception (spec §3.1, dep graph
baseline -e normal 보존).
spec: docs/superpowers/specs/2026-05-27-pdf-scanned-ocr-spec.md
plan: docs/superpowers/plans/2026-05-27-pdf-scanned-ocr-plan.md (Step 9 I3+I4+I5)
prior: c9e0594 (Step 8 CLI printer)
contract: §9
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
120
crates/kebab-app/tests/ingest_pdf_ocr_smoke.rs
Normal file
120
crates/kebab-app/tests/ingest_pdf_ocr_smoke.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
//! Integration smoke tests for the PDF OCR pipeline (§ Acceptance §9 #1 + #2).
|
||||
//!
|
||||
//! Tests 1 and 2 require a live Ollama endpoint — `#[ignore]` by default.
|
||||
//! Manual invoke:
|
||||
//! KEBAB_PDF_OCR_ENDPOINT=http://192.168.0.47:11434 \
|
||||
//! cargo test -p kebab-app --test ingest_pdf_ocr_smoke --ignored -j 4
|
||||
//!
|
||||
//! Test 3 (cancel) uses a dummy endpoint + pre-set cancel — runs by default
|
||||
//! to verify the cancel wiring doesn't panic/deadlock.
|
||||
|
||||
mod common;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use common::TestEnv;
|
||||
|
||||
fn ollama_endpoint() -> String {
|
||||
std::env::var("KEBAB_PDF_OCR_ENDPOINT")
|
||||
.unwrap_or_else(|_| "http://localhost:11434".to_string())
|
||||
}
|
||||
|
||||
fn make_ocr_env_real() -> TestEnv {
|
||||
let mut env = TestEnv::lexical_only();
|
||||
env.config.pdf.ocr.enabled = true;
|
||||
env.config.pdf.ocr.endpoint = Some(ollama_endpoint());
|
||||
env.config.models.embedding.provider = "none".to_string();
|
||||
|
||||
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("kebab-parse-pdf/tests/fixtures/scanned_page1.pdf");
|
||||
let dest = env.workspace_root.join("scanned_page1.pdf");
|
||||
std::fs::copy(&src, &dest).expect("copy scanned_page1.pdf to workspace");
|
||||
|
||||
env
|
||||
}
|
||||
|
||||
/// § Acceptance §9 #1 — real Ollama OCR + IngestItem.pdf_ocr_pages = Some(1).
|
||||
#[test]
|
||||
#[ignore = "real Ollama qwen2.5vl:3b dependency"]
|
||||
fn ingest_with_mock_ocr_yields_pdf_ocr_summary() {
|
||||
let env = make_ocr_env_real();
|
||||
|
||||
let report = kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
|
||||
.expect("ingest");
|
||||
|
||||
assert!(report.new >= 1, "at least one PDF ingested: {report:?}");
|
||||
|
||||
let items = report.items.unwrap_or_default();
|
||||
let pdf_item = items.iter().find(|i| i.doc_path.0.ends_with(".pdf"));
|
||||
assert!(
|
||||
pdf_item.is_some(),
|
||||
"PDF item must appear in ingest report items: {items:?}"
|
||||
);
|
||||
let pdf_item = pdf_item.unwrap();
|
||||
assert!(
|
||||
pdf_item.pdf_ocr_pages.is_some(),
|
||||
"pdf_ocr_pages must be set for scanned PDF: {pdf_item:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
pdf_item.pdf_ocr_pages.unwrap(),
|
||||
1,
|
||||
"scanned_page1.pdf has exactly 1 page"
|
||||
);
|
||||
}
|
||||
|
||||
/// § Acceptance §9 #2 — OCR text indexed and retrievable via lexical search.
|
||||
#[test]
|
||||
#[ignore = "real Ollama qwen2.5vl:3b dependency"]
|
||||
fn ocr_text_indexed_and_searchable() {
|
||||
let env = make_ocr_env_real();
|
||||
|
||||
kebab_app::ingest_with_config(env.config.clone(), env.scope(), false)
|
||||
.expect("ingest");
|
||||
|
||||
// Search for a Korean morpheme expected to appear in qwen2.5vl:3b OCR
|
||||
// output of the PoC ground-truth page. "다음" is a high-frequency token
|
||||
// in page1.txt truth file.
|
||||
let query = common::lexical_query("다음");
|
||||
let hits =
|
||||
kebab_app::search_with_config(env.config.clone(), query).expect("search");
|
||||
|
||||
assert!(
|
||||
!hits.is_empty(),
|
||||
"OCR-indexed text must surface in lexical search results"
|
||||
);
|
||||
}
|
||||
|
||||
/// Production cancel wiring smoke — pre-set cancel exits before any OCR call.
|
||||
/// Dummy endpoint (port 1 = connection-refused) means OCR HTTP calls would
|
||||
/// fail, but cancel=true prevents the loop from reaching OCR at all.
|
||||
/// Verifies no panic/deadlock regardless of Ok/Err outcome.
|
||||
#[test]
|
||||
fn ingest_with_cancel_aborts_mid_pdf() {
|
||||
let mut env = TestEnv::lexical_only();
|
||||
env.config.pdf.ocr.enabled = true;
|
||||
env.config.pdf.ocr.endpoint = Some("http://127.0.0.1:1".to_string());
|
||||
|
||||
let src = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("kebab-parse-pdf/tests/fixtures/scanned_page1.pdf");
|
||||
let dest = env.workspace_root.join("scanned_page1.pdf");
|
||||
std::fs::copy(&src, &dest).expect("copy scanned_page1.pdf to workspace");
|
||||
|
||||
let cancel = Arc::new(AtomicBool::new(true)); // pre-set — abort immediately
|
||||
|
||||
let result = kebab_app::ingest_with_config_cancellable(
|
||||
env.config.clone(),
|
||||
env.scope(),
|
||||
false,
|
||||
None,
|
||||
Some(cancel),
|
||||
);
|
||||
// Both Ok (pre-cancel exit) and Err (eager OCR engine fail) are acceptable —
|
||||
// key assertion is no panic/deadlock.
|
||||
let _ = result;
|
||||
}
|
||||
@@ -23,7 +23,10 @@ tracing = { workspace = true }
|
||||
lopdf = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
blake3 = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
blake3 = { workspace = true }
|
||||
kebab-parse-image = { path = "../kebab-parse-image" }
|
||||
strsim = "0.11"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
32
crates/kebab-parse-pdf/tests/fixtures/scanned_page1_truth.txt
vendored
Normal file
32
crates/kebab-parse-pdf/tests/fixtures/scanned_page1_truth.txt
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
한국어 OCR 품질 검증을 위한 합성 문서
|
||||
|
||||
본 문서는 Tesseract OCR 의 한국어 인식 정확도를 측정하기 위한 ground truth 입니다. 다양한 한국어 문자 유형을 포함합니다.
|
||||
|
||||
1. 일반 한국어 문장
|
||||
오늘은 2026년 5월 27일 화요일이다. 날씨가 맑고 기온은 섭씨 22도이다. 봄의 끝자락에서 여름이 다가오고 있다.
|
||||
|
||||
2. 한자 혼용
|
||||
大韓民國은 동아시아의 한 국가이다. 首都는 서울特別市이며 인구는 약 5천만 명이다. 國語는 한국어이고 文字는 한글을 사용한다.
|
||||
|
||||
3. 영문 혼용
|
||||
RAG (Retrieval-Augmented Generation) 는 검색 기반 생성 모델이다. Tesseract 는 Google 이 후원하는 OCR 엔진이다. Apache 2.0 라이선스로 배포된다.
|
||||
|
||||
4. 숫자와 기호 혼합
|
||||
- 가격: 12,345원 (USD $9.99)
|
||||
- 좌표: (37.5665, 126.9780)
|
||||
- 비율: 95.7% 정확도 달성
|
||||
- 날짜: 2026-05-27, 14:30:00
|
||||
|
||||
5. 기술 용어
|
||||
RAM 16GB, CPU 8코어, GPU 없음. 디스크 사용량은 250GB 중 51GB. PostgreSQL 와 SQLite 데이터베이스 모두 지원한다.
|
||||
|
||||
6. 짧은 문장 모음
|
||||
- 빛의 속도는 약 30만 km/s 이다.
|
||||
- 지구는 태양 주위를 1년에 한 번 돈다.
|
||||
- 사람의 평균 체온은 36.5도이다.
|
||||
- 한 시간은 60분, 1분은 60초이다.
|
||||
|
||||
7. 어려운 글자
|
||||
의사: 의의있는 의미를 의문스럽게 의도한다.
|
||||
받침이 복잡한 글자: 깎, 닭, 흙, 읊, 닮, 값.
|
||||
이중 모음: 의자, 예의, 외출, 위험, 왜냐하면.
|
||||
53
crates/kebab-parse-pdf/tests/fixtures/scanned_page2_truth.txt
vendored
Normal file
53
crates/kebab-parse-pdf/tests/fixtures/scanned_page2_truth.txt
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
한국어 OCR 의 받침 처리 정확도 측정용 page
|
||||
|
||||
본 페이지는 Tesseract 의 "Detected diacritics" 경고가 가리키는 받침 (종성)
|
||||
인식 약점을 isolate 측정하기 위한 fixture 입니다.
|
||||
|
||||
1. 단순 받침 모음
|
||||
- 단일 자음: 각, 간, 갈, 감, 갑, 갓, 강, 낙, 난, 날, 남, 납, 낫, 낭
|
||||
- 받침 ㄱ: 학생, 식당, 약속, 작품, 음악, 책방, 박사, 직업, 목표, 적극적
|
||||
- 받침 ㄴ: 친구, 인생, 운명, 분야, 단어, 관심, 인간, 진리, 안녕, 인사
|
||||
- 받침 ㄷ: 받침, 듣다, 닫다, 굳다, 곧, 뜻, 멋, 옷, 짓다, 맺다
|
||||
- 받침 ㄹ: 알다, 살다, 일상, 길이, 발음, 별명, 졸업, 길거리, 일요일
|
||||
- 받침 ㅁ: 그림, 사람, 마음, 점심, 봄, 새벽, 검사, 점점, 다음, 처음
|
||||
- 받침 ㅂ: 입학, 답안, 합격, 잡지, 컵, 밥, 십대, 압력, 깁스, 갈증
|
||||
- 받침 ㅅ: 옷, 곳, 멋, 짓, 벗, 깃, 갓, 빗, 솟, 첫째, 갓길, 옷장
|
||||
- 받침 ㅇ: 영화, 강의, 정답, 평균, 동물, 풍경, 송별, 응원, 항상, 통화
|
||||
- 받침 ㅈ: 낮, 빛, 옻, 갖다, 빚, 짓, 맞다, 잊다, 좇다, 갖춤, 짖다
|
||||
- 받침 ㅊ: 꽃, 빛, 낯, 숯, 옻, 닻, 빚, 갖, 닻, 쫓다, 옻나무, 닻줄
|
||||
- 받침 ㅋ: 들녘, 부엌, 들녘길, 부엌일, 들녘 풍경
|
||||
- 받침 ㅌ: 밭, 솥, 끝, 곁, 팥, 받, 솥뚜껑, 밭일, 끝없다, 팥죽
|
||||
- 받침 ㅍ: 잎, 숲, 옆, 짚, 늪, 깊다, 갚다, 높다, 옆자리, 무릎
|
||||
- 받침 ㅎ: 좋다, 많다, 끊다, 닿다, 옳다, 잃다, 싫다, 뚫다, 놓다, 쌓다
|
||||
|
||||
2. 겹받침 (이중 자음)
|
||||
- ㄳ: 몫, 삯, 넋, 몫이, 삯을, 넋이
|
||||
- ㄵ: 앉다, 얹다, 앉아, 얹어, 앉으니
|
||||
- ㄶ: 많다, 끊다, 괜찮다, 않다, 많이, 끊임없이
|
||||
- ㄺ: 닭, 흙, 칡, 읽다, 굵다, 늙다, 닭고기, 흙길
|
||||
- ㄻ: 삶, 닮다, 굶다, 옮기다, 곪다, 삶의, 닮은
|
||||
- ㄼ: 넓다, 짧다, 얇다, 떫다, 넓이, 짧게, 얇은
|
||||
- ㄽ: 외곬, 외곬으로 (드문 받침)
|
||||
- ㄾ: 핥다, 훑다, 핥아, 훑어, 핥는
|
||||
- ㄿ: 읊다, 읊어, 읊는 (드문)
|
||||
- ㅀ: 잃다, 싫다, 끓다, 닳다, 뚫다, 잃어, 싫어
|
||||
- ㅄ: 값, 없다, 가엾다, 값이, 없이, 가엾은
|
||||
|
||||
3. 한자 + 한글 받침 혼용
|
||||
大韓民國의 韓國語는 表音文字 한글로 表記한다. 學生은 學校에서 工夫하고
|
||||
先生님은 講義하신다. 古代 朝鮮의 文化는 中國과 日本에 影響을 주었다.
|
||||
經濟學者들은 物價 上昇을 警告한다. 國民은 政府의 政策을 信賴해야 한다.
|
||||
이 문장의 받침이 한자와 함께 정확히 인식되는지가 중요한 측정 지표다.
|
||||
|
||||
4. 받침 의미 변화 예시
|
||||
- 산다 (live) vs 산 (mountain) vs 살다 (live) vs 살 (flesh)
|
||||
- 가다 (go) vs 간 (liver) vs 갔다 (went) vs 강 (river)
|
||||
- 먹다 (eat) vs 먹 (ink stick) vs 먹었다 (ate) vs 멍 (bruise)
|
||||
- 보다 (see) vs 본 (origin) vs 봤다 (saw) vs 봄 (spring)
|
||||
받침 한 글자 차이로 의미 완전 변경 → OCR 정확도가 검색 결과 직결.
|
||||
|
||||
5. 외래어 받침
|
||||
- 마이크, 컴퓨터, 인터넷, 텔레비전, 라디오, 카메라, 스피커
|
||||
- 시스템, 프로그램, 데이터, 알고리즘, 인공지능, 머신러닝
|
||||
- 박테리아, 바이러스, 백신, 안티바이러스, 방화벽
|
||||
- 파일, 폴더, 디렉토리, 다운로드, 업로드, 백업
|
||||
62
crates/kebab-parse-pdf/tests/ocr_e2e.rs
Normal file
62
crates/kebab-parse-pdf/tests/ocr_e2e.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
// § Acceptance §9 #3: real Ollama qwen2.5vl:3b 의 alnum accuracy.
|
||||
// F1 ≥ 0.85, F2 ≥ 0.70. real Ollama 의존 — `#[ignore]` default.
|
||||
//
|
||||
// Manual invoke:
|
||||
// KEBAB_PDF_OCR_ENDPOINT=http://192.168.0.47:11434 \
|
||||
// cargo test -p kebab-parse-pdf --test ocr_e2e --ignored -j 4
|
||||
|
||||
use kebab_core::Lang;
|
||||
use kebab_parse_image::{OcrEngine, OllamaVisionOcr};
|
||||
use kebab_parse_pdf::extract_dctdecode_page_image;
|
||||
use lopdf::Document;
|
||||
|
||||
fn run_real_ollama_ocr(pdf: &[u8], page: u32) -> anyhow::Result<String> {
|
||||
let endpoint = std::env::var("KEBAB_PDF_OCR_ENDPOINT")
|
||||
.unwrap_or_else(|_| "http://localhost:11434".to_string());
|
||||
let doc = Document::load_mem(pdf)?;
|
||||
let jpeg = extract_dctdecode_page_image(&doc, page)?
|
||||
.ok_or_else(|| anyhow::anyhow!("page {page} 의 DCTDecode image XObject 부재"))?;
|
||||
|
||||
let engine = OllamaVisionOcr::from_parts(
|
||||
endpoint,
|
||||
"qwen2.5vl:3b".to_string(),
|
||||
vec!["eng".to_string(), "kor".to_string()],
|
||||
2048,
|
||||
600,
|
||||
)?;
|
||||
|
||||
let result = engine.recognize(&jpeg, Some(&Lang("kor".into())))?;
|
||||
Ok(result.joined)
|
||||
}
|
||||
|
||||
fn alnum_accuracy(actual: &str, expected: &str) -> f32 {
|
||||
let a: String = actual.chars().filter(|c| c.is_alphanumeric()).collect();
|
||||
let e: String = expected.chars().filter(|c| c.is_alphanumeric()).collect();
|
||||
if e.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let dist = strsim::levenshtein(&a, &e) as f32;
|
||||
((e.chars().count() as f32 - dist) / e.chars().count() as f32).max(0.0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "real Ollama qwen2.5vl:3b dependency"]
|
||||
fn f1_alnum_accuracy_ge_85() {
|
||||
let pdf = include_bytes!("fixtures/scanned_page1.pdf");
|
||||
let ocr = run_real_ollama_ocr(pdf, 1).expect("OCR");
|
||||
let expected = include_str!("fixtures/scanned_page1_truth.txt");
|
||||
let accuracy = alnum_accuracy(&ocr, expected);
|
||||
println!("F1 alnum accuracy = {accuracy:.4}");
|
||||
assert!(accuracy >= 0.85, "F1 alnum accuracy {accuracy:.4} < 0.85");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "real Ollama qwen2.5vl:3b dependency"]
|
||||
fn f2_alnum_accuracy_ge_70() {
|
||||
let pdf = include_bytes!("fixtures/scanned_page2.pdf");
|
||||
let ocr = run_real_ollama_ocr(pdf, 1).expect("OCR");
|
||||
let expected = include_str!("fixtures/scanned_page2_truth.txt");
|
||||
let accuracy = alnum_accuracy(&ocr, expected);
|
||||
println!("F2 alnum accuracy = {accuracy:.4}");
|
||||
assert!(accuracy >= 0.70, "F2 alnum accuracy {accuracy:.4} < 0.70");
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"doc_id": "c90fae7576fe514fb08190cb29d1ef5d",
|
||||
"source_asset_id": "babe9824b6b28237c0898575a40ba48d",
|
||||
"workspace_path": "mojibake.pdf",
|
||||
"title": "mojibake",
|
||||
"lang": "und",
|
||||
"blocks": [],
|
||||
"metadata": {
|
||||
"aliases": [],
|
||||
"tags": [],
|
||||
"created_at": "1970-01-01T00:00:00Z",
|
||||
"updated_at": "1970-01-01T00:00:00Z",
|
||||
"source_type": "paper",
|
||||
"trust_level": "primary",
|
||||
"user_id_alias": null,
|
||||
"user": {
|
||||
"pdf": {
|
||||
"page_count": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"provenance": {
|
||||
"events": [
|
||||
{
|
||||
"at": "1970-01-01T00:00:00Z",
|
||||
"agent": "kb-source-fs",
|
||||
"kind": "discovered",
|
||||
"note": null
|
||||
},
|
||||
{
|
||||
"at": "1970-01-01T00:00:00Z",
|
||||
"agent": "kb-parse-pdf",
|
||||
"kind": "parsed",
|
||||
"note": "parser_version=pdf-text-v1; page_count=0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parser_version": "pdf-text-v1",
|
||||
"schema_version": 1,
|
||||
"doc_version": 1,
|
||||
"last_chunker_version": null,
|
||||
"last_embedding_version": null
|
||||
}
|
||||
70
crates/kebab-parse-pdf/tests/text_extractor_regression.rs
Normal file
70
crates/kebab-parse-pdf/tests/text_extractor_regression.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
//! Byte-identical regression for the vector PDF extraction path (spec §5.4).
|
||||
//! Uses F4 (mojibake.pdf) — the only fixture with extractable text content.
|
||||
//! First invocation creates the baseline snapshot; subsequent runs verify
|
||||
//! identity to detect silent regressions across all Step 1-8 changes.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use kebab_core::{
|
||||
AssetStorage, Checksum, ExtractConfig, ExtractContext, Extractor, MediaType, RawAsset,
|
||||
SourceUri, WorkspacePath, id_for_asset,
|
||||
};
|
||||
use kebab_parse_pdf::PdfTextExtractor;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
/// Normalize all provenance timestamps to UNIX_EPOCH so the snapshot is
|
||||
/// byte-stable across runs (R-3 mitigation — no workspace helper exists).
|
||||
fn normalize_provenance_timestamps(doc: &mut kebab_core::CanonicalDocument) {
|
||||
for event in &mut doc.provenance.events {
|
||||
event.at = OffsetDateTime::UNIX_EPOCH;
|
||||
}
|
||||
}
|
||||
|
||||
fn make_raw_asset(path: &str) -> RawAsset {
|
||||
let fake_hash = "0".repeat(64);
|
||||
let asset_id = id_for_asset(&fake_hash);
|
||||
RawAsset {
|
||||
asset_id,
|
||||
source_uri: SourceUri::File(std::path::PathBuf::from(path)),
|
||||
workspace_path: WorkspacePath::new(path.to_string()).unwrap(),
|
||||
media_type: MediaType::Pdf,
|
||||
byte_len: 0,
|
||||
checksum: Checksum(fake_hash),
|
||||
discovered_at: OffsetDateTime::UNIX_EPOCH,
|
||||
stored: AssetStorage::Copied {
|
||||
path: std::path::PathBuf::from(path),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vector_pdf_extract_byte_identical_to_baseline() {
|
||||
let bytes = include_bytes!("fixtures/mojibake.pdf");
|
||||
let asset = make_raw_asset("mojibake.pdf");
|
||||
let workspace_root = Path::new("/");
|
||||
let config = ExtractConfig::default();
|
||||
let ctx = ExtractContext {
|
||||
asset: &asset,
|
||||
workspace_root,
|
||||
config: &config,
|
||||
};
|
||||
|
||||
let mut canonical = PdfTextExtractor::new()
|
||||
.extract(&ctx, bytes)
|
||||
.expect("PdfTextExtractor::extract");
|
||||
normalize_provenance_timestamps(&mut canonical);
|
||||
|
||||
let actual = serde_json::to_string_pretty(&canonical).expect("serialize canonical");
|
||||
|
||||
let baseline_path = "tests/snapshots/vector_pdf_canonical.json";
|
||||
let baseline = std::fs::read_to_string(baseline_path).unwrap_or_else(|_| {
|
||||
std::fs::create_dir_all("tests/snapshots").ok();
|
||||
std::fs::write(baseline_path, &actual).expect("write baseline snapshot");
|
||||
actual.clone()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
actual, baseline,
|
||||
"vector PDF canonical must be byte-identical to baseline (Step 1-8 regression)"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user