From d06f715185be63e8f2d7695f640e6009df528c3a Mon Sep 17 00:00:00 2001 From: Sindre Grindheim Date: Wed, 21 Oct 2026 13:26:32 +0200 Subject: [PATCH 1/2] fix: normalize source freshness statuses case-insensitively (dbt Fusion writes `Pass`) dbt Fusion writes source freshness statuses capitalized (`Pass`, `Warn`, `Error`), dbt-core lowercase. `normalized_source_freshness_status` only matched lowercase, so `Pass` reached the report unchanged, the status cell found no entry in the report's status map and the test-results page rendered blank. Lowercase in the macro, which edr applies when reading, so rows already stored are fixed too, and make the source freshness alert's runtime-error check case-insensitive. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../monitor/alerts/source_freshness_alert.py | 9 +++++++-- .../normalized_source_freshness_status.sql | 11 ++++++++--- .../unit/monitor/alerts/test_alert_models.py | 19 +++++++++++++++++-- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/elementary/monitor/alerts/source_freshness_alert.py b/elementary/monitor/alerts/source_freshness_alert.py index 692e79b59..f0b76459b 100644 --- a/elementary/monitor/alerts/source_freshness_alert.py +++ b/elementary/monitor/alerts/source_freshness_alert.py @@ -163,9 +163,14 @@ def asset_type(self) -> str: def concise_name(self) -> str: return f"{self.source_name}.{self.identifier}" + @property + def is_runtime_error(self) -> bool: + # dbt-core writes "runtime error", dbt Fusion writes it capitalized. + return (self.original_status or "").lower() == "runtime error" + @property def error_message(self) -> str: - if self.original_status == "runtime error": + if self.is_runtime_error: return f"Failed to calculate the source freshness\n```{self.error}```" return self.result_description @@ -178,7 +183,7 @@ def summary(self) -> str: if part ] ) - if self.original_status == "runtime error": + if self.is_runtime_error: return f'Failed to calculate the source freshness of "{source_fqn}"' return f'Freshness exceeded the acceptable times on source "{source_fqn}"' diff --git a/elementary/monitor/dbt_project/macros/utils/normalized_source_freshness_status.sql b/elementary/monitor/dbt_project/macros/utils/normalized_source_freshness_status.sql index 20c3e7fb5..c6c0c38be 100644 --- a/elementary/monitor/dbt_project/macros/utils/normalized_source_freshness_status.sql +++ b/elementary/monitor/dbt_project/macros/utils/normalized_source_freshness_status.sql @@ -1,7 +1,12 @@ +{# + dbt-core writes source freshness statuses in lowercase ("pass", "warn", "error", + "runtime error"), while dbt Fusion writes them capitalized ("Pass", "Warn", "Error"). + Compare and return them lowercased, so rows from either engine get the same status. +#} {% macro normalized_source_freshness_status(status_column='status') %} case - when {{ status_column }} = 'error' then 'fail' - when {{ status_column }} = 'runtime error' then 'error' - else {{ status_column }} + when lower({{ status_column }}) = 'error' then 'fail' + when lower({{ status_column }}) = 'runtime error' then 'error' + else lower({{ status_column }}) end as normalized_status {% endmacro %} diff --git a/tests/unit/monitor/alerts/test_alert_models.py b/tests/unit/monitor/alerts/test_alert_models.py index d00bdd0a5..dfd751e03 100644 --- a/tests/unit/monitor/alerts/test_alert_models.py +++ b/tests/unit/monitor/alerts/test_alert_models.py @@ -55,13 +55,15 @@ def _make_model_alert( def _make_source_freshness_alert( - source_name: str = "my_source", identifier: str = "my_table" + source_name: str = "my_source", + identifier: str = "my_table", + original_status: str = "fail", ) -> SourceFreshnessAlertModel: return SourceFreshnessAlertModel( id="id", source_name=source_name, identifier=identifier, - original_status="fail", + original_status=original_status, path="models/src.yml", error=None, alert_class_id="acid", @@ -114,6 +116,19 @@ def test_concise_name_is_source_dot_identifier(self) -> None: alert = _make_source_freshness_alert(source_name="raw", identifier="orders") assert alert.concise_name == "raw.orders" + # dbt-core writes lowercase statuses, dbt Fusion capitalized ones. + @pytest.mark.parametrize("status", ["runtime error", "Runtime Error"]) + def test_runtime_error_status_is_case_insensitive(self, status: str) -> None: + alert = _make_source_freshness_alert(original_status=status) + assert alert.is_runtime_error + assert alert.summary.startswith("Failed to calculate the source freshness") + + @pytest.mark.parametrize("status", ["error", "Error", "warn", "Pass"]) + def test_other_statuses_are_not_runtime_errors(self, status: str) -> None: + alert = _make_source_freshness_alert(original_status=status) + assert not alert.is_runtime_error + assert alert.summary.startswith("Freshness exceeded the acceptable times") + class TestAlertModelDetectedAtTimezone: """detected_at is stored in UTC (naive) in the alerts table and must be From 691f0ab535fbc2258a4254eb5cafe5574be37d24 Mon Sep 17 00:00:00 2001 From: Sindre Grindheim Date: Wed, 21 Oct 2026 13:45:18 +0200 Subject: [PATCH 2/2] test: assert the runtime-error message, add docstrings to new functions Co-Authored-By: Claude Opus 5.5 (1M context) --- elementary/monitor/alerts/source_freshness_alert.py | 5 ++++- tests/unit/monitor/alerts/test_alert_models.py | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/elementary/monitor/alerts/source_freshness_alert.py b/elementary/monitor/alerts/source_freshness_alert.py index f0b76459b..a78bd61dd 100644 --- a/elementary/monitor/alerts/source_freshness_alert.py +++ b/elementary/monitor/alerts/source_freshness_alert.py @@ -165,7 +165,10 @@ def concise_name(self) -> str: @property def is_runtime_error(self) -> bool: - # dbt-core writes "runtime error", dbt Fusion writes it capitalized. + """Whether the freshness check failed to run, in either engine's casing. + + dbt-core writes "runtime error", dbt Fusion writes it capitalized. + """ return (self.original_status or "").lower() == "runtime error" @property diff --git a/tests/unit/monitor/alerts/test_alert_models.py b/tests/unit/monitor/alerts/test_alert_models.py index dfd751e03..62f72487a 100644 --- a/tests/unit/monitor/alerts/test_alert_models.py +++ b/tests/unit/monitor/alerts/test_alert_models.py @@ -116,15 +116,20 @@ def test_concise_name_is_source_dot_identifier(self) -> None: alert = _make_source_freshness_alert(source_name="raw", identifier="orders") assert alert.concise_name == "raw.orders" - # dbt-core writes lowercase statuses, dbt Fusion capitalized ones. @pytest.mark.parametrize("status", ["runtime error", "Runtime Error"]) def test_runtime_error_status_is_case_insensitive(self, status: str) -> None: + """dbt-core writes "runtime error", dbt Fusion "Runtime Error": both are runtime errors.""" alert = _make_source_freshness_alert(original_status=status) assert alert.is_runtime_error + assert ( + alert.error_message + == "Failed to calculate the source freshness\n```None```" + ) assert alert.summary.startswith("Failed to calculate the source freshness") @pytest.mark.parametrize("status", ["error", "Error", "warn", "Pass"]) def test_other_statuses_are_not_runtime_errors(self, status: str) -> None: + """Other statuses, in either casing, get the freshness-exceeded summary.""" alert = _make_source_freshness_alert(original_status=status) assert not alert.is_runtime_error assert alert.summary.startswith("Freshness exceeded the acceptable times")