diff --git a/notebooks/code_sharing/markdown_html_logging_regression.ipynb b/notebooks/code_sharing/markdown_html_logging_regression.ipynb new file mode 100644 index 000000000..ed0b230b5 --- /dev/null +++ b/notebooks/code_sharing/markdown_html_logging_regression.ipynb @@ -0,0 +1,364 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "copyright-sc17911", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "\n", + "***\n", + "\n", + "Copyright © 2023-2026 ValidMind Inc. All rights reserved.
\n", + "Refer to [LICENSE](https://github.com/validmind/validmind-library/blob/main/LICENSE) for details.
\n", + "SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial
" + ] + }, + { + "cell_type": "markdown", + "id": "title", + "metadata": {}, + "source": [ + "# Markdown and HTML logging regression (SC-17911)\n", + "\n", + "This notebook verifies the compatibility contract for `log_text()` and `/tracking/log_metadata` after the WAF-safe TeX change. It covers plain Markdown, inline and display TeX, Markdown extensions, mixed Markdown/HTML, explicit HTML, and explicit MathJax HTML.\n", + "\n", + "The first two test matrices are deterministic and do not require credentials or a running backend:\n", + "\n", + "- When the backend advertises `log_metadata_markdown`, Markdown remains raw on the wire and carries `text_format=\"markdown\"`. The SDK does not put generated MathJax `

',\n", + " },\n", + "]\n", + "\n", + "assert {is_html(case[\"text\"]) for case in CASES} == {False, True}\n", + "pd.DataFrame(\n", + " {\n", + " \"case\": [case[\"name\"] for case in CASES],\n", + " \"detected_as\": [\"html\" if is_html(case[\"text\"]) else \"markdown\" for case in CASES],\n", + " }\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "capture-helper", + "metadata": {}, + "outputs": [], + "source": [ + "async def capture_log_text_request(case, supports_markdown):\n", + " \"\"\"Run alog_text while capturing the serialized log_metadata body.\"\"\"\n", + " captured = {}\n", + "\n", + " async def capture_post(path, params=None, data=None, **kwargs):\n", + " captured[\"path\"] = path\n", + " captured[\"params\"] = params\n", + " captured[\"body\"] = json.loads(data)\n", + " return {\"content_id\": captured[\"body\"][\"content_id\"], \"text\": captured[\"body\"].get(\"text\")}\n", + "\n", + " original_flags = client_config.feature_flags\n", + " try:\n", + " client_config.feature_flags = {\"log_metadata_markdown\": supports_markdown}\n", + " with patch.object(api_client, \"_post\", new=capture_post):\n", + " await api_client.alog_text(\n", + " content_id=f'test_description:sc17911_{case[\"name\"]}',\n", + " text=case[\"text\"],\n", + " )\n", + " finally:\n", + " client_config.feature_flags = original_flags\n", + "\n", + " assert captured[\"path\"] == \"log_metadata\"\n", + " return captured[\"body\"]" + ] + }, + { + "cell_type": "markdown", + "id": "new-backend-heading", + "metadata": {}, + "source": [ + "## New backend: WAF-safe Markdown transport\n", + "\n", + "Markdown and TeX must stay raw in the request. Fully formed HTML must retain the legacy pass-through behavior." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "new-backend-matrix", + "metadata": {}, + "outputs": [], + "source": [ + "new_backend_rows = []\n", + "for case in CASES:\n", + " body = await capture_log_text_request(case, supports_markdown=True)\n", + " html_input = is_html(case[\"text\"])\n", + "\n", + " assert body[\"text\"] == case[\"text\"]\n", + " if html_input:\n", + " assert \"text_format\" not in body\n", + " else:\n", + " assert body[\"text_format\"] == \"markdown\"\n", + " assert \"

', + }, + ) + + with patch.dict( + api_client.client_config.feature_flags, + {"log_metadata_markdown": True}, + ): + self.run_async( + api_client.alog_text, + "text_woe_equation", + text=equation, + ) + + mock_post.assert_called_once_with( + f"{os.environ['VM_API_HOST']}/log_metadata", + data=json.dumps( + { + "content_id": "text_woe_equation", + "text": equation, + "text_format": "markdown", + } + ), + ) + request_body = mock_post.call_args.kwargs["data"] + self.assertNotIn("Generated text

") + self.assertIn('