import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { resolve } from 'path';
import { existsSync } from 'fs';
import { rm, readFile } from 'fs/promises';
import { PathResolver } from '../../src/utils/path-resolver.js';
import { convertMdxTool } from '../../src/tools/convert-mdx.js';
const FIXTURES_DIR = resolve(process.cwd(), 'tests', 'fixtures');
const OUTPUT_DIR = resolve(FIXTURES_DIR, 'output');
describe('convert_mdx_to_md tool', () => {
let pathResolver: PathResolver;
beforeEach(() => {
pathResolver = new PathResolver(FIXTURES_DIR);
});
afterEach(async () => {
// Clean up any generated files
try {
await rm(OUTPUT_DIR, { recursive: true, force: true });
} catch (error) {
// Ignore cleanup errors
}
});
describe('successful conversions', () => {
it('should convert MDX file to Markdown file', async () => {
const result = await convertMdxTool(
{
sourcePath: 'basic-with-frontmatter.mdx',
outputPath: 'output/converted.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
expect(result.content).toHaveLength(1);
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed).toHaveProperty('sourcePath');
expect(parsed).toHaveProperty('outputPath');
expect(parsed).toHaveProperty('message');
// Verify file was created
const outputPath = resolve(FIXTURES_DIR, 'output', 'converted.md');
expect(existsSync(outputPath)).toBe(true);
});
it('should create parent directories if they dont exist', async () => {
const result = await convertMdxTool(
{
sourcePath: 'basic-with-frontmatter.mdx',
outputPath: 'output/nested/deep/file.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
// Verify nested directories were created
const outputPath = resolve(FIXTURES_DIR, 'output', 'nested', 'deep', 'file.md');
expect(existsSync(outputPath)).toBe(true);
});
it('should write valid Markdown content to output file', async () => {
const result = await convertMdxTool(
{
sourcePath: 'basic-with-frontmatter.mdx',
outputPath: 'output/test.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
// Read and verify the output file
const outputPath = resolve(FIXTURES_DIR, 'output', 'test.md');
const content = await readFile(outputPath, 'utf-8');
expect(content).toContain('Getting Started with MDX');
expect(content).toContain('Installation');
expect(typeof content).toBe('string');
expect(content.length).toBeGreaterThan(0);
});
it('should convert MDX with JSX components', async () => {
const result = await convertMdxTool(
{
sourcePath: 'simple-jsx.mdx',
outputPath: 'output/jsx-converted.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
// Verify the output
const outputPath = resolve(FIXTURES_DIR, 'output', 'jsx-converted.md');
const content = await readFile(outputPath, 'utf-8');
expect(content).toContain('Interactive Documentation');
});
it('should gracefully handle MDX with undefined components', async () => {
const result = await convertMdxTool(
{
sourcePath: 'missing-component.mdx',
outputPath: 'output/missing-component.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
const outputPath = resolve(FIXTURES_DIR, 'output', 'missing-component.md');
const content = await readFile(outputPath, 'utf-8');
expect(content).toContain('This content uses an Alert component');
});
it('should return absolute paths in response', async () => {
const result = await convertMdxTool(
{
sourcePath: 'basic-with-frontmatter.mdx',
outputPath: 'output/converted.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.sourcePath).toBe(
resolve(FIXTURES_DIR, 'basic-with-frontmatter.mdx')
);
expect(parsed.outputPath).toBe(
resolve(FIXTURES_DIR, 'output', 'converted.md')
);
});
it('should include descriptive message in response', async () => {
const result = await convertMdxTool(
{
sourcePath: 'no-frontmatter.mdx',
outputPath: 'output/simple.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.message).toBeTruthy();
expect(parsed.message).toContain('Successfully converted');
expect(parsed.message).toContain('no-frontmatter.mdx');
expect(parsed.message).toContain('output/simple.md');
});
});
describe('error handling', () => {
it('should handle non-existent source file', async () => {
const result = await convertMdxTool(
{
sourcePath: 'nonexistent.mdx',
outputPath: 'output/fail.md'
},
pathResolver
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('not found');
});
it('should handle unsafe source paths', async () => {
const result = await convertMdxTool(
{
sourcePath: '../../etc/passwd',
outputPath: 'output/fail.md'
},
pathResolver
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('outside workspace root');
});
it('should handle unsafe output paths', async () => {
const result = await convertMdxTool(
{
sourcePath: 'basic-with-frontmatter.mdx',
outputPath: '../../tmp/malicious.md'
},
pathResolver
);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('outside workspace root');
});
it('should prevent overwriting existing files', async () => {
// First conversion
const result1 = await convertMdxTool(
{
sourcePath: 'basic-with-frontmatter.mdx',
outputPath: 'output/existing.md'
},
pathResolver
);
expect(result1.isError).toBeFalsy();
// Try to overwrite
const result2 = await convertMdxTool(
{
sourcePath: 'no-frontmatter.mdx',
outputPath: 'output/existing.md'
},
pathResolver
);
expect(result2.isError).toBe(true);
expect(result2.content[0].text).toContain('already exists');
});
});
describe('content verification', () => {
it('should preserve markdown structure in output', async () => {
const result = await convertMdxTool(
{
sourcePath: 'search-test.mdx',
outputPath: 'output/structure-test.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
const outputPath = resolve(FIXTURES_DIR, 'output', 'structure-test.md');
const content = await readFile(outputPath, 'utf-8');
// Check for key structural elements
expect(content).toContain('# API Reference');
expect(content).toContain('## Authentication');
expect(content).toContain('### API Keys');
});
it('should handle complex MDX documents', async () => {
const result = await convertMdxTool(
{
sourcePath: 'search-test.mdx',
outputPath: 'output/complex.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
const outputPath = resolve(FIXTURES_DIR, 'output', 'complex.md');
const content = await readFile(outputPath, 'utf-8');
expect(content.length).toBeGreaterThan(100);
expect(content).toContain('Authentication');
expect(content).toContain('GraphQL');
expect(content).toContain('Rate Limiting');
});
it('should write UTF-8 encoded files', async () => {
const result = await convertMdxTool(
{
sourcePath: 'basic-with-frontmatter.mdx',
outputPath: 'output/utf8-test.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
// Read with explicit UTF-8 encoding
const outputPath = resolve(FIXTURES_DIR, 'output', 'utf8-test.md');
const content = await readFile(outputPath, 'utf-8');
expect(typeof content).toBe('string');
expect(content).toBeTruthy();
});
});
describe('path normalization', () => {
it('should handle relative paths in source', async () => {
const result = await convertMdxTool(
{
sourcePath: './basic-with-frontmatter.mdx',
outputPath: 'output/relative-source.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
});
it('should handle relative paths in output', async () => {
const result = await convertMdxTool(
{
sourcePath: 'basic-with-frontmatter.mdx',
outputPath: './output/relative-output.md'
},
pathResolver
);
expect(result.isError).toBeFalsy();
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
const outputPath = resolve(FIXTURES_DIR, 'output', 'relative-output.md');
expect(existsSync(outputPath)).toBe(true);
});
});
});