diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6f7e784..e91b7a484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Fixed +- Redacted dependency-controlled exception messages and tracebacks from routine articulation-analysis failure logs while retaining the BandScope-owned operation and exception class for bounded diagnostics. - Upgraded the local score PDF parser to `pdfjs-dist` 6.2.108, pinned Undici 7.29.0 across the workspace, and constrained PDF loading to copied in-memory bytes with a same-origin bundled worker and npm-generated lock provenance. ## [0.1.3] - 2026-04-29 diff --git a/services/analysis-engine/src/bandscope_analysis/roles/articulation.py b/services/analysis-engine/src/bandscope_analysis/roles/articulation.py index 7fe118fda..5ce2abff9 100644 --- a/services/analysis-engine/src/bandscope_analysis/roles/articulation.py +++ b/services/analysis-engine/src/bandscope_analysis/roles/articulation.py @@ -18,6 +18,9 @@ - All computations are bounded by the input array sizes. - Fails safe: invalid, empty, or silent audio yields a neutral "mixed" result with zeroed metrics, and no exceptions escape the public API. +- Unexpected dependency failures are logged with only the BandScope-owned + operation and exception class; dependency messages and tracebacks are not + retained in routine logs. """ from __future__ import annotations @@ -138,8 +141,11 @@ def analyze_articulation( "onset_density_per_s": round(onset_density, 3), "duty_cycle": round(duty_cycle, 3), } - except Exception: - logger.warning("Articulation analysis failed; returning safe default", exc_info=True) + except Exception as error: + logger.warning( + "Articulation analysis failed; returning safe default (%s)", + type(error).__name__, + ) return dict(_SAFE_DEFAULT) diff --git a/services/analysis-engine/tests/test_articulation.py b/services/analysis-engine/tests/test_articulation.py index 620bb8104..65ced4113 100644 --- a/services/analysis-engine/tests/test_articulation.py +++ b/services/analysis-engine/tests/test_articulation.py @@ -1,5 +1,6 @@ """Tests for sustained-versus-choppy articulation detection.""" +import logging from typing import Any import numpy as np @@ -99,14 +100,35 @@ def _zero_rms(**_kwargs: Any) -> NDArray[np.float32]: assert analyze_articulation(_sine(1.0), SR) == SAFE_DEFAULT -def test_internal_failure_returns_safe_default(monkeypatch: pytest.MonkeyPatch) -> None: - """No exception escapes: analysis failures return the safe default.""" +def test_internal_failure_returns_payload_safe_default( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + """Unexpected dependency failures stay out of routine articulation logs.""" + sensitive_detail = "/Users/Alice/private-articulation.wav token=super-secret" def _boom(**_kwargs: Any) -> NDArray[np.float32]: - raise RuntimeError("synthetic failure") + raise RuntimeError(sensitive_detail) monkeypatch.setattr(articulation.librosa.onset, "onset_strength", _boom) + caplog.set_level(logging.WARNING, logger=articulation.__name__) + assert analyze_articulation(_sine(1.0), SR) == SAFE_DEFAULT + assert "Articulation analysis failed; returning safe default" in caplog.text + assert "/Users/Alice" not in caplog.text + assert "private-articulation.wav" not in caplog.text + assert "super-secret" not in caplog.text + matching_records = [ + record + for record in caplog.records + if record.name == articulation.__name__ + and record.getMessage().startswith("Articulation analysis failed; returning safe default") + ] + assert len(matching_records) == 1 + assert matching_records[0].getMessage() == ( + "Articulation analysis failed; returning safe default (RuntimeError)" + ) + assert matching_records[0].exc_info is None def test_empty_stems_dict_returns_empty() -> None: