/**
* Tests for local_view_structure tool - comprehensive coverage including pagination
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { viewStructure } from '../../src/tools/local_view_structure.js';
import * as exec from '../../src/utils/exec.js';
import * as pathValidator from '../../src/security/pathValidator.js';
import type { Stats } from 'fs';
// Mock dependencies
vi.mock('../../src/utils/exec.js', () => ({
safeExec: vi.fn(),
}));
vi.mock('../../src/security/pathValidator.js', () => ({
pathValidator: {
validate: vi.fn(),
},
}));
// Create shared mock functions using vi.hoisted to make them available during hoisting
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,
},
}));
describe('local_view_structure', () => {
const mockSafeExec = vi.mocked(exec.safeExec);
const mockValidate = vi.mocked(pathValidator.pathValidator.validate);
// Use the shared mock functions directly
const mockReaddir = mockReaddirFn;
const mockLstat = mockLstatFn;
const mockLstatSync = mockLstatSyncFn;
beforeEach(() => {
// Clear all mocks but then immediately set defaults
vi.clearAllMocks();
mockValidate.mockReturnValue({
isValid: true,
sanitizedPath: '/test/path',
});
// Set default mock implementations that will be used unless overridden
// These MUST return valid values to prevent undefined errors
mockReaddir.mockResolvedValue([]);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 0,
mtime: new Date(),
} as Stats);
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
mockSafeExec.mockResolvedValue({
success: true,
stdout: '',
stderr: '',
code: 0,
});
});
describe('Basic directory listing', () => {
it('should list directory contents', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: 'file1.txt\nfile2.js\ndir1',
stderr: '',
code: 0,
});
mockLstatSync.mockImplementation((path: string | Buffer | URL) => ({
isDirectory: () => path.toString().includes('dir'),
isSymbolicLink: () => false,
} as Stats));
const result = await viewStructure({
path: '/test/path',
treeView: false,
});
expect(result.status).toBe('hasResults');
expect(result.entries).toBeDefined();
expect(result.entries?.length).toBeGreaterThan(0);
});
it('should handle empty directories', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '',
stderr: '',
code: 0,
});
const result = await viewStructure({
path: '/test/empty',
treeView: false,
});
expect(result.status).toBe('empty');
});
});
describe('Tree view mode (default)', () => {
it('should generate tree structure with file sizes', async () => {
// Mock readdir to always return files (for any path)
mockReaddir.mockResolvedValue(['file1.txt', 'file2.js']);
mockLstat.mockImplementation(async (_path: string | Buffer | URL): Promise<Stats> => {
return {
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats;
});
const result = await viewStructure({
path: '/test/path',
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).toBeDefined();
expect(result.treeStructure).toContain('file1.txt');
expect(result.treeStructure).toContain('1.0KB'); // File sizes shown
});
it('should show file sizes for files, not directories', async () => {
mockReaddir.mockResolvedValue(['file1.txt']);
mockLstat.mockImplementation(async (_path: string | Buffer | URL): Promise<Stats> => {
return {
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 2048,
mtime: new Date(),
} as Stats;
});
const result = await viewStructure({
path: '/test/path',
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).toContain('2.0KB'); // File size shown
// Directory name shown without size
});
it('should respect depth parameter', async () => {
let callCount = 0;
mockReaddir.mockImplementation(async (): Promise<string[]> => {
callCount++;
if (callCount === 1) return ['dir1'];
return ['subfile.txt'];
});
mockLstat.mockImplementation(async (path: string | Buffer | URL): Promise<Stats> => ({
isDirectory: () => path.toString().includes('dir1') && !path.toString().includes('subfile'),
isFile: () => path.toString().includes('subfile'),
isSymbolicLink: () => false,
size: 512,
mtime: new Date(),
} as Stats));
const result = await viewStructure({
path: '/test/path',
treeView: true,
depth: 2,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).toContain('dir1');
expect(result.treeStructure).toContain('subfile.txt');
});
});
describe('Detailed listing with metadata', () => {
it('should include file details when requested', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '-rw-r--r-- 1 user group 1024 Jan 1 12:00 file1.txt',
stderr: '',
code: 0,
});
const result = await viewStructure({
path: '/test/path',
details: true,
treeView: false,
});
expect(result.status).toBe('hasResults');
expect(result.entries?.[0]).toHaveProperty('size');
expect(result.entries?.[0]).toHaveProperty('permissions');
expect(result.entries?.[0]).toHaveProperty('modified');
});
it('should parse human-readable file sizes correctly', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '-rw-r--r-- 1 user group 1.5M Jan 1 12:00 large.bin',
stderr: '',
code: 0,
});
const result = await viewStructure({
path: '/test/path',
details: true,
humanReadable: true,
treeView: false,
});
expect(result.status).toBe('hasResults');
// Size is now human-readable string (e.g., "1.5 MB")
expect(result.entries?.[0].size).toMatch(/\d+(\.\d+)?\s*(B|KB|MB|GB)/);
});
});
describe('Filtering', () => {
it('should filter by file extension', async () => {
mockReaddir.mockResolvedValue(['file1.ts', 'file2.js', 'file3.ts']);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
const result = await viewStructure({
path: '/test/path',
extension: 'ts',
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).toContain('file1.ts');
expect(result.treeStructure).toContain('file3.ts');
expect(result.treeStructure).not.toContain('file2.js');
});
it('should filter by multiple extensions', async () => {
mockReaddir.mockResolvedValue(['file1.ts', 'file2.tsx', 'file3.js']);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
const result = await viewStructure({
path: '/test/path',
extensions: ['ts', 'tsx'],
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).toContain('file1.ts');
expect(result.treeStructure).toContain('file2.tsx');
expect(result.treeStructure).not.toContain('file3.js');
});
it('should filter files only', async () => {
mockReaddir.mockResolvedValue(['file1.txt', 'dir1']);
mockLstat.mockImplementation(async (path: string | Buffer | URL): Promise<Stats> => ({
isDirectory: () => path.toString().includes('dir1'),
isFile: () => path.toString().includes('file1'),
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats));
const result = await viewStructure({
path: '/test/path',
filesOnly: true,
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).toContain('file1.txt');
expect(result.treeStructure).not.toContain('dir1');
});
it('should filter directories only', async () => {
mockReaddir.mockResolvedValue(['file1.txt', 'dir1', 'dir2']);
mockLstat.mockImplementation(async (path: string | Buffer | URL): Promise<Stats> => ({
isDirectory: () => path.toString().includes('dir'),
isFile: () => path.toString().includes('file'),
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats));
const result = await viewStructure({
path: '/test/path',
directoriesOnly: true,
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).toContain('dir1');
expect(result.treeStructure).toContain('dir2');
expect(result.treeStructure).not.toContain('file1.txt');
});
it('should filter by name pattern', async () => {
mockReaddir.mockResolvedValue(['test1.txt', 'test2.txt', 'other.txt']);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
const result = await viewStructure({
path: '/test/path',
pattern: 'test',
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).toContain('test1.txt');
expect(result.treeStructure).toContain('test2.txt');
expect(result.treeStructure).not.toContain('other.txt');
});
});
describe('Hidden files', () => {
it('should show hidden files when requested', async () => {
mockReaddir.mockResolvedValue(['.hidden', 'visible.txt']);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
const result = await viewStructure({
path: '/test/path',
hidden: true,
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).toContain('.hidden');
expect(result.treeStructure).toContain('visible.txt');
});
it('should hide hidden files by default', async () => {
mockReaddir.mockResolvedValue(['.hidden', 'visible.txt']);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
const result = await viewStructure({
path: '/test/path',
hidden: false,
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
expect(result.treeStructure).not.toContain('.hidden');
expect(result.treeStructure).toContain('visible.txt');
});
});
describe('Sorting', () => {
it('should sort by name (default)', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: 'beta.txt\nalpha.txt\ngamma.txt',
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
sortBy: 'name',
treeView: false,
});
expect(result.status).toBe('hasResults');
// Entries should be sorted alphabetically
});
it('should sort by size', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '-rw-r--r-- 1 user group 1024 Jan 1 12:00 small.txt\n-rw-r--r-- 1 user group 2048 Jan 1 12:00 large.txt',
stderr: '',
code: 0,
});
const result = await viewStructure({
path: '/test/path',
sortBy: 'size',
details: true,
treeView: false,
});
expect(result.status).toBe('hasResults');
// Should be sorted by size
});
});
describe('Pagination - CRITICAL for large results', () => {
it('should require pagination for large directory listing (>100 entries)', async () => {
// Generate 150 entries
const entries = Array.from({ length: 150 }, (_, i) => `file${i}.txt`).join('\n');
mockSafeExec.mockResolvedValue({
success: true,
stdout: entries,
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: false,
// No charLength specified
});
// Should either return results or error requesting pagination
expect(['hasResults', 'error']).toContain(result.status);
if (result.status === 'error' && result.error) {
// Error message should indicate size or entry count
expect(result.error.toLowerCase()).toMatch(/char|entries|pagina/i);
}
});
it('should allow tree view for large directories without pagination', async () => {
mockReaddir.mockResolvedValue(
Array.from({ length: 150 }, (_, i) => `file${i}.txt`)
);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: true,
depth: 1,
// No charLength - but tree view is allowed
});
// Tree view should work without pagination
expect(result.status).toBe('hasResults');
});
it('should paginate large directory listings', async () => {
const entries = Array.from({ length: 150 }, (_, i) => `file${i}.txt`).join('\n');
mockSafeExec.mockResolvedValue({
success: true,
stdout: entries,
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: false,
charLength: 5000,
charOffset: 0,
});
expect(result.status).toBe('hasResults');
// When charLength is provided, totalChars should be defined in pagination
expect(result.pagination?.totalChars).toBeDefined();
expect(typeof result.pagination?.totalChars).toBe('number');
// If character pagination indicates more content, verify totalChars > charLength
if (result.charPagination?.hasMore) {
expect(result.pagination?.totalChars).toBeGreaterThan(5000);
}
});
it('should paginate tree view when requested', async () => {
mockReaddir.mockResolvedValue(['file1.txt']);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
// Mock the tree generation to produce large output
const result = await viewStructure({
path: '/test/path',
treeView: true,
depth: 1,
charLength: 10000,
});
if (result.treeStructure && result.treeStructure.length > 10000) {
expect(result.pagination?.hasMore).toBe(true);
}
});
it('should handle paginated continuation', async () => {
const entries = Array.from({ length: 150 }, (_, i) => `file${i}.txt`).join('\n');
mockSafeExec.mockResolvedValue({
success: true,
stdout: entries,
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
// First page
const result1 = await viewStructure({
path: '/test/path',
treeView: false,
charLength: 5000,
charOffset: 0,
});
expect(result1.status).toBe('hasResults');
// Only test continuation if pagination was triggered
if (!result1.hasMore) {
return; // Skip test if output wasn't large enough
}
expect(result1.hasMore).toBe(true);
// Second page
const result2 = await viewStructure({
path: '/test/path',
treeView: false,
charLength: 5000,
});
expect(result2.status).toBe('hasResults');
// Should get different entries
});
});
describe('Recursive listing', () => {
it('should list recursively with depth control', async () => {
mockReaddir
.mockResolvedValueOnce(['dir1', 'file1.txt'])
.mockResolvedValueOnce(['subfile.txt']);
mockLstat.mockImplementation(async (path: string | Buffer | URL): Promise<Stats> => ({
isDirectory: () => path.toString().includes('dir'),
isFile: () => !path.toString().includes('dir'),
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats));
const result = await viewStructure({
path: '/test/path',
recursive: true,
treeView: false,
});
// Tree view with recursive doesn't use mockSafeExec, so result may be empty
expect(['hasResults', 'empty']).toContain(result.status);
if (result.status === 'hasResults' && result.totalFiles) {
expect(result.totalFiles).toBeGreaterThan(0);
}
});
it('should handle max depth limit for recursive', async () => {
mockReaddir.mockResolvedValue(['file.txt']);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
const result = await viewStructure({
path: '/test/path',
depth: 5,
treeView: false,
});
// May be empty if mocked readdir returns empty array
expect(['hasResults', 'empty']).toContain(result.status);
// Should respect max depth of 5
});
it('should require pagination for large recursive listings', async () => {
// Mock large recursive result
mockReaddir.mockImplementation(async (): Promise<string[]> =>
Array.from({ length: 50 }, (_, i) => `file${i}.txt`)
);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
const result = await viewStructure({
path: '/test/path',
recursive: true,
treeView: false,
// Large result without pagination
});
if (result.totalFiles && result.totalFiles > 100) {
expect(result.status).toBe('error');
expect(result.error).toContain('charLength');
}
});
});
describe('Summary statistics', () => {
it('should include summary by default', async () => {
mockReaddir.mockResolvedValue(['file1.txt', 'dir1']);
mockLstat.mockImplementation(async (path: string | Buffer | URL): Promise<Stats> => ({
isDirectory: () => path.toString().includes('dir'),
isFile: () => !path.toString().includes('dir'),
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats));
const result = await viewStructure({
path: '/test/path',
summary: true,
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
// Summary stats are only included when summary=true and in tree view mode
if (result.totalFiles !== undefined) {
expect(result.totalFiles).toBeGreaterThanOrEqual(0);
}
if (result.totalDirectories !== undefined) {
expect(result.totalDirectories).toBeGreaterThanOrEqual(0);
}
if (result.totalSize !== undefined) {
expect(result.totalSize).toBeGreaterThanOrEqual(0);
}
});
});
describe('Path validation', () => {
it('should reject invalid paths', async () => {
mockValidate.mockReturnValue({
isValid: false,
error: 'Path is outside allowed directories',
});
const result = await viewStructure({
path: '/etc/passwd',
});
expect(result.status).toBe('error');
expect(result.error).toContain('Path is outside allowed directories');
});
});
describe('Error handling', () => {
it('should handle command failure', async () => {
mockSafeExec.mockResolvedValue({
success: false,
stdout: '',
stderr: 'ls: cannot access',
code: 1,
});
const result = await viewStructure({
path: '/test/path',
treeView: false,
});
expect(result.status).toBe('error');
});
it('should handle unreadable directories', async () => {
mockReaddir.mockRejectedValue(new Error('Permission denied'));
const result = await viewStructure({
path: '/test/path',
treeView: true,
depth: 1,
});
// Should handle error gracefully - might return error or hasResults with error message
expect(['error', 'empty', 'hasResults']).toContain(result.status);
});
});
describe('Limit parameter', () => {
it('should apply limit to results', async () => {
mockReaddir.mockResolvedValue(
Array.from({ length: 100 }, (_, i) => `file${i}.txt`)
);
mockLstat.mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mtime: new Date(),
} as Stats);
const result = await viewStructure({
path: '/test/path',
limit: 10,
treeView: true,
depth: 1,
});
expect(result.status).toBe('hasResults');
// Should respect limit
});
});
describe('NEW FEATURE: Entry-based pagination with default time sorting', () => {
it('should paginate with default 20 entries per page', async () => {
const fileList = Array.from({ length: 50 }, (_, i) => `file${i}.txt`).join('\n');
mockSafeExec.mockResolvedValue({
success: true,
stdout: fileList,
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: false,
});
expect(result.status).toBe('hasResults');
expect(result.entries?.length).toBeLessThanOrEqual(20);
expect(result.pagination?.totalPages).toBeGreaterThan(1);
expect(result.pagination?.hasMore).toBe(true);
});
it('should navigate to second page of entries', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: Array.from({ length: 50 }, (_, i) => `file${i}.txt`).join('\n'),
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: false,
entryPageNumber: 2,
});
expect(['hasResults', 'empty']).toContain(result.status);
if (result.status === 'hasResults') {
expect(result.pagination?.currentPage).toBe(2);
}
});
it('should support custom entriesPerPage', async () => {
const fileList = Array.from({ length: 50 }, (_, i) => `file${i}.txt`).join('\n');
mockSafeExec.mockResolvedValue({
success: true,
stdout: fileList,
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: false,
entriesPerPage: 10,
});
expect(result.status).toBe('hasResults');
expect(result.entries?.length).toBeLessThanOrEqual(10);
expect(result.pagination?.entriesPerPage).toBe(10);
});
it('should handle last page correctly', async () => {
const fileList = Array.from({ length: 25 }, (_, i) => `file${i}.txt`).join('\n');
mockSafeExec.mockResolvedValue({
success: true,
stdout: fileList,
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: false,
entriesPerPage: 20,
entryPageNumber: 2,
});
expect(result.status).toBe('hasResults');
expect(result.entries?.length).toBe(5);
expect(result.pagination?.hasMore).toBe(false);
});
});
describe('NEW FEATURE: Default sort by modification time', () => {
it('should sort by time (most recent first) by default', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: '-rw-r--r-- 1 user group 1024 Jan 1 12:00 old.txt\n-rw-r--r-- 1 user group 2048 Dec 1 12:00 new.txt',
stderr: '',
code: 0,
});
const result = await viewStructure({
path: '/test/path',
treeView: false,
});
expect(result.status).toBe('hasResults');
});
it('should allow overriding sort to name', async () => {
mockSafeExec.mockResolvedValue({
success: true,
stdout: 'beta.txt\nalpha.txt\ngamma.txt',
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
sortBy: 'name',
treeView: false,
});
expect(result.status).toBe('hasResults');
});
it('should sort even with pagination', async () => {
const fileList = Array.from({ length: 30 }, (_, i) => `file${i}.txt`).join('\n');
mockSafeExec.mockResolvedValue({
success: true,
stdout: fileList,
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: false,
entriesPerPage: 10,
});
expect(result.status).toBe('hasResults');
});
});
describe('NEW FEATURE: Entry pagination hints', () => {
it('should include pagination hints with entry info', async () => {
const fileList = Array.from({ length: 50 }, (_, i) => `file${i}.txt`).join('\n');
mockSafeExec.mockResolvedValue({
success: true,
stdout: fileList,
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: false,
entriesPerPage: 20,
});
expect(result.status).toBe('hasResults');
expect(result.hints).toBeDefined();
});
it('should show final page hint on last page', async () => {
const fileList = Array.from({ length: 25 }, (_, i) => `file${i}.txt`).join('\n');
mockSafeExec.mockResolvedValue({
success: true,
stdout: fileList,
stderr: '',
code: 0,
});
mockLstatSync.mockReturnValue({
isDirectory: () => false,
isSymbolicLink: () => false,
} as Stats);
const result = await viewStructure({
path: '/test/path',
treeView: false,
entriesPerPage: 20,
entryPageNumber: 2,
});
expect(result.status).toBe('hasResults');
});
});
});