"""Tests for the OCaml SX bridge.""" import asyncio import os import sys import unittest # Add project root to path _project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) if _project_root not in sys.path: sys.path.insert(0, _project_root) from shared.sx.ocaml_bridge import OcamlBridge, OcamlBridgeError, _escape, _parse_response, _serialize_for_ocaml class TestHelpers(unittest.TestCase): """Test helper functions (no subprocess needed).""" def test_escape_basic(self): self.assertEqual(_escape('hello'), 'hello') self.assertEqual(_escape('say "hi"'), 'say \\"hi\\"') self.assertEqual(_escape('a\\b'), 'a\\\\b') self.assertEqual(_escape('line\nbreak'), 'line\\nbreak') def test_parse_response_ok_empty(self): self.assertEqual(_parse_response("(ok)"), ("ok", None)) def test_parse_response_ok_number(self): self.assertEqual(_parse_response("(ok 3)"), ("ok", "3")) def test_parse_response_ok_string(self): kind, val = _parse_response('(ok "
hello
hi
", html) async def test_render_void_element(self): html = await self.bridge.render("(br)") self.assertEqual(html, "yes
") async def test_render_let(self): html = await self.bridge.render('(let (x "hi") (p x))') self.assertEqual(html, "hi
") async def test_render_map(self): html = await self.bridge.render( "(map (lambda (x) (li x)) (list \"a\" \"b\" \"c\"))" ) self.assertEqual(html, "a
b
") async def test_eval_error(self): with self.assertRaises(OcamlBridgeError): await self.bridge.eval("(undefined-symbol-xyz)") async def test_render_component_with_children(self): await self.bridge.load_source( '(defcomp ~wrapper (&rest children) (div :class "wrap" children))' ) html = await self.bridge.render('(~wrapper (p "inside"))') self.assertIn("wrap", html) self.assertIn("inside
", html) async def test_render_macro(self): await self.bridge.load_source( "(defmacro unless (cond &rest body) (list 'if (list 'not cond) (cons 'do body)))" ) html = await self.bridge.render('(unless false (p "shown"))') self.assertEqual(html, "shown
") # ------------------------------------------------------------------ # ListRef regression tests — the `list` primitive returns ListRef # (mutable), not List (immutable). Macro expansions that construct # AST via `list` produce ListRef nodes. The renderer must handle # both List and ListRef at every structural match point. # ------------------------------------------------------------------ async def test_render_macro_generates_cond(self): """Macro that programmatically builds a (cond ...) with list.""" await self.bridge.load_source( "(defmacro pick (x) " " (list 'cond " " (list (list '= x 1) '(p \"one\")) " " (list (list '= x 2) '(p \"two\")) " " (list ':else '(p \"other\"))))" ) html = await self.bridge.render("(pick 2)") self.assertEqual(html, "two
") async def test_render_macro_generates_let(self): """Macro that programmatically builds a (let ...) with list.""" await self.bridge.load_source( "(defmacro with-greeting (name &rest body) " " (list 'let (list (list 'greeting (list 'str \"Hello \" name))) " " (cons 'do body)))" ) html = await self.bridge.render('(with-greeting "World" (p greeting))') self.assertEqual(html, "Hello World
") async def test_render_macro_nested_html_tags(self): """Macro expansion containing nested HTML tags via list.""" await self.bridge.load_source( "(defmacro card (title &rest body) " " (list 'div ':class \"card\" " " (list 'h2 title) " " (cons 'do body)))" ) html = await self.bridge.render('(card "Title" (p "content"))') self.assertIn('content
", html) async def test_render_eval_returns_listref(self): """Values created at runtime via (list ...) are ListRef.""" await self.bridge.load_source( "(define make-items (lambda () (list " ' (list "a") (list "b") (list "c"))))' ) html = await self.bridge.render( "(ul (map (lambda (x) (li (first x))) (make-items)))" ) self.assertIn("