@@ -261,6 +261,108 @@ async def test_helpers_message(kernel: Kernel):
261261 assert "Assistant message" in rendered
262262
263263
264+ async def test_helpers_message_escapes_xml_metacharacters (kernel : Kernel ):
265+ template = """
266+ {{#each chat_history}}
267+ {{#message role=role}}
268+ {{~content~}}
269+ {{/message}}
270+ {{/each}}
271+ """
272+ target = create_handlebars_prompt_template (template , allow_dangerously_set_content = True )
273+ chat_history = ChatHistory ()
274+ chat_history .add_user_message ('What does a < b & a > c & "d" mean?' )
275+
276+ rendered = await target .render (kernel , KernelArguments (chat_history = chat_history ))
277+
278+ assert "<" in rendered
279+ assert "&" in rendered
280+ # ElementTree does not escape > in text content (valid XML); verify round-trip works regardless.
281+ assert "a > c" in rendered or "a > c" in rendered
282+ assert '"d"' in rendered
283+ assert ChatHistory .from_rendered_prompt (rendered ) == chat_history
284+
285+
286+ async def test_helpers_message_uses_text_element (kernel : Kernel ):
287+ """Verify handlebars {{#message}} wraps content in <text> like the Jinja2 path."""
288+ template = """
289+ {{#each chat_history}}
290+ {{#message role=role}}
291+ {{~content~}}
292+ {{/message}}
293+ {{/each}}
294+ """
295+ target = create_handlebars_prompt_template (template , allow_dangerously_set_content = True )
296+ chat_history = ChatHistory ()
297+ chat_history .add_user_message ("User message" )
298+ chat_history .add_assistant_message ("Assistant message" )
299+ rendered = await target .render (kernel , KernelArguments (chat_history = chat_history ))
300+ assert '<message role="user"><text>User message</text></message>' in rendered
301+ assert '<message role="assistant"><text>Assistant message</text></message>' in rendered
302+ chat_history2 = ChatHistory .from_rendered_prompt (rendered )
303+ assert chat_history2 == chat_history
304+
305+
306+ async def test_helpers_message_empty_content (kernel : Kernel ):
307+ """Empty message content should produce <message role="..."></message>, not self-closing."""
308+ template = """
309+ {{#each chat_history}}
310+ {{#message role=role}}
311+ {{~content~}}
312+ {{/message}}
313+ {{/each}}
314+ """
315+ target = create_handlebars_prompt_template (template , allow_dangerously_set_content = True )
316+ chat_history = ChatHistory ()
317+ chat_history .add_user_message ("" )
318+ rendered = await target .render (kernel , KernelArguments (chat_history = chat_history ))
319+ assert "<message" in rendered
320+ assert "/>" not in rendered
321+ assert ChatHistory .from_rendered_prompt (rendered ) is not None
322+
323+
324+ async def test_helpers_message_fallback_empty_content (kernel : Kernel ):
325+ """Fallback path (non-ChatMessageContent context) with empty block content.
326+
327+ Should produce <message role="..."></message>, not self-closing.
328+ """
329+ template = '{{#message role="user"}}{{/message}}'
330+ target = create_handlebars_prompt_template (template , allow_dangerously_set_content = True )
331+ rendered = await target .render (kernel , KernelArguments ())
332+ assert '<message role="user"></message>' in rendered
333+ assert "/>" not in rendered
334+ assert ChatHistory .from_rendered_prompt (rendered ) is not None
335+
336+
337+ async def test_helpers_message_fallback_with_content (kernel : Kernel ):
338+ """Fallback path wraps block content in a <text> child element."""
339+ template = '{{#message role="user"}}Hello world{{/message}}'
340+ target = create_handlebars_prompt_template (template , allow_dangerously_set_content = True )
341+ rendered = await target .render (kernel , KernelArguments ())
342+ assert '<message role="user"><text>Hello world</text></message>' in rendered
343+ chat_history = ChatHistory .from_rendered_prompt (rendered )
344+ assert chat_history is not None
345+ assert len (chat_history ) == 1
346+ assert chat_history [0 ].content == "Hello world"
347+
348+
349+ async def test_helpers_message_escapes_greater_than (kernel : Kernel ):
350+ """ElementTree does not escape > in text; verify round-trip still works."""
351+ template = """
352+ {{#each chat_history}}
353+ {{#message role=role}}
354+ {{~content~}}
355+ {{/message}}
356+ {{/each}}
357+ """
358+ target = create_handlebars_prompt_template (template , allow_dangerously_set_content = True )
359+ chat_history = ChatHistory ()
360+ chat_history .add_user_message ("Is a > b true?" )
361+ rendered = await target .render (kernel , KernelArguments (chat_history = chat_history ))
362+ assert "a > b" in rendered or "a > b" in rendered
363+ assert ChatHistory .from_rendered_prompt (rendered ) == chat_history
364+
365+
264366async def test_helpers_message_to_prompt (kernel : Kernel ):
265367 template = """{{#each chat_history}}{{message_to_prompt}} {{/each}}"""
266368 target = create_handlebars_prompt_template (template , allow_dangerously_set_content = True )
0 commit comments