- 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 문서 지도 갱신
85 lines
3.5 KiB
Python
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 를 갱신해야 한다"
|