diff --git a/app/lib/runtime/message-parser.spec.ts b/app/lib/runtime/message-parser.spec.ts index ef4ef8e421..94132c8350 100644 --- a/app/lib/runtime/message-parser.spec.ts +++ b/app/lib/runtime/message-parser.spec.ts @@ -157,6 +157,58 @@ describe('StreamingMessageParser', () => { runTest(input, expected); }); }); + + describe('file action with missing filePath', () => { + it('should not crash when filePath attribute is missing from file action', () => { + const callbacks = { + onArtifactOpen: vi.fn(), + onArtifactClose: vi.fn(), + onActionOpen: vi.fn(), + onActionClose: vi.fn(), + }; + + const parser = new StreamingMessageParser({ + artifactElement: () => '', + callbacks, + }); + + // This input has a file action without filePath - previously caused + // "TypeError: Cannot read properties of undefined (reading 'endsWith')" + const input = + 'Before some file content After'; + + expect(() => parser.parse('message_1', input)).not.toThrow(); + + expect(callbacks.onActionOpen).toHaveBeenCalledTimes(1); + expect(callbacks.onActionClose).toHaveBeenCalledTimes(1); + }); + + it('should not crash during streaming when filePath attribute is missing', () => { + const callbacks = { + onArtifactOpen: vi.fn(), + onArtifactClose: vi.fn(), + onActionOpen: vi.fn(), + onActionStream: vi.fn(), + onActionClose: vi.fn(), + }; + + const parser = new StreamingMessageParser({ + artifactElement: () => '', + callbacks, + }); + + // Simulate streaming: first chunk has the action open but no close tag yet + const chunk1 = + 'Before partial content'; + + expect(() => parser.parse('message_1', chunk1)).not.toThrow(); + + // Second chunk completes the action + const chunk2 = chunk1 + ' more content After'; + + expect(() => parser.parse('message_1', chunk2)).not.toThrow(); + }); + }); }); describe('EnhancedStreamingMessageParser', () => { diff --git a/app/lib/runtime/message-parser.ts b/app/lib/runtime/message-parser.ts index 47769e2589..9d0f060ffd 100644 --- a/app/lib/runtime/message-parser.ts +++ b/app/lib/runtime/message-parser.ts @@ -150,7 +150,7 @@ export class StreamingMessageParser { if ('type' in currentAction && currentAction.type === 'file') { // Remove markdown code block syntax if present and file is not markdown - if (!currentAction.filePath.endsWith('.md')) { + if (!currentAction.filePath?.endsWith('.md')) { content = cleanoutMarkdownSyntax(content); content = cleanEscapedTags(content); } @@ -182,7 +182,7 @@ export class StreamingMessageParser { if ('type' in currentAction && currentAction.type === 'file') { let content = input.slice(i); - if (!currentAction.filePath.endsWith('.md')) { + if (!currentAction.filePath?.endsWith('.md')) { content = cleanoutMarkdownSyntax(content); content = cleanEscapedTags(content); } @@ -366,7 +366,7 @@ export class StreamingMessageParser { (actionAttributes as SupabaseAction).filePath = filePath; } } else if (actionType === 'file') { - const filePath = this.#extractAttribute(actionTag, 'filePath') as string; + const filePath = this.#extractAttribute(actionTag, 'filePath') ?? ''; if (!filePath) { logger.debug('File path not specified');