CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
serve.test.ts555 linesDownload Raw Back to commands
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import { spawn, spawnSync } from 'node:child_process';8import * as fs from 'node:fs';9import * as os from 'node:os';10import * as path from 'node:path';11import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';12import yargs, { type Argv } from 'yargs';13import { serveCommand, maybeOpenWebShellBrowser } from './serve.js';14 15const mockOpenBrowserSecurely = vi.hoisted(() => vi.fn());16const mockShouldLaunchBrowser = vi.hoisted(() => vi.fn(() => true));17const mockRunQwenServe = vi.hoisted(() => vi.fn());18vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {19  const actual =20    await importOriginal<typeof import('@qwen-code/qwen-code-core')>();21  return {22    ...actual,23    openBrowserSecurely: mockOpenBrowserSecurely,24    shouldLaunchBrowser: mockShouldLaunchBrowser,25  };26});27vi.mock('../serve/run-qwen-serve.js', () => ({28  runQwenServe: mockRunQwenServe,29}));30 31function buildParser(): Argv {32  return (serveCommand.builder as (argv: Argv) => Argv)(33    yargs([]).exitProcess(false).fail(false).locale('en'),34  );35}36 37describe('serve command args', () => {38  it('parses --enable-session-shell', () => {39    const parsed = buildParser().parseSync('--enable-session-shell');40    expect(parsed['enable-session-shell']).toBe(true);41  });42 43  it('defaults direct session shell to disabled', () => {44    const parsed = buildParser().parseSync('');45    expect(parsed['enable-session-shell']).toBe(false);46  });47 48  it('accepts --experimental-lsp in strict parser mode', () => {49    const parsed = buildParser().strict().parseSync('--experimental-lsp');50    expect(parsed['experimentalLsp']).toBe(true);51  });52 53  it('parses --permission-response-timeout-ms as a number', () => {54    const parsed = buildParser().parseSync(55      '--permission-response-timeout-ms 60000',56    );57    expect(parsed['permission-response-timeout-ms']).toBe(60000);58  });59 60  it('parses --max-total-sessions as a number', () => {61    const parsed = buildParser().parseSync('--max-total-sessions 42');62    expect(parsed['max-total-sessions']).toBe(42);63  });64 65  it('leaves --permission-response-timeout-ms unset by default', () => {66    const parsed = buildParser().parseSync('');67    expect(parsed['permission-response-timeout-ms']).toBeUndefined();68  });69 70  it('parses --experimental-lsp for daemon child opt-in', () => {71    const parsed = buildParser().parseSync('--experimental-lsp');72    expect(parsed['experimentalLsp']).toBe(true);73  });74 75  it('registers --experimental-lsp as an explicit serve option', () => {76    const options = (77      buildParser() as Argv & {78        getOptions(): { key: Record<string, boolean> };79      }80    ).getOptions();81    expect(options.key['experimental-lsp']).toBe(true);82  });83 84  it('parses --web (default true) and --no-web', () => {85    expect(buildParser().parseSync('')['web']).toBe(true);86    expect(buildParser().parseSync('--no-web')['web']).toBe(false);87  });88 89  it('parses --open (default false)', () => {90    expect(buildParser().parseSync('')['open']).toBe(false);91    expect(buildParser().parseSync('--open')['open']).toBe(true);92  });93 94  it('parses repeatable --channel values', () => {95    const parsed = buildParser().parseSync(96      '--channel telegram --channel feishu',97    );98 99    expect(parsed['channel']).toEqual(['telegram', 'feishu']);100  });101 102  it('parses a single --workspace value as a single-element array', () => {103    const parsed = buildParser().parseSync('--workspace /tmp/primary');104 105    expect(parsed['workspace']).toEqual(['/tmp/primary']);106  });107 108  it('parses repeatable --workspace values as an array', () => {109    const parsed = buildParser().parseSync(110      '--workspace /tmp/primary --workspace /tmp/secondary',111    );112 113    expect(parsed['workspace']).toEqual(['/tmp/primary', '/tmp/secondary']);114  });115 116  it('rejects valueless --workspace forms', () => {117    for (const input of [118      '--workspace',119      '--workspace=',120      '--workspace /tmp/primary --workspace',121    ]) {122      expect(() => buildParser().parseSync(input)).toThrow(123        /Not enough arguments following: workspace/,124      );125    }126  });127 128  it('preserves repeatable --workspace values in command mode', () => {129    let captured: unknown;130    yargs([])131      .exitProcess(false)132      .fail(false)133      .locale('en')134      .command({135        ...serveCommand,136        handler: (argv) => {137          captured = argv.workspace;138        },139      })140      .parseSync('serve --workspace /tmp/primary --workspace /tmp/secondary');141 142    expect(captured).toEqual(['/tmp/primary', '/tmp/secondary']);143  });144});145 146describe('serve rate limit env parsing', () => {147  const originalEnv = process.env;148 149  beforeEach(() => {150    vi.clearAllMocks();151    process.env = { ...originalEnv, QWEN_CODE_SUPPRESS_YOLO_WARNING: '1' };152  });153 154  afterEach(() => {155    process.env = originalEnv;156    vi.restoreAllMocks();157  });158 159  async function invokeServeHandler() {160    const handler = serveCommand.handler;161    if (!handler) throw new Error('serve handler missing');162    const argv = buildParser().parseSync('--rate-limit --no-web');163    await handler(argv as Parameters<typeof handler>[0]);164  }165 166  async function startServeHandler() {167    const handler = serveCommand.handler;168    if (!handler) throw new Error('serve handler missing');169    const argv = buildParser().parseSync('--rate-limit --no-web');170    void handler(argv as Parameters<typeof handler>[0]);171    await vi.waitFor(() => {172      expect(mockRunQwenServe).toHaveBeenCalled();173    });174  }175 176  async function startServeHandlerWithArgs(args: string) {177    const handler = serveCommand.handler;178    if (!handler) throw new Error('serve handler missing');179    const argv = buildParser().parseSync(args);180    void handler(argv as Parameters<typeof handler>[0]);181    await vi.waitFor(() => {182      expect(mockRunQwenServe).toHaveBeenCalled();183    });184  }185 186  it.each([187    ['QWEN_SERVE_RATE_LIMIT_PROMPT', '0x10'],188    ['QWEN_SERVE_RATE_LIMIT_MUTATION', '1e3'],189    ['QWEN_SERVE_RATE_LIMIT_READ', '2.5'],190    ['QWEN_SERVE_RATE_LIMIT_WINDOW_MS', '0x3e8'],191  ])('rejects non-decimal %s=%s', async (key, value) => {192    process.env[key] = value;193    vi.spyOn(process, 'exit').mockImplementation((code) => {194      throw new Error(`process.exit(${code}) called`);195    });196 197    await expect(invokeServeHandler()).rejects.toThrow(198      'process.exit(1) called',199    );200    expect(mockRunQwenServe).not.toHaveBeenCalled();201  });202 203  it('passes decimal env values to runQwenServe', async () => {204    process.env['QWEN_SERVE_RATE_LIMIT_PROMPT'] = '11';205    process.env['QWEN_SERVE_RATE_LIMIT_MUTATION'] = ' 31 ';206    process.env['QWEN_SERVE_RATE_LIMIT_READ'] = '121';207    process.env['QWEN_SERVE_RATE_LIMIT_WINDOW_MS'] = '60000';208    mockRunQwenServe.mockResolvedValueOnce({209      url: 'http://127.0.0.1:4170/',210      webShellMounted: false,211    });212 213    await startServeHandler();214 215    expect(mockRunQwenServe).toHaveBeenCalledWith(216      expect.objectContaining({217        rateLimit: true,218        rateLimitPrompt: 11,219        rateLimitMutation: 31,220        rateLimitRead: 121,221        rateLimitWindowMs: 60000,222      }),223    );224  });225 226  it('passes normalized named channels to runQwenServe', async () => {227    mockRunQwenServe.mockResolvedValueOnce({228      url: 'http://127.0.0.1:4170/',229      webShellMounted: false,230    });231 232    await startServeHandlerWithArgs(233      '--no-web --channel telegram --channel telegram --channel feishu',234    );235 236    expect(mockRunQwenServe).toHaveBeenCalledWith(237      expect.objectContaining({238        channelSelection: { mode: 'names', names: ['telegram', 'feishu'] },239      }),240    );241  });242 243  it('passes --max-total-sessions to runQwenServe', async () => {244    mockRunQwenServe.mockResolvedValueOnce({245      url: 'http://127.0.0.1:4170/',246      webShellMounted: false,247    });248 249    await startServeHandlerWithArgs('--no-web --max-total-sessions 42');250 251    expect(mockRunQwenServe).toHaveBeenCalledWith(252      expect.objectContaining({ maxTotalSessions: 42 }),253    );254  });255 256  it('passes --channel all as an all-channel selection', async () => {257    mockRunQwenServe.mockResolvedValueOnce({258      url: 'http://127.0.0.1:4170/',259      webShellMounted: false,260    });261 262    await startServeHandlerWithArgs('--no-web --channel all');263 264    expect(mockRunQwenServe).toHaveBeenCalledWith(265      expect.objectContaining({266        channelSelection: { mode: 'all' },267      }),268    );269  });270 271  it('rejects --channel all mixed with concrete channels', async () => {272    vi.spyOn(process, 'exit').mockImplementation((code) => {273      throw new Error(`process.exit(${code}) called`);274    });275 276    const handler = serveCommand.handler;277    if (!handler) throw new Error('serve handler missing');278    const argv = buildParser().parseSync(279      '--no-web --channel all --channel telegram',280    );281 282    await expect(283      handler(argv as Parameters<typeof handler>[0]),284    ).rejects.toThrow('process.exit(1) called');285    expect(mockRunQwenServe).not.toHaveBeenCalled();286  });287});288 289describe('maybeOpenWebShellBrowser', () => {290  beforeEach(() => {291    vi.clearAllMocks();292    mockShouldLaunchBrowser.mockReturnValue(true);293  });294  afterEach(() => {295    vi.restoreAllMocks();296  });297 298  const firstOpenedUrl = () =>299    String(mockOpenBrowserSecurely.mock.calls[0]?.[0]);300 301  it('does nothing when --open is false', async () => {302    await maybeOpenWebShellBrowser(303      { url: 'http://127.0.0.1:4170/', webShellMounted: true },304      false,305    );306    expect(mockOpenBrowserSecurely).not.toHaveBeenCalled();307  });308 309  it('does nothing when the Web Shell is not mounted', async () => {310    await maybeOpenWebShellBrowser(311      { url: 'http://127.0.0.1:4170/', webShellMounted: false },312      true,313    );314    expect(mockOpenBrowserSecurely).not.toHaveBeenCalled();315  });316 317  it('does nothing when shouldLaunchBrowser() is false', async () => {318    mockShouldLaunchBrowser.mockReturnValue(false);319    await maybeOpenWebShellBrowser(320      { url: 'http://127.0.0.1:4170/', webShellMounted: true },321      true,322    );323    expect(mockOpenBrowserSecurely).not.toHaveBeenCalled();324  });325 326  it('rewrites a wildcard bind host to loopback', async () => {327    await maybeOpenWebShellBrowser(328      { url: 'http://0.0.0.0:4170/', webShellMounted: true },329      true,330    );331    expect(firstOpenedUrl()).toContain('127.0.0.1');332    expect(firstOpenedUrl()).not.toContain('0.0.0.0');333  });334 335  it('puts the token in the URL fragment, not the query', async () => {336    await maybeOpenWebShellBrowser(337      {338        url: 'http://127.0.0.1:4170/',339        webShellMounted: true,340        resolvedToken: 'secret',341      },342      true,343    );344    expect(firstOpenedUrl()).toContain('#token=secret');345    expect(firstOpenedUrl()).not.toContain('?token=');346  });347 348  it('skips --open when the runtime failed to mount', async () => {349    const stderrWrites: string[] = [];350    vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => {351      stderrWrites.push(String(chunk));352      return true;353    });354 355    await maybeOpenWebShellBrowser(356      {357        url: 'http://127.0.0.1:4170/',358        webShellMounted: true,359        runtimeReady: Promise.reject(new Error('runtime boom')),360      },361      true,362    );363 364    expect(mockOpenBrowserSecurely).not.toHaveBeenCalled();365    expect(stderrWrites.join('')).toContain(366      'qwen serve: Web Shell runtime not ready; skipping --open: runtime boom',367    );368  });369 370  it('swallows openBrowserSecurely failures (never throws)', async () => {371    mockOpenBrowserSecurely.mockRejectedValueOnce(new Error('boom'));372    await expect(373      maybeOpenWebShellBrowser(374        { url: 'http://127.0.0.1:4170/', webShellMounted: true },375        true,376      ),377    ).resolves.toBeUndefined();378  });379});380 381describe('serve startup import boundary', () => {382  it('reaches listening through the dev entrypoint without loading interactive Ink internals first', async () => {383    const workspace = fs.realpathSync(384      fs.mkdtempSync(path.join(os.tmpdir(), 'qws-import-boundary-')),385    );386    const qwenHome = fs.realpathSync(387      fs.mkdtempSync(path.join(os.tmpdir(), 'qws-import-boundary-home-')),388    );389    const root = path.resolve(process.cwd(), '../..');390    const childEnv: NodeJS.ProcessEnv = {391      ...process.env,392      QWEN_CODE_NO_RELAUNCH: '1',393      QWEN_CODE_SUPPRESS_YOLO_WARNING: '1',394      QWEN_HOME: qwenHome,395      QWEN_RUNTIME_DIR: workspace,396      QWEN_SERVE_RATE_LIMIT: '0',397    };398    delete childEnv['VITEST_WORKER_ID'];399    const child = spawn(400      process.execPath,401      [402        path.join(root, 'scripts/dev.js'),403        'serve',404        '--port',405        '0',406        '--hostname',407        '127.0.0.1',408        '--workspace',409        workspace,410        '--no-web',411        '--no-open',412        '--rate-limit-prompt',413        '0',414        '--rate-limit-window-ms',415        '1',416      ],417      {418        cwd: root,419        detached: process.platform !== 'win32',420        env: childEnv,421        stdio: ['ignore', 'pipe', 'pipe'],422      },423    );424 425    let stdout = '';426    let stderr = '';427    let childExited = false;428    const exited = new Promise<void>((resolve) => {429      child.once('exit', () => {430        childExited = true;431        resolve();432      });433    });434    const waitForExit = (ms: number) =>435      Promise.race([436        exited,437        new Promise<'timeout'>((resolve) => setTimeout(resolve, ms, 'timeout')),438      ]);439    const cleanup = async () => {440      if (child.pid === undefined) return;441      const childPid = child.pid;442      const signalProcessTree = (signal: NodeJS.Signals) => {443        if (process.platform === 'win32') {444          spawnSync('taskkill', ['/pid', String(childPid), '/T', '/F']);445          return;446        }447        process.kill(-childPid, signal);448      };449      try {450        signalProcessTree('SIGTERM');451      } catch {452        // Process may have already exited.453      }454      if (!childExited) {455        await waitForExit(2_000);456      }457      if (process.platform !== 'win32') {458        try {459          signalProcessTree('SIGKILL');460        } catch {461          // Process may have already exited.462        }463        if (!childExited) {464          await waitForExit(2_000);465        }466      }467    };468    const removeTempDir = async (dir: string) => {469      for (let attempt = 0; attempt < 5; attempt++) {470        try {471          fs.rmSync(dir, { recursive: true, force: true });472          return;473        } catch (err) {474          if (attempt === 4) throw err;475          await new Promise((resolve) => setTimeout(resolve, 100));476        }477      }478    };479    const processGroupHasMembers = (pgid: number): boolean => {480      if (process.platform === 'win32') return false;481      const result = spawnSync('ps', ['-o', 'pid=', '-g', String(pgid)], {482        encoding: 'utf8',483      });484      if (result.status !== 0) return false;485      return result.stdout486        .split(/\s+/)487        .some((pid) => pid.length > 0 && Number(pid) > 0);488    };489    const waitForProcessGroupExit = async (pgid: number) => {490      if (process.platform === 'win32') return;491      for (let attempt = 0; attempt < 20; attempt++) {492        if (!processGroupHasMembers(pgid)) return;493        await new Promise((resolve) => setTimeout(resolve, 100));494      }495      throw new Error(`serve process group ${pgid} did not exit`);496    };497 498    try {499      const reachedListening = await new Promise<boolean>((resolve, reject) => {500        const timeout = setTimeout(() => {501          void cleanup();502          reject(503            new Error(504              `serve did not reach listening\nstdout:\n${stdout}\nstderr:\n${stderr}`,505            ),506          );507        }, 15_000);508 509        child.stdout.on('data', (chunk: Buffer) => {510          stdout += chunk.toString('utf8');511          if (stdout.includes('qwen serve listening on')) {512            clearTimeout(timeout);513            void cleanup();514            resolve(true);515          }516        });517        child.stderr.on('data', (chunk: Buffer) => {518          stderr += chunk.toString('utf8');519          if (520            stderr.includes('ERR_PACKAGE_PATH_NOT_EXPORTED') ||521            stderr.includes('ink/dom') ||522            stderr.includes('ink/components/CursorContext')523          ) {524            clearTimeout(timeout);525            void cleanup();526            reject(new Error(stderr));527          }528        });529        child.on('error', (err) => {530          clearTimeout(timeout);531          reject(err);532        });533        child.on('exit', (code, signal) => {534          if (stdout.includes('qwen serve listening on')) return;535          clearTimeout(timeout);536          reject(537            new Error(538              `serve exited before listening: code=${code} signal=${signal}\nstdout:\n${stdout}\nstderr:\n${stderr}`,539            ),540          );541        });542      });543 544      expect(reachedListening).toBe(true);545    } finally {546      await cleanup();547      if (child.pid !== undefined) {548        await waitForProcessGroupExit(child.pid);549      }550      await removeTempDir(workspace);551      await removeTempDir(qwenHome);552    }553  }, 20_000);554});555 
basant307/AI_Governance_Project · CoolFace