/**
* Tests for local_fetch_content tool
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { fetchContent } from '../../src/tools/local_fetch_content.js';
import * as pathValidator from '../../src/security/pathValidator.js';
import * as fs from 'fs/promises';
// Mock fs/promises
vi.mock('fs/promises', () => ({
readFile: vi.fn(),
stat: vi.fn(),
}));
// Mock pathValidator
vi.mock('../../src/security/pathValidator.js', () => ({
pathValidator: {
validate: vi.fn(),
},
}));
describe('local_fetch_content', () => {
const mockReadFile = vi.mocked(fs.readFile);
const mockStat = vi.mocked(fs.stat);
const mockValidate = vi.mocked(pathValidator.pathValidator.validate);
beforeEach(() => {
vi.clearAllMocks();
mockValidate.mockReturnValue({ isValid: true });
// Default: small file size (< 100KB)
mockStat.mockResolvedValue({ size: 1024 } as unknown as ReturnType<typeof fs.stat>);
});
describe('Full content fetch', () => {
it('should fetch full file content', async () => {
const testContent = 'line 1\nline 2\nline 3';
mockReadFile.mockResolvedValue(testContent);
const result = await fetchContent({
path: 'test.txt',
fullContent: true,
});
expect(result.status).toBe('hasResults');
expect(result.content).toBe(testContent);
expect(result.isPartial).toBe(false);
expect(result.totalLines).toBe(3);
});
it('should apply minification when requested', async () => {
const testContent = 'function test() {\n return true;\n}';
mockReadFile.mockResolvedValue(testContent);
const result = await fetchContent({
path: 'test.js',
fullContent: true,
minified: true,
});
expect(result.status).toBe('hasResults');
expect(result.minified).toBe(true);
});
});
describe('Match string fetch', () => {
it('should fetch lines matching pattern with context', async () => {
const testContent = 'line 1\nline 2\nMATCH\nline 4\nline 5';
mockReadFile.mockResolvedValue(testContent);
const result = await fetchContent({
path: 'test.txt',
matchString: 'MATCH',
matchStringContextLines: 1,
});
expect(result.status).toBe('hasResults');
expect(result.content).toContain('line 2');
expect(result.content).toContain('MATCH');
expect(result.content).toContain('line 4');
expect(result.isPartial).toBe(true);
});
it('should return empty when pattern not found', async () => {
const testContent = 'line 1\nline 2\nline 3';
mockReadFile.mockResolvedValue(testContent);
const result = await fetchContent({
path: 'test.txt',
matchString: 'NOTFOUND',
});
expect(result.status).toBe('empty');
expect(result.error).toContain('No matches found');
});
});
describe('Error handling', () => {
it('should handle invalid paths', async () => {
mockValidate.mockReturnValue({
isValid: false,
error: 'Invalid path',
});
const result = await fetchContent({
path: '/invalid/path',
});
expect(result.status).toBe('error');
expect(result.error).toBe('Invalid path');
});
it('should handle file read errors', async () => {
mockReadFile.mockRejectedValue(new Error('File not found'));
const result = await fetchContent({
path: 'nonexistent.txt',
});
expect(result.status).toBe('error');
expect(result.error).toContain('Failed to read file');
});
});
describe('Empty content handling', () => {
it('should handle empty files', async () => {
mockReadFile.mockResolvedValue('');
const result = await fetchContent({
path: 'empty.txt',
fullContent: true,
});
expect(result.status).toBe('empty');
});
});
describe('Large file handling', () => {
it('should warn about large file without pagination options', async () => {
// Mock large file (150KB)
mockStat.mockResolvedValue({ size: 150 * 1024 } as unknown as ReturnType<typeof fs.stat>);
const result = await fetchContent({
path: 'large-file.txt',
// No charLength or matchString
});
expect(result.status).toBe('error');
expect(result.error).toContain('150KB');
expect(result.hints).toBeDefined();
expect(result.hints?.some(h => h.includes('charLength=10000'))).toBe(true);
expect(result.hints?.some(h => h.includes('matchString'))).toBe(true);
});
it('should allow large file with charLength pagination', async () => {
mockStat.mockResolvedValue({ size: 150 * 1024 } as unknown as ReturnType<typeof fs.stat>);
mockReadFile.mockResolvedValue('test content for large file');
const result = await fetchContent({
path: 'large-file.txt',
charLength: 10000,
});
expect(result.status).toBe('hasResults');
});
it('should allow large file with matchString extraction', async () => {
mockStat.mockResolvedValue({ size: 150 * 1024 } as unknown as ReturnType<typeof fs.stat>);
mockReadFile.mockResolvedValue('line 1\nMATCH\nline 3');
const result = await fetchContent({
path: 'large-file.txt',
matchString: 'MATCH',
});
expect(result.status).toBe('hasResults');
});
it('should allow large file with fullContent flag and charLength', async () => {
mockStat.mockResolvedValue({ size: 150 * 1024 } as unknown as ReturnType<typeof fs.stat>);
mockReadFile.mockResolvedValue('full content of large file');
const result = await fetchContent({
path: 'large-file.txt',
fullContent: true,
charLength: 10000,
});
expect(result.status).toBe('hasResults');
});
it('should not warn for files under 100KB', async () => {
mockStat.mockResolvedValue({ size: 50 * 1024 } as unknown as ReturnType<typeof fs.stat>);
mockReadFile.mockResolvedValue('content of small file');
const result = await fetchContent({
path: 'small-file.txt',
// No pagination options
});
expect(result.status).toBe('hasResults');
});
});
describe('Research context', () => {
it('should preserve research goal and reasoning', async () => {
const testContent = 'test content';
mockReadFile.mockResolvedValue(testContent);
const result = await fetchContent({
path: 'test.txt',
fullContent: true,
researchGoal: 'Find implementation',
reasoning: 'Testing feature X',
});
expect(result.researchGoal).toBe('Find implementation');
expect(result.reasoning).toBe('Testing feature X');
});
});
});