| import { describe, expect, it, vi } from 'vitest'; |
| import { StreamingMessageParser, type ActionCallback, type ArtifactCallback } from './message-parser'; |
|
|
| interface ExpectedResult { |
| output: string; |
| callbacks?: { |
| onArtifactOpen?: number; |
| onArtifactClose?: number; |
| onActionOpen?: number; |
| onActionClose?: number; |
| }; |
| } |
|
|
| describe('StreamingMessageParser', () => { |
| it('should pass through normal text', () => { |
| const parser = new StreamingMessageParser(); |
| expect(parser.parse('test_id', 'Hello, world!')).toBe('Hello, world!'); |
| }); |
|
|
| it('should allow normal HTML tags', () => { |
| const parser = new StreamingMessageParser(); |
| expect(parser.parse('test_id', 'Hello <strong>world</strong>!')).toBe('Hello <strong>world</strong>!'); |
| }); |
|
|
| describe('no artifacts', () => { |
| it.each<[string | string[], ExpectedResult | string]>([ |
| ['Foo bar', 'Foo bar'], |
| ['Foo bar <', 'Foo bar '], |
| ['Foo bar <p', 'Foo bar <p'], |
| [['Foo bar <', 's', 'p', 'an>some text</span>'], 'Foo bar <span>some text</span>'], |
| ])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => { |
| runTest(input, expected); |
| }); |
| }); |
|
|
| describe('invalid or incomplete artifacts', () => { |
| it.each<[string | string[], ExpectedResult | string]>([ |
| ['Foo bar <b', 'Foo bar '], |
| ['Foo bar <ba', 'Foo bar <ba'], |
| ['Foo bar <bol', 'Foo bar '], |
| ['Foo bar <bolt', 'Foo bar '], |
| ['Foo bar <bolta', 'Foo bar <bolta'], |
| ['Foo bar <boltA', 'Foo bar '], |
| ['Foo bar <boltArtifacs></boltArtifact>', 'Foo bar <boltArtifacs></boltArtifact>'], |
| ['Before <oltArtfiact>foo</boltArtifact> After', 'Before <oltArtfiact>foo</boltArtifact> After'], |
| ['Before <boltArtifactt>foo</boltArtifact> After', 'Before <boltArtifactt>foo</boltArtifact> After'], |
| ])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => { |
| runTest(input, expected); |
| }); |
| }); |
|
|
| describe('valid artifacts without actions', () => { |
| it.each<[string | string[], ExpectedResult | string]>([ |
| [ |
| 'Some text before <boltArtifact title="Some title" id="artifact_1">foo bar</boltArtifact> Some more text', |
| { |
| output: 'Some text before Some more text', |
| callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 }, |
| }, |
| ], |
| [ |
| [ |
| 'Some text before <boltArti', |
| 'fact', |
| ' title="Some title" id="artifact_1" type="bundled" >foo</boltArtifact> Some more text', |
| ], |
| { |
| output: 'Some text before Some more text', |
| callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 }, |
| }, |
| ], |
| [ |
| [ |
| 'Some text before <boltArti', |
| 'fac', |
| 't title="Some title" id="artifact_1"', |
| ' ', |
| '>', |
| 'foo</boltArtifact> Some more text', |
| ], |
| { |
| output: 'Some text before Some more text', |
| callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 }, |
| }, |
| ], |
| [ |
| [ |
| 'Some text before <boltArti', |
| 'fact', |
| ' title="Some title" id="artifact_1"', |
| ' >fo', |
| 'o</boltArtifact> Some more text', |
| ], |
| { |
| output: 'Some text before Some more text', |
| callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 }, |
| }, |
| ], |
| [ |
| [ |
| 'Some text before <boltArti', |
| 'fact tit', |
| 'le="Some ', |
| 'title" id="artifact_1">fo', |
| 'o', |
| '<', |
| '/boltArtifact> Some more text', |
| ], |
| { |
| output: 'Some text before Some more text', |
| callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 }, |
| }, |
| ], |
| [ |
| [ |
| 'Some text before <boltArti', |
| 'fact title="Some title" id="artif', |
| 'act_1">fo', |
| 'o<', |
| '/boltArtifact> Some more text', |
| ], |
| { |
| output: 'Some text before Some more text', |
| callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 }, |
| }, |
| ], |
| [ |
| 'Before <boltArtifact title="Some title" id="artifact_1">foo</boltArtifact> After', |
| { |
| output: 'Before After', |
| callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 0, onActionClose: 0 }, |
| }, |
| ], |
| ])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => { |
| runTest(input, expected); |
| }); |
| }); |
|
|
| describe('valid artifacts with actions', () => { |
| it.each<[string | string[], ExpectedResult | string]>([ |
| [ |
| 'Before <boltArtifact title="Some title" id="artifact_1"><boltAction type="shell">npm install</boltAction></boltArtifact> After', |
| { |
| output: 'Before After', |
| callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 1, onActionClose: 1 }, |
| }, |
| ], |
| [ |
| 'Before <boltArtifact title="Some title" id="artifact_1"><boltAction type="shell">npm install</boltAction><boltAction type="file" filePath="index.js">some content</boltAction></boltArtifact> After', |
| { |
| output: 'Before After', |
| callbacks: { onArtifactOpen: 1, onArtifactClose: 1, onActionOpen: 2, onActionClose: 2 }, |
| }, |
| ], |
| ])('should correctly parse chunks and strip out bolt artifacts (%#)', (input, expected) => { |
| runTest(input, expected); |
| }); |
| }); |
| }); |
|
|
| function runTest(input: string | string[], outputOrExpectedResult: string | ExpectedResult) { |
| let expected: ExpectedResult; |
|
|
| if (typeof outputOrExpectedResult === 'string') { |
| expected = { output: outputOrExpectedResult }; |
| } else { |
| expected = outputOrExpectedResult; |
| } |
|
|
| const callbacks = { |
| onArtifactOpen: vi.fn<ArtifactCallback>((data) => { |
| expect(data).toMatchSnapshot('onArtifactOpen'); |
| }), |
| onArtifactClose: vi.fn<ArtifactCallback>((data) => { |
| expect(data).toMatchSnapshot('onArtifactClose'); |
| }), |
| onActionOpen: vi.fn<ActionCallback>((data) => { |
| expect(data).toMatchSnapshot('onActionOpen'); |
| }), |
| onActionClose: vi.fn<ActionCallback>((data) => { |
| expect(data).toMatchSnapshot('onActionClose'); |
| }), |
| }; |
|
|
| const parser = new StreamingMessageParser({ |
| artifactElement: () => '', |
| callbacks, |
| }); |
|
|
| let message = ''; |
|
|
| let result = ''; |
|
|
| const chunks = Array.isArray(input) ? input : input.split(''); |
|
|
| for (const chunk of chunks) { |
| message += chunk; |
|
|
| result += parser.parse('message_1', message); |
| } |
|
|
| for (const name in expected.callbacks) { |
| const callbackName = name; |
|
|
| expect(callbacks[callbackName as keyof typeof callbacks]).toHaveBeenCalledTimes( |
| expected.callbacks[callbackName as keyof typeof expected.callbacks] ?? 0, |
| ); |
| } |
|
|
| expect(result).toEqual(expected.output); |
| } |
|
|