agent-gear 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,14 +2,25 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { EventEmitter } from 'events';
3
3
  import { Writable, Readable } from 'node:stream';
4
4
  import type { ChildProcess } from 'child_process';
5
+ import * as os from 'os';
6
+
7
+ vi.mock('os', () => ({
8
+ platform: vi.fn(() => 'linux'),
9
+ }));
5
10
 
6
11
  vi.mock('child_process', () => ({
7
12
  spawn: vi.fn(),
8
13
  execFileSync: vi.fn(() => '/usr/bin/opencode'),
9
14
  }));
10
15
 
16
+ vi.mock('fs', async (importOriginal) => {
17
+ const actual = await importOriginal<typeof import('fs')>();
18
+ return { ...actual, existsSync: vi.fn(() => false), mkdirSync: vi.fn() };
19
+ });
20
+
11
21
  import { AcpSpawner } from '../../src/acp/spawner.js';
12
- import { spawn } from 'child_process';
22
+ import { spawn, execFileSync } from 'child_process';
23
+ import { existsSync } from 'fs';
13
24
 
14
25
  function createMockChildProcess(): ChildProcess & EventEmitter {
15
26
  const proc = new EventEmitter() as ChildProcess & EventEmitter;
@@ -146,4 +157,88 @@ describe('AcpSpawner', () => {
146
157
  await vi.waitFor(() => expect(resolved).toBe(true));
147
158
  });
148
159
  });
160
+
161
+ describe('Windows platform', () => {
162
+ beforeEach(() => {
163
+ vi.mocked(os.platform).mockReturnValue('win32');
164
+ vi.mocked(execFileSync).mockReset();
165
+ vi.mocked(existsSync).mockReset();
166
+ vi.mocked(spawn).mockClear();
167
+ });
168
+
169
+ afterEach(() => {
170
+ vi.mocked(os.platform).mockReturnValue('linux');
171
+ });
172
+
173
+ describe('resolveCommand', () => {
174
+ it('should use `where` instead of `which`', () => {
175
+ vi.mocked(execFileSync).mockReturnValue('C:\\npm\\copilot.cmd\r\n');
176
+ const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot', args: ['--acp', '--stdio'] });
177
+ sp.start();
178
+ expect(execFileSync).toHaveBeenCalledWith('where', ['copilot'], { encoding: 'utf8', timeout: 1000 });
179
+ });
180
+
181
+ it('should return full path from `where`', () => {
182
+ vi.mocked(execFileSync).mockReturnValue('C:\\Users\\ash\\AppData\\Roaming\\npm\\copilot.cmd\r\n');
183
+ const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot', args: ['--acp', '--stdio'] });
184
+ sp.start();
185
+ expect(spawn).toHaveBeenCalledWith(
186
+ 'C:\\Users\\ash\\AppData\\Roaming\\npm\\copilot.cmd',
187
+ ['--acp', '--stdio'],
188
+ expect.objectContaining({ shell: true }),
189
+ );
190
+ });
191
+
192
+ it('should fall back to PATHEXT check when `where` fails', () => {
193
+ vi.mocked(execFileSync).mockImplementation(() => { throw new Error('not found'); });
194
+ vi.mocked(existsSync).mockImplementation((p: string) => p === 'copilot.cmd');
195
+ const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
196
+ sp.start();
197
+ expect(spawn).toHaveBeenCalledWith('copilot.cmd', expect.any(Array), expect.objectContaining({ shell: true }));
198
+ });
199
+
200
+ it('should return bare command as last resort', () => {
201
+ vi.mocked(execFileSync).mockImplementation(() => { throw new Error('not found'); });
202
+ vi.mocked(existsSync).mockReturnValue(false);
203
+ const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
204
+ sp.start();
205
+ expect(spawn).toHaveBeenCalledWith('copilot', expect.any(Array), expect.objectContaining({ shell: true }));
206
+ });
207
+ });
208
+
209
+ describe('start', () => {
210
+ it('should spawn with shell: true', () => {
211
+ vi.mocked(execFileSync).mockReturnValue('C:\\npm\\copilot.cmd\r\n');
212
+ const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
213
+ sp.start();
214
+ expect(spawn).toHaveBeenCalledWith(
215
+ expect.any(String),
216
+ expect.any(Array),
217
+ expect.objectContaining({ shell: true }),
218
+ );
219
+ });
220
+ });
221
+
222
+ describe('stop', () => {
223
+ it('should call kill() without signal on Windows', () => {
224
+ const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
225
+ vi.mocked(execFileSync).mockReturnValue('C:\\npm\\copilot.cmd\r\n');
226
+ sp.start();
227
+ sp.stop();
228
+ expect(mockProcess.kill).toHaveBeenCalledWith();
229
+ expect(mockProcess.kill).not.toHaveBeenCalledWith('SIGTERM');
230
+ });
231
+
232
+ it('should call kill() again after 5s on Windows', () => {
233
+ const sp = new AcpSpawner({ sessionId: 's', cwd: 'C:\\project', command: 'copilot' });
234
+ vi.mocked(execFileSync).mockReturnValue('C:\\npm\\copilot.cmd\r\n');
235
+ sp.start();
236
+ sp.stop();
237
+ expect(mockProcess.kill).toHaveBeenCalledTimes(1);
238
+ vi.advanceTimersByTime(5000);
239
+ expect(mockProcess.kill).toHaveBeenCalledTimes(2);
240
+ expect(mockProcess.kill).not.toHaveBeenCalledWith('SIGKILL');
241
+ });
242
+ });
243
+ });
149
244
  });
@@ -25,11 +25,25 @@ vi.mock('../src/config.js', () => ({
25
25
 
26
26
  vi.mock('os', () => ({
27
27
  hostname: vi.fn().mockReturnValue('test-machine'),
28
+ platform: vi.fn(() => 'linux'),
29
+ homedir: vi.fn(() => '/home/user'),
28
30
  }));
29
31
 
32
+ vi.mock('fs/promises', () => ({
33
+ readdir: vi.fn(),
34
+ access: vi.fn(),
35
+ }));
36
+
37
+ vi.mock('path', async (importOriginal) => {
38
+ const actual = await importOriginal<typeof import('path')>();
39
+ return actual;
40
+ });
41
+
30
42
  import { connectToServer, unregister } from '../src/socketio-client.js';
31
43
  import { io } from 'socket.io-client';
32
44
  import { clearConfigKey } from '../src/config.js';
45
+ import * as os from 'os';
46
+ import { readdir, access } from 'fs/promises';
33
47
 
34
48
  function createMockSocket() {
35
49
  const handlers: Record<string, (...args: unknown[]) => void> = {};
@@ -87,7 +101,7 @@ describe('SocketIO Client', () => {
87
101
  expect(mockSocket.emit).toHaveBeenCalledWith('register', {
88
102
  key: 'zag_k_1_abc123',
89
103
  hostname: 'my-machine',
90
- version: '1.0.0',
104
+ version: '1.0.4',
91
105
  });
92
106
  });
93
107
 
@@ -102,7 +116,7 @@ describe('SocketIO Client', () => {
102
116
  expect(mockSocket.emit).toHaveBeenCalledWith('register', {
103
117
  key: 'zag_k_1_abc123',
104
118
  hostname: 'test-machine',
105
- version: '1.0.0',
119
+ version: '1.0.4',
106
120
  });
107
121
  });
108
122
 
@@ -193,4 +207,108 @@ describe('SocketIO Client', () => {
193
207
  expect(mockSocket.emit).toHaveBeenCalledWith('unregister', { reason: 'user_detach' });
194
208
  });
195
209
  });
210
+
211
+ describe('fs:browse', () => {
212
+ it('should list drives on Windows when path is /', async () => {
213
+ vi.mocked(os.platform).mockReturnValue('win32');
214
+ vi.mocked(access).mockImplementation(async (p: string) => {
215
+ if (p === 'C:\\' || p === 'D:\\') return;
216
+ throw new Error('not found');
217
+ });
218
+
219
+ const mockSocket = createMockSocket();
220
+ vi.mocked(io).mockReturnValue(mockSocket as never);
221
+
222
+ connectToServer('http://localhost:4001', 'zag_k_1_abc123');
223
+ mockSocket._trigger('connect');
224
+ mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
225
+
226
+ mockSocket._trigger('fs:browse', { requestId: 'req-1', path: '/' });
227
+ await vi.waitFor(() => {
228
+ expect(mockSocket.emit).toHaveBeenCalledWith('fs:browse:result', {
229
+ requestId: 'req-1',
230
+ entries: [
231
+ { name: 'C:', path: 'C:\\', isDirectory: true },
232
+ { name: 'D:', path: 'D:\\', isDirectory: true },
233
+ ],
234
+ });
235
+ });
236
+ });
237
+
238
+ it('should list drives on Windows when path is empty string', async () => {
239
+ vi.mocked(os.platform).mockReturnValue('win32');
240
+ vi.mocked(access).mockImplementation(async (p: string) => {
241
+ if (p === 'C:\\') return;
242
+ throw new Error('not found');
243
+ });
244
+
245
+ const mockSocket = createMockSocket();
246
+ vi.mocked(io).mockReturnValue(mockSocket as never);
247
+
248
+ connectToServer('http://localhost:4001', 'zag_k_1_abc123');
249
+ mockSocket._trigger('connect');
250
+ mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
251
+
252
+ mockSocket._trigger('fs:browse', { requestId: 'req-2', path: '' });
253
+ await vi.waitFor(() => {
254
+ expect(mockSocket.emit).toHaveBeenCalledWith('fs:browse:result', {
255
+ requestId: 'req-2',
256
+ entries: [
257
+ { name: 'C:', path: 'C:\\', isDirectory: true },
258
+ ],
259
+ });
260
+ });
261
+ });
262
+
263
+ it('should NOT list drives on Linux when path is /', async () => {
264
+ vi.mocked(os.platform).mockReturnValue('linux');
265
+ vi.mocked(readdir).mockResolvedValue([
266
+ { name: 'home', isDirectory: () => true },
267
+ { name: 'usr', isDirectory: () => true },
268
+ ] as never);
269
+
270
+ const mockSocket = createMockSocket();
271
+ vi.mocked(io).mockReturnValue(mockSocket as never);
272
+
273
+ connectToServer('http://localhost:4001', 'zag_k_1_abc123');
274
+ mockSocket._trigger('connect');
275
+ mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
276
+
277
+ mockSocket._trigger('fs:browse', { requestId: 'req-3', path: '/' });
278
+ await vi.waitFor(() => {
279
+ expect(mockSocket.emit).toHaveBeenCalledWith('fs:browse:result', {
280
+ requestId: 'req-3',
281
+ entries: [
282
+ { name: 'home', path: '/home', isDirectory: true },
283
+ { name: 'usr', path: '/usr', isDirectory: true },
284
+ ],
285
+ });
286
+ });
287
+ });
288
+
289
+ it('should skip unavailable drives on Windows', async () => {
290
+ vi.mocked(os.platform).mockReturnValue('win32');
291
+ vi.mocked(access).mockImplementation(async (p: string) => {
292
+ if (p === 'C:\\') return;
293
+ throw new Error('not found');
294
+ });
295
+
296
+ const mockSocket = createMockSocket();
297
+ vi.mocked(io).mockReturnValue(mockSocket as never);
298
+
299
+ connectToServer('http://localhost:4001', 'zag_k_1_abc123');
300
+ mockSocket._trigger('connect');
301
+ mockSocket._trigger('registered', { controllerId: 'ctrl-1' });
302
+
303
+ mockSocket._trigger('fs:browse', { requestId: 'req-4', path: '/' });
304
+ await vi.waitFor(() => {
305
+ expect(mockSocket.emit).toHaveBeenCalledWith('fs:browse:result', {
306
+ requestId: 'req-4',
307
+ entries: [
308
+ { name: 'C:', path: 'C:\\', isDirectory: true },
309
+ ],
310
+ });
311
+ });
312
+ });
313
+ });
196
314
  });