| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- /// <reference types="bun-types" />
- import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
- import {
- existsSync,
- mkdirSync,
- mkdtempSync,
- readFileSync,
- rmSync,
- writeFileSync,
- } from 'node:fs';
- import { tmpdir } from 'node:os';
- import { join } from 'node:path';
- import {
- 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 };
- const originalArgv = [...process.argv];
- beforeEach(() => {
- tmpDir = mkdtempSync(join(tmpdir(), 'opencode-io-test-'));
- delete process.env.OPENCODE_CONFIG_DIR;
- process.env.XDG_CONFIG_HOME = tmpDir;
- });
- afterEach(() => {
- process.env = { ...originalEnv };
- process.argv = [...originalArgv];
- if (tmpDir && existsSync(tmpDir)) {
- rmSync(tmpDir, { recursive: true, force: true });
- }
- mock.restore();
- });
- function writePackageJson(dir: string): void {
- mkdirSync(dir, { recursive: true });
- writeFileSync(
- join(dir, 'package.json'),
- JSON.stringify({ name: 'oh-my-opencode-slim' }),
- );
- }
- 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'] }),
- );
- process.argv[1] = '';
- 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('addPluginToOpenCodeConfig stores package name for bunx temp paths', async () => {
- const configPath = join(tmpDir, 'opencode', 'opencode.json');
- const packageRoot = join(
- tmpDir,
- 'bunx-1000-oh-my-opencode-slim@latest',
- 'node_modules',
- 'oh-my-opencode-slim',
- );
- paths.ensureConfigDir();
- writeFileSync(configPath, JSON.stringify({ plugin: [] }));
- writePackageJson(packageRoot);
- process.argv[1] = join(packageRoot, 'dist', 'cli', 'index.js');
- const result = await addPluginToOpenCodeConfig();
- expect(result.success).toBe(true);
- const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
- expect(saved.plugin).toEqual(['oh-my-opencode-slim']);
- });
- test('addPluginToOpenCodeConfig stores local repo path for local dev paths', async () => {
- const configPath = join(tmpDir, 'opencode', 'opencode.json');
- const packageRoot = join(tmpDir, 'repo');
- const localCliPath = join(packageRoot, 'dist', 'cli', 'index.js');
- paths.ensureConfigDir();
- writeFileSync(configPath, JSON.stringify({ plugin: [] }));
- writePackageJson(packageRoot);
- process.argv[1] = localCliPath;
- const result = await addPluginToOpenCodeConfig();
- expect(result.success).toBe(true);
- const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
- expect(saved.plugin).toEqual([packageRoot]);
- });
- test('addPluginToOpenCodeConfig stores local repo path for local paths containing bunx-', async () => {
- const configPath = join(tmpDir, 'opencode', 'opencode.json');
- const packageRoot = join(tmpDir, 'repo', 'bunx-tools');
- const localCliPath = join(packageRoot, 'dist', 'cli', 'index.js');
- paths.ensureConfigDir();
- writeFileSync(configPath, JSON.stringify({ plugin: [] }));
- writePackageJson(packageRoot);
- process.argv[1] = localCliPath;
- const result = await addPluginToOpenCodeConfig();
- expect(result.success).toBe(true);
- const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
- expect(saved.plugin).toEqual([packageRoot]);
- });
- test('addPluginToOpenCodeConfig deduplicates existing local repo path entries', async () => {
- const configPath = join(tmpDir, 'opencode', 'opencode.json');
- const packageRoot = join(tmpDir, 'repo');
- const localCliPath = join(packageRoot, 'dist', 'cli', 'index.js');
- paths.ensureConfigDir();
- writePackageJson(packageRoot);
- writeFileSync(
- configPath,
- JSON.stringify({ plugin: ['other', packageRoot] }),
- );
- process.argv[1] = localCliPath;
- const result = await addPluginToOpenCodeConfig();
- expect(result.success).toBe(true);
- const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
- expect(saved.plugin).toEqual(['other', packageRoot]);
- });
- test('addPluginToOpenCodeConfig preserves non-string plugin entries when refreshing', async () => {
- const configPath = join(tmpDir, 'opencode', 'opencode.json');
- paths.ensureConfigDir();
- process.argv[1] = '';
- const objectPlugin = { name: 'some-config-plugin', enabled: true };
- writeFileSync(
- configPath,
- JSON.stringify({
- plugin: ['other-plugin', objectPlugin, '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).toContain('other-plugin');
- expect(saved.plugin).not.toContain('oh-my-opencode-slim@1.0.0');
- // Non-string entries (objects) must survive the plugin refresh
- expect(saved.plugin).toContainEqual(objectPlugin);
- expect(saved.plugin.length).toBe(3);
- });
- test('writeLiteConfig writes lite config with OpenAI preset', () => {
- const litePath = join(tmpDir, 'opencode', 'oh-my-opencode-slim.json');
- paths.ensureConfigDir();
- const result = writeLiteConfig({
- hasTmux: true,
- installSkills: false,
- installCustomSkills: false,
- reset: false,
- });
- expect(result.success).toBe(true);
- const saved = JSON.parse(readFileSync(litePath, 'utf-8'));
- expect(saved.$schema).toBe(
- 'https://unpkg.com/oh-my-opencode-slim@latest/oh-my-opencode-slim.schema.json',
- );
- expect(saved.preset).toBe('openai');
- expect(saved.presets.openai).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('detectCurrentConfig treats local repo path entries as installed', () => {
- const configPath = join(tmpDir, 'opencode', 'opencode.json');
- const packageRoot = join(tmpDir, 'repo');
- paths.ensureConfigDir();
- writePackageJson(packageRoot);
- writeFileSync(configPath, JSON.stringify({ plugin: [packageRoot] }));
- const detected = detectCurrentConfig();
- expect(detected.isInstalled).toBe(true);
- });
- });
|