Skip to content

Commit 0876bbd

Browse files
authored
Improved code coverage for cli/src/zed-integration (#13570)
1 parent 5a355fe commit 0876bbd

File tree

4 files changed

+1121
-2
lines changed

4 files changed

+1121
-2
lines changed
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
8+
import { Connection, RequestError } from './connection.js';
9+
import { ReadableStream, WritableStream } from 'node:stream/web';
10+
11+
describe('Connection', () => {
12+
let toPeer: WritableStream<Uint8Array>;
13+
let fromPeer: ReadableStream<Uint8Array>;
14+
let peerController: ReadableStreamDefaultController<Uint8Array>;
15+
let receivedChunks: string[] = [];
16+
let connection: Connection;
17+
let handler: ReturnType<typeof vi.fn>;
18+
19+
beforeEach(() => {
20+
receivedChunks = [];
21+
toPeer = new WritableStream({
22+
write(chunk) {
23+
const str = new TextDecoder().decode(chunk);
24+
receivedChunks.push(str);
25+
},
26+
});
27+
28+
fromPeer = new ReadableStream({
29+
start(controller) {
30+
peerController = controller;
31+
},
32+
});
33+
34+
handler = vi.fn();
35+
connection = new Connection(handler, toPeer, fromPeer);
36+
});
37+
38+
afterEach(() => {
39+
vi.clearAllMocks();
40+
});
41+
42+
it('should send a request and receive a response', async () => {
43+
const responsePromise = connection.sendRequest('testMethod', {
44+
key: 'value',
45+
});
46+
47+
// Verify request was sent
48+
await vi.waitFor(() => {
49+
expect(receivedChunks.length).toBeGreaterThan(0);
50+
});
51+
const request = JSON.parse(receivedChunks[0]);
52+
expect(request).toMatchObject({
53+
jsonrpc: '2.0',
54+
method: 'testMethod',
55+
params: { key: 'value' },
56+
});
57+
expect(request.id).toBeDefined();
58+
59+
// Simulate response
60+
const response = {
61+
jsonrpc: '2.0',
62+
id: request.id,
63+
result: { success: true },
64+
};
65+
peerController.enqueue(
66+
new TextEncoder().encode(JSON.stringify(response) + '\n'),
67+
);
68+
69+
const result = await responsePromise;
70+
expect(result).toEqual({ success: true });
71+
});
72+
73+
it('should send a notification', async () => {
74+
await connection.sendNotification('notifyMethod', { key: 'value' });
75+
76+
await vi.waitFor(() => {
77+
expect(receivedChunks.length).toBeGreaterThan(0);
78+
});
79+
const notification = JSON.parse(receivedChunks[0]);
80+
expect(notification).toMatchObject({
81+
jsonrpc: '2.0',
82+
method: 'notifyMethod',
83+
params: { key: 'value' },
84+
});
85+
expect(notification.id).toBeUndefined();
86+
});
87+
88+
it('should handle incoming requests', async () => {
89+
handler.mockResolvedValue({ result: 'ok' });
90+
91+
const request = {
92+
jsonrpc: '2.0',
93+
id: 1,
94+
method: 'incomingMethod',
95+
params: { foo: 'bar' },
96+
};
97+
peerController.enqueue(
98+
new TextEncoder().encode(JSON.stringify(request) + '\n'),
99+
);
100+
101+
// Wait for handler to be called and response to be written
102+
await vi.waitFor(() => {
103+
expect(handler).toHaveBeenCalledWith('incomingMethod', { foo: 'bar' });
104+
expect(receivedChunks.length).toBeGreaterThan(0);
105+
});
106+
107+
const response = JSON.parse(receivedChunks[receivedChunks.length - 1]);
108+
expect(response).toMatchObject({
109+
jsonrpc: '2.0',
110+
id: 1,
111+
result: { result: 'ok' },
112+
});
113+
});
114+
115+
it('should handle incoming notifications', async () => {
116+
const notification = {
117+
jsonrpc: '2.0',
118+
method: 'incomingNotify',
119+
params: { foo: 'bar' },
120+
};
121+
peerController.enqueue(
122+
new TextEncoder().encode(JSON.stringify(notification) + '\n'),
123+
);
124+
125+
// Wait for handler to be called
126+
await vi.waitFor(() => {
127+
expect(handler).toHaveBeenCalledWith('incomingNotify', { foo: 'bar' });
128+
});
129+
// Notifications don't send responses
130+
expect(receivedChunks.length).toBe(0);
131+
});
132+
133+
it('should handle request errors from handler', async () => {
134+
handler.mockRejectedValue(new Error('Handler failed'));
135+
136+
const request = {
137+
jsonrpc: '2.0',
138+
id: 2,
139+
method: 'failMethod',
140+
};
141+
peerController.enqueue(
142+
new TextEncoder().encode(JSON.stringify(request) + '\n'),
143+
);
144+
145+
await vi.waitFor(() => {
146+
expect(receivedChunks.length).toBeGreaterThan(0);
147+
});
148+
149+
const response = JSON.parse(receivedChunks[receivedChunks.length - 1]);
150+
expect(response).toMatchObject({
151+
jsonrpc: '2.0',
152+
id: 2,
153+
error: {
154+
code: -32603,
155+
message: 'Internal error',
156+
data: { details: 'Handler failed' },
157+
},
158+
});
159+
});
160+
161+
it('should handle RequestError from handler', async () => {
162+
handler.mockRejectedValue(RequestError.methodNotFound('Unknown method'));
163+
164+
const request = {
165+
jsonrpc: '2.0',
166+
id: 3,
167+
method: 'unknown',
168+
};
169+
peerController.enqueue(
170+
new TextEncoder().encode(JSON.stringify(request) + '\n'),
171+
);
172+
173+
await vi.waitFor(() => {
174+
expect(receivedChunks.length).toBeGreaterThan(0);
175+
});
176+
177+
const response = JSON.parse(receivedChunks[receivedChunks.length - 1]);
178+
expect(response).toMatchObject({
179+
jsonrpc: '2.0',
180+
id: 3,
181+
error: {
182+
code: -32601,
183+
message: 'Method not found',
184+
data: { details: 'Unknown method' },
185+
},
186+
});
187+
});
188+
189+
it('should handle response errors', async () => {
190+
const responsePromise = connection.sendRequest('testMethod');
191+
192+
// Verify request was sent
193+
await vi.waitFor(() => {
194+
expect(receivedChunks.length).toBeGreaterThan(0);
195+
});
196+
const request = JSON.parse(receivedChunks[0]);
197+
198+
// Simulate error response
199+
const response = {
200+
jsonrpc: '2.0',
201+
id: request.id,
202+
error: {
203+
code: -32000,
204+
message: 'Custom error',
205+
},
206+
};
207+
peerController.enqueue(
208+
new TextEncoder().encode(JSON.stringify(response) + '\n'),
209+
);
210+
211+
await expect(responsePromise).rejects.toMatchObject({
212+
code: -32000,
213+
message: 'Custom error',
214+
});
215+
});
216+
});
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { describe, it, expect, vi, beforeEach, type Mocked } from 'vitest';
8+
import { AcpFileSystemService } from './fileSystemService.js';
9+
import type { Client } from './acp.js';
10+
import type { FileSystemService } from '@google/gemini-cli-core';
11+
12+
describe('AcpFileSystemService', () => {
13+
let mockClient: Mocked<Client>;
14+
let mockFallback: Mocked<FileSystemService>;
15+
let service: AcpFileSystemService;
16+
17+
beforeEach(() => {
18+
mockClient = {
19+
requestPermission: vi.fn(),
20+
sessionUpdate: vi.fn(),
21+
writeTextFile: vi.fn(),
22+
readTextFile: vi.fn(),
23+
};
24+
mockFallback = {
25+
readTextFile: vi.fn(),
26+
writeTextFile: vi.fn(),
27+
findFiles: vi.fn(),
28+
};
29+
});
30+
31+
describe('readTextFile', () => {
32+
it.each([
33+
{
34+
capability: true,
35+
desc: 'client if capability exists',
36+
setup: () => {
37+
mockClient.readTextFile.mockResolvedValue({ content: 'content' });
38+
},
39+
verify: () => {
40+
expect(mockClient.readTextFile).toHaveBeenCalledWith({
41+
path: '/path/to/file',
42+
sessionId: 'session-1',
43+
line: null,
44+
limit: null,
45+
});
46+
expect(mockFallback.readTextFile).not.toHaveBeenCalled();
47+
},
48+
},
49+
{
50+
capability: false,
51+
desc: 'fallback if capability missing',
52+
setup: () => {
53+
mockFallback.readTextFile.mockResolvedValue('content');
54+
},
55+
verify: () => {
56+
expect(mockFallback.readTextFile).toHaveBeenCalledWith(
57+
'/path/to/file',
58+
);
59+
expect(mockClient.readTextFile).not.toHaveBeenCalled();
60+
},
61+
},
62+
])('should use $desc', async ({ capability, setup, verify }) => {
63+
service = new AcpFileSystemService(
64+
mockClient,
65+
'session-1',
66+
{ readTextFile: capability, writeTextFile: true },
67+
mockFallback,
68+
);
69+
setup();
70+
71+
const result = await service.readTextFile('/path/to/file');
72+
73+
expect(result).toBe('content');
74+
verify();
75+
});
76+
});
77+
78+
describe('writeTextFile', () => {
79+
it.each([
80+
{
81+
capability: true,
82+
desc: 'client if capability exists',
83+
verify: () => {
84+
expect(mockClient.writeTextFile).toHaveBeenCalledWith({
85+
path: '/path/to/file',
86+
content: 'content',
87+
sessionId: 'session-1',
88+
});
89+
expect(mockFallback.writeTextFile).not.toHaveBeenCalled();
90+
},
91+
},
92+
{
93+
capability: false,
94+
desc: 'fallback if capability missing',
95+
verify: () => {
96+
expect(mockFallback.writeTextFile).toHaveBeenCalledWith(
97+
'/path/to/file',
98+
'content',
99+
);
100+
expect(mockClient.writeTextFile).not.toHaveBeenCalled();
101+
},
102+
},
103+
])('should use $desc', async ({ capability, verify }) => {
104+
service = new AcpFileSystemService(
105+
mockClient,
106+
'session-1',
107+
{ writeTextFile: capability, readTextFile: true },
108+
mockFallback,
109+
);
110+
111+
await service.writeTextFile('/path/to/file', 'content');
112+
113+
verify();
114+
});
115+
});
116+
117+
it('should always use fallback for findFiles', () => {
118+
service = new AcpFileSystemService(
119+
mockClient,
120+
'session-1',
121+
{ readTextFile: true, writeTextFile: true },
122+
mockFallback,
123+
);
124+
mockFallback.findFiles.mockReturnValue(['file1', 'file2']);
125+
126+
const result = service.findFiles('pattern', ['/path']);
127+
128+
expect(mockFallback.findFiles).toHaveBeenCalledWith('pattern', ['/path']);
129+
expect(result).toEqual(['file1', 'file2']);
130+
});
131+
});

0 commit comments

Comments
 (0)