CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes48downloads
ide-client.test.ts1621 linesDownload Raw Back to ide
1/**2 * @license3 * Copyright 2025 Google LLC4 * SPDX-License-Identifier: Apache-2.05 */6 7import {8  describe,9  it,10  expect,11  vi,12  beforeEach,13  afterEach,14  type Mocked,15  type Mock,16} from 'vitest';17 18const { mockUndiciFetch, mockProxyAgent, mockEnvHttpProxyAgent } = vi.hoisted(19  () => {20    const proxyAgent = { kind: 'env-proxy-agent' };21    return {22      mockUndiciFetch: vi.fn(),23      mockProxyAgent: proxyAgent,24      mockEnvHttpProxyAgent: vi.fn(() => proxyAgent),25    };26  },27);28const { mockDebugLogger } = vi.hoisted(() => ({29  mockDebugLogger: {30    debug: vi.fn(),31    error: vi.fn(),32  },33}));34 35vi.mock('undici', () => ({36  EnvHttpProxyAgent: mockEnvHttpProxyAgent,37  fetch: mockUndiciFetch,38}));39vi.mock('../utils/debugLogger.js', () => ({40  createDebugLogger: () => mockDebugLogger,41}));42 43import {44  IdeClient,45  IDEConnectionStatus,46  getIdeServerHost,47  _resetCachedIdeServerHost,48} from './ide-client.js';49import * as fs from 'node:fs';50import type { FileHandle } from 'node:fs/promises';51import * as dns from 'node:dns';52import { getIdeProcessInfo } from './process-utils.js';53import { Client } from '@modelcontextprotocol/sdk/client/index.js';54import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';55import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';56import { detectIde, IDE_DEFINITIONS } from './detect-ide.js';57import * as os from 'node:os';58import * as path from 'node:path';59 60vi.mock('node:fs', async (importOriginal) => {61  const actual = await importOriginal<typeof fs>();62  return {63    ...(actual as object),64    promises: {65      ...actual.promises,66      readFile: vi.fn(),67      readdir: vi.fn(),68      stat: vi.fn(),69      unlink: vi.fn(),70    },71    realpathSync: (p: string) => p,72    existsSync: vi.fn().mockReturnValue(false),73  };74});75vi.mock('node:dns', async (importOriginal) => {76  const actual = await importOriginal<typeof dns>();77  return {78    ...(actual as object),79    lookup: vi.fn(),80  };81});82vi.mock('./process-utils.js');83vi.mock('@modelcontextprotocol/sdk/client/index.js');84vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js');85vi.mock('@modelcontextprotocol/sdk/client/stdio.js');86vi.mock('./detect-ide.js');87vi.mock('node:os');88 89describe('IdeClient', () => {90  let mockClient: Mocked<Client>;91  let mockHttpTransport: Mocked<StreamableHTTPClientTransport>;92  let mockStdioTransport: Mocked<StdioClientTransport>;93 94  beforeEach(async () => {95    // Reset singleton instance and cached host for test isolation96    (97      IdeClient as unknown as {98        instancePromise: Promise<IdeClient> | null;99      }100    ).instancePromise = null;101    _resetCachedIdeServerHost();102 103    // Mock environment variables104    process.env['QWEN_CODE_IDE_WORKSPACE_PATH'] = '/test/workspace';105    delete process.env['QWEN_CODE_IDE_SERVER_PORT'];106    delete process.env['QWEN_CODE_IDE_SERVER_STDIO_COMMAND'];107    delete process.env['QWEN_CODE_IDE_SERVER_STDIO_ARGS'];108 109    // Mock dependencies110    vi.spyOn(process, 'cwd').mockReturnValue('/test/workspace/sub-dir');111    vi.mocked(fs.existsSync).mockImplementation((filePath: fs.PathLike) => {112      const file = String(filePath);113      return file !== '/.dockerenv' && file !== '/run/.containerenv';114    });115    vi.mocked(detectIde).mockReturnValue(IDE_DEFINITIONS.vscode);116    vi.mocked(getIdeProcessInfo).mockResolvedValue({117      pid: 12345,118      command: 'test-ide',119    });120    vi.mocked(os.tmpdir).mockReturnValue('/tmp');121    vi.mocked(os.homedir).mockReturnValue('/home/test');122    mockDebugLogger.debug.mockClear();123    mockDebugLogger.error.mockClear();124 125    // Mock MCP client and transports126    mockClient = {127      connect: vi.fn().mockResolvedValue(undefined),128      close: vi.fn(),129      setNotificationHandler: vi.fn(),130      callTool: vi.fn(),131      request: vi.fn(),132    } as unknown as Mocked<Client>;133    mockHttpTransport = {134      close: vi.fn(),135    } as unknown as Mocked<StreamableHTTPClientTransport>;136    mockStdioTransport = {137      close: vi.fn(),138    } as unknown as Mocked<StdioClientTransport>;139 140    vi.mocked(Client).mockReturnValue(mockClient);141    vi.mocked(StreamableHTTPClientTransport).mockReturnValue(mockHttpTransport);142    vi.mocked(StdioClientTransport).mockReturnValue(mockStdioTransport);143    mockUndiciFetch.mockReset();144    mockEnvHttpProxyAgent.mockClear();145 146    await IdeClient.getInstance();147  });148 149  afterEach(() => {150    vi.useRealTimers();151    vi.restoreAllMocks();152  });153 154  describe('createProxyAwareFetch', () => {155    it('uses undici fetch with the proxy-aware dispatcher', async () => {156      mockUndiciFetch.mockResolvedValue(157        new Response('ok', {158          status: 201,159          statusText: 'Created',160          headers: { 'x-test': 'yes' },161        }),162      );163      const ideClient = await IdeClient.getInstance();164      const fetch = (165        ideClient as unknown as {166          createProxyAwareFetch: (167            host: string,168          ) => (url: string, init?: RequestInit) => Promise<Response>;169        }170      ).createProxyAwareFetch('127.0.0.1');171 172      const response = await fetch('http://127.0.0.1:8080/mcp', {173        method: 'POST',174      });175 176      expect(response.status).toBe(201);177      expect(response.headers.get('x-test')).toBe('yes');178      expect(mockEnvHttpProxyAgent).toHaveBeenCalled();179      expect(mockUndiciFetch).toHaveBeenCalledWith(180        'http://127.0.0.1:8080/mcp',181        expect.objectContaining({182          method: 'POST',183          dispatcher: mockProxyAgent,184        }),185      );186    });187  });188 189  describe('connect', () => {190    it('should connect using HTTP when port is provided in config file', async () => {191      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '8080';192      const config = { port: '8080' };193      vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));194 195      const ideClient = await IdeClient.getInstance();196      await ideClient.connect();197 198      expect(fs.promises.readFile).toHaveBeenCalledWith(199        path.join('/home/test', '.qwen', 'ide', '8080.lock'),200        'utf8',201      );202      expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(203        new URL('http://127.0.0.1:8080/mcp'),204        expect.any(Object),205      );206      expect(mockClient.connect).toHaveBeenCalledWith(mockHttpTransport);207      expect(ideClient.getConnectionStatus().status).toBe(208        IDEConnectionStatus.Connected,209      );210      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];211    });212 213    it('should connect using stdio when stdio config is provided in file', async () => {214      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '8080';215      const config = { stdio: { command: 'test-cmd', args: ['--foo'] } };216      vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));217 218      const ideClient = await IdeClient.getInstance();219      await ideClient.connect();220 221      expect(StdioClientTransport).toHaveBeenCalledWith({222        command: 'test-cmd',223        args: ['--foo'],224      });225      expect(mockClient.connect).toHaveBeenCalledWith(mockStdioTransport);226      expect(ideClient.getConnectionStatus().status).toBe(227        IDEConnectionStatus.Connected,228      );229      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];230    });231 232    it('should prioritize port over stdio when both are in config file', async () => {233      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '8080';234      const config = {235        port: '8080',236        stdio: { command: 'test-cmd', args: ['--foo'] },237      };238      vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));239 240      const ideClient = await IdeClient.getInstance();241      await ideClient.connect();242 243      expect(StreamableHTTPClientTransport).toHaveBeenCalled();244      expect(StdioClientTransport).not.toHaveBeenCalled();245      expect(ideClient.getConnectionStatus().status).toBe(246        IDEConnectionStatus.Connected,247      );248      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];249    });250 251    it('should connect using HTTP when port is provided in environment variables', async () => {252      vi.mocked(fs.promises.readFile).mockRejectedValue(253        new Error('File not found'),254      );255      (256        vi.mocked(fs.promises.readdir) as Mock<257          (path: fs.PathLike) => Promise<string[]>258        >259      ).mockResolvedValue([]);260      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '9090';261 262      const ideClient = await IdeClient.getInstance();263      await ideClient.connect();264 265      expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(266        new URL('http://127.0.0.1:9090/mcp'),267        expect.any(Object),268      );269      expect(mockClient.connect).toHaveBeenCalledWith(mockHttpTransport);270      expect(ideClient.getConnectionStatus().status).toBe(271        IDEConnectionStatus.Connected,272      );273    });274 275    it('should fall back to host.docker.internal when localhost fails in container', async () => {276      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '9090';277      vi.mocked(fs.promises.readFile).mockRejectedValue(278        new Error('File not found'),279      );280      (281        vi.mocked(fs.promises.readdir) as Mock<282          (path: fs.PathLike) => Promise<string[]>283        >284      ).mockResolvedValue([]);285      vi.mocked(fs.existsSync).mockImplementation(286        (filePath: fs.PathLike) => filePath === '/.dockerenv',287      );288      (dns.lookup as unknown as Mock).mockImplementation(289        (290          _hostname: string,291          callback: (292            err: Error | null,293            address?: string,294            family?: number,295          ) => void,296        ) => {297          callback(null, '192.168.65.254', 4);298        },299      );300      mockClient.connect301        .mockRejectedValueOnce(new Error('localhost unreachable'))302        .mockResolvedValueOnce(undefined);303 304      const ideClient = await IdeClient.getInstance();305      await ideClient.connect();306 307      // Localhost is always tried first.308      expect(StreamableHTTPClientTransport).toHaveBeenNthCalledWith(309        1,310        new URL('http://127.0.0.1:9090/mcp'),311        expect.any(Object),312      );313      // In a container, host.docker.internal is used as fallback.314      expect(StreamableHTTPClientTransport).toHaveBeenNthCalledWith(315        2,316        new URL('http://host.docker.internal:9090/mcp'),317        expect.any(Object),318      );319      expect(ideClient.getConnectionStatus().status).toBe(320        IDEConnectionStatus.Connected,321      );322 323      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];324    });325 326    it('should try a newer lock-file port when the configured port is stale', async () => {327      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '1111';328      const primaryConfig = {329        port: '1111',330        authToken: 'stale-token',331        workspacePath: '/test/workspace',332      };333      const fallbackConfig = {334        port: '2222',335        authToken: 'fresh-token',336        workspacePath: '/test/workspace',337      };338      vi.mocked(fs.promises.readFile).mockImplementation(339        async (filePath: fs.PathLike | FileHandle) => {340          const file = String(filePath);341          if (file === path.join('/home/test', '.qwen', 'ide', '1111.lock')) {342            return JSON.stringify(primaryConfig);343          }344          if (file === path.join('/home/test', '.qwen', 'ide', '2222.lock')) {345            return JSON.stringify(fallbackConfig);346          }347          throw new Error(`unexpected path: ${file}`);348        },349      );350      (351        vi.mocked(fs.promises.readdir) as Mock<352          (path: fs.PathLike) => Promise<string[]>353        >354      ).mockResolvedValue(['1111.lock', '2222.lock']);355      (356        vi.mocked(fs.promises.stat) as Mock<357          (path: fs.PathLike) => Promise<fs.Stats>358        >359      ).mockImplementation(async (filePath: fs.PathLike) => {360        const now = Date.now();361        const file = String(filePath);362        return {363          mtimeMs: file.endsWith('2222.lock') ? now : now - 1000,364        } as fs.Stats;365      });366      vi.mocked(fs.existsSync).mockImplementation(367        (filePath: fs.PathLike) => String(filePath) === '/test/workspace',368      );369      mockClient.request.mockResolvedValue({ tools: [] });370      mockClient.connect371        .mockRejectedValueOnce(new Error('stale port'))372        .mockResolvedValueOnce(undefined);373 374      const ideClient = await IdeClient.getInstance();375      await ideClient.connect();376 377      expect(StreamableHTTPClientTransport).toHaveBeenNthCalledWith(378        1,379        new URL('http://127.0.0.1:1111/mcp'),380        expect.objectContaining({381          requestInit: {382            headers: {383              Authorization: 'Bearer stale-token',384            },385          },386        }),387      );388      expect(StreamableHTTPClientTransport).toHaveBeenNthCalledWith(389        2,390        new URL('http://127.0.0.1:2222/mcp'),391        expect.objectContaining({392          requestInit: {393            headers: {394              Authorization: 'Bearer fresh-token',395            },396          },397        }),398      );399      expect(ideClient.getConnectionStatus().status).toBe(400        IDEConnectionStatus.Connected,401      );402      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];403    });404 405    it('should not retry raw env port when its lock belongs to another workspace', async () => {406      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '1234';407      const envConfig = {408        port: '1234',409        workspacePath: '/other/workspace',410      };411      vi.mocked(fs.promises.readFile).mockImplementation(412        async (filePath: fs.PathLike | FileHandle) => {413          const file = String(filePath);414          if (file === path.join('/home/test', '.qwen', 'ide', '1234.lock')) {415            return JSON.stringify(envConfig);416          }417          if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {418            throw new Error('not found');419          }420          if (file === path.join('/tmp', 'qwen-code-ide-server-1234.json')) {421            throw new Error('not found');422          }423          throw new Error(`unexpected path: ${file}`);424        },425      );426      (427        vi.mocked(fs.promises.readdir) as Mock<428          (path: fs.PathLike) => Promise<string[]>429        >430      ).mockResolvedValue([]);431 432      const ideClient = await IdeClient.getInstance();433      await ideClient.connect();434 435      expect(StreamableHTTPClientTransport).not.toHaveBeenCalled();436      expect(mockClient.connect).not.toHaveBeenCalled();437      expect(ideClient.getConnectionStatus().status).toBe(438        IDEConnectionStatus.Disconnected,439      );440      expect(ideClient.getConnectionStatus().details).toContain(441        'workspace does not match',442      );443      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];444    });445 446    it('should skip explicit workspace mismatches when trying fallback ports', async () => {447      const primaryConfig = {448        port: '1111',449        workspacePath: '/test/workspace',450        ppid: 12345,451      };452      const otherWorkspaceConfig = {453        port: '2222',454        workspacePath: '/other/workspace',455      };456      const legacyConfig = {457        port: '3333',458      };459      vi.mocked(fs.promises.readFile).mockImplementation(460        async (filePath: fs.PathLike | FileHandle) => {461          const file = String(filePath);462          if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {463            return JSON.stringify(primaryConfig);464          }465          if (file === path.join('/home/test', '.qwen', 'ide', '2222.lock')) {466            return JSON.stringify(otherWorkspaceConfig);467          }468          if (file === path.join('/home/test', '.qwen', 'ide', '3333.lock')) {469            return JSON.stringify(legacyConfig);470          }471          throw new Error(`unexpected path: ${file}`);472        },473      );474      (475        vi.mocked(fs.promises.readdir) as Mock<476          (path: fs.PathLike) => Promise<string[]>477        >478      ).mockResolvedValue(['2222.lock', '3333.lock']);479      (480        vi.mocked(fs.promises.stat) as Mock<481          (path: fs.PathLike) => Promise<fs.Stats>482        >483      ).mockImplementation(async (filePath: fs.PathLike) => {484        const now = Date.now();485        const file = String(filePath);486        return {487          mtimeMs: file.endsWith('2222.lock') ? now : now - 1000,488        } as fs.Stats;489      });490      mockClient.request.mockResolvedValue({ tools: [] });491      mockClient.connect492        .mockRejectedValueOnce(new Error('primary port failed'))493        .mockResolvedValueOnce(undefined);494 495      const ideClient = await IdeClient.getInstance();496      await ideClient.connect();497 498      expect(StreamableHTTPClientTransport).toHaveBeenNthCalledWith(499        1,500        new URL('http://127.0.0.1:1111/mcp'),501        expect.any(Object),502      );503      expect(StreamableHTTPClientTransport).toHaveBeenNthCalledWith(504        2,505        new URL('http://127.0.0.1:3333/mcp'),506        expect.any(Object),507      );508      expect(StreamableHTTPClientTransport).not.toHaveBeenCalledWith(509        new URL('http://127.0.0.1:2222/mcp'),510        expect.any(Object),511      );512      expect(ideClient.getConnectionStatus().status).toBe(513        IDEConnectionStatus.Connected,514      );515    });516 517    it('should connect using stdio when stdio config is in environment variables', async () => {518      vi.mocked(fs.promises.readFile).mockRejectedValue(519        new Error('File not found'),520      );521 522      (523        vi.mocked(fs.promises.readdir) as Mock<524          (path: fs.PathLike) => Promise<string[]>525        >526      ).mockResolvedValue([]);527      process.env['QWEN_CODE_IDE_SERVER_STDIO_COMMAND'] = 'env-cmd';528      process.env['QWEN_CODE_IDE_SERVER_STDIO_ARGS'] = '["--bar"]';529 530      const ideClient = await IdeClient.getInstance();531      await ideClient.connect();532 533      expect(StdioClientTransport).toHaveBeenCalledWith({534        command: 'env-cmd',535        args: ['--bar'],536      });537      expect(mockClient.connect).toHaveBeenCalledWith(mockStdioTransport);538      expect(ideClient.getConnectionStatus().status).toBe(539        IDEConnectionStatus.Connected,540      );541    });542 543    it('should prioritize file config over environment variables', async () => {544      const config = { port: '8080' };545      vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));546      (547        vi.mocked(fs.promises.readdir) as Mock<548          (path: fs.PathLike) => Promise<string[]>549        >550      ).mockResolvedValue([]);551      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '9090';552 553      const ideClient = await IdeClient.getInstance();554      await ideClient.connect();555 556      expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(557        new URL('http://127.0.0.1:8080/mcp'),558        expect.any(Object),559      );560      expect(ideClient.getConnectionStatus().status).toBe(561        IDEConnectionStatus.Connected,562      );563    });564 565    it('should be disconnected if no config is found', async () => {566      vi.mocked(fs.promises.readFile).mockRejectedValue(567        new Error('File not found'),568      );569      (570        vi.mocked(fs.promises.readdir) as Mock<571          (path: fs.PathLike) => Promise<string[]>572        >573      ).mockResolvedValue([]);574 575      const ideClient = await IdeClient.getInstance();576      await ideClient.connect();577 578      expect(StreamableHTTPClientTransport).not.toHaveBeenCalled();579      expect(StdioClientTransport).not.toHaveBeenCalled();580      expect(ideClient.getConnectionStatus().status).toBe(581        IDEConnectionStatus.Disconnected,582      );583      expect(ideClient.getConnectionStatus().details).toContain(584        'Failed to connect',585      );586    });587 588    it('should report workspace mismatch when discovered locks belong to another workspace', async () => {589      vi.mocked(fs.promises.readFile).mockImplementation(590        async (filePath: fs.PathLike | FileHandle) => {591          const file = String(filePath);592          if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {593            throw new Error('not found');594          }595          if (file === path.join('/home/test', '.qwen', 'ide', '2222.lock')) {596            return JSON.stringify({597              port: '2222',598              workspacePath: '/other/workspace',599            });600          }601          throw new Error(`unexpected path: ${file}`);602        },603      );604      (605        vi.mocked(fs.promises.readdir) as Mock<606          (path: fs.PathLike) => Promise<string[]>607        >608      ).mockResolvedValue(['2222.lock']);609      (610        vi.mocked(fs.promises.stat) as Mock<611          (path: fs.PathLike) => Promise<fs.Stats>612        >613      ).mockResolvedValue({ mtimeMs: Date.now() } as fs.Stats);614 615      const ideClient = await IdeClient.getInstance();616      await ideClient.connect();617 618      expect(StreamableHTTPClientTransport).not.toHaveBeenCalled();619      expect(ideClient.getConnectionStatus().status).toBe(620        IDEConnectionStatus.Disconnected,621      );622      expect(ideClient.getConnectionStatus().details).toContain(623        'workspace does not match',624      );625    });626  });627 628  describe('validateWorkspacePath', () => {629    it('accepts JSON encoded multi-root workspace paths', () => {630      const result = IdeClient.validateWorkspacePath(631        JSON.stringify(['/test/other', '/test/workspace']),632        '/test/workspace/sub-dir',633      );634 635      expect(result.isValid).toBe(true);636    });637 638    it('ignores relative workspace entries in IDE env parsing', () => {639      const result = IdeClient.validateWorkspacePath(640        JSON.stringify(['relative/path', '/test/workspace']),641        '/test/workspace/sub-dir',642      );643 644      expect(result.isValid).toBe(true);645    });646 647    it('keeps delimiter encoded workspace paths working', () => {648      const result = IdeClient.validateWorkspacePath(649        ['/test/other', '/test/workspace'].join(path.delimiter),650        '/test/workspace/sub-dir',651      );652 653      expect(result.isValid).toBe(true);654    });655  });656 657  describe('getPortFromEnv', () => {658    const invalidPorts = [659      undefined,660      '',661      '0',662      '65536',663      '99999',664      '../evil',665      '12345/../../etc',666      'abc',667      ' 8080 ',668      '8080.0',669    ];670 671    it.each(['1', '12345', '65535'])(672      'should return valid env port %s',673      async (port) => {674        process.env['QWEN_CODE_IDE_SERVER_PORT'] = port;675 676        const ideClient = await IdeClient.getInstance();677        const result = (678          ideClient as unknown as {679            getPortFromEnv: () => string | undefined;680          }681        ).getPortFromEnv();682 683        expect(result).toBe(port);684      },685    );686 687    it.each(invalidPorts)('should ignore invalid env port %s', async (port) => {688      if (port === undefined) {689        delete process.env['QWEN_CODE_IDE_SERVER_PORT'];690      } else {691        process.env['QWEN_CODE_IDE_SERVER_PORT'] = port;692      }693 694      const ideClient = await IdeClient.getInstance();695      const result = (696        ideClient as unknown as {697          getPortFromEnv: () => string | undefined;698        }699      ).getPortFromEnv();700 701      expect(result).toBeUndefined();702    });703  });704 705  describe('getConnectionConfigFromFile', () => {706    it('should return config from the env port lock file if it exists', async () => {707      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '12345';708      const config = { port: '12345', workspacePath: '/test/workspace' };709      vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));710 711      const ideClient = await IdeClient.getInstance();712      // In tests, the private method can be accessed like this.713      const result = await (714        ideClient as unknown as {715          getConnectionConfigFromFile: () => Promise<unknown>;716        }717      ).getConnectionConfigFromFile();718 719      expect(result).toEqual(config);720      expect(fs.promises.readFile).toHaveBeenCalledWith(721        path.join('/home/test', '.qwen', 'ide', '12345.lock'),722        'utf8',723      );724      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];725    });726 727    it('should not scan the lock directory when the env port lock file exists', async () => {728      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '1234';729      const config = { port: '1234', workspacePath: '/test/workspace' };730      vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));731 732      const ideClient = await IdeClient.getInstance();733      vi.mocked(fs.promises.readdir).mockClear();734      const result = await (735        ideClient as unknown as {736          getConnectionConfigFromFile: () => Promise<unknown>;737        }738      ).getConnectionConfigFromFile();739 740      expect(result).toEqual(config);741      expect(fs.promises.readdir).not.toHaveBeenCalled();742      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];743    });744 745    it('should fall back to scanned locks when the env port lock belongs to another workspace', async () => {746      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '1234';747      const envConfig = {748        port: '1234',749        workspacePath: '/other/workspace',750      };751      const matchingConfig = {752        port: '5678',753        workspacePath: '/test/workspace',754      };755      vi.mocked(fs.promises.readFile).mockImplementation(756        async (filePath: fs.PathLike | FileHandle) => {757          const file = String(filePath);758          if (file === path.join('/home/test', '.qwen', 'ide', '1234.lock')) {759            return JSON.stringify(envConfig);760          }761          if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {762            throw new Error('not found');763          }764          if (file === path.join('/tmp', 'qwen-code-ide-server-1234.json')) {765            throw new Error('not found');766          }767          if (file === path.join('/home/test', '.qwen', 'ide', '5678.lock')) {768            return JSON.stringify(matchingConfig);769          }770          throw new Error(`unexpected path: ${file}`);771        },772      );773      (774        vi.mocked(fs.promises.readdir) as Mock<775          (path: fs.PathLike) => Promise<string[]>776        >777      ).mockResolvedValue(['1234.lock', '5678.lock']);778      (779        vi.mocked(fs.promises.stat) as Mock<780          (path: fs.PathLike) => Promise<fs.Stats>781        >782      ).mockImplementation(async (filePath: fs.PathLike) => {783        const now = Date.now();784        const file = String(filePath);785        return {786          mtimeMs: file.endsWith('1234.lock') ? now : now - 1000,787        } as fs.Stats;788      });789 790      const ideClient = await IdeClient.getInstance();791      const result = await (792        ideClient as unknown as {793          getConnectionConfigFromFile: () => Promise<unknown>;794        }795      ).getConnectionConfigFromFile();796 797      expect(result).toEqual(matchingConfig);798      expect(fs.promises.readFile).toHaveBeenCalledWith(799        path.join('/home/test', '.qwen', 'ide', '1234.lock'),800        'utf8',801      );802      expect(fs.promises.readdir).toHaveBeenCalledWith(803        path.join('/home/test', '.qwen', 'ide'),804      );805      expect(mockDebugLogger.debug).toHaveBeenCalledWith(806        'Ignoring IDE env lock file: workspace "/other/workspace" does not match cwd "/test/workspace/sub-dir".',807      );808      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];809    });810 811    it('should accept env lock config when workspacePath is undefined', async () => {812      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '1234';813      const config = { port: '1234' };814      vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(config));815      vi.mocked(fs.promises.readdir).mockClear();816 817      const ideClient = await IdeClient.getInstance();818      const result = await (819        ideClient as unknown as {820          getConnectionConfigFromFile: () => Promise<unknown>;821        }822      ).getConnectionConfigFromFile();823 824      expect(result).toEqual(config);825      expect(fs.promises.readdir).not.toHaveBeenCalled();826      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];827    });828 829    it('should return legacy config when workspacePath is undefined', async () => {830      const config = { port: '1111', ppid: 12345 };831      vi.mocked(fs.promises.readFile).mockResolvedValueOnce(832        JSON.stringify(config),833      );834      vi.mocked(fs.promises.readdir).mockClear();835 836      const ideClient = await IdeClient.getInstance();837      const result = await (838        ideClient as unknown as {839          getConnectionConfigFromFile: () => Promise<unknown>;840        }841      ).getConnectionConfigFromFile();842 843      expect(result).toEqual(config);844      expect(fs.promises.readFile).toHaveBeenCalledWith(845        path.join('/tmp', 'qwen-code-ide-server-12345.json'),846        'utf8',847      );848      expect(fs.promises.readdir).not.toHaveBeenCalled();849    });850 851    it('should return legacy config when env lock belongs to another workspace', async () => {852      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '1234';853      const envConfig = {854        port: '1234',855        workspacePath: '/other/workspace',856      };857      const legacyConfig = {858        port: '1111',859        workspacePath: '/test/workspace',860        ppid: 12345,861      };862      vi.mocked(fs.promises.readFile).mockImplementation(863        async (filePath: fs.PathLike | FileHandle) => {864          const file = String(filePath);865          if (file === path.join('/home/test', '.qwen', 'ide', '1234.lock')) {866            return JSON.stringify(envConfig);867          }868          if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {869            return JSON.stringify(legacyConfig);870          }871          throw new Error(`unexpected path: ${file}`);872        },873      );874      vi.mocked(fs.promises.readdir).mockClear();875 876      const ideClient = await IdeClient.getInstance();877      const result = await (878        ideClient as unknown as {879          getConnectionConfigFromFile: () => Promise<unknown>;880        }881      ).getConnectionConfigFromFile();882 883      expect(result).toEqual(legacyConfig);884      expect(fs.promises.readdir).not.toHaveBeenCalled();885      expect(mockDebugLogger.debug).toHaveBeenCalledWith(886        'Ignoring IDE env lock file: workspace "/other/workspace" does not match cwd "/test/workspace/sub-dir".',887      );888      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];889    });890 891    it('should reject env-port legacy config from another workspace', async () => {892      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '1234';893      const legacyConfig = {894        port: '9999',895        workspacePath: '/other/workspace',896      };897      vi.mocked(fs.promises.readFile).mockImplementation(898        async (filePath: fs.PathLike | FileHandle) => {899          const file = String(filePath);900          if (file === path.join('/home/test', '.qwen', 'ide', '1234.lock')) {901            throw new Error('not found');902          }903          if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {904            throw new Error('not found');905          }906          if (file === path.join('/tmp', 'qwen-code-ide-server-1234.json')) {907            return JSON.stringify(legacyConfig);908          }909          throw new Error(`unexpected path: ${file}`);910        },911      );912      (913        vi.mocked(fs.promises.readdir) as Mock<914          (path: fs.PathLike) => Promise<string[]>915        >916      ).mockResolvedValue([]);917 918      const ideClient = await IdeClient.getInstance();919      const result = await (920        ideClient as unknown as {921          getConnectionConfigFromFile: () => Promise<unknown>;922        }923      ).getConnectionConfigFromFile();924      const rejectedPorts = (925        ideClient as unknown as {926          workspaceRejectedPorts: Set<string>;927        }928      ).workspaceRejectedPorts;929 930      expect(result).toBeUndefined();931      expect(rejectedPorts.has('9999')).toBe(true);932      expect(rejectedPorts.has('1234')).toBe(true);933      expect(fs.promises.readdir).toHaveBeenCalledWith(934        path.join('/home/test', '.qwen', 'ide'),935      );936      expect(mockDebugLogger.debug).toHaveBeenCalledWith(937        'Ignoring legacy IDE connection config: workspace "/other/workspace" does not match cwd "/test/workspace/sub-dir".',938      );939      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];940    });941 942    it.each(['../evil', '12345/../../etc', 'abc', ' 8080 ', '8080.0'])(943      'should scan the lock directory when env port is invalid: %s',944      async (port) => {945        process.env['QWEN_CODE_IDE_SERVER_PORT'] = port;946        const ideDir = path.join('/home/test', '.qwen', 'ide');947        const config = { port: '2345', workspacePath: '/test/workspace' };948        vi.mocked(fs.promises.readFile).mockImplementation(949          async (filePath: fs.PathLike | FileHandle) => {950            const file = String(filePath);951            if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {952              throw new Error('not found');953            }954            if (file === path.join(ideDir, '2345.lock')) {955              return JSON.stringify(config);956            }957            throw new Error(`unexpected path: ${file}`);958          },959        );960        (961          vi.mocked(fs.promises.readdir) as Mock<962            (path: fs.PathLike) => Promise<string[]>963          >964        ).mockResolvedValue(['2345.lock']);965        (966          vi.mocked(fs.promises.stat) as Mock<967            (path: fs.PathLike) => Promise<fs.Stats>968          >969        ).mockResolvedValue({ mtimeMs: Date.now() } as fs.Stats);970        vi.mocked(fs.promises.readFile).mockClear();971 972        const ideClient = await IdeClient.getInstance();973        const result = await (974          ideClient as unknown as {975            getConnectionConfigFromFile: () => Promise<unknown>;976          }977        ).getConnectionConfigFromFile();978 979        expect(result).toEqual(config);980        expect(fs.promises.readFile).not.toHaveBeenCalledWith(981          path.join(ideDir, `${port}.lock`),982          'utf8',983        );984        expect(fs.promises.readFile).not.toHaveBeenCalledWith(985          path.join('/tmp', `qwen-code-ide-server-${port}.json`),986          'utf8',987        );988        expect(fs.promises.readdir).toHaveBeenCalledWith(ideDir);989      },990    );991 992    it('should return undefined if no config files are found', async () => {993      vi.mocked(fs.promises.readFile).mockRejectedValue(new Error('not found'));994 995      const ideClient = await IdeClient.getInstance();996      const result = await (997        ideClient as unknown as {998          getConnectionConfigFromFile: () => Promise<unknown>;999        }1000      ).getConnectionConfigFromFile();1001 1002      expect(result).toBeUndefined();1003    });1004 1005    it('should read legacy pid config when available', async () => {1006      const config = {1007        port: '5678',1008        workspacePath: '/test/workspace',1009        ppid: 12345,1010      };1011      vi.mocked(fs.promises.readFile).mockResolvedValueOnce(1012        JSON.stringify(config),1013      );1014 1015      const ideClient = await IdeClient.getInstance();1016      const result = await (1017        ideClient as unknown as {1018          getConnectionConfigFromFile: () => Promise<unknown>;1019        }1020      ).getConnectionConfigFromFile();1021 1022      expect(result).toEqual(config);1023      expect(fs.promises.readFile).toHaveBeenCalledWith(1024        path.join('/tmp', 'qwen-code-ide-server-12345.json'),1025        'utf8',1026      );1027    });1028 1029    it('should fall back to scanned locks when the legacy config belongs to another workspace', async () => {1030      const legacyConfig = {1031        port: '1111',1032        workspacePath: '/other/workspace',1033        ppid: 12345,1034      };1035      const matchingConfig = {1036        port: '5678',1037        workspacePath: '/test/workspace',1038      };1039      vi.mocked(fs.promises.readFile).mockImplementation(1040        async (filePath: fs.PathLike | FileHandle) => {1041          const file = String(filePath);1042          if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {1043            return JSON.stringify(legacyConfig);1044          }1045          if (file === path.join('/home/test', '.qwen', 'ide', '5678.lock')) {1046            return JSON.stringify(matchingConfig);1047          }1048          throw new Error(`unexpected path: ${file}`);1049        },1050      );1051      (1052        vi.mocked(fs.promises.readdir) as Mock<1053          (path: fs.PathLike) => Promise<string[]>1054        >1055      ).mockResolvedValue(['5678.lock']);1056      (1057        vi.mocked(fs.promises.stat) as Mock<1058          (path: fs.PathLike) => Promise<fs.Stats>1059        >1060      ).mockResolvedValue({1061        mtimeMs: Date.now(),1062      } as fs.Stats);1063 1064      const ideClient = await IdeClient.getInstance();1065      const result = await (1066        ideClient as unknown as {1067          getConnectionConfigFromFile: () => Promise<unknown>;1068        }1069      ).getConnectionConfigFromFile();1070 1071      expect(result).toEqual(matchingConfig);1072      expect(fs.promises.readdir).toHaveBeenCalledWith(1073        path.join('/home/test', '.qwen', 'ide'),1074      );1075      expect(mockDebugLogger.debug).toHaveBeenCalledWith(1076        'Ignoring legacy IDE connection config: workspace "/other/workspace" does not match cwd "/test/workspace/sub-dir".',1077      );1078    });1079 1080    it('should fall back to legacy port file when pid file is missing', async () => {1081      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '2222';1082      const config2 = { port: '2222', workspacePath: '/test/workspace' };1083      vi.mocked(fs.promises.readFile)1084        .mockRejectedValueOnce(new Error('not found')) // ~/.qwen/ide/<port>.lock1085        .mockRejectedValueOnce(new Error('not found')) // legacy pid file1086        .mockResolvedValueOnce(JSON.stringify(config2));1087 1088      const ideClient = await IdeClient.getInstance();1089      const result = await (1090        ideClient as unknown as {1091          getConnectionConfigFromFile: () => Promise<unknown>;1092        }1093      ).getConnectionConfigFromFile();1094 1095      expect(result).toEqual(config2);1096      expect(fs.promises.readFile).toHaveBeenCalledWith(1097        path.join('/tmp', 'qwen-code-ide-server-12345.json'),1098        'utf8',1099      );1100      expect(fs.promises.readFile).toHaveBeenCalledWith(1101        path.join('/tmp', 'qwen-code-ide-server-2222.json'),1102        'utf8',1103      );1104      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];1105    });1106 1107    it('should fall back to legacy config when env lock file has invalid JSON', async () => {1108      process.env['QWEN_CODE_IDE_SERVER_PORT'] = '3333';1109      const config = { port: '1111', workspacePath: '/test/workspace' };1110      vi.mocked(fs.promises.readFile)1111        .mockResolvedValueOnce('invalid json')1112        .mockResolvedValueOnce(JSON.stringify(config));1113 1114      const ideClient = await IdeClient.getInstance();1115      const result = await (1116        ideClient as unknown as {1117          getConnectionConfigFromFile: () => Promise<unknown>;1118        }1119      ).getConnectionConfigFromFile();1120 1121      expect(result).toEqual(config);1122      delete process.env['QWEN_CODE_IDE_SERVER_PORT'];1123    });1124 1125    it('should keep a live lock file even when it is older than 7 days', async () => {1126      const liveConfig = {1127        port: '1000',1128        workspacePath: '/test/workspace',1129        ppid: 4242,1130      };1131      const oldTime = Date.now() - 8 * 24 * 60 * 60 * 1000;1132 1133      vi.mocked(fs.promises.readFile).mockImplementation(1134        async (filePath: fs.PathLike | FileHandle) => {1135          const file = String(filePath);1136          if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {1137            throw new Error('not found');1138          }1139          if (file === path.join('/home/test', '.qwen', 'ide', '1000.lock')) {1140            return JSON.stringify(liveConfig);1141          }1142          throw new Error(`unexpected path: ${file}`);1143        },1144      );1145      (1146        vi.mocked(fs.promises.readdir) as Mock<1147          (path: fs.PathLike) => Promise<string[]>1148        >1149      ).mockResolvedValue(['1000.lock']);1150      (1151        vi.mocked(fs.promises.stat) as Mock<1152          (path: fs.PathLike) => Promise<fs.Stats>1153        >1154      ).mockResolvedValue({ mtimeMs: oldTime } as fs.Stats);1155      vi.spyOn(process, 'kill').mockImplementation(() => true);1156 1157      const ideClient = await IdeClient.getInstance();1158      const result = await (1159        ideClient as unknown as {1160          getConnectionConfigFromFile: () => Promise<unknown>;1161        }1162      ).getConnectionConfigFromFile();1163 1164      expect(result).toEqual(liveConfig);1165      expect(fs.promises.unlink).not.toHaveBeenCalled();1166    });1167 1168    it('should keep incomplete old lock files when there is no stronger stale signal', async () => {1169      const latestConfig = {1170        port: '2000',1171        workspacePath: '/test/workspace',1172      };1173      const now = Date.now();1174      const staleTime = now - 7 * 24 * 60 * 60 * 1000 - 1000;1175 1176      vi.mocked(fs.promises.readFile).mockImplementation(1177        async (filePath: fs.PathLike | FileHandle) => {1178          const file = String(filePath);1179          if (file === path.join('/tmp', 'qwen-code-ide-server-12345.json')) {1180            throw new Error('not found');1181          }1182          if (file === path.join('/home/test', '.qwen', 'ide', '1000.lock')) {1183            return JSON.stringify({ port: '1000' });1184          }1185          if (file === path.join('/home/test', '.qwen', 'ide', '2000.lock')) {1186            return JSON.stringify(latestConfig);1187          }1188          throw new Error(`unexpected path: ${file}`);1189        },1190      );1191      (1192        vi.mocked(fs.promises.readdir) as Mock<1193          (path: fs.PathLike) => Promise<string[]>1194        >1195      ).mockResolvedValue(['1000.lock', '2000.lock']);1196      (1197        vi.mocked(fs.promises.stat) as Mock<1198          (path: fs.PathLike) => Promise<fs.Stats>1199        >1200      ).mockImplementation(async (filePath: fs.PathLike) => {

Showing the first 1,200 of 1621 lines. Download the file for the rest.

basant307/AI_Governance_Project · CoolFace