doctool
Version:
AI-powered documentation validation and management system
364 lines (354 loc) • 15.5 kB
JavaScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { FileSystemValidator } from './fileSystemValidator';
describe('FileSystemValidator', () => {
let tempDir;
let validator;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'fs-validator-test-'));
validator = new FileSystemValidator(tempDir);
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
describe('extractFileReferences', () => {
it('should extract markdown link references', () => {
const content = `
# Test Doc
See [config file](config/app.json) for settings.
Check out [the docs](docs/api.md) too.
Visit [external link](https://example.com) - should be ignored.
`.trim();
const references = validator.extractFileReferences(content, 'test.md');
expect(references).toHaveLength(2);
expect(references[0].path).toBe('config/app.json');
expect(references[0].type).toBe('file');
expect(references[1].path).toBe('docs/api.md');
expect(references[1].type).toBe('file');
});
it('should extract code block file references', () => {
const content = `
Run \`npm install\` first.
Edit the \`package.json\` file.
Then run \`src/build.sh\` script.
`.trim();
const references = validator.extractFileReferences(content, 'test.md');
expect(references.some(ref => ref.path === 'package.json')).toBe(true);
expect(references.some(ref => ref.path === 'src/build.sh')).toBe(true);
});
it('should extract directory references', () => {
const content = `
The src/utils/ directory contains utilities.
Check the config/ folder for settings.
`.trim();
const references = validator.extractFileReferences(content, 'test.md');
expect(references.some(ref => ref.path === 'src/utils/' && ref.type === 'directory')).toBe(true);
expect(references.some(ref => ref.path === 'config/' && ref.type === 'directory')).toBe(true);
});
it('should extract common file patterns', () => {
const content = `
First, update your package.json file.
Make sure .env is configured.
Check the README.md for more info.
`.trim();
const references = validator.extractFileReferences(content, 'test.md');
expect(references.some(ref => ref.path === 'package.json')).toBe(true);
expect(references.some(ref => ref.path === '.env')).toBe(true);
expect(references.some(ref => ref.path === 'README.md')).toBe(true);
});
it('should deduplicate references from same line', () => {
const content = `
Edit \`config.json\` and then check \`config.json\` again.
`.trim();
const references = validator.extractFileReferences(content, 'test.md');
const configRefs = references.filter(ref => ref.path === 'config.json');
expect(configRefs).toHaveLength(1);
});
it('should ignore URLs and mailto links', () => {
const content = `
Visit [our site](https://example.com).
Email [us](mailto:test@example.com).
Check [local file](config.json).
`.trim();
const references = validator.extractFileReferences(content, 'test.md');
expect(references).toHaveLength(1);
expect(references[0].path).toBe('config.json');
});
});
describe('extractDirectoryStructureClaims', () => {
it('should extract directory tree structures', () => {
const content = `
# Project Structure
\`\`\`
project/
├── src/
│ ├── index.ts
│ └── utils/
│ └── helper.ts
├── tests/
│ └── test.spec.ts
└── package.json
\`\`\`
`.trim();
const claims = validator.extractDirectoryStructureClaims(content, 'test.md');
expect(claims).toHaveLength(1);
expect(claims[0].claimed_structure.length).toBeGreaterThan(0);
expect(claims[0].directory_path).toBe('.');
});
it('should handle multiple directory trees', () => {
const content = `
# Structure 1
\`\`\`
├── file1.txt
└── file2.txt
\`\`\`
# Structure 2
\`\`\`tree
├── other.txt
\`\`\`
`.trim();
const claims = validator.extractDirectoryStructureClaims(content, 'test.md');
expect(claims).toHaveLength(2);
});
});
describe('validateFileReference', () => {
beforeEach(() => {
// Create test files and directories
fs.writeFileSync(path.join(tempDir, 'existing-file.txt'), 'content');
fs.mkdirSync(path.join(tempDir, 'existing-dir'));
fs.writeFileSync(path.join(tempDir, 'existing-dir', 'nested-file.txt'), 'content');
});
it('should validate existing file references', () => {
const fileRef = {
path: 'existing-file.txt',
type: 'file',
mentioned_in: {
file: 'test.md',
line: 1,
context: 'test context'
},
exists: false
};
const docPath = path.join(tempDir, 'test.md');
const issues = validator.validateFileReference(fileRef, docPath);
expect(issues).toHaveLength(0);
expect(fileRef.exists).toBe(true);
});
it('should detect missing files', () => {
const fileRef = {
path: 'missing-file.txt',
type: 'file',
mentioned_in: {
file: 'test.md',
line: 1,
context: 'test context'
},
exists: false
};
const docPath = path.join(tempDir, 'test.md');
const issues = validator.validateFileReference(fileRef, docPath);
expect(issues).toHaveLength(1);
expect(issues[0].type).toBe('missing_file');
expect(issues[0].severity).toBe('error');
expect(fileRef.exists).toBe(false);
});
it('should detect type mismatches', () => {
const fileRef = {
path: 'existing-dir',
type: 'file', // Wrong type - it's actually a directory
mentioned_in: {
file: 'test.md',
line: 1,
context: 'test context'
},
exists: false
};
const docPath = path.join(tempDir, 'test.md');
const issues = validator.validateFileReference(fileRef, docPath);
expect(issues).toHaveLength(1);
expect(issues[0].type).toBe('directory_mismatch');
expect(issues[0].severity).toBe('warning');
});
it('should validate nested file paths', () => {
const fileRef = {
path: 'existing-dir/nested-file.txt',
type: 'file',
mentioned_in: {
file: 'test.md',
line: 1,
context: 'test context'
},
exists: false
};
const docPath = path.join(tempDir, 'test.md');
const issues = validator.validateFileReference(fileRef, docPath);
expect(issues).toHaveLength(0);
expect(fileRef.exists).toBe(true);
});
it('should suggest similar files when file is missing', () => {
// Create a similar file
fs.writeFileSync(path.join(tempDir, 'config.json'), '{}');
const fileRef = {
path: 'config.js', // Missing, but config.json exists
type: 'file',
mentioned_in: {
file: 'test.md',
line: 1,
context: 'test context'
},
exists: false
};
const docPath = path.join(tempDir, 'test.md');
const issues = validator.validateFileReference(fileRef, docPath);
expect(issues).toHaveLength(1);
expect(issues[0].suggestion).toContain('config.json');
});
});
describe('validateDirectoryStructure', () => {
beforeEach(() => {
// Create test structure
fs.mkdirSync(path.join(tempDir, 'src'));
fs.writeFileSync(path.join(tempDir, 'src', 'index.ts'), 'export {};');
fs.writeFileSync(path.join(tempDir, 'src', 'helper.ts'), 'export {};');
fs.writeFileSync(path.join(tempDir, 'package.json'), '{}');
});
it('should validate correct directory structure', () => {
const claim = {
claimed_structure: [
'├── src/',
'│ ├── index.ts',
'│ └── helper.ts',
'└── package.json'
],
actual_structure: [],
location: {
file: 'test.md',
line: 1,
context: 'directory tree'
},
directory_path: '.'
};
const issues = validator.validateDirectoryStructure(claim);
// All claimed files exist, so no missing file issues
const missingFileIssues = issues.filter(i => i.type === 'missing_file');
expect(missingFileIssues).toHaveLength(0);
});
it('should detect missing files in directory structure', () => {
const claim = {
claimed_structure: [
'├── src/',
'│ ├── index.ts',
'│ ├── helper.ts',
'│ └── missing.ts', // This file doesn't exist
'└── package.json'
],
actual_structure: [],
location: {
file: 'test.md',
line: 1,
context: 'directory tree'
},
directory_path: '.'
};
const issues = validator.validateDirectoryStructure(claim);
// Should find at least one missing file issue for missing.ts
const missingFileIssues = issues.filter(i => i.type === 'missing_file' && i.message.includes('missing.ts'));
expect(missingFileIssues.length).toBeGreaterThanOrEqual(1);
});
});
describe('validateDocumentationFile', () => {
it('should validate a complete documentation file', () => {
// Create test files
fs.writeFileSync(path.join(tempDir, 'config.json'), '{}');
fs.mkdirSync(path.join(tempDir, 'src'));
fs.writeFileSync(path.join(tempDir, 'src', 'index.ts'), 'export {};');
// Create documentation file
const docContent = `
# Test Project
## Configuration
Edit the \`config.json\` file for settings.
## Structure
The src/ directory contains the source code.
See [index file](src/index.ts) for the main entry point.
## Missing References
This references a [missing file](nonexistent.txt).
`.trim();
const docPath = path.join(tempDir, 'test.md');
fs.writeFileSync(docPath, docContent);
const issues = validator.validateDocumentationFile(docPath);
// Should find the missing file but not flag existing ones
const missingFileIssues = issues.filter(issue => issue.type === 'missing_file');
expect(missingFileIssues).toHaveLength(1);
expect(missingFileIssues[0].file_reference.path).toBe('nonexistent.txt');
// No issues for existing files
const existingFileIssues = issues.filter(issue => issue.file_reference.path === 'config.json' ||
issue.file_reference.path === 'src/index.ts');
expect(existingFileIssues).toHaveLength(0);
});
it('should handle file reading errors gracefully', () => {
const nonExistentDoc = path.join(tempDir, 'missing-doc.md');
const issues = validator.validateDocumentationFile(nonExistentDoc);
expect(issues).toHaveLength(1);
expect(issues[0].type).toBe('invalid_path');
expect(issues[0].severity).toBe('error');
});
});
describe('path type detection', () => {
it('should correctly guess file vs directory types', () => {
const testCases = [
{ path: 'file.txt', expected: 'file' },
{ path: 'directory/', expected: 'directory' },
{ path: 'path/to/file.js', expected: 'file' },
{ path: 'path/to/dir/', expected: 'directory' },
{ path: '.env', expected: 'file' },
{ path: '.hidden', expected: 'file' }
];
testCases.forEach(({ path, expected }) => {
const ref = validator.extractFileReferences(`\`${path}\``, 'test.md')[0];
expect(ref?.type).toBe(expected);
});
// Test that 'no-extension' files are not detected as file references
// since they're ambiguous and could be commands or other text
const noExtensionRefs = validator.extractFileReferences('`no-extension`', 'test.md');
expect(noExtensionRefs).toHaveLength(0);
});
});
describe('edge cases', () => {
it('should handle empty documentation files', () => {
const docPath = path.join(tempDir, 'empty.md');
fs.writeFileSync(docPath, '');
const issues = validator.validateDocumentationFile(docPath);
expect(issues).toHaveLength(0);
});
it('should handle documentation with no file references', () => {
const docContent = `
# Pure Text Document
This document contains no file references.
Just some regular text and explanations.
`.trim();
const docPath = path.join(tempDir, 'no-refs.md');
fs.writeFileSync(docPath, docContent);
const issues = validator.validateDocumentationFile(docPath);
expect(issues).toHaveLength(0);
});
it('should handle very long file paths', () => {
const longPath = 'very/long/path/that/goes/deep/into/nested/directories/file.txt';
const content = `Check [\`${longPath}\`](${longPath})`;
const references = validator.extractFileReferences(content, 'test.md');
expect(references).toHaveLength(1);
expect(references[0].path).toBe(longPath);
});
it('should handle special characters in file names', () => {
const specialFile = 'file-with_special.chars@2024.txt';
fs.writeFileSync(path.join(tempDir, specialFile), 'content');
const content = `See [\`${specialFile}\`](${specialFile})`;
const docPath = path.join(tempDir, 'test.md');
fs.writeFileSync(docPath, content);
const issues = validator.validateDocumentationFile(docPath);
expect(issues).toHaveLength(0);
});
});
});
//# sourceMappingURL=fileSystemValidator.test.js.map