bug: fix ide-client connection to ide-companion when inside docker via ssh/devcontainer (#15049)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
kapsner
2026-01-22 20:19:04 +01:00
committed by GitHub
parent 902e5d6dae
commit 233fe90f17
2 changed files with 229 additions and 23 deletions
+182 -6
View File
@@ -23,6 +23,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { detectIde, IDE_DEFINITIONS } from './detect-ide.js';
import * as os from 'node:os';
import * as path from 'node:path';
import { getIdeServerHost } from './ide-client.js';
vi.mock('node:fs', async (importOriginal) => {
const actual = await importOriginal<typeof fs>();
@@ -34,7 +35,7 @@ vi.mock('node:fs', async (importOriginal) => {
readdir: vi.fn(),
},
realpathSync: (p: string) => p,
existsSync: () => false,
existsSync: vi.fn(() => false),
};
});
vi.mock('./process-utils.js');
@@ -110,7 +111,7 @@ describe('IdeClient', () => {
await ideClient.connect();
expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp/', 'gemini-ide-server-12345.json'),
path.join('/tmp', 'gemini', 'ide', 'gemini-ide-server-12345.json'),
'utf8',
);
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
@@ -277,7 +278,7 @@ describe('IdeClient', () => {
expect(result).toEqual(config);
expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp', 'gemini-ide-server-12345.json'),
path.join('/tmp', 'gemini', 'ide', 'gemini-ide-server-12345.json'),
'utf8',
);
});
@@ -324,7 +325,7 @@ describe('IdeClient', () => {
expect(result).toEqual(config);
expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp/gemini/ide', 'gemini-ide-server-12345-123.json'),
path.join('/tmp', 'gemini', 'ide', 'gemini-ide-server-12345-123.json'),
'utf8',
);
});
@@ -517,11 +518,11 @@ describe('IdeClient', () => {
expect(result).toEqual(validConfig);
expect(fs.promises.readFile).toHaveBeenCalledWith(
path.join('/tmp/gemini/ide', 'gemini-ide-server-12345-111.json'),
path.join('/tmp', 'gemini', 'ide', 'gemini-ide-server-12345-111.json'),
'utf8',
);
expect(fs.promises.readFile).not.toHaveBeenCalledWith(
path.join('/tmp/gemini/ide', 'not-a-config-file.txt'),
path.join('/tmp', 'gemini', 'ide', 'not-a-config-file.txt'),
'utf8',
);
});
@@ -956,3 +957,178 @@ describe('IdeClient', () => {
});
});
});
describe('getIdeServerHost', () => {
let existsSyncMock: Mock;
let originalSshConnection: string | undefined;
let originalVscodeRemoteSession: string | undefined;
let originalRemoteContainers: string | undefined;
beforeEach(() => {
existsSyncMock = vi.mocked(fs.existsSync);
existsSyncMock.mockClear();
originalSshConnection = process.env['SSH_CONNECTION'];
originalVscodeRemoteSession =
process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
originalRemoteContainers = process.env['REMOTE_CONTAINERS'];
delete process.env['SSH_CONNECTION'];
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
delete process.env['REMOTE_CONTAINERS'];
});
afterEach(() => {
vi.clearAllMocks();
if (originalSshConnection !== undefined) {
process.env['SSH_CONNECTION'] = originalSshConnection;
} else {
delete process.env['SSH_CONNECTION'];
}
if (originalVscodeRemoteSession !== undefined) {
process.env['VSCODE_REMOTE_CONTAINERS_SESSION'] =
originalVscodeRemoteSession;
} else {
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
}
if (originalRemoteContainers !== undefined) {
process.env['REMOTE_CONTAINERS'] = originalRemoteContainers;
} else {
delete process.env['REMOTE_CONTAINERS'];
}
});
// Helper to set existsSync mock behavior
const setupFsMocks = (
dockerenvExists: boolean,
containerenvExists: boolean,
) => {
existsSyncMock.mockImplementation((path: string) => {
if (path === '/.dockerenv') {
return dockerenvExists;
}
if (path === '/run/.containerenv') {
return containerenvExists;
}
return false;
});
};
it('should return 127.0.0.1 when not in container and no SSH_CONNECTION or Dev Container env vars', () => {
setupFsMocks(false, false);
delete process.env['SSH_CONNECTION'];
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
delete process.env['REMOTE_CONTAINERS'];
expect(getIdeServerHost()).toBe('127.0.0.1');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
});
it('should return 127.0.0.1 when not in container but SSH_CONNECTION is set', () => {
setupFsMocks(false, false);
process.env['SSH_CONNECTION'] = 'some_ssh_value';
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
delete process.env['REMOTE_CONTAINERS'];
expect(getIdeServerHost()).toBe('127.0.0.1');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
});
it('should return host.docker.internal when in .dockerenv container and no SSH_CONNECTION or Dev Container env vars', () => {
setupFsMocks(true, false);
delete process.env['SSH_CONNECTION'];
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
delete process.env['REMOTE_CONTAINERS'];
expect(getIdeServerHost()).toBe('host.docker.internal');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
'/run/.containerenv',
); // Short-circuiting
});
it('should return 127.0.0.1 when in .dockerenv container and SSH_CONNECTION is set', () => {
setupFsMocks(true, false);
process.env['SSH_CONNECTION'] = 'some_ssh_value';
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
delete process.env['REMOTE_CONTAINERS'];
expect(getIdeServerHost()).toBe('127.0.0.1');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
'/run/.containerenv',
); // Short-circuiting
});
it('should return 127.0.0.1 when in .dockerenv container and VSCODE_REMOTE_CONTAINERS_SESSION is set', () => {
setupFsMocks(true, false);
delete process.env['SSH_CONNECTION'];
process.env['VSCODE_REMOTE_CONTAINERS_SESSION'] = 'some_session_id';
expect(getIdeServerHost()).toBe('127.0.0.1');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
'/run/.containerenv',
); // Short-circuiting
});
it('should return host.docker.internal when in .containerenv container and no SSH_CONNECTION or Dev Container env vars', () => {
setupFsMocks(false, true);
delete process.env['SSH_CONNECTION'];
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
delete process.env['REMOTE_CONTAINERS'];
expect(getIdeServerHost()).toBe('host.docker.internal');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
});
it('should return 127.0.0.1 when in .containerenv container and SSH_CONNECTION is set', () => {
setupFsMocks(false, true);
process.env['SSH_CONNECTION'] = 'some_ssh_value';
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
delete process.env['REMOTE_CONTAINERS'];
expect(getIdeServerHost()).toBe('127.0.0.1');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
});
it('should return 127.0.0.1 when in .containerenv container and REMOTE_CONTAINERS is set', () => {
setupFsMocks(false, true);
delete process.env['SSH_CONNECTION'];
process.env['REMOTE_CONTAINERS'] = 'true';
expect(getIdeServerHost()).toBe('127.0.0.1');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
});
it('should return host.docker.internal when in both containers and no SSH_CONNECTION or Dev Container env vars', () => {
setupFsMocks(true, true);
delete process.env['SSH_CONNECTION'];
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
delete process.env['REMOTE_CONTAINERS'];
expect(getIdeServerHost()).toBe('host.docker.internal');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
'/run/.containerenv',
); // Short-circuiting
});
it('should return 127.0.0.1 when in both containers and SSH_CONNECTION is set', () => {
setupFsMocks(true, true);
process.env['SSH_CONNECTION'] = 'some_ssh_value';
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
delete process.env['REMOTE_CONTAINERS'];
expect(getIdeServerHost()).toBe('127.0.0.1');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
'/run/.containerenv',
); // Short-circuiting
});
it('should return 127.0.0.1 when in both containers and VSCODE_REMOTE_CONTAINERS_SESSION is set', () => {
setupFsMocks(true, true);
delete process.env['SSH_CONNECTION'];
process.env['VSCODE_REMOTE_CONTAINERS_SESSION'] = 'some_session_id';
expect(getIdeServerHost()).toBe('127.0.0.1');
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
'/run/.containerenv',
); // Short-circuiting
});
});