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 문서 지도 갱신
This commit is contained in:
commit
56a6e2da93
159 changed files with 145825 additions and 0 deletions
85
tests/test_pipeline_preflight_ux.py
Normal file
85
tests/test_pipeline_preflight_ux.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
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 를 갱신해야 한다"
|
||||
Loading…
Add table
Add a link
Reference in a new issue