| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- /// <reference types="bun-types" />
- 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 {
- addChutesProvider,
- addPluginToOpenCodeConfig,
- 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);
- });
- test('writeLiteConfig writes lite config', () => {
- const litePath = join(tmpDir, 'opencode', 'oh-my-opencode-slim.json');
- paths.ensureConfigDir();
- const result = writeLiteConfig({
- hasKimi: true,
- hasOpenAI: false,
- hasAntigravity: false,
- hasOpencodeZen: false,
- hasTmux: true,
- installSkills: false,
- installCustomSkills: false,
- });
- expect(result.success).toBe(true);
- const saved = JSON.parse(readFileSync(litePath, 'utf-8'));
- expect(saved.preset).toBe('kimi');
- expect(saved.presets.kimi).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: {
- kimi: {
- npm: '@ai-sdk/openai-compatible',
- },
- },
- }),
- );
- writeFileSync(
- litePath,
- JSON.stringify({
- preset: 'openai',
- presets: {
- openai: {
- orchestrator: { model: 'openai/gpt-4' },
- oracle: { model: 'anthropic/claude-opus-4-6' },
- explorer: { model: 'github-copilot/grok-code-fast-1' },
- librarian: { model: 'zai-coding-plan/glm-4.7' },
- },
- },
- tmux: { enabled: true },
- }),
- );
- const detected = detectCurrentConfig();
- expect(detected.isInstalled).toBe(true);
- expect(detected.hasKimi).toBe(true);
- expect(detected.hasOpenAI).toBe(true);
- expect(detected.hasAnthropic).toBe(true);
- expect(detected.hasCopilot).toBe(true);
- expect(detected.hasZaiPlan).toBe(true);
- expect(detected.hasTmux).toBe(true);
- });
- test('addChutesProvider keeps OpenCode auth-based chutes flow intact', () => {
- 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'] }),
- );
- writeFileSync(
- litePath,
- JSON.stringify({
- preset: 'chutes',
- presets: {
- chutes: {
- orchestrator: { model: 'chutes/kimi-k2.5' },
- },
- },
- }),
- );
- const result = addChutesProvider();
- expect(result.success).toBe(true);
- const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
- expect(saved.plugin).toContain('oh-my-opencode-slim');
- expect(saved.provider).toBeUndefined();
- const detected = detectCurrentConfig();
- expect(detected.hasChutes).toBe(true);
- });
- });
|