From 0751fbfa0a656d577791441e565cd638bf483873 Mon Sep 17 00:00:00 2001 From: Ken Date: Tue, 26 May 2026 20:08:22 +0000 Subject: [PATCH] feat: visual artifact runtime for AI-generated inline visualizations - Add bundle/apps/runtime.html: universal runtime that executes AI-generated visualization code in a sandboxed context - Pre-loads CDN libraries: Tabler CSS, Leaflet CSS+JS, Chart.js v4, React 19 UMD, ReactDOM 19 UMD, Babel standalone 7 - Implements mcpBridge with JSON-RPC 2.0 protocol: sendToHost, callTool (with UUID ids + Promise), sendMessage, resize - executeVisual detects JSX/HTML/plain-JS paths via regex - Auto-resize via requestAnimationFrame + scrollHeight + 32, capped at 800 - Initial ui/initialize handshake with runtime='visual-artifact' v1.0.0 - Listens for 'render' and legacy 'tool/input' methods - Add tests/test_runtime_html.py: 61 tests covering all spec requirements --- bundle/apps/runtime.html | 202 +++++++++++++++++++++++++++ tests/test_runtime_html.py | 273 +++++++++++++++++++++++++++++++++++++ 2 files changed, 475 insertions(+) create mode 100644 bundle/apps/runtime.html create mode 100644 tests/test_runtime_html.py diff --git a/bundle/apps/runtime.html b/bundle/apps/runtime.html new file mode 100644 index 0000000..e810bae --- /dev/null +++ b/bundle/apps/runtime.html @@ -0,0 +1,202 @@ + + + + + + Visual Artifact Runtime + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/tests/test_runtime_html.py b/tests/test_runtime_html.py new file mode 100644 index 0000000..4074145 --- /dev/null +++ b/tests/test_runtime_html.py @@ -0,0 +1,273 @@ +""" +Tests for bundle/apps/runtime.html + +Verifies the visual artifact runtime HTML file exists with all required +CDN references, script structures, and message protocol implementations +as specified in task-5. +""" + +import os +import re +import pytest + +HTML_FILE = os.path.join( + os.path.dirname(__file__), + "..", + "bundle", + "apps", + "runtime.html", +) + + +@pytest.fixture(scope="module") +def html_content(): + assert os.path.exists(HTML_FILE), f"runtime.html not found at {HTML_FILE}" + with open(HTML_FILE) as f: + return f.read() + + +class TestFileExists: + def test_runtime_html_exists(self): + assert os.path.exists(HTML_FILE), f"runtime.html not found at {HTML_FILE}" + + def test_is_complete_html_document(self, html_content): + assert "" in html_content or "" in html_content.lower() + assert "" in html_content + + +class TestCDNLibraries: + """Verify all required CDN libraries are present in the head section.""" + + def test_tabler_css_cdn(self, html_content): + assert "https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta20/dist/css/tabler.min.css" in html_content + + def test_leaflet_css_cdn(self, html_content): + assert "https://cdn.jsdelivr.net/npm/leaflet@1.9/dist/leaflet.css" in html_content + + def test_chartjs_cdn(self, html_content): + assert "https://cdn.jsdelivr.net/npm/chart.js@4" in html_content + + def test_leaflet_js_cdn(self, html_content): + assert "https://cdn.jsdelivr.net/npm/leaflet@1.9/dist/leaflet.js" in html_content + + def test_react_19_umd(self, html_content): + # React 19 production UMD + assert "react@19" in html_content + assert "umd" in html_content.lower() or "unpkg.com" in html_content or "cdn.jsdelivr.net" in html_content + + def test_react_dom_19_umd(self, html_content): + # ReactDOM 19 production UMD + assert "react-dom@19" in html_content + + def test_babel_standalone_7(self, html_content): + assert "@babel/standalone@7" in html_content + assert "babel.min.js" in html_content + + +class TestStyles: + """Verify required CSS styles.""" + + def test_transparent_body_background(self, html_content): + # transparent bg + assert "transparent" in html_content + + def test_dark_color_scheme(self, html_content): + assert "color-scheme: dark" in html_content or "color-scheme:dark" in html_content + + def test_body_padding_16px(self, html_content): + assert "16px" in html_content + + def test_visual_error_class(self, html_content): + assert ".visual-error" in html_content + + def test_visual_error_red_color(self, html_content): + # red text color for error class + assert "#ff" in html_content.lower() or "red" in html_content or "color: #" in html_content + + def test_visual_error_monospace(self, html_content): + assert "monospace" in html_content + + def test_visual_error_pre_wrap(self, html_content): + assert "pre-wrap" in html_content + + +class TestBodyStructure: + """Verify body has root div.""" + + def test_root_div_exists(self, html_content): + assert '
' in html_content or "
" in html_content + + +class TestMcpBridge: + """Verify mcpBridge object implementation.""" + + def test_mcp_bridge_declared(self, html_content): + assert "mcpBridge" in html_content + + def test_pending_map(self, html_content): + assert "_pending" in html_content + assert "Map" in html_content + + def test_send_to_host_method(self, html_content): + assert "sendToHost" in html_content + assert "window.parent.postMessage" in html_content + + def test_call_tool_method(self, html_content): + assert "callTool" in html_content + + def test_call_tool_jsonrpc_20(self, html_content): + # jsonrpc key may appear as unquoted JS property (jsonrpc:) or quoted ("jsonrpc" / 'jsonrpc') + assert '"jsonrpc"' in html_content or "'jsonrpc'" in html_content or "jsonrpc:" in html_content + assert '"2.0"' in html_content or "'2.0'" in html_content + + def test_call_tool_method_name(self, html_content): + assert "tools/call" in html_content + + def test_call_tool_returns_promise(self, html_content): + assert "Promise" in html_content + + def test_call_tool_uses_uuid(self, html_content): + # Random UUID for call id + assert "crypto" in html_content or "Math.random" in html_content or "uuid" in html_content.lower() + + def test_send_message_method(self, html_content): + assert "sendMessage" in html_content + assert "ui/sendMessage" in html_content + + def test_send_message_role_user(self, html_content): + assert "role" in html_content + assert "user" in html_content + + def test_resize_method(self, html_content): + assert "resize" in html_content + assert "ui/resize" in html_content + + def test_resize_capped_at_800(self, html_content): + assert "800" in html_content + assert "Math.min" in html_content + + +class TestExecuteVisual: + """Verify executeVisual function implementation.""" + + def test_execute_visual_function(self, html_content): + assert "executeVisual" in html_content + + def test_sets_visual_data_global(self, html_content): + assert "__VISUAL_DATA__" in html_content + + def test_sets_mcp_global(self, html_content): + assert "__MCP__" in html_content + + def test_jsx_detection_capital_tag_regex(self, html_content): + # Regex for JSX detection: /<[A-Z]/ + assert "<[A-Z]" in html_content + + def test_jsx_detection_react_create_element(self, html_content): + assert "React.createElement" in html_content + + def test_jsx_detection_type_text_babel(self, html_content): + # The regex pattern in JS source: /type="text\/babel"/ contains text\/babel + assert 'text/babel' in html_content or r'text\/babel' in html_content + + def test_jsx_path_uses_babel_transform(self, html_content): + assert "Babel.transform" in html_content + assert "react" in html_content + + def test_jsx_path_auto_render_app(self, html_content): + assert "App" in html_content + assert "ReactDOM.createRoot" in html_content + + def test_html_path_detection(self, html_content): + # HTML path: code contains <[a-z] + assert "<[a-z]" in html_content + + def test_html_path_sets_inner_html(self, html_content): + assert "innerHTML" in html_content + + def test_html_path_clones_script_tags(self, html_content): + # Clone script elements for HTML path + assert "createElement" in html_content + assert "script" in html_content.lower() + + def test_html_path_inline_scripts_wrapped(self, html_content): + # Inline scripts wrapped with data and mcp params + assert "__VISUAL_DATA__" in html_content + assert "__MCP__" in html_content + + def test_plain_js_uses_new_function(self, html_content): + assert "new Function" in html_content + + def test_plain_js_passes_chart_arg(self, html_content): + assert '"Chart"' in html_content or "'Chart'" in html_content or "Chart" in html_content + + def test_plain_js_passes_leaflet_arg(self, html_content): + assert '"L"' in html_content or "'L'" in html_content + + def test_auto_resize_via_raf(self, html_content): + assert "requestAnimationFrame" in html_content + + def test_auto_resize_scroll_height(self, html_content): + assert "scrollHeight" in html_content + + def test_auto_resize_adds_32(self, html_content): + assert "32" in html_content + + def test_auto_resize_min_100(self, html_content): + assert "100" in html_content + assert "Math.max" in html_content + + def test_error_renders_visual_error_div(self, html_content): + assert "visual-error" in html_content + + def test_error_resize_120(self, html_content): + assert "120" in html_content + + +class TestMessageListener: + """Verify window message event listener.""" + + def test_message_event_listener(self, html_content): + assert "addEventListener" in html_content + assert '"message"' in html_content or "'message'" in html_content + + def test_pending_response_handler(self, html_content): + # If msg.id in _pending, resolve and delete + assert "_pending" in html_content + assert "resolve" in html_content + assert "delete" in html_content + + def test_render_method_handler(self, html_content): + assert '"render"' in html_content or "'render'" in html_content + + def test_render_calls_execute_visual(self, html_content): + assert "executeVisual" in html_content + + def test_render_params_code(self, html_content): + assert "params" in html_content + assert "code" in html_content + + def test_legacy_tool_input_method(self, html_content): + assert "tool/input" in html_content + + def test_legacy_tool_input_triggers_render(self, html_content): + # Legacy tool/input with code param should call executeVisual + assert "tool/input" in html_content + assert "executeVisual" in html_content + + +class TestInitialHandshake: + """Verify initial ui/initialize handshake is sent on load.""" + + def test_handshake_sent_on_load(self, html_content): + assert "ui/initialize" in html_content + + def test_handshake_runtime_visual_artifact(self, html_content): + assert "visual-artifact" in html_content + + def test_handshake_version_1_0_0(self, html_content): + assert "1.0.0" in html_content + + def test_handshake_uses_send_to_host(self, html_content): + assert "sendToHost" in html_content or "window.parent.postMessage" in html_content