|
| 1 | +"""Tests for sources v3 parser: resilient freshness Status enums. |
| 2 | +
|
| 3 | +Regression coverage for dbt 2.0 (>= 2.0.0-preview.202) emitting CAPITALIZED |
| 4 | +freshness statuses ("Pass", "Error") where earlier dbt emitted lowercase. |
| 5 | +Previously every result failed validation and the entire sources.json was |
| 6 | +silently dropped during ingestion (observed live: harvestgroup, 50 runs/day). |
| 7 | +Same defect class as run_results_v6 ``reused`` (AI-7435 / PR #106). |
| 8 | +""" |
| 9 | +from vendor.dbt_artifacts_parser.parser import parse_sources |
| 10 | +from vendor.dbt_artifacts_parser.parsers.sources.sources_v3 import Status |
| 11 | +from vendor.dbt_artifacts_parser.parsers.sources.sources_v3 import Status1 |
| 12 | + |
| 13 | +V3_SCHEMA = "https://schemas.getdbt.com/dbt/sources/v3.json" |
| 14 | + |
| 15 | + |
| 16 | +def _freshness_result(status: str, unique_id: str) -> dict: |
| 17 | + return { |
| 18 | + "unique_id": unique_id, |
| 19 | + "max_loaded_at": "2026-07-28T00:00:00Z", |
| 20 | + "snapshotted_at": "2026-07-28T01:00:00Z", |
| 21 | + "max_loaded_at_time_ago_in_s": 3600.0, |
| 22 | + "status": status, |
| 23 | + "criteria": { |
| 24 | + "warn_after": {"count": 12, "period": "hour"}, |
| 25 | + "error_after": {"count": 24, "period": "hour"}, |
| 26 | + }, |
| 27 | + "adapter_response": {}, |
| 28 | + "timing": [], |
| 29 | + "thread_id": "Thread-1", |
| 30 | + "execution_time": 1.0, |
| 31 | + } |
| 32 | + |
| 33 | + |
| 34 | +def _sources(results: list) -> dict: |
| 35 | + return { |
| 36 | + "metadata": { |
| 37 | + "dbt_schema_version": V3_SCHEMA, |
| 38 | + "dbt_version": "2.0.0-preview.202", |
| 39 | + "generated_at": "2026-07-28T01:00:00Z", |
| 40 | + "invocation_id": "11111111-1111-1111-1111-111111111111", |
| 41 | + "env": {}, |
| 42 | + }, |
| 43 | + "results": results, |
| 44 | + "elapsed_time": 2.0, |
| 45 | + } |
| 46 | + |
| 47 | + |
| 48 | +def test_capitalized_pass_and_error_parse_and_canonicalize(): |
| 49 | + """The exact live failure: dbt 2.0 'Pass'/'Error' must parse, and known |
| 50 | + statuses canonicalize to their lowercase members.""" |
| 51 | + parsed = parse_sources( |
| 52 | + _sources( |
| 53 | + [ |
| 54 | + _freshness_result("Pass", "source.p.s.a"), |
| 55 | + _freshness_result("Error", "source.p.s.b"), |
| 56 | + ] |
| 57 | + ) |
| 58 | + ) |
| 59 | + statuses = [r.status for r in parsed.results] |
| 60 | + assert statuses[0] is Status1.pass_ |
| 61 | + assert statuses[0].value == "pass" |
| 62 | + assert statuses[1] is Status1.error |
| 63 | + assert statuses[1].value == "error" |
| 64 | + |
| 65 | + |
| 66 | +def test_lowercase_statuses_still_parse_unchanged(): |
| 67 | + parsed = parse_sources( |
| 68 | + _sources( |
| 69 | + [ |
| 70 | + _freshness_result("pass", "source.p.s.a"), |
| 71 | + _freshness_result("warn", "source.p.s.b"), |
| 72 | + _freshness_result("error", "source.p.s.c"), |
| 73 | + ] |
| 74 | + ) |
| 75 | + ) |
| 76 | + assert [r.status.value for r in parsed.results] == ["pass", "warn", "error"] |
| 77 | + |
| 78 | + |
| 79 | +def test_unknown_future_status_surfaces_as_member(): |
| 80 | + """Forward-compat: a status dbt invents later must not drop the file.""" |
| 81 | + parsed = parse_sources(_sources([_freshness_result("Stale", "source.p.s.a")])) |
| 82 | + assert parsed.results[0].status.value == "Stale" |
| 83 | + |
| 84 | + |
| 85 | +def test_runtime_error_arm_canonicalizes_case_but_stays_narrow(): |
| 86 | + """The runtime-error union arm accepts case variants of its one value and |
| 87 | + nothing else — it must not swallow freshness rows.""" |
| 88 | + assert Status("Runtime Error") is Status.runtime_error |
| 89 | + assert Status1("Runtime Error") is Status1.runtime_error |
| 90 | + runtime_row = {"unique_id": "source.p.s.x", "status": "runtime error", "error": "boom"} |
| 91 | + parsed = parse_sources(_sources([runtime_row])) |
| 92 | + assert parsed.results[0].status.value == "runtime error" |
0 commit comments