mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
feat: add markdown composer backend foundation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { createHeadlessEditor } from '@lexical/headless';
|
||||
import { $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown';
|
||||
|
||||
import {
|
||||
inspectMarkdownPaste,
|
||||
markdownEditorNamespace,
|
||||
markdownEditorNodes,
|
||||
markdownEditorTransformers,
|
||||
} from '@renderer/lib/markdownEditor';
|
||||
|
||||
function roundTripMarkdown(markdown: string): string {
|
||||
const editor = createHeadlessEditor({
|
||||
namespace: markdownEditorNamespace,
|
||||
nodes: [...markdownEditorNodes],
|
||||
onError(error) {
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
|
||||
editor.update(() => {
|
||||
$convertFromMarkdownString(markdown, [...markdownEditorTransformers], undefined, true, false);
|
||||
}, { discrete: true });
|
||||
|
||||
let result = '';
|
||||
editor.getEditorState().read(() => {
|
||||
result = $convertToMarkdownString([...markdownEditorTransformers], undefined, true);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('markdown editor contract', () => {
|
||||
test('round-trips markdown structures with the chosen node set', () => {
|
||||
const markdown = [
|
||||
'# Release notes',
|
||||
'',
|
||||
'- item one',
|
||||
'- item two',
|
||||
'',
|
||||
'> keep the markdown contract stable',
|
||||
'',
|
||||
'```ts',
|
||||
'const answer = 42;',
|
||||
'```',
|
||||
'',
|
||||
'[Docs](https://example.com)',
|
||||
].join('\n');
|
||||
|
||||
const roundTripped = roundTripMarkdown(markdown);
|
||||
|
||||
expect(roundTripped).toContain('# Release notes');
|
||||
expect(roundTripped).toContain('- item one');
|
||||
expect(roundTripped).toContain('```ts');
|
||||
expect(roundTripped).toContain('[Docs](https://example.com)');
|
||||
});
|
||||
|
||||
test('classifies fenced code as markdown paste', () => {
|
||||
expect(inspectMarkdownPaste('```ts\nconst answer = 42;\n```')).toEqual({
|
||||
normalizedText: '```ts\nconst answer = 42;\n```',
|
||||
shouldImportMarkdown: true,
|
||||
reason: 'fenced-code',
|
||||
});
|
||||
});
|
||||
|
||||
test('keeps plain prose on the plain-text path', () => {
|
||||
expect(inspectMarkdownPaste('Just a normal sentence about the release plan.')).toEqual({
|
||||
normalizedText: 'Just a normal sentence about the release plan.',
|
||||
shouldImportMarkdown: false,
|
||||
reason: 'plain-text',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -86,6 +86,7 @@ describe('run timeline formatting', () => {
|
||||
expect(truncateContent('Short content')).toBe('Short content');
|
||||
expect(truncateContent('A'.repeat(100), 80)).toBe('A'.repeat(80) + '…');
|
||||
expect(truncateContent('Line 1\nLine 2')).toBe('Line 1 Line 2');
|
||||
expect(truncateContent('**Bold** [Docs](https://example.com)')).toBe('Bold Docs');
|
||||
});
|
||||
|
||||
test('collapses consecutive thinking events from the same agent', () => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import {
|
||||
hasMeaningfulChatMessageContent,
|
||||
normalizeChatMessageLineEndings,
|
||||
prepareChatMessageContent,
|
||||
} from '@shared/utils/chatMessage';
|
||||
|
||||
describe('chat message helpers', () => {
|
||||
test('normalizes Windows line endings without trimming meaningful markdown', () => {
|
||||
expect(
|
||||
prepareChatMessageContent('```ts\r\n const answer = 42;\r\n```\r\n'),
|
||||
).toBe('```ts\n const answer = 42;\n```\n');
|
||||
});
|
||||
|
||||
test('treats whitespace-only input as empty', () => {
|
||||
expect(hasMeaningfulChatMessageContent(' \r\n\t ')).toBe(false);
|
||||
expect(prepareChatMessageContent(' \r\n\t ')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('normalizes mixed newline sequences consistently', () => {
|
||||
expect(normalizeChatMessageLineEndings('line 1\r\nline 2\rline 3')).toBe(
|
||||
'line 1\nline 2\nline 3',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { buildMarkdownExcerpt, extractPlainTextFromMarkdown } from '@shared/utils/markdownText';
|
||||
|
||||
describe('markdown text helpers', () => {
|
||||
test('extracts readable text from common markdown syntax', () => {
|
||||
expect(
|
||||
extractPlainTextFromMarkdown('# Title\n\n**Bold** [Docs](https://example.com) and `code`.'),
|
||||
).toBe('Title Bold Docs and code.');
|
||||
});
|
||||
|
||||
test('turns fenced code blocks into readable excerpts', () => {
|
||||
expect(buildMarkdownExcerpt('```ts\nconst answer = 42;\n```', 48)).toBe('const answer = 42;');
|
||||
});
|
||||
|
||||
test('truncates after stripping markdown syntax', () => {
|
||||
expect(buildMarkdownExcerpt('**Alpha** beta gamma delta epsilon zeta eta theta', 18)).toBe(
|
||||
'Alpha beta gamma d…',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -121,6 +121,23 @@ describe('session title helpers', () => {
|
||||
]),
|
||||
).toBe('Release readiness review');
|
||||
});
|
||||
|
||||
test('builds auto titles from markdown-heavy first messages', () => {
|
||||
const pattern = createPattern();
|
||||
const session = createSession();
|
||||
|
||||
expect(
|
||||
resolveSessionTitle(session, pattern, [
|
||||
{
|
||||
id: 'msg-1',
|
||||
role: 'user',
|
||||
authorName: 'You',
|
||||
content: '```ts\nconst answer = 42;\n```',
|
||||
createdAt: '2026-03-23T00:00:00.000Z',
|
||||
},
|
||||
]),
|
||||
).toBe('const answer = 42;');
|
||||
});
|
||||
});
|
||||
|
||||
describe('session tooling helpers', () => {
|
||||
|
||||
Reference in New Issue
Block a user