/**
* Sanity test: exercises all tools in one end-to-end test
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { viewStructure } from '../../src/tools/local_view_structure.js';
import { findFiles } from '../../src/tools/local_find_files.js';
import { searchContentRipgrep } from '../../src/tools/local_ripgrep.js';
import { fetchContent } from '../../src/tools/local_fetch_content.js';
import * as exec from '../../src/utils/exec.js';
import * as pathValidator from '../../src/security/pathValidator.js';
import type { Stats } from 'fs';
// Mocks
vi.mock('../../src/utils/exec.js', () => ({ safeExec: vi.fn() }));
vi.mock('../../src/security/pathValidator.js', () => ({
pathValidator: { validate: vi.fn() },
}));
// Shareable fs mocks for view_structure and find_files
const { mockReaddirFn, mockLstatFn, mockLstatSyncFn } = vi.hoisted(() => ({
mockReaddirFn: vi.fn(),
mockLstatFn: vi.fn(),
mockLstatSyncFn: vi.fn(),
}));
vi.mock('fs', () => ({
default: {
lstatSync: mockLstatSyncFn,
promises: { readdir: mockReaddirFn, lstat: mockLstatFn },
},
lstatSync: mockLstatSyncFn,
promises: { readdir: mockReaddirFn, lstat: mockLstatFn },
}));
// fs/promises mock for fetch_content
vi.mock('fs/promises', () => ({
readFile: vi.fn(),
stat: vi.fn(),
}));
import * as fsp from 'fs/promises';
describe('Integration sanity: all tools', () => {
const mockSafeExec = vi.mocked(exec.safeExec);
const mockValidate = vi.mocked(pathValidator.pathValidator.validate);
const mockReaddir = mockReaddirFn;
const mockLstat = mockLstatFn;
const mockLstatSync = mockLstatSyncFn;
const mockReadFile = vi.mocked(fsp.readFile);
const mockStat = vi.mocked(fsp.stat);
beforeEach(() => {
vi.clearAllMocks();
mockValidate.mockReturnValue({ isValid: true, sanitizedPath: '/workspace' });
mockReaddir.mockResolvedValue([]);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 100,
mtime: new Date('2024-06-01T00:00:00.000Z'),
} as unknown as Stats);
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as unknown as Stats);
mockReadFile.mockResolvedValue('function demo() {}\n// line2\n// line3');
mockStat.mockResolvedValue({ size: 20000 } as unknown as Awaited<ReturnType<typeof fsp.stat>>);
});
it('should run all tools end-to-end with core functionality', async () => {
// 1) local_view_structure: simple listing with pagination
mockSafeExec.mockResolvedValueOnce({
success: true,
code: 0,
stdout: 'a.txt\nb.js\ndir',
stderr: '',
});
mockLstatSync.mockImplementation((p: string | Buffer | URL) => ({
isDirectory: () => p.toString().endsWith('dir'),
isSymbolicLink: () => false,
} as unknown as Stats));
const vs = await viewStructure({ path: '/workspace', entriesPerPage: 2, entryPageNumber: 1 });
expect(vs.status).toBe('hasResults');
expect(vs.structuredOutput).toBeDefined();
expect(vs.pagination?.currentPage).toBe(1);
expect(vs.pagination?.entriesPerPage).toBe(2);
// 2) local_find_files: NUL output, details, pagination
mockSafeExec.mockResolvedValueOnce({
success: true,
code: 0,
stdout: '/workspace/a.txt\0/workspace/b.js\0',
stderr: '',
});
// ensure lstat returns size/permissions
mockLstat.mockResolvedValueOnce({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 123,
mode: parseInt('100644', 8),
mtime: new Date('2024-06-01T00:00:00.000Z'),
} as unknown as Stats);
mockLstat.mockResolvedValueOnce({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 321,
mode: parseInt('100755', 8),
mtime: new Date('2024-06-02T00:00:00.000Z'),
} as unknown as Stats);
const ff = await findFiles({ path: '/workspace', details: true, filesPerPage: 2, filePageNumber: 1 });
expect(ff.status).toBe('hasResults');
expect(ff.files?.length).toBeLessThanOrEqual(2);
expect(ff.pagination?.currentPage).toBe(1);
// 3) local_ripgrep: NDJSON matches, per-file pagination, show modified
const rgJson = [
JSON.stringify({
type: 'match',
data: {
path: { text: '/workspace/a.txt' },
lines: { text: 'first line match' },
line_number: 10,
absolute_offset: 100,
submatches: [{ start: 0, end: 5, match: { text: 'first' } }],
},
}),
JSON.stringify({
type: 'match',
data: {
path: { text: '/workspace/a.txt' },
lines: { text: 'second line match' },
line_number: 20,
absolute_offset: 200,
submatches: [{ start: 0, end: 6, match: { text: 'second' } }],
},
}),
].join('\n');
mockSafeExec.mockResolvedValueOnce({ success: true, code: 0, stdout: rgJson, stderr: '' });
const rg = await searchContentRipgrep({
pattern: 'match',
path: '/workspace',
showFileLastModified: true,
matchesPerPage: 1,
} as any);
expect(rg.status).toBe('hasResults');
expect(rg.files?.[0].matchCount).toBe(2);
expect(rg.files?.[0].matches.length).toBe(1); // paginated matches
expect(rg.files?.[0].pagination?.totalPages).toBe(2);
// 4) local_fetch_content: large file paginated and matchString
mockReadFile.mockResolvedValueOnce('x'.repeat(20000));
mockStat.mockResolvedValueOnce({ size: 20000 } as unknown as Awaited<ReturnType<typeof fsp.stat>>);
const fc = await fetchContent({ path: '/workspace/a.txt', matchString: 'x', charLength: 5000 });
expect(fc.status).toBe('hasResults');
expect(fc.pagination?.hasMore).toBe(true);
// 5) Negative path validation for a tool (error path)
mockValidate.mockReturnValueOnce({ isValid: false, error: 'Invalid path' });
const vsErr = await viewStructure({ path: '/etc/passwd' });
expect(vsErr.status).toBe('error');
});
});