| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- import { describe, expect, it } from 'vitest';
- import { buildArgs, DEFAULT_API_KEY_ENV, toToolSummaries, type CodexProviderSpec } from './codexRuntime';
- /** 这组断言锁的是「喂给 codex.exe 的 -c 参数」——provider 能不能生效全在这里 */
- const API_KEY = 'sk-unit-secret-abcdef123456';
- const custom: CodexProviderSpec = {
- id: 'zsjz',
- name: '知数 · 第三方',
- baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
- model: 'qwen3-max',
- apiKey: API_KEY,
- httpHeaders: { 'X-DashScope-WorkSpace': 'ws-1' },
- extra: { request_max_retries: 3, temperature: 0.5, query_params: { version: 'v2' } },
- };
- describe('buildArgs', () => {
- it('没有 provider 时只起 app-server,且不传 --strict-config', () => {
- expect(buildArgs(null)).toEqual(['app-server', '--listen', 'stdio://']);
- expect(buildArgs(null)).not.toContain('--strict-config');
- });
- it('自定义 provider 走 responses + 免登录 + env_key', () => {
- const args = buildArgs(custom);
- expect(args).toContain('-c');
- expect(args.join(' ')).toContain('model_provider="zsjz"');
- expect(args.join(' ')).toContain('model="qwen3-max"');
- expect(args.join(' ')).toContain('model_providers.zsjz.wire_api="responses"');
- expect(args.join(' ')).toContain('model_providers.zsjz.requires_openai_auth=false');
- expect(args.join(' ')).toContain(`model_providers.zsjz.env_key="${DEFAULT_API_KEY_ENV}"`);
- expect(args.join(' ')).toContain('model_providers.zsjz.base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"');
- });
- it('API Key 绝不出现在命令行参数里', () => {
- expect(JSON.stringify(buildArgs(custom))).not.toContain(API_KEY);
- });
- it('没有 apiKey 时不写 env_key(本地无鉴权服务)', () => {
- const args = buildArgs({ ...custom, apiKey: null });
- expect(args.join(' ')).not.toContain('env_key');
- });
- it('http_headers 与 query_params 输出成 TOML inline table', () => {
- const joined = buildArgs(custom).join(' ');
- // 只含 A-Za-z0-9_- 的键按 TOML bare key 规则输出,无需加引号
- expect(joined).toContain('model_providers.zsjz.http_headers={X-DashScope-WorkSpace="ws-1"}');
- expect(joined).toContain('model_providers.zsjz.query_params={version="v2"}');
- });
- it('config 白名单外的键被丢弃', () => {
- const joined = buildArgs(custom).join(' ');
- expect(joined).toContain('request_max_retries=3');
- expect(joined).not.toContain('temperature');
- });
- it('provider 一律用自定义 id,绝不落在 Codex 保留的内置 id 上', () => {
- const joined = buildArgs(custom).join(' ');
- expect(joined).toContain('model_provider="zsjz"');
- // 覆盖 model_providers.ollama 之类会让 app-server 启动即退出
- expect(joined).not.toMatch(/model_providers\.(ollama|lmstudio|openai)\b/u);
- });
- it('重试次数 / 空闲超时 / websocket 开关都会写进 -c', () => {
- const joined = buildArgs({
- ...custom,
- extra: {
- request_max_retries: 0,
- stream_max_retries: 0,
- stream_idle_timeout_ms: 300_000,
- supports_websockets: false,
- },
- }).join(' ');
- expect(joined).toContain('model_providers.zsjz.stream_max_retries=0');
- expect(joined).toContain('model_providers.zsjz.request_max_retries=0');
- expect(joined).toContain('model_providers.zsjz.stream_idle_timeout_ms=300000');
- expect(joined).toContain('model_providers.zsjz.supports_websockets=false');
- });
- it('extra 里没有的键绝不写进 -c(supports_websockets 是 serde 默认值,属于空操作)', () => {
- const joined = buildArgs({ ...custom, extra: { request_max_retries: 0 } }).join(' ');
- expect(joined).not.toContain('supports_websockets');
- // websocket_connect_timeout_ms 是真实键(15 秒那个),允许显式覆盖
- const withWsTimeout = buildArgs({ ...custom, extra: { websocket_connect_timeout_ms: 5_000 } }).join(' ');
- expect(withWsTimeout).toContain('model_providers.zsjz.websocket_connect_timeout_ms=5000');
- });
- it('modelCatalogPath 会以 model_catalog_json 传入,未设置时不出现该键', () => {
- const withCatalog = buildArgs({ ...custom, modelCatalogPath: 'C:\\data\\codex-data\\model-catalog.json' }).join(' ');
- expect(withCatalog).toContain('model_catalog_json="C:\\\\data\\\\codex-data\\\\model-catalog.json"');
- expect(buildArgs(custom).join(' ')).not.toContain('model_catalog_json');
- });
- it('值里的引号与反斜杠被 TOML 安全转义', () => {
- const joined = buildArgs({
- ...custom,
- name: '带"引号"和\\反斜杠',
- baseUrl: 'https://x/v1',
- }).join(' ');
- expect(joined).toContain('model_providers.zsjz.name="带\\"引号\\"和\\\\反斜杠"');
- });
- it('嵌套 map 输出成 TOML inline table,而不是 JSON', () => {
- const joined = buildArgs({
- ...custom,
- extra: { query_params: { version: 'v2', flag: 'on' } },
- }).join(' ');
- expect(joined).toContain('model_providers.zsjz.query_params={version="v2",flag="on"}');
- expect(joined).not.toContain('{"version"');
- });
- it('不符合 bare key 规则的键加引号', () => {
- const joined = buildArgs({ ...custom, httpHeaders: { 'X Token': 'a' } }).join(' ');
- expect(joined).toContain('model_providers.zsjz.http_headers={"X Token"="a"}');
- });
- });
- /** mcpServerStatus/list 的 tools 详情结构不由我们决定,这里锁住退化行为 */
- describe('toToolSummaries', () => {
- it('详情里带 name / description 时优先取详情', () => {
- expect(toToolSummaries({ query_case: { name: 'query_case_v2', description: '按条件查案件' } })).toEqual([
- { name: 'query_case_v2', description: '按条件查案件' },
- ]);
- });
- it('详情形态不认识时退化成用 key 当工具名,描述留空', () => {
- expect(toToolSummaries({ list_tables: { inputSchema: {} }, ping: null })).toEqual([
- { name: 'list_tables', description: '' },
- { name: 'ping', description: '' },
- ]);
- });
- it('tools 缺失时返回空列表', () => {
- expect(toToolSummaries(undefined)).toEqual([]);
- expect(toToolSummaries(null)).toEqual([]);
- });
- });
|