config.test.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. import { test, expect } from "bun:test"
  2. import { Config } from "../../src/config/config"
  3. import { Instance } from "../../src/project/instance"
  4. import { tmpdir } from "../fixture/fixture"
  5. import path from "path"
  6. import fs from "fs/promises"
  7. test("loads config with defaults when no files exist", async () => {
  8. await using tmp = await tmpdir()
  9. await Instance.provide({
  10. directory: tmp.path,
  11. fn: async () => {
  12. const config = await Config.get()
  13. expect(config.username).toBeDefined()
  14. },
  15. })
  16. })
  17. test("loads JSON config file", async () => {
  18. await using tmp = await tmpdir({
  19. init: async (dir) => {
  20. await Bun.write(
  21. path.join(dir, "opencode.json"),
  22. JSON.stringify({
  23. $schema: "https://opencode.ai/config.json",
  24. model: "test/model",
  25. username: "testuser",
  26. }),
  27. )
  28. },
  29. })
  30. await Instance.provide({
  31. directory: tmp.path,
  32. fn: async () => {
  33. const config = await Config.get()
  34. expect(config.model).toBe("test/model")
  35. expect(config.username).toBe("testuser")
  36. },
  37. })
  38. })
  39. test("loads JSONC config file", async () => {
  40. await using tmp = await tmpdir({
  41. init: async (dir) => {
  42. await Bun.write(
  43. path.join(dir, "opencode.jsonc"),
  44. `{
  45. // This is a comment
  46. "$schema": "https://opencode.ai/config.json",
  47. "model": "test/model",
  48. "username": "testuser"
  49. }`,
  50. )
  51. },
  52. })
  53. await Instance.provide({
  54. directory: tmp.path,
  55. fn: async () => {
  56. const config = await Config.get()
  57. expect(config.model).toBe("test/model")
  58. expect(config.username).toBe("testuser")
  59. },
  60. })
  61. })
  62. test("merges multiple config files with correct precedence", async () => {
  63. await using tmp = await tmpdir({
  64. init: async (dir) => {
  65. await Bun.write(
  66. path.join(dir, "opencode.jsonc"),
  67. JSON.stringify({
  68. $schema: "https://opencode.ai/config.json",
  69. model: "base",
  70. username: "base",
  71. }),
  72. )
  73. await Bun.write(
  74. path.join(dir, "opencode.json"),
  75. JSON.stringify({
  76. $schema: "https://opencode.ai/config.json",
  77. model: "override",
  78. }),
  79. )
  80. },
  81. })
  82. await Instance.provide({
  83. directory: tmp.path,
  84. fn: async () => {
  85. const config = await Config.get()
  86. expect(config.model).toBe("override")
  87. expect(config.username).toBe("base")
  88. },
  89. })
  90. })
  91. test("handles environment variable substitution", async () => {
  92. const originalEnv = process.env["TEST_VAR"]
  93. process.env["TEST_VAR"] = "test_theme"
  94. try {
  95. await using tmp = await tmpdir({
  96. init: async (dir) => {
  97. await Bun.write(
  98. path.join(dir, "opencode.json"),
  99. JSON.stringify({
  100. $schema: "https://opencode.ai/config.json",
  101. theme: "{env:TEST_VAR}",
  102. }),
  103. )
  104. },
  105. })
  106. await Instance.provide({
  107. directory: tmp.path,
  108. fn: async () => {
  109. const config = await Config.get()
  110. expect(config.theme).toBe("test_theme")
  111. },
  112. })
  113. } finally {
  114. if (originalEnv !== undefined) {
  115. process.env["TEST_VAR"] = originalEnv
  116. } else {
  117. delete process.env["TEST_VAR"]
  118. }
  119. }
  120. })
  121. test("handles file inclusion substitution", async () => {
  122. await using tmp = await tmpdir({
  123. init: async (dir) => {
  124. await Bun.write(path.join(dir, "included.txt"), "test_theme")
  125. await Bun.write(
  126. path.join(dir, "opencode.json"),
  127. JSON.stringify({
  128. $schema: "https://opencode.ai/config.json",
  129. theme: "{file:included.txt}",
  130. }),
  131. )
  132. },
  133. })
  134. await Instance.provide({
  135. directory: tmp.path,
  136. fn: async () => {
  137. const config = await Config.get()
  138. expect(config.theme).toBe("test_theme")
  139. },
  140. })
  141. })
  142. test("validates config schema and throws on invalid fields", async () => {
  143. await using tmp = await tmpdir({
  144. init: async (dir) => {
  145. await Bun.write(
  146. path.join(dir, "opencode.json"),
  147. JSON.stringify({
  148. $schema: "https://opencode.ai/config.json",
  149. invalid_field: "should cause error",
  150. }),
  151. )
  152. },
  153. })
  154. await Instance.provide({
  155. directory: tmp.path,
  156. fn: async () => {
  157. // Strict schema should throw an error for invalid fields
  158. await expect(Config.get()).rejects.toThrow()
  159. },
  160. })
  161. })
  162. test("throws error for invalid JSON", async () => {
  163. await using tmp = await tmpdir({
  164. init: async (dir) => {
  165. await Bun.write(path.join(dir, "opencode.json"), "{ invalid json }")
  166. },
  167. })
  168. await Instance.provide({
  169. directory: tmp.path,
  170. fn: async () => {
  171. await expect(Config.get()).rejects.toThrow()
  172. },
  173. })
  174. })
  175. test("handles agent configuration", async () => {
  176. await using tmp = await tmpdir({
  177. init: async (dir) => {
  178. await Bun.write(
  179. path.join(dir, "opencode.json"),
  180. JSON.stringify({
  181. $schema: "https://opencode.ai/config.json",
  182. agent: {
  183. test_agent: {
  184. model: "test/model",
  185. temperature: 0.7,
  186. description: "test agent",
  187. },
  188. },
  189. }),
  190. )
  191. },
  192. })
  193. await Instance.provide({
  194. directory: tmp.path,
  195. fn: async () => {
  196. const config = await Config.get()
  197. expect(config.agent?.["test_agent"]).toEqual({
  198. model: "test/model",
  199. temperature: 0.7,
  200. description: "test agent",
  201. })
  202. },
  203. })
  204. })
  205. test("handles command configuration", async () => {
  206. await using tmp = await tmpdir({
  207. init: async (dir) => {
  208. await Bun.write(
  209. path.join(dir, "opencode.json"),
  210. JSON.stringify({
  211. $schema: "https://opencode.ai/config.json",
  212. command: {
  213. test_command: {
  214. template: "test template",
  215. description: "test command",
  216. agent: "test_agent",
  217. },
  218. },
  219. }),
  220. )
  221. },
  222. })
  223. await Instance.provide({
  224. directory: tmp.path,
  225. fn: async () => {
  226. const config = await Config.get()
  227. expect(config.command?.["test_command"]).toEqual({
  228. template: "test template",
  229. description: "test command",
  230. agent: "test_agent",
  231. })
  232. },
  233. })
  234. })
  235. test("migrates autoshare to share field", async () => {
  236. await using tmp = await tmpdir({
  237. init: async (dir) => {
  238. await Bun.write(
  239. path.join(dir, "opencode.json"),
  240. JSON.stringify({
  241. $schema: "https://opencode.ai/config.json",
  242. autoshare: true,
  243. }),
  244. )
  245. },
  246. })
  247. await Instance.provide({
  248. directory: tmp.path,
  249. fn: async () => {
  250. const config = await Config.get()
  251. expect(config.share).toBe("auto")
  252. expect(config.autoshare).toBe(true)
  253. },
  254. })
  255. })
  256. test("migrates mode field to agent field", async () => {
  257. await using tmp = await tmpdir({
  258. init: async (dir) => {
  259. await Bun.write(
  260. path.join(dir, "opencode.json"),
  261. JSON.stringify({
  262. $schema: "https://opencode.ai/config.json",
  263. mode: {
  264. test_mode: {
  265. model: "test/model",
  266. temperature: 0.5,
  267. },
  268. },
  269. }),
  270. )
  271. },
  272. })
  273. await Instance.provide({
  274. directory: tmp.path,
  275. fn: async () => {
  276. const config = await Config.get()
  277. expect(config.agent?.["test_mode"]).toEqual({
  278. model: "test/model",
  279. temperature: 0.5,
  280. mode: "primary",
  281. })
  282. },
  283. })
  284. })
  285. test("loads config from .opencode directory", async () => {
  286. await using tmp = await tmpdir({
  287. init: async (dir) => {
  288. const opencodeDir = path.join(dir, ".opencode")
  289. await fs.mkdir(opencodeDir, { recursive: true })
  290. const agentDir = path.join(opencodeDir, "agent")
  291. await fs.mkdir(agentDir, { recursive: true })
  292. await Bun.write(
  293. path.join(agentDir, "test.md"),
  294. `---
  295. model: test/model
  296. ---
  297. Test agent prompt`,
  298. )
  299. },
  300. })
  301. await Instance.provide({
  302. directory: tmp.path,
  303. fn: async () => {
  304. const config = await Config.get()
  305. expect(config.agent?.["test"]).toEqual({
  306. name: "test",
  307. model: "test/model",
  308. prompt: "Test agent prompt",
  309. })
  310. },
  311. })
  312. })
  313. test("updates config and writes to file", async () => {
  314. await using tmp = await tmpdir()
  315. await Instance.provide({
  316. directory: tmp.path,
  317. fn: async () => {
  318. const newConfig = { model: "updated/model" }
  319. await Config.update(newConfig as any)
  320. const writtenConfig = JSON.parse(await Bun.file(path.join(tmp.path, "config.json")).text())
  321. expect(writtenConfig.model).toBe("updated/model")
  322. },
  323. })
  324. })
  325. test("gets config directories", async () => {
  326. await using tmp = await tmpdir()
  327. await Instance.provide({
  328. directory: tmp.path,
  329. fn: async () => {
  330. const dirs = await Config.directories()
  331. expect(dirs.length).toBeGreaterThanOrEqual(1)
  332. },
  333. })
  334. })