/// import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'; import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { addPluginToOpenCodeConfig, addProviderConfig, detectCurrentConfig, disableDefaultAgents, parseConfig, parseConfigFile, stripJsonComments, writeConfig, writeLiteConfig, } from './config-io'; import * as paths from './paths'; describe('config-io', () => { let tmpDir: string; const originalEnv = { ...process.env }; beforeEach(() => { tmpDir = mkdtempSync(join(tmpdir(), 'opencode-io-test-')); process.env.XDG_CONFIG_HOME = tmpDir; }); afterEach(() => { process.env = { ...originalEnv }; if (tmpDir && existsSync(tmpDir)) { rmSync(tmpDir, { recursive: true, force: true }); } mock.restore(); }); test('stripJsonComments strips comments and trailing commas', () => { const jsonc = `{ // comment "a": 1, /* multi line */ "b": [2,], }`; const stripped = stripJsonComments(jsonc); expect(JSON.parse(stripped)).toEqual({ a: 1, b: [2] }); }); test('parseConfigFile parses valid JSON', () => { const path = join(tmpDir, 'test.json'); writeFileSync(path, '{"a": 1}'); const result = parseConfigFile(path); expect(result.config).toEqual({ a: 1 } as any); expect(result.error).toBeUndefined(); }); test('parseConfigFile returns null for non-existent file', () => { const result = parseConfigFile(join(tmpDir, 'nonexistent.json')); expect(result.config).toBeNull(); }); test('parseConfigFile returns null for empty or whitespace-only file', () => { const emptyPath = join(tmpDir, 'empty.json'); writeFileSync(emptyPath, ''); expect(parseConfigFile(emptyPath).config).toBeNull(); const whitespacePath = join(tmpDir, 'whitespace.json'); writeFileSync(whitespacePath, ' \n '); expect(parseConfigFile(whitespacePath).config).toBeNull(); }); test('parseConfigFile returns error for invalid JSON', () => { const path = join(tmpDir, 'invalid.json'); writeFileSync(path, '{"a": 1'); const result = parseConfigFile(path); expect(result.config).toBeNull(); expect(result.error).toBeDefined(); }); test('parseConfig tries .jsonc if .json is missing', () => { const jsoncPath = join(tmpDir, 'test.jsonc'); writeFileSync(jsoncPath, '{"a": 1}'); // We pass .json path, it should try .jsonc const result = parseConfig(join(tmpDir, 'test.json')); expect(result.config).toEqual({ a: 1 } as any); }); test('writeConfig writes JSON and creates backup', () => { const path = join(tmpDir, 'test.json'); writeFileSync(path, '{"old": true}'); writeConfig(path, { new: true } as any); expect(JSON.parse(readFileSync(path, 'utf-8'))).toEqual({ new: true }); expect(JSON.parse(readFileSync(`${path}.bak`, 'utf-8'))).toEqual({ old: true, }); }); test('addPluginToOpenCodeConfig adds plugin and removes duplicates', async () => { const configPath = join(tmpDir, 'opencode', 'opencode.json'); paths.ensureConfigDir(); writeFileSync( configPath, JSON.stringify({ plugin: ['other', 'oh-my-opencode-slim@1.0.0'] }), ); const result = await addPluginToOpenCodeConfig(); expect(result.success).toBe(true); const saved = JSON.parse(readFileSync(configPath, 'utf-8')); expect(saved.plugin).toContain('oh-my-opencode-slim'); expect(saved.plugin).not.toContain('oh-my-opencode-slim@1.0.0'); expect(saved.plugin.length).toBe(2); }); // Removed: addAuthPlugins test - auth plugin no longer used with cliproxy test('addProviderConfig adds cliproxy provider config', () => { const configPath = join(tmpDir, 'opencode', 'opencode.json'); paths.ensureConfigDir(); writeFileSync(configPath, JSON.stringify({})); const result = addProviderConfig({ hasAntigravity: true, hasOpenAI: false, hasOpencodeZen: false, hasTmux: false, }); expect(result.success).toBe(true); const saved = JSON.parse(readFileSync(configPath, 'utf-8')); expect(saved.provider.cliproxy).toBeDefined(); }); test('writeLiteConfig writes lite config', () => { const litePath = join(tmpDir, 'opencode', 'oh-my-opencode-slim.json'); paths.ensureConfigDir(); const result = writeLiteConfig({ hasAntigravity: true, hasOpenAI: false, hasOpencodeZen: false, hasTmux: true, }); expect(result.success).toBe(true); const saved = JSON.parse(readFileSync(litePath, 'utf-8')); expect(saved.preset).toBe('cliproxy'); expect(saved.presets.cliproxy).toBeDefined(); expect(saved.tmux.enabled).toBe(true); }); test('disableDefaultAgents disables explore and general agents', () => { const configPath = join(tmpDir, 'opencode', 'opencode.json'); paths.ensureConfigDir(); writeFileSync(configPath, JSON.stringify({})); const result = disableDefaultAgents(); expect(result.success).toBe(true); const saved = JSON.parse(readFileSync(configPath, 'utf-8')); expect(saved.agent.explore.disable).toBe(true); expect(saved.agent.general.disable).toBe(true); }); test('detectCurrentConfig detects installed status', () => { const configPath = join(tmpDir, 'opencode', 'opencode.json'); const litePath = join(tmpDir, 'opencode', 'oh-my-opencode-slim.json'); paths.ensureConfigDir(); writeFileSync( configPath, JSON.stringify({ plugin: ['oh-my-opencode-slim'], provider: { cliproxy: { npm: '@ai-sdk/openai-compatible', }, }, }), ); writeFileSync( litePath, JSON.stringify({ preset: 'openai', presets: { openai: { orchestrator: { model: 'openai/gpt-4' }, }, }, tmux: { enabled: true }, }), ); const detected = detectCurrentConfig(); expect(detected.isInstalled).toBe(true); expect(detected.hasAntigravity).toBe(true); expect(detected.hasOpenAI).toBe(true); expect(detected.hasTmux).toBe(true); }); });