mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-23 18:22:37 -07:00
237 lines
6.7 KiB
TypeScript
237 lines
6.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
describe,
|
|
it,
|
|
expect,
|
|
vi,
|
|
beforeEach,
|
|
afterEach,
|
|
type Mocked,
|
|
} from 'vitest';
|
|
import { AcpFileSystemService } from './fileSystemService.js';
|
|
import type { AgentSideConnection } from '@agentclientprotocol/sdk';
|
|
import type { FileSystemService } from '@google/gemini-cli-core';
|
|
import os from 'node:os';
|
|
|
|
vi.mock('node:os', () => ({
|
|
default: {
|
|
homedir: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
describe('AcpFileSystemService', () => {
|
|
let mockConnection: Mocked<AgentSideConnection>;
|
|
let mockFallback: Mocked<FileSystemService>;
|
|
let service: AcpFileSystemService;
|
|
|
|
beforeEach(() => {
|
|
mockConnection = {
|
|
requestPermission: vi.fn(),
|
|
sessionUpdate: vi.fn(),
|
|
writeTextFile: vi.fn(),
|
|
readTextFile: vi.fn(),
|
|
} as unknown as Mocked<AgentSideConnection>;
|
|
mockFallback = {
|
|
readTextFile: vi.fn(),
|
|
writeTextFile: vi.fn(),
|
|
};
|
|
vi.mocked(os.homedir).mockReturnValue('/home/user');
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe('readTextFile', () => {
|
|
it.each([
|
|
{
|
|
capability: true,
|
|
path: '/path/to/file',
|
|
desc: 'connection if capability exists and file is inside root',
|
|
setup: () => {
|
|
mockConnection.readTextFile.mockResolvedValue({ content: 'content' });
|
|
},
|
|
verify: () => {
|
|
expect(mockConnection.readTextFile).toHaveBeenCalledWith({
|
|
path: '/path/to/file',
|
|
sessionId: 'session-1',
|
|
});
|
|
expect(mockFallback.readTextFile).not.toHaveBeenCalled();
|
|
},
|
|
},
|
|
{
|
|
capability: false,
|
|
path: '/path/to/file',
|
|
desc: 'fallback if capability missing',
|
|
setup: () => {
|
|
mockFallback.readTextFile.mockResolvedValue('content');
|
|
},
|
|
verify: () => {
|
|
expect(mockFallback.readTextFile).toHaveBeenCalledWith(
|
|
'/path/to/file',
|
|
);
|
|
expect(mockConnection.readTextFile).not.toHaveBeenCalled();
|
|
},
|
|
},
|
|
{
|
|
capability: true,
|
|
path: '/outside/file',
|
|
desc: 'fallback if capability exists but file is outside root',
|
|
setup: () => {
|
|
mockFallback.readTextFile.mockResolvedValue('content');
|
|
},
|
|
verify: () => {
|
|
expect(mockFallback.readTextFile).toHaveBeenCalledWith(
|
|
'/outside/file',
|
|
);
|
|
expect(mockConnection.readTextFile).not.toHaveBeenCalled();
|
|
},
|
|
},
|
|
{
|
|
capability: true,
|
|
path: '/home/user/.gemini/tmp/file.md',
|
|
root: '/home/user',
|
|
desc: 'fallback if file is inside global gemini dir, even if root overlaps',
|
|
setup: () => {
|
|
mockFallback.readTextFile.mockResolvedValue('content');
|
|
},
|
|
verify: () => {
|
|
expect(mockFallback.readTextFile).toHaveBeenCalledWith(
|
|
'/home/user/.gemini/tmp/file.md',
|
|
);
|
|
expect(mockConnection.readTextFile).not.toHaveBeenCalled();
|
|
},
|
|
},
|
|
])(
|
|
'should use $desc',
|
|
async ({ capability, path, root, setup, verify }) => {
|
|
service = new AcpFileSystemService(
|
|
mockConnection,
|
|
'session-1',
|
|
{ readTextFile: capability, writeTextFile: true },
|
|
mockFallback,
|
|
root || '/path/to',
|
|
);
|
|
setup();
|
|
|
|
const result = await service.readTextFile(path);
|
|
|
|
expect(result).toBe('content');
|
|
verify();
|
|
},
|
|
);
|
|
|
|
it('should throw normalized ENOENT error when readTextFile encounters "Resource not found"', async () => {
|
|
service = new AcpFileSystemService(
|
|
mockConnection,
|
|
'session-1',
|
|
{ readTextFile: true, writeTextFile: true },
|
|
mockFallback,
|
|
'/path/to',
|
|
);
|
|
mockConnection.readTextFile.mockRejectedValue(
|
|
new Error('Resource not found for document'),
|
|
);
|
|
|
|
await expect(
|
|
service.readTextFile('/path/to/missing'),
|
|
).rejects.toMatchObject({
|
|
code: 'ENOENT',
|
|
message: 'Resource not found for document',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('writeTextFile', () => {
|
|
it.each([
|
|
{
|
|
capability: true,
|
|
path: '/path/to/file',
|
|
desc: 'connection if capability exists and file is inside root',
|
|
verify: () => {
|
|
expect(mockConnection.writeTextFile).toHaveBeenCalledWith({
|
|
path: '/path/to/file',
|
|
content: 'content',
|
|
sessionId: 'session-1',
|
|
});
|
|
expect(mockFallback.writeTextFile).not.toHaveBeenCalled();
|
|
},
|
|
},
|
|
{
|
|
capability: false,
|
|
path: '/path/to/file',
|
|
desc: 'fallback if capability missing',
|
|
verify: () => {
|
|
expect(mockFallback.writeTextFile).toHaveBeenCalledWith(
|
|
'/path/to/file',
|
|
'content',
|
|
);
|
|
expect(mockConnection.writeTextFile).not.toHaveBeenCalled();
|
|
},
|
|
},
|
|
{
|
|
capability: true,
|
|
path: '/outside/file',
|
|
desc: 'fallback if capability exists but file is outside root',
|
|
verify: () => {
|
|
expect(mockFallback.writeTextFile).toHaveBeenCalledWith(
|
|
'/outside/file',
|
|
'content',
|
|
);
|
|
expect(mockConnection.writeTextFile).not.toHaveBeenCalled();
|
|
},
|
|
},
|
|
{
|
|
capability: true,
|
|
path: '/home/user/.gemini/tmp/file.md',
|
|
root: '/home/user',
|
|
desc: 'fallback if file is inside global gemini dir, even if root overlaps',
|
|
verify: () => {
|
|
expect(mockFallback.writeTextFile).toHaveBeenCalledWith(
|
|
'/home/user/.gemini/tmp/file.md',
|
|
'content',
|
|
);
|
|
expect(mockConnection.writeTextFile).not.toHaveBeenCalled();
|
|
},
|
|
},
|
|
])('should use $desc', async ({ capability, path, root, verify }) => {
|
|
service = new AcpFileSystemService(
|
|
mockConnection,
|
|
'session-1',
|
|
{ writeTextFile: capability, readTextFile: true },
|
|
mockFallback,
|
|
root || '/path/to',
|
|
);
|
|
|
|
await service.writeTextFile(path, 'content');
|
|
|
|
verify();
|
|
});
|
|
|
|
it('should throw normalized ENOENT error when writeTextFile encounters "Resource not found"', async () => {
|
|
service = new AcpFileSystemService(
|
|
mockConnection,
|
|
'session-1',
|
|
{ readTextFile: true, writeTextFile: true },
|
|
mockFallback,
|
|
'/path/to',
|
|
);
|
|
mockConnection.writeTextFile.mockRejectedValue(
|
|
new Error('Resource not found for directory'),
|
|
);
|
|
|
|
await expect(
|
|
service.writeTextFile('/path/to/missing', 'content'),
|
|
).rejects.toMatchObject({
|
|
code: 'ENOENT',
|
|
message: 'Resource not found for directory',
|
|
});
|
|
});
|
|
});
|
|
});
|