From e509ba5be046736307536c695c5d2c1701d5eb77 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Tue, 20 Jan 2026 08:45:43 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=84=20fix:=20Code=20Block=20handling?= =?UTF-8?q?=20in=20Artifact=20Updates=20(#11417)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improved detection of code blocks to support both language identifiers and plain code fences. * Updated tests to cover various scenarios, including edge cases with different language identifiers and multiline content. * Ensured proper handling of code blocks with trailing whitespace and complex syntax. --- api/server/services/Artifacts/update.js | 18 +- api/server/services/Artifacts/update.spec.js | 263 +++++++++++++++++++ 2 files changed, 277 insertions(+), 4 deletions(-) diff --git a/api/server/services/Artifacts/update.js b/api/server/services/Artifacts/update.js index d068593f8c..be1644b11c 100644 --- a/api/server/services/Artifacts/update.js +++ b/api/server/services/Artifacts/update.js @@ -73,15 +73,25 @@ const replaceArtifactContent = (originalText, artifact, original, updated) => { return null; } - // Check if there are code blocks - const codeBlockStart = artifactContent.indexOf('```\n', contentStart); + // Check if there are code blocks - handle both ```\n and ```lang\n formats + let codeBlockStart = artifactContent.indexOf('```', contentStart); const codeBlockEnd = artifactContent.lastIndexOf('\n```', contentEnd); + // If we found opening backticks, find the actual newline (skipping any language identifier) + if (codeBlockStart !== -1) { + const newlineAfterBackticks = artifactContent.indexOf('\n', codeBlockStart); + if (newlineAfterBackticks !== -1 && newlineAfterBackticks < contentEnd) { + codeBlockStart = newlineAfterBackticks; + } else { + codeBlockStart = -1; + } + } + // Determine where to look for the original content let searchStart, searchEnd; if (codeBlockStart !== -1) { - // Code block starts - searchStart = codeBlockStart + 4; // after ```\n + // Code block starts - searchStart is right after the newline following ```[lang] + searchStart = codeBlockStart + 1; // after the newline if (codeBlockEnd !== -1 && codeBlockEnd > codeBlockStart) { // Code block has proper ending diff --git a/api/server/services/Artifacts/update.spec.js b/api/server/services/Artifacts/update.spec.js index 2a3e0bbe39..39a4f02863 100644 --- a/api/server/services/Artifacts/update.spec.js +++ b/api/server/services/Artifacts/update.spec.js @@ -494,5 +494,268 @@ ${original}`; /```\n {2}function test\(\) \{\n {4}return \{\n {6}value: 100\n {4}\};\n {2}\}\n```/, ); }); + + test('should handle code blocks with language identifiers (```svg, ```html, etc.)', () => { + const svgContent = ` + + +`; + + /** Artifact with language identifier in code block */ + const artifactText = `${ARTIFACT_START}{identifier="test-svg" type="image/svg+xml" title="Test SVG"} +\`\`\`svg +${svgContent} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + expect(artifacts).toHaveLength(1); + + const updatedSvg = svgContent.replace('#FFFFFF', '#131313'); + const result = replaceArtifactContent(artifactText, artifacts[0], svgContent, updatedSvg); + + expect(result).not.toBeNull(); + expect(result).toContain('#131313'); + expect(result).not.toContain('#FFFFFF'); + expect(result).toMatch(/```svg\n/); + }); + + test('should handle code blocks with complex language identifiers', () => { + const htmlContent = ` + +Test +Hello +`; + + const artifactText = `${ARTIFACT_START}{identifier="test-html" type="text/html" title="Test HTML"} +\`\`\`html +${htmlContent} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + + const updatedHtml = htmlContent.replace('Hello', 'Updated'); + const result = replaceArtifactContent(artifactText, artifacts[0], htmlContent, updatedHtml); + + expect(result).not.toBeNull(); + expect(result).toContain('Updated'); + expect(result).toMatch(/```html\n/); + }); + }); + + describe('code block edge cases', () => { + test('should handle code block without language identifier (```\\n)', () => { + const content = 'const x = 1;\nconst y = 2;'; + const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"} +\`\`\` +${content} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + + const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated'); + + expect(result).not.toBeNull(); + expect(result).toContain('updated'); + expect(result).toMatch(/```\nupdated\n```/); + }); + + test('should handle various language identifiers', () => { + const languages = [ + 'javascript', + 'typescript', + 'python', + 'jsx', + 'tsx', + 'css', + 'json', + 'xml', + 'markdown', + 'md', + ]; + + for (const lang of languages) { + const content = `test content for ${lang}`; + const artifactText = `${ARTIFACT_START}{identifier="test-${lang}" type="text/plain" title="Test"} +\`\`\`${lang} +${content} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + expect(artifacts).toHaveLength(1); + + const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated'); + + expect(result).not.toBeNull(); + expect(result).toContain('updated'); + expect(result).toMatch(new RegExp(`\`\`\`${lang}\\n`)); + } + }); + + test('should handle single character language identifier', () => { + const content = 'single char lang'; + const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"} +\`\`\`r +${content} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + + const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated'); + + expect(result).not.toBeNull(); + expect(result).toContain('updated'); + expect(result).toMatch(/```r\n/); + }); + + test('should handle code block with content that looks like code fence', () => { + const content = 'Line 1\nSome text with ``` backticks in middle\nLine 3'; + const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"} +\`\`\`text +${content} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + + const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated'); + + expect(result).not.toBeNull(); + expect(result).toContain('updated'); + }); + + test('should handle code block with trailing whitespace in language line', () => { + const content = 'whitespace test'; + /** Note: trailing spaces after 'python' */ + const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"} +\`\`\`python +${content} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + + const result = replaceArtifactContent(artifactText, artifacts[0], content, 'updated'); + + expect(result).not.toBeNull(); + expect(result).toContain('updated'); + }); + + test('should handle react/jsx content with complex syntax', () => { + const jsxContent = `function App() { + const [count, setCount] = useState(0); + return ( +
+

Count: {count}

+ +
+ ); +}`; + + const artifactText = `${ARTIFACT_START}{identifier="react-app" type="application/vnd.react" title="React App"} +\`\`\`jsx +${jsxContent} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + + const updatedJsx = jsxContent.replace('Increment', 'Click me'); + const result = replaceArtifactContent(artifactText, artifacts[0], jsxContent, updatedJsx); + + expect(result).not.toBeNull(); + expect(result).toContain('Click me'); + expect(result).not.toContain('Increment'); + expect(result).toMatch(/```jsx\n/); + }); + + test('should handle mermaid diagram content', () => { + const mermaidContent = `graph TD + A[Start] --> B{Is it?} + B -->|Yes| C[OK] + B -->|No| D[End]`; + + const artifactText = `${ARTIFACT_START}{identifier="diagram" type="application/vnd.mermaid" title="Flow"} +\`\`\`mermaid +${mermaidContent} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + + const updatedMermaid = mermaidContent.replace('Start', 'Begin'); + const result = replaceArtifactContent( + artifactText, + artifacts[0], + mermaidContent, + updatedMermaid, + ); + + expect(result).not.toBeNull(); + expect(result).toContain('Begin'); + expect(result).toMatch(/```mermaid\n/); + }); + + test('should handle artifact without code block (plain text)', () => { + const content = 'Just plain text without code fences'; + const artifactText = `${ARTIFACT_START}{identifier="plain" type="text/plain" title="Plain"} +${content} +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + + const result = replaceArtifactContent( + artifactText, + artifacts[0], + content, + 'updated plain text', + ); + + expect(result).not.toBeNull(); + expect(result).toContain('updated plain text'); + expect(result).not.toContain('```'); + }); + + test('should handle multiline content with various newline patterns', () => { + const content = `Line 1 +Line 2 + +Line 4 after empty line + Indented line + Double indented`; + + const artifactText = `${ARTIFACT_START}{identifier="test" type="text/plain" title="Test"} +\`\`\` +${content} +\`\`\` +${ARTIFACT_END}`; + + const message = { text: artifactText }; + const artifacts = findAllArtifacts(message); + + const updated = content.replace('Line 1', 'First Line'); + const result = replaceArtifactContent(artifactText, artifacts[0], content, updated); + + expect(result).not.toBeNull(); + expect(result).toContain('First Line'); + expect(result).toContain(' Indented line'); + expect(result).toContain(' Double indented'); + }); }); });