config-io.test.ts 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. /// <reference types="bun-types" />
  2. import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
  3. import {
  4. existsSync,
  5. mkdtempSync,
  6. readFileSync,
  7. rmSync,
  8. writeFileSync,
  9. } from 'node:fs';
  10. import { tmpdir } from 'node:os';
  11. import { join } from 'node:path';
  12. import {
  13. addPluginToOpenCodeConfig,
  14. addProviderConfig,
  15. detectCurrentConfig,
  16. disableDefaultAgents,
  17. parseConfig,
  18. parseConfigFile,
  19. stripJsonComments,
  20. writeConfig,
  21. writeLiteConfig,
  22. } from './config-io';
  23. import * as paths from './paths';
  24. describe('config-io', () => {
  25. let tmpDir: string;
  26. const originalEnv = { ...process.env };
  27. beforeEach(() => {
  28. tmpDir = mkdtempSync(join(tmpdir(), 'opencode-io-test-'));
  29. process.env.XDG_CONFIG_HOME = tmpDir;
  30. });
  31. afterEach(() => {
  32. process.env = { ...originalEnv };
  33. if (tmpDir && existsSync(tmpDir)) {
  34. rmSync(tmpDir, { recursive: true, force: true });
  35. }
  36. mock.restore();
  37. });
  38. test('stripJsonComments strips comments and trailing commas', () => {
  39. const jsonc = `{
  40. // comment
  41. "a": 1, /* multi
  42. line */
  43. "b": [2,],
  44. }`;
  45. const stripped = stripJsonComments(jsonc);
  46. expect(JSON.parse(stripped)).toEqual({ a: 1, b: [2] });
  47. });
  48. test('parseConfigFile parses valid JSON', () => {
  49. const path = join(tmpDir, 'test.json');
  50. writeFileSync(path, '{"a": 1}');
  51. const result = parseConfigFile(path);
  52. expect(result.config).toEqual({ a: 1 } as any);
  53. expect(result.error).toBeUndefined();
  54. });
  55. test('parseConfigFile returns null for non-existent file', () => {
  56. const result = parseConfigFile(join(tmpDir, 'nonexistent.json'));
  57. expect(result.config).toBeNull();
  58. });
  59. test('parseConfigFile returns null for empty or whitespace-only file', () => {
  60. const emptyPath = join(tmpDir, 'empty.json');
  61. writeFileSync(emptyPath, '');
  62. expect(parseConfigFile(emptyPath).config).toBeNull();
  63. const whitespacePath = join(tmpDir, 'whitespace.json');
  64. writeFileSync(whitespacePath, ' \n ');
  65. expect(parseConfigFile(whitespacePath).config).toBeNull();
  66. });
  67. test('parseConfigFile returns error for invalid JSON', () => {
  68. const path = join(tmpDir, 'invalid.json');
  69. writeFileSync(path, '{"a": 1');
  70. const result = parseConfigFile(path);
  71. expect(result.config).toBeNull();
  72. expect(result.error).toBeDefined();
  73. });
  74. test('parseConfig tries .jsonc if .json is missing', () => {
  75. const jsoncPath = join(tmpDir, 'test.jsonc');
  76. writeFileSync(jsoncPath, '{"a": 1}');
  77. // We pass .json path, it should try .jsonc
  78. const result = parseConfig(join(tmpDir, 'test.json'));
  79. expect(result.config).toEqual({ a: 1 } as any);
  80. });
  81. test('writeConfig writes JSON and creates backup', () => {
  82. const path = join(tmpDir, 'test.json');
  83. writeFileSync(path, '{"old": true}');
  84. writeConfig(path, { new: true } as any);
  85. expect(JSON.parse(readFileSync(path, 'utf-8'))).toEqual({ new: true });
  86. expect(JSON.parse(readFileSync(`${path}.bak`, 'utf-8'))).toEqual({
  87. old: true,
  88. });
  89. });
  90. test('addPluginToOpenCodeConfig adds plugin and removes duplicates', async () => {
  91. const configPath = join(tmpDir, 'opencode', 'opencode.json');
  92. paths.ensureConfigDir();
  93. writeFileSync(
  94. configPath,
  95. JSON.stringify({ plugin: ['other', 'oh-my-opencode-slim@1.0.0'] }),
  96. );
  97. const result = await addPluginToOpenCodeConfig();
  98. expect(result.success).toBe(true);
  99. const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
  100. expect(saved.plugin).toContain('oh-my-opencode-slim');
  101. expect(saved.plugin).not.toContain('oh-my-opencode-slim@1.0.0');
  102. expect(saved.plugin.length).toBe(2);
  103. });
  104. // Removed: addAuthPlugins test - auth plugin no longer used with cliproxy
  105. test('addProviderConfig adds cliproxy provider config', () => {
  106. const configPath = join(tmpDir, 'opencode', 'opencode.json');
  107. paths.ensureConfigDir();
  108. writeFileSync(configPath, JSON.stringify({}));
  109. const result = addProviderConfig({
  110. hasAntigravity: true,
  111. hasOpenAI: false,
  112. hasOpencodeZen: false,
  113. hasTmux: false,
  114. });
  115. expect(result.success).toBe(true);
  116. const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
  117. expect(saved.provider.cliproxy).toBeDefined();
  118. });
  119. test('writeLiteConfig writes lite config', () => {
  120. const litePath = join(tmpDir, 'opencode', 'oh-my-opencode-slim.json');
  121. paths.ensureConfigDir();
  122. const result = writeLiteConfig({
  123. hasAntigravity: true,
  124. hasOpenAI: false,
  125. hasOpencodeZen: false,
  126. hasTmux: true,
  127. });
  128. expect(result.success).toBe(true);
  129. const saved = JSON.parse(readFileSync(litePath, 'utf-8'));
  130. expect(saved.preset).toBe('cliproxy');
  131. expect(saved.presets.cliproxy).toBeDefined();
  132. expect(saved.tmux.enabled).toBe(true);
  133. });
  134. test('disableDefaultAgents disables explore and general agents', () => {
  135. const configPath = join(tmpDir, 'opencode', 'opencode.json');
  136. paths.ensureConfigDir();
  137. writeFileSync(configPath, JSON.stringify({}));
  138. const result = disableDefaultAgents();
  139. expect(result.success).toBe(true);
  140. const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
  141. expect(saved.agent.explore.disable).toBe(true);
  142. expect(saved.agent.general.disable).toBe(true);
  143. });
  144. test('detectCurrentConfig detects installed status', () => {
  145. const configPath = join(tmpDir, 'opencode', 'opencode.json');
  146. const litePath = join(tmpDir, 'opencode', 'oh-my-opencode-slim.json');
  147. paths.ensureConfigDir();
  148. writeFileSync(
  149. configPath,
  150. JSON.stringify({
  151. plugin: ['oh-my-opencode-slim'],
  152. provider: {
  153. cliproxy: {
  154. npm: '@ai-sdk/openai-compatible',
  155. },
  156. },
  157. }),
  158. );
  159. writeFileSync(
  160. litePath,
  161. JSON.stringify({
  162. preset: 'openai',
  163. presets: {
  164. openai: {
  165. orchestrator: { model: 'openai/gpt-4' },
  166. },
  167. },
  168. tmux: { enabled: true },
  169. }),
  170. );
  171. const detected = detectCurrentConfig();
  172. expect(detected.isInstalled).toBe(true);
  173. expect(detected.hasAntigravity).toBe(true);
  174. expect(detected.hasOpenAI).toBe(true);
  175. expect(detected.hasTmux).toBe(true);
  176. });
  177. });