diff --git a/packages/host/tests/integration/components/room-message-test.gts b/packages/host/tests/integration/components/room-message-test.gts
index c7a698d196..922151a107 100644
--- a/packages/host/tests/integration/components/room-message-test.gts
+++ b/packages/host/tests/integration/components/room-message-test.gts
@@ -23,11 +23,12 @@ module('Integration | Component | RoomMessage', function (hooks) {
isStreaming: boolean,
timeAgoForCreated: number,
timeAgoForUpdated: number,
+ messageContent: string,
) {
let message = {
author: { userId: '@aibot:localhost' },
- message: 'Hello,',
- formattedMessage: 'Hello, ',
+ message: messageContent,
+ formattedMessage: messageContent,
created: new Date(new Date().getTime() - timeAgoForCreated * 60 * 1000),
updated: new Date(new Date().getTime() - timeAgoForUpdated * 60 * 1000),
};
@@ -67,7 +68,7 @@ module('Integration | Component | RoomMessage', function (hooks) {
}
test('it shows an error when AI bot message streaming timeouts', async function (assert) {
- let testScenario = await setupTestScenario(true, 2, 1); // Streaming, created 2 mins ago, updated 1 min ago
+ let testScenario = await setupTestScenario(true, 2, 1, 'Hello,'); // Streaming, created 2 mins ago, updated 1 min ago
await renderRoomMessageComponent(testScenario);
await waitUntil(
@@ -85,7 +86,7 @@ module('Integration | Component | RoomMessage', function (hooks) {
});
test('it does not show an error when last streaming chunk is still within reasonable time limit', async function (assert) {
- let testScenario = await setupTestScenario(true, 2, 0.5); // Streaming, created 2 mins ago, updated 30 seconds ago
+ let testScenario = await setupTestScenario(true, 2, 0.5, 'Hello,'); // Streaming, created 2 mins ago, updated 30 seconds ago
await renderRoomMessageComponent(testScenario);
assert
@@ -96,4 +97,30 @@ module('Integration | Component | RoomMessage', function (hooks) {
.dom('[data-test-ai-message-content] span.streaming-text')
.includesText('Hello,');
});
+
+ test('it escapes html code that is in code tags', async function (assert) {
+ let testScenario = await setupTestScenario(
+ true,
+ 0,
+ 0,
+ `
+\`\`\`typescript
+
+ Hello, world!
+
+\`\`\`
+`,
+ );
+ await renderRoomMessageComponent(testScenario);
+
+ let content = document.querySelector(
+ '[data-test-ai-message-content]',
+ )?.innerHTML;
+ assert.ok(
+ content?.includes(`<template>
+ <h1>Hello, world!</h1>
+</template>`),
+ 'rendered code snippet in a streaming message should contain escaped HTML template so that we see code, not rendered html',
+ );
+ });
});
diff --git a/packages/runtime-common/marked-sync.ts b/packages/runtime-common/marked-sync.ts
index d672a09e81..1ae85cd661 100644
--- a/packages/runtime-common/marked-sync.ts
+++ b/packages/runtime-common/marked-sync.ts
@@ -22,7 +22,7 @@ export function markedSync(markdown: string) {
// also note that since we are in common, we don't have ember-window-mock
// available to us.
globalThis.localStorage?.setItem(id, code);
- return `
${code}`; + return `
${escapeHtmlInPreTags(code)}`; }, }, }) @@ -32,3 +32,11 @@ export function markedSync(markdown: string) { export function markdownToHtml(markdown: string | null | undefined): string { return markdown ? sanitizeHtml(markedSync(markdown)) : ''; } + +function escapeHtmlInPreTags(html: string) { + // For example, html can be
Hello
+ // We want to escape the <h1>Hello</h1>
, otherwise the h1 will
+ // be rendered as a real header, not code (same applies for other html tags, such as ,