DMF_Crawler/tests/test_pipeline_preflight_ux.py
Yun Chan 56a6e2da93 chore: 저장소 구조 정리 및 문서화, 첫 커밋
- src/dist 산출물 분리 원칙 정리(.gitignore, .gitattributes)
- 루트 및 주요 폴더(config/scripts/prompts/tests/src, 런타임 폴더 5종)에
  안내용 README.md 추가
- CHANGELOG.md, LICENSE, docs/ops/05-release-and-versioning.md 추가
- docs/README.md 문서 지도 갱신
2026-09-04 09:25:44 +09:00

85 lines
3.5 KiB
Python

from __future__ import annotations
"""R3 사용자 시나리오 RED: API 키 없음은 조용한 실패가 아니라 복구 가능한 BLOCKED 여야 한다."""
from pathlib import Path
from typing import Callable
from dmf_crawler.config import AgyCfg, BackupCfg, Config, LoggingCfg, ReportCfg, SourceCfg, StorageCfg
from dmf_crawler.models import DmfRecord, FetchResult, RunStatus
def test_run_without_service_key_uses_existing_baseline_instead_of_blocking(
tmp_path: Path, monkeypatch, record_factory: Callable[..., DmfRecord]
) -> None:
"""공식 API 모드에서도 인증키 없음은 BLOCKED 가 아니라 기존 기준선 리포트로 폴백한다."""
from dmf_crawler import alerts, pipeline, paths, secrets_dpapi
from dmf_crawler.storage import db, repo
monkeypatch.setattr(secrets_dpapi, "load_service_key", lambda: None)
monkeypatch.setattr(paths, "RUN_LOCK_PATH", tmp_path / "state" / "run.lock")
monkeypatch.setattr(paths, "HEARTBEAT_PATH", tmp_path / "state" / "heartbeat.json")
monkeypatch.setattr(paths, "ALERTS_MIRROR", tmp_path / "state" / "alerts.json")
cfg = Config().with_overrides(
storage=StorageCfg(sqlite_path=str(tmp_path / "data" / "dmf.sqlite3")),
report=ReportCfg(output_dir=str(tmp_path / "reports")),
backup=BackupCfg(dir=str(tmp_path / "backup")),
logging=LoggingCfg(dir=str(tmp_path / "logs"), console=False),
source=SourceCfg(mode="api"),
agy=AgyCfg(enabled=False),
)
baseline_records = (record_factory("20200101-1-A-1-1", ingredient_name="기준성분"),)
conn = db.connect(cfg.db_path())
try:
db.apply_migrations(conn, paths.MIGRATIONS_DIR, cfg.backup_dir())
repo.start_run(conn, "baseline_no_key", "2026-09-02", "manual")
repo.record_fetch_stats(
conn,
"baseline_no_key",
FetchResult(
records=(),
total_count_reported=len(baseline_records),
pages_fetched=1,
pages_expected=1,
http_calls=0,
elapsed_seconds=0.0,
archive_dir=None,
body_signature_ok=True,
payload_sha256="baseline",
),
)
repo.insert_snapshot(conn, "baseline_no_key", baseline_records)
repo.upsert_current_records(conn, "baseline_no_key", baseline_records)
repo.finish_run(
conn,
"baseline_no_key",
"SUCCESS",
0,
"기준선",
is_baseline=True,
integrity_ok=True,
diff_performed=False,
)
finally:
conn.close()
run_id = "run_no_key_uses_baseline"
result = pipeline.run_once(cfg, trigger="manual", force=True, skip_agy=True, run_id=run_id)
assert result.status == RunStatus.PARTIAL
assert result.exit_code == 0
assert result.report_path is not None and result.report_path.exists()
assert any(stage.stage == "preflight" and stage.status == "SUCCESS" for stage in result.stages)
assert any(stage.stage == "fetch" and stage.status == "SKIPPED" for stage in result.stages)
assert any(stage.stage == "report" and stage.status == "SUCCESS" for stage in result.stages)
conn = db.connect(cfg.db_path())
try:
pending = alerts.pending(conn)
finally:
conn.close()
assert all(row.code != "API_KEY_INVALID" for row in pending)
assert paths.HEARTBEAT_PATH.exists(), "PARTIAL 도 운영 기준 heartbeat 를 갱신해야 한다"