mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
Revert other files to main, keep only process-utils changes
This commit is contained in:
@@ -8,6 +8,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|||||||
import { detectIde, IDE_DEFINITIONS } from './detect-ide.js';
|
import { detectIde, IDE_DEFINITIONS } from './detect-ide.js';
|
||||||
|
|
||||||
describe('detectIde', () => {
|
describe('detectIde', () => {
|
||||||
|
const ideProcessInfo = { pid: 123, command: 'some/path/to/code' };
|
||||||
|
const ideProcessInfoNoCode = { pid: 123, command: 'some/path/to/fork' };
|
||||||
|
|
||||||
// Clear all IDE-related environment variables before each test
|
// Clear all IDE-related environment variables before each test
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubEnv('__COG_BASHRC_SOURCED', '');
|
vi.stubEnv('__COG_BASHRC_SOURCED', '');
|
||||||
@@ -26,65 +29,73 @@ describe('detectIde', () => {
|
|||||||
|
|
||||||
it('should return undefined if TERM_PROGRAM is not vscode', () => {
|
it('should return undefined if TERM_PROGRAM is not vscode', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', '');
|
vi.stubEnv('TERM_PROGRAM', '');
|
||||||
expect(detectIde()).toBeUndefined();
|
expect(detectIde(ideProcessInfo)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect Devin', () => {
|
it('should detect Devin', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
vi.stubEnv('__COG_BASHRC_SOURCED', '1');
|
vi.stubEnv('__COG_BASHRC_SOURCED', '1');
|
||||||
expect(detectIde()).toBe(IDE_DEFINITIONS.devin);
|
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.devin);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect Replit', () => {
|
it('should detect Replit', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
vi.stubEnv('REPLIT_USER', 'testuser');
|
vi.stubEnv('REPLIT_USER', 'testuser');
|
||||||
expect(detectIde()).toBe(IDE_DEFINITIONS.replit);
|
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.replit);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect Cursor', () => {
|
it('should detect Cursor', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
vi.stubEnv('CURSOR_TRACE_ID', 'some-id');
|
vi.stubEnv('CURSOR_TRACE_ID', 'some-id');
|
||||||
expect(detectIde()).toBe(IDE_DEFINITIONS.cursor);
|
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cursor);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect Codespaces', () => {
|
it('should detect Codespaces', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
vi.stubEnv('CODESPACES', 'true');
|
vi.stubEnv('CODESPACES', 'true');
|
||||||
expect(detectIde()).toBe(IDE_DEFINITIONS.codespaces);
|
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.codespaces);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect Cloud Shell via EDITOR_IN_CLOUD_SHELL', () => {
|
it('should detect Cloud Shell via EDITOR_IN_CLOUD_SHELL', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
vi.stubEnv('EDITOR_IN_CLOUD_SHELL', 'true');
|
vi.stubEnv('EDITOR_IN_CLOUD_SHELL', 'true');
|
||||||
expect(detectIde()).toBe(IDE_DEFINITIONS.cloudshell);
|
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cloudshell);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect Cloud Shell via CLOUD_SHELL', () => {
|
it('should detect Cloud Shell via CLOUD_SHELL', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
vi.stubEnv('CLOUD_SHELL', 'true');
|
vi.stubEnv('CLOUD_SHELL', 'true');
|
||||||
expect(detectIde()).toBe(IDE_DEFINITIONS.cloudshell);
|
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.cloudshell);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect Trae', () => {
|
it('should detect Trae', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
vi.stubEnv('TERM_PRODUCT', 'Trae');
|
vi.stubEnv('TERM_PRODUCT', 'Trae');
|
||||||
expect(detectIde()).toBe(IDE_DEFINITIONS.trae);
|
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.trae);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect Firebase Studio via MONOSPACE_ENV', () => {
|
it('should detect Firebase Studio via MONOSPACE_ENV', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
vi.stubEnv('MONOSPACE_ENV', 'true');
|
vi.stubEnv('MONOSPACE_ENV', 'true');
|
||||||
expect(detectIde()).toBe(IDE_DEFINITIONS.firebasestudio);
|
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.firebasestudio);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should detect VSCodeFork when no other IDE is detected and no process info provided', () => {
|
it('should detect VSCode when no other IDE is detected and command includes "code"', () => {
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
vi.stubEnv('MONOSPACE_ENV', '');
|
vi.stubEnv('MONOSPACE_ENV', '');
|
||||||
expect(detectIde()).toBe(IDE_DEFINITIONS.vscodefork);
|
expect(detectIde(ideProcessInfo)).toBe(IDE_DEFINITIONS.vscode);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect VSCodeFork when no other IDE is detected and command does not include "code"', () => {
|
||||||
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
|
vi.stubEnv('MONOSPACE_ENV', '');
|
||||||
|
expect(detectIde(ideProcessInfoNoCode)).toBe(IDE_DEFINITIONS.vscodefork);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('detectIde with ideInfoFromFile', () => {
|
describe('detectIde with ideInfoFromFile', () => {
|
||||||
|
const ideProcessInfo = { pid: 123, command: 'some/path/to/code' };
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.stubEnv('__COG_BASHRC_SOURCED', '');
|
vi.stubEnv('__COG_BASHRC_SOURCED', '');
|
||||||
vi.stubEnv('REPLIT_USER', '');
|
vi.stubEnv('REPLIT_USER', '');
|
||||||
@@ -105,22 +116,22 @@ describe('detectIde with ideInfoFromFile', () => {
|
|||||||
name: 'custom-ide',
|
name: 'custom-ide',
|
||||||
displayName: 'Custom IDE',
|
displayName: 'Custom IDE',
|
||||||
};
|
};
|
||||||
expect(detectIde(undefined, ideInfoFromFile)).toEqual(ideInfoFromFile);
|
expect(detectIde(ideProcessInfo, ideInfoFromFile)).toEqual(ideInfoFromFile);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fall back to env detection if name is missing', () => {
|
it('should fall back to env detection if name is missing', () => {
|
||||||
const ideInfoFromFile = { displayName: 'Custom IDE' };
|
const ideInfoFromFile = { displayName: 'Custom IDE' };
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
expect(detectIde(undefined, ideInfoFromFile)).toBe(
|
expect(detectIde(ideProcessInfo, ideInfoFromFile)).toBe(
|
||||||
IDE_DEFINITIONS.vscodefork,
|
IDE_DEFINITIONS.vscode,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fall back to env detection if displayName is missing', () => {
|
it('should fall back to env detection if displayName is missing', () => {
|
||||||
const ideInfoFromFile = { name: 'custom-ide' };
|
const ideInfoFromFile = { name: 'custom-ide' };
|
||||||
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
vi.stubEnv('TERM_PROGRAM', 'vscode');
|
||||||
expect(detectIde(undefined, ideInfoFromFile)).toBe(
|
expect(detectIde(ideProcessInfo, ideInfoFromFile)).toBe(
|
||||||
IDE_DEFINITIONS.vscodefork,
|
IDE_DEFINITIONS.vscode,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function detectIdeFromEnv(): IdeInfo {
|
|||||||
|
|
||||||
function verifyVSCode(
|
function verifyVSCode(
|
||||||
ide: IdeInfo,
|
ide: IdeInfo,
|
||||||
ideProcessInfo?: {
|
ideProcessInfo: {
|
||||||
pid: number;
|
pid: number;
|
||||||
command: string;
|
command: string;
|
||||||
},
|
},
|
||||||
@@ -61,7 +61,7 @@ function verifyVSCode(
|
|||||||
return ide;
|
return ide;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
ideProcessInfo?.command &&
|
ideProcessInfo.command &&
|
||||||
ideProcessInfo.command.toLowerCase().includes('code')
|
ideProcessInfo.command.toLowerCase().includes('code')
|
||||||
) {
|
) {
|
||||||
return IDE_DEFINITIONS.vscode;
|
return IDE_DEFINITIONS.vscode;
|
||||||
@@ -70,7 +70,7 @@ function verifyVSCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function detectIde(
|
export function detectIde(
|
||||||
ideProcessInfo?: {
|
ideProcessInfo: {
|
||||||
pid: number;
|
pid: number;
|
||||||
command: string;
|
command: string;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
import { detectIde, IDE_DEFINITIONS } from './detect-ide.js';
|
import { detectIde, IDE_DEFINITIONS } from './detect-ide.js';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
|
import * as path from 'node:path';
|
||||||
|
|
||||||
vi.mock('node:fs', async (importOriginal) => {
|
vi.mock('node:fs', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof fs>();
|
const actual = await importOriginal<typeof fs>();
|
||||||
@@ -96,20 +97,21 @@ describe('IdeClient', () => {
|
|||||||
|
|
||||||
describe('connect', () => {
|
describe('connect', () => {
|
||||||
it('should connect using HTTP when port is provided in config file', async () => {
|
it('should connect using HTTP when port is provided in config file', async () => {
|
||||||
const config = { port: '8080', workspacePath: '/test/workspace' };
|
const config = { port: '8080' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-8080.json']);
|
).mockResolvedValue([]);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ideClient = await IdeClient.getInstance();
|
const ideClient = await IdeClient.getInstance();
|
||||||
await ideClient.connect();
|
await ideClient.connect();
|
||||||
|
|
||||||
|
expect(fs.promises.readFile).toHaveBeenCalledWith(
|
||||||
|
path.join('/tmp', 'qwen-code-ide-server-12345.json'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
|
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
|
||||||
new URL('http://127.0.0.1:8080/mcp'),
|
new URL('http://127.0.0.1:8080/mcp'),
|
||||||
expect.any(Object),
|
expect.any(Object),
|
||||||
@@ -121,19 +123,13 @@ describe('IdeClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should connect using stdio when stdio config is provided in file', async () => {
|
it('should connect using stdio when stdio config is provided in file', async () => {
|
||||||
const config = {
|
const config = { stdio: { command: 'test-cmd', args: ['--foo'] } };
|
||||||
stdio: { command: 'test-cmd', args: ['--foo'] },
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
workspacePath: '/test/workspace',
|
|
||||||
};
|
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-12345.json']);
|
).mockResolvedValue([]);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ideClient = await IdeClient.getInstance();
|
const ideClient = await IdeClient.getInstance();
|
||||||
await ideClient.connect();
|
await ideClient.connect();
|
||||||
@@ -152,17 +148,13 @@ describe('IdeClient', () => {
|
|||||||
const config = {
|
const config = {
|
||||||
port: '8080',
|
port: '8080',
|
||||||
stdio: { command: 'test-cmd', args: ['--foo'] },
|
stdio: { command: 'test-cmd', args: ['--foo'] },
|
||||||
workspacePath: '/test/workspace',
|
|
||||||
};
|
};
|
||||||
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-8080.json']);
|
).mockResolvedValue([]);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ideClient = await IdeClient.getInstance();
|
const ideClient = await IdeClient.getInstance();
|
||||||
await ideClient.connect();
|
await ideClient.connect();
|
||||||
@@ -225,16 +217,13 @@ describe('IdeClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should prioritize file config over environment variables', async () => {
|
it('should prioritize file config over environment variables', async () => {
|
||||||
const config = { port: '8080', workspacePath: '/test/workspace' };
|
const config = { port: '8080' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-8080.json']);
|
).mockResolvedValue([]);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '9090';
|
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '9090';
|
||||||
|
|
||||||
const ideClient = await IdeClient.getInstance();
|
const ideClient = await IdeClient.getInstance();
|
||||||
@@ -276,15 +265,7 @@ describe('IdeClient', () => {
|
|||||||
describe('getConnectionConfigFromFile', () => {
|
describe('getConnectionConfigFromFile', () => {
|
||||||
it('should return config from the specific pid file if it exists', async () => {
|
it('should return config from the specific pid file if it exists', async () => {
|
||||||
const config = { port: '1234', workspacePath: '/test/workspace' };
|
const config = { port: '1234', workspacePath: '/test/workspace' };
|
||||||
(
|
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
|
||||||
>
|
|
||||||
).mockResolvedValue(['qwen-code-ide-server-1234.json']);
|
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ideClient = await IdeClient.getInstance();
|
const ideClient = await IdeClient.getInstance();
|
||||||
// In tests, the private method can be accessed like this.
|
// In tests, the private method can be accessed like this.
|
||||||
@@ -295,6 +276,10 @@ describe('IdeClient', () => {
|
|||||||
).getConnectionConfigFromFile();
|
).getConnectionConfigFromFile();
|
||||||
|
|
||||||
expect(result).toEqual(config);
|
expect(result).toEqual(config);
|
||||||
|
expect(fs.promises.readFile).toHaveBeenCalledWith(
|
||||||
|
path.join('/tmp', 'qwen-code-ide-server-12345.json'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined if no config files are found', async () => {
|
it('should return undefined if no config files are found', async () => {
|
||||||
@@ -317,11 +302,14 @@ describe('IdeClient', () => {
|
|||||||
|
|
||||||
it('should find and parse a single config file with the new naming scheme', async () => {
|
it('should find and parse a single config file with the new naming scheme', async () => {
|
||||||
const config = { port: '5678', workspacePath: '/test/workspace' };
|
const config = { port: '5678', workspacePath: '/test/workspace' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
|
||||||
|
new Error('not found'),
|
||||||
|
); // For old path
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-5678.json']);
|
).mockResolvedValue(['qwen-code-ide-server-12345-123.json']);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
||||||
isValid: true,
|
isValid: true,
|
||||||
@@ -335,6 +323,10 @@ describe('IdeClient', () => {
|
|||||||
).getConnectionConfigFromFile();
|
).getConnectionConfigFromFile();
|
||||||
|
|
||||||
expect(result).toEqual(config);
|
expect(result).toEqual(config);
|
||||||
|
expect(fs.promises.readFile).toHaveBeenCalledWith(
|
||||||
|
path.join('/tmp/gemini/ide', 'qwen-code-ide-server-12345-123.json'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out configs with invalid workspace paths', async () => {
|
it('should filter out configs with invalid workspace paths', async () => {
|
||||||
@@ -346,13 +338,16 @@ describe('IdeClient', () => {
|
|||||||
port: '1111',
|
port: '1111',
|
||||||
workspacePath: '/invalid/workspace',
|
workspacePath: '/invalid/workspace',
|
||||||
};
|
};
|
||||||
|
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
|
||||||
|
new Error('not found'),
|
||||||
|
);
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue([
|
).mockResolvedValue([
|
||||||
'qwen-code-ide-server-1111.json',
|
'qwen-code-ide-server-12345-111.json',
|
||||||
'qwen-code-ide-server-5678.json',
|
'qwen-code-ide-server-12345-222.json',
|
||||||
]);
|
]);
|
||||||
vi.mocked(fs.promises.readFile)
|
vi.mocked(fs.promises.readFile)
|
||||||
.mockResolvedValueOnce(JSON.stringify(invalidConfig))
|
.mockResolvedValueOnce(JSON.stringify(invalidConfig))
|
||||||
@@ -384,13 +379,16 @@ describe('IdeClient', () => {
|
|||||||
it('should return the first valid config when multiple workspaces are valid', async () => {
|
it('should return the first valid config when multiple workspaces are valid', async () => {
|
||||||
const config1 = { port: '1111', workspacePath: '/test/workspace' };
|
const config1 = { port: '1111', workspacePath: '/test/workspace' };
|
||||||
const config2 = { port: '2222', workspacePath: '/test/workspace2' };
|
const config2 = { port: '2222', workspacePath: '/test/workspace2' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
|
||||||
|
new Error('not found'),
|
||||||
|
);
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue([
|
).mockResolvedValue([
|
||||||
'qwen-code-ide-server-1111.json',
|
'qwen-code-ide-server-12345-111.json',
|
||||||
'qwen-code-ide-server-2222.json',
|
'qwen-code-ide-server-12345-222.json',
|
||||||
]);
|
]);
|
||||||
vi.mocked(fs.promises.readFile)
|
vi.mocked(fs.promises.readFile)
|
||||||
.mockResolvedValueOnce(JSON.stringify(config1))
|
.mockResolvedValueOnce(JSON.stringify(config1))
|
||||||
@@ -413,13 +411,16 @@ describe('IdeClient', () => {
|
|||||||
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '2222';
|
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '2222';
|
||||||
const config1 = { port: '1111', workspacePath: '/test/workspace' };
|
const config1 = { port: '1111', workspacePath: '/test/workspace' };
|
||||||
const config2 = { port: '2222', workspacePath: '/test/workspace2' };
|
const config2 = { port: '2222', workspacePath: '/test/workspace2' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
|
||||||
|
new Error('not found'),
|
||||||
|
);
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue([
|
).mockResolvedValue([
|
||||||
'qwen-code-ide-server-1111.json',
|
'qwen-code-ide-server-12345-111.json',
|
||||||
'qwen-code-ide-server-2222.json',
|
'qwen-code-ide-server-12345-222.json',
|
||||||
]);
|
]);
|
||||||
vi.mocked(fs.promises.readFile)
|
vi.mocked(fs.promises.readFile)
|
||||||
.mockResolvedValueOnce(JSON.stringify(config1))
|
.mockResolvedValueOnce(JSON.stringify(config1))
|
||||||
@@ -441,13 +442,16 @@ describe('IdeClient', () => {
|
|||||||
|
|
||||||
it('should handle invalid JSON in one of the config files', async () => {
|
it('should handle invalid JSON in one of the config files', async () => {
|
||||||
const validConfig = { port: '2222', workspacePath: '/test/workspace' };
|
const validConfig = { port: '2222', workspacePath: '/test/workspace' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
|
||||||
|
new Error('not found'),
|
||||||
|
);
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue([
|
).mockResolvedValue([
|
||||||
'qwen-code-ide-server-1111.json',
|
'qwen-code-ide-server-12345-111.json',
|
||||||
'qwen-code-ide-server-2222.json',
|
'qwen-code-ide-server-12345-222.json',
|
||||||
]);
|
]);
|
||||||
vi.mocked(fs.promises.readFile)
|
vi.mocked(fs.promises.readFile)
|
||||||
.mockResolvedValueOnce('invalid json')
|
.mockResolvedValueOnce('invalid json')
|
||||||
@@ -467,11 +471,12 @@ describe('IdeClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined if readdir throws an error', async () => {
|
it('should return undefined if readdir throws an error', async () => {
|
||||||
(
|
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
new Error('not found'),
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
);
|
||||||
>
|
vi.mocked(fs.promises.readdir).mockRejectedValue(
|
||||||
).mockRejectedValue(new Error('readdir failed'));
|
new Error('readdir failed'),
|
||||||
|
);
|
||||||
|
|
||||||
const ideClient = await IdeClient.getInstance();
|
const ideClient = await IdeClient.getInstance();
|
||||||
const result = await (
|
const result = await (
|
||||||
@@ -485,12 +490,15 @@ describe('IdeClient', () => {
|
|||||||
|
|
||||||
it('should ignore files with invalid names', async () => {
|
it('should ignore files with invalid names', async () => {
|
||||||
const validConfig = { port: '3333', workspacePath: '/test/workspace' };
|
const validConfig = { port: '3333', workspacePath: '/test/workspace' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
|
||||||
|
new Error('not found'),
|
||||||
|
);
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue([
|
).mockResolvedValue([
|
||||||
'qwen-code-ide-server-3333.json', // valid
|
'qwen-code-ide-server-12345-111.json', // valid
|
||||||
'not-a-config-file.txt', // invalid
|
'not-a-config-file.txt', // invalid
|
||||||
'qwen-code-ide-server-asdf.json', // invalid
|
'qwen-code-ide-server-asdf.json', // invalid
|
||||||
]);
|
]);
|
||||||
@@ -509,19 +517,30 @@ describe('IdeClient', () => {
|
|||||||
).getConnectionConfigFromFile();
|
).getConnectionConfigFromFile();
|
||||||
|
|
||||||
expect(result).toEqual(validConfig);
|
expect(result).toEqual(validConfig);
|
||||||
|
expect(fs.promises.readFile).toHaveBeenCalledWith(
|
||||||
|
path.join('/tmp/gemini/ide', 'qwen-code-ide-server-12345-111.json'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
expect(fs.promises.readFile).not.toHaveBeenCalledWith(
|
||||||
|
path.join('/tmp/gemini/ide', 'not-a-config-file.txt'),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match env port string to a number port in the config', async () => {
|
it('should match env port string to a number port in the config', async () => {
|
||||||
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '3333';
|
process.env['QWEN_CODE_IDE_SERVER_PORT'] = '3333';
|
||||||
const config1 = { port: 1111, workspacePath: '/test/workspace' };
|
const config1 = { port: 1111, workspacePath: '/test/workspace' };
|
||||||
const config2 = { port: 3333, workspacePath: '/test/workspace2' };
|
const config2 = { port: 3333, workspacePath: '/test/workspace2' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
|
||||||
|
new Error('not found'),
|
||||||
|
);
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue([
|
).mockResolvedValue([
|
||||||
'qwen-code-ide-server-1111.json',
|
'qwen-code-ide-server-12345-111.json',
|
||||||
'qwen-code-ide-server-3333.json',
|
'qwen-code-ide-server-12345-222.json',
|
||||||
]);
|
]);
|
||||||
vi.mocked(fs.promises.readFile)
|
vi.mocked(fs.promises.readFile)
|
||||||
.mockResolvedValueOnce(JSON.stringify(config1))
|
.mockResolvedValueOnce(JSON.stringify(config1))
|
||||||
@@ -549,16 +568,13 @@ describe('IdeClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if tool discovery fails', async () => {
|
it('should return false if tool discovery fails', async () => {
|
||||||
const config = { port: '8080', workspacePath: '/test/workspace' };
|
const config = { port: '8080' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-8080.json']);
|
).mockResolvedValue([]);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
mockClient.request.mockRejectedValue(new Error('Method not found'));
|
mockClient.request.mockRejectedValue(new Error('Method not found'));
|
||||||
|
|
||||||
const ideClient = await IdeClient.getInstance();
|
const ideClient = await IdeClient.getInstance();
|
||||||
@@ -571,16 +587,13 @@ describe('IdeClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if diffing tools are not available', async () => {
|
it('should return false if diffing tools are not available', async () => {
|
||||||
const config = { port: '8080', workspacePath: '/test/workspace' };
|
const config = { port: '8080' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-8080.json']);
|
).mockResolvedValue([]);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
mockClient.request.mockResolvedValue({
|
mockClient.request.mockResolvedValue({
|
||||||
tools: [{ name: 'someOtherTool' }],
|
tools: [{ name: 'someOtherTool' }],
|
||||||
});
|
});
|
||||||
@@ -595,16 +608,13 @@ describe('IdeClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if only openDiff tool is available', async () => {
|
it('should return false if only openDiff tool is available', async () => {
|
||||||
const config = { port: '8080', workspacePath: '/test/workspace' };
|
const config = { port: '8080' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-8080.json']);
|
).mockResolvedValue([]);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
mockClient.request.mockResolvedValue({
|
mockClient.request.mockResolvedValue({
|
||||||
tools: [{ name: 'openDiff' }],
|
tools: [{ name: 'openDiff' }],
|
||||||
});
|
});
|
||||||
@@ -619,16 +629,13 @@ describe('IdeClient', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return true if connected and diffing tools are available', async () => {
|
it('should return true if connected and diffing tools are available', async () => {
|
||||||
const config = { port: '8080', workspacePath: '/test/workspace' };
|
const config = { port: '8080' };
|
||||||
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-8080.json']);
|
).mockResolvedValue([]);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
mockClient.request.mockResolvedValue({
|
mockClient.request.mockResolvedValue({
|
||||||
tools: [{ name: 'openDiff' }, { name: 'closeDiff' }],
|
tools: [{ name: 'openDiff' }, { name: 'closeDiff' }],
|
||||||
});
|
});
|
||||||
@@ -646,20 +653,13 @@ describe('IdeClient', () => {
|
|||||||
describe('authentication', () => {
|
describe('authentication', () => {
|
||||||
it('should connect with an auth token if provided in the discovery file', async () => {
|
it('should connect with an auth token if provided in the discovery file', async () => {
|
||||||
const authToken = 'test-auth-token';
|
const authToken = 'test-auth-token';
|
||||||
const config = {
|
const config = { port: '8080', authToken };
|
||||||
port: '8080',
|
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
||||||
authToken,
|
|
||||||
workspacePath: '/test/workspace',
|
|
||||||
};
|
|
||||||
(
|
(
|
||||||
vi.mocked(fs.promises.readdir) as Mock<
|
vi.mocked(fs.promises.readdir) as Mock<
|
||||||
(path: fs.PathLike) => Promise<string[]>
|
(path: fs.PathLike) => Promise<string[]>
|
||||||
>
|
>
|
||||||
).mockResolvedValue(['qwen-code-ide-server-8080.json']);
|
).mockResolvedValue([]);
|
||||||
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));
|
|
||||||
vi.spyOn(IdeClient, 'validateWorkspacePath').mockReturnValue({
|
|
||||||
isValid: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ideClient = await IdeClient.getInstance();
|
const ideClient = await IdeClient.getInstance();
|
||||||
await ideClient.connect();
|
await ideClient.connect();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
IdeDiffClosedNotificationSchema,
|
IdeDiffClosedNotificationSchema,
|
||||||
IdeDiffRejectedNotificationSchema,
|
IdeDiffRejectedNotificationSchema,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
import { getIdeProcessInfo } from './process-utils.js';
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||||
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
@@ -85,6 +86,7 @@ export class IdeClient {
|
|||||||
'IDE integration is currently disabled. To enable it, run /ide enable.',
|
'IDE integration is currently disabled. To enable it, run /ide enable.',
|
||||||
};
|
};
|
||||||
private currentIde: IdeInfo | undefined;
|
private currentIde: IdeInfo | undefined;
|
||||||
|
private ideProcessInfo: { pid: number; command: string } | undefined;
|
||||||
private connectionConfig:
|
private connectionConfig:
|
||||||
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
|
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
|
||||||
| undefined;
|
| undefined;
|
||||||
@@ -106,9 +108,10 @@ export class IdeClient {
|
|||||||
if (!IdeClient.instancePromise) {
|
if (!IdeClient.instancePromise) {
|
||||||
IdeClient.instancePromise = (async () => {
|
IdeClient.instancePromise = (async () => {
|
||||||
const client = new IdeClient();
|
const client = new IdeClient();
|
||||||
|
client.ideProcessInfo = await getIdeProcessInfo();
|
||||||
client.connectionConfig = await client.getConnectionConfigFromFile();
|
client.connectionConfig = await client.getConnectionConfigFromFile();
|
||||||
client.currentIde = detectIde(
|
client.currentIde = detectIde(
|
||||||
undefined,
|
client.ideProcessInfo,
|
||||||
client.connectionConfig?.ideInfo,
|
client.connectionConfig?.ideInfo,
|
||||||
);
|
);
|
||||||
return client;
|
return client;
|
||||||
@@ -569,7 +572,26 @@ export class IdeClient {
|
|||||||
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
|
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
|
||||||
| undefined
|
| undefined
|
||||||
> {
|
> {
|
||||||
const portFileDir = os.tmpdir();
|
if (!this.ideProcessInfo) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For backwards compatability
|
||||||
|
try {
|
||||||
|
const portFile = path.join(
|
||||||
|
os.tmpdir(),
|
||||||
|
`qwen-code-ide-server-${this.ideProcessInfo.pid}.json`,
|
||||||
|
);
|
||||||
|
const portFileContents = await fs.promises.readFile(portFile, 'utf8');
|
||||||
|
return JSON.parse(portFileContents);
|
||||||
|
} catch (_) {
|
||||||
|
// For newer extension versions, the file name matches the pattern
|
||||||
|
// /^qwen-code-ide-server-${pid}-\d+\.json$/. If multiple IDE
|
||||||
|
// windows are open, multiple files matching the pattern are expected to
|
||||||
|
// exist.
|
||||||
|
}
|
||||||
|
|
||||||
|
const portFileDir = path.join(os.tmpdir(), 'gemini', 'ide');
|
||||||
let portFiles;
|
let portFiles;
|
||||||
try {
|
try {
|
||||||
portFiles = await fs.promises.readdir(portFileDir);
|
portFiles = await fs.promises.readdir(portFileDir);
|
||||||
@@ -582,7 +604,9 @@ export class IdeClient {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileRegex = /^qwen-code-ide-server-\d+\.json$/;
|
const fileRegex = new RegExp(
|
||||||
|
`^qwen-code-ide-server-${this.ideProcessInfo.pid}-\\d+\\.json$`,
|
||||||
|
);
|
||||||
const matchingFiles = portFiles
|
const matchingFiles = portFiles
|
||||||
.filter((file) => fileRegex.test(file))
|
.filter((file) => fileRegex.test(file))
|
||||||
.sort();
|
.sort();
|
||||||
|
|||||||
Reference in New Issue
Block a user