|
| 1 | +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; |
| 2 | +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'; |
| 3 | +import { tmpdir } from 'os'; |
| 4 | +import { join } from 'path'; |
| 5 | +import { parse } from 'jsonc-parser/lib/esm/main.js'; |
| 6 | + |
| 7 | +import { applyAgentConfigurations, prepareAgentConfigurations } from '../../src/agent/configurator'; |
| 8 | +import type { AgentSetupOptions } from '../../src/agent/types'; |
| 9 | + |
| 10 | +describe('Zed configuration', () => { |
| 11 | + let homeDir: string; |
| 12 | + let options: AgentSetupOptions; |
| 13 | + |
| 14 | + beforeEach(() => { |
| 15 | + homeDir = mkdtempSync(join(tmpdir(), 'mmx-zed-')); |
| 16 | + options = { |
| 17 | + agents: ['zed'], homeDir, env: {}, region: 'global', |
| 18 | + apiKey: 'sk-cp-test-only', model: 'MiniMax-M3.1-Flash', |
| 19 | + }; |
| 20 | + }); |
| 21 | + |
| 22 | + afterEach(() => rmSync(homeDir, { recursive: true, force: true })); |
| 23 | + |
| 24 | + it('merges JSONC settings for both regions, preserves user settings, and is idempotent', () => { |
| 25 | + const dir = join(homeDir, '.config', 'zed'); |
| 26 | + const path = join(dir, 'settings.json'); |
| 27 | + mkdirSync(dir, { recursive: true }); |
| 28 | + writeFileSync(path, '// keep comment\n{\n "theme": "One Dark",\n "language_models": {\n' |
| 29 | + + ' "openai_compatible": {\n "other": { "api_url": "https://x", "available_models": [] },\n' |
| 30 | + + ' "minimax": { "available_models": [{ "name": "keep-model", "max_tokens": 1 },' |
| 31 | + + ' { "name": "MiniMax-M3", "max_tokens": 2 }] },\n },\n },\n' |
| 32 | + + ' "agent": { "default_model": { "provider": "zed.dev", "model": "x" }, "dock": "left" },\n}\n'); |
| 33 | + for (const region of ['global', 'cn'] as const) { |
| 34 | + const prepared = prepareAgentConfigurations({ ...options, region }); |
| 35 | + expect(prepared.map(file => file.path)).toEqual([path]); |
| 36 | + expect(applyAgentConfigurations(prepared)[0]?.backup).toBeDefined(); |
| 37 | + const text = readFileSync(path, 'utf8'); |
| 38 | + const settings = parse(text); |
| 39 | + expect(text).toContain('// keep comment'); |
| 40 | + expect(settings.theme).toBe('One Dark'); |
| 41 | + expect(settings.agent.dock).toBe('left'); |
| 42 | + expect(settings.language_models.openai_compatible.other.api_url).toBe('https://x'); |
| 43 | + const provider = settings.language_models.openai_compatible.minimax; |
| 44 | + expect(provider.api_url).toBe(`https://api.minimax.${region === 'cn' ? 'cn' : 'io'}/v1`); |
| 45 | + expect(JSON.stringify(settings)).not.toContain(options.apiKey); |
| 46 | + const models = provider.available_models; |
| 47 | + expect(models.map((model: { name: string }) => model.name)) |
| 48 | + .toEqual(['keep-model', 'MiniMax-M3', 'MiniMax-M3.1-Flash']); |
| 49 | + expect(models[2]).toMatchObject({ |
| 50 | + max_tokens: 524288, max_output_tokens: 128000, reasoning_effort: 'max', |
| 51 | + capabilities: { tools: true, images: true, interleaved_reasoning: true, chat_completions: true }, |
| 52 | + }); |
| 53 | + expect(models[1]).toMatchObject({ max_tokens: 1_000_000, reasoning_effort: 'high' }); |
| 54 | + for (const model of models) expect(model.reasoning_effort).not.toBe('none'); |
| 55 | + expect(settings.agent.default_model).toEqual({ |
| 56 | + provider: 'minimax', model: 'MiniMax-M3.1-Flash', enable_thinking: true, effort: 'max', |
| 57 | + }); |
| 58 | + } |
| 59 | + const again = prepareAgentConfigurations({ ...options, region: 'cn' }); |
| 60 | + expect(again[0]?.before).toBe(again[0]?.after); |
| 61 | + }); |
| 62 | + |
| 63 | + it('writes a fresh config with the selected model and honors XDG_CONFIG_HOME', () => { |
| 64 | + const xdg = join(homeDir, 'xdg'); |
| 65 | + const [file] = prepareAgentConfigurations({ |
| 66 | + ...options, model: 'MiniMax-M3', env: { XDG_CONFIG_HOME: xdg }, |
| 67 | + }); |
| 68 | + expect(file?.path).toBe(join(xdg, 'zed', 'settings.json')); |
| 69 | + const settings = parse(file!.after); |
| 70 | + expect(settings.agent.default_model).toMatchObject({ model: 'MiniMax-M3', effort: 'high' }); |
| 71 | + expect(settings.language_models.openai_compatible.minimax.available_models).toHaveLength(2); |
| 72 | + }); |
| 73 | + |
| 74 | + it('uses the 1M Flash window when selected', () => { |
| 75 | + const [file] = prepareAgentConfigurations({ ...options, m31ContextWindow: 1_000_000 }); |
| 76 | + const models = parse(file!.after).language_models.openai_compatible.minimax.available_models; |
| 77 | + expect(models[0]).toMatchObject({ name: 'MiniMax-M3.1-Flash', max_tokens: 1_000_000 }); |
| 78 | + }); |
| 79 | + |
| 80 | + it('rejects a non-array available_models without changing files', () => { |
| 81 | + const dir = join(homeDir, '.config', 'zed'); |
| 82 | + mkdirSync(dir, { recursive: true }); |
| 83 | + writeFileSync(join(dir, 'settings.json'), |
| 84 | + '{ "language_models": { "openai_compatible": { "minimax": { "available_models": {} } } } }'); |
| 85 | + expect(() => prepareAgentConfigurations(options)).toThrow('available_models must be an array'); |
| 86 | + }); |
| 87 | +}); |
0 commit comments