diff --git a/diff_diff/business_report.py b/diff_diff/business_report.py index b3160a6e..f8331ac7 100644 --- a/diff_diff/business_report.py +++ b/diff_diff/business_report.py @@ -353,12 +353,21 @@ def full_report(self) -> str: """Return a structured multi-section markdown report.""" base = _render_full_report(self.to_dict()) if self._include_appendix: + appendix_text = None try: appendix = self._results.summary() - except Exception: # noqa: BLE001 - appendix = None - if appendix: - base = base + "\n\n## Technical Appendix\n\n```\n" + str(appendix) + "\n```\n" + if appendix: + appendix_text = str(appendix) + except Exception as exc: # noqa: BLE001 + appendix_error = type(exc).__name__ or "Exception" + base = ( + base + + "\n\n## Technical Appendix\n\n" + + "Technical appendix unavailable: estimator summary rendering failed " + + f"({appendix_error}).\n" + ) + if appendix_text: + base = base + "\n\n## Technical Appendix\n\n```\n" + appendix_text + "\n```\n" return base def export_markdown(self) -> str: diff --git a/tests/test_business_report.py b/tests/test_business_report.py index 80b04fdf..57e82013 100644 --- a/tests/test_business_report.py +++ b/tests/test_business_report.py @@ -480,6 +480,40 @@ def test_include_appendix_false_omits(self, event_study_fit): md = br.full_report() assert "## Technical Appendix" not in md + def test_appendix_summary_failure_is_visible(self, event_study_fit): + fit, _ = event_study_fit + with patch.object(fit, "summary", side_effect=RuntimeError("internal path /tmp/private")): + br = BusinessReport(fit, auto_diagnostics=False, include_appendix=True) + md = br.full_report() + + assert "## Technical Appendix" in md + assert "Technical appendix unavailable" in md + assert "RuntimeError" in md + + def test_appendix_summary_failure_does_not_leak_exception_details(self, event_study_fit): + fit, _ = event_study_fit + with patch.object(fit, "summary", side_effect=RuntimeError("secret-token-123")): + br = BusinessReport(fit, auto_diagnostics=False, include_appendix=True) + md = br.full_report() + + assert "secret-token-123" not in md + assert "Traceback" not in md + + def test_appendix_stringification_failure_is_visible(self, event_study_fit): + class _BadSummary: + def __str__(self): + raise ValueError("sensitive-render-context") + + fit, _ = event_study_fit + with patch.object(fit, "summary", return_value=_BadSummary()): + br = BusinessReport(fit, auto_diagnostics=False, include_appendix=True) + md = br.full_report() + + assert "## Technical Appendix" in md + assert "Technical appendix unavailable" in md + assert "ValueError" in md + assert "sensitive-render-context" not in md + # --------------------------------------------------------------------------- # BaconDecompositionResults