runtime-flags.test.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import { describe, expect } from "bun:test"
  2. import { ConfigProvider, Effect, Layer } from "effect"
  3. import { AppNodeBuilder } from "@kirincode-ai/core/effect/app-node-builder"
  4. import { RuntimeFlags } from "../../src/effect/runtime-flags"
  5. import { it } from "../lib/effect"
  6. const fromConfig = (input: Record<string, unknown>) =>
  7. AppNodeBuilder.build(RuntimeFlags.node).pipe(Layer.provide(ConfigProvider.layer(ConfigProvider.fromUnknown(input))))
  8. const readFlags = RuntimeFlags.Service.useSync((flags) => flags)
  9. describe("RuntimeFlags", () => {
  10. it.effect("layer defaults autoShare to false", () =>
  11. Effect.gen(function* () {
  12. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
  13. expect(flags.autoShare).toBe(false)
  14. }),
  15. )
  16. it.effect("layer parses plugin flags from the active ConfigProvider", () =>
  17. Effect.gen(function* () {
  18. const flags = yield* readFlags.pipe(
  19. Effect.provide(
  20. fromConfig({
  21. KIRINCODE_PURE: "true",
  22. KIRINCODE_DISABLE_DEFAULT_PLUGINS: "true",
  23. KIRINCODE_AUTO_SHARE: "true",
  24. KIRINCODE_DISABLE_EMBEDDED_WEB_UI: "true",
  25. KIRINCODE_DISABLE_EXTERNAL_SKILLS: "true",
  26. KIRINCODE_DISABLE_LSP_DOWNLOAD: "true",
  27. KIRINCODE_EXPERIMENTAL: "true",
  28. KIRINCODE_ENABLE_EXA: "true",
  29. KIRINCODE_ENABLE_PARALLEL: "true",
  30. KIRINCODE_ENABLE_EXPERIMENTAL_MODELS: "true",
  31. KIRINCODE_ENABLE_QUESTION_TOOL: "true",
  32. KIRINCODE_CLIENT: "desktop",
  33. }),
  34. ),
  35. )
  36. expect(flags.pure).toBe(true)
  37. expect(flags.autoShare).toBe(true)
  38. expect(flags.disableDefaultPlugins).toBe(true)
  39. expect(flags.disableEmbeddedWebUi).toBe(true)
  40. expect(flags.disableExternalSkills).toBe(true)
  41. expect(flags.disableLspDownload).toBe(true)
  42. expect(flags.disableClaudeCodePrompt).toBe(false)
  43. expect(flags.enableExa).toBe(true)
  44. expect(flags.enableParallel).toBe(true)
  45. expect(flags.enableExperimentalModels).toBe(true)
  46. expect(flags.enableQuestionTool).toBe(true)
  47. expect(flags.experimentalReferences).toBe(true)
  48. expect(flags.experimentalBackgroundSubagents).toBe(true)
  49. expect(flags.experimentalLspTy).toBe(false)
  50. expect(flags.experimentalLspTool).toBe(true)
  51. expect(flags.experimentalOxfmt).toBe(true)
  52. expect(flags.experimentalPlanMode).toBe(true)
  53. expect(flags.experimentalEventSystem).toBe(true)
  54. expect(flags.experimentalWorkspaces).toBe(true)
  55. expect(flags.experimentalIconDiscovery).toBe(true)
  56. expect(flags.experimentalNativeLlm).toBe(false)
  57. expect(flags.experimentalWebSockets).toBe(false)
  58. expect(flags.client).toBe("desktop")
  59. }),
  60. )
  61. it.effect("layer parses KIRINCODE_EXPERIMENTAL_LSP_TY", () =>
  62. Effect.gen(function* () {
  63. const flags = yield* readFlags.pipe(
  64. Effect.provide(
  65. fromConfig({
  66. KIRINCODE_EXPERIMENTAL_LSP_TY: "true",
  67. }),
  68. ),
  69. )
  70. expect(flags.experimentalLspTy).toBe(true)
  71. }),
  72. )
  73. it.effect("enables native LLM via dedicated flag only", () =>
  74. Effect.gen(function* () {
  75. const explicit = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_EXPERIMENTAL_NATIVE_LLM: "true" })))
  76. const umbrella = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_EXPERIMENTAL: "true" })))
  77. expect(explicit.experimentalNativeLlm).toBe(true)
  78. expect(umbrella.experimentalNativeLlm).toBe(false)
  79. }),
  80. )
  81. it.effect("enables WebSockets via dedicated flag only", () =>
  82. Effect.gen(function* () {
  83. const explicit = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_EXPERIMENTAL_WEBSOCKETS: "true" })))
  84. const umbrella = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_EXPERIMENTAL: "true" })))
  85. expect(explicit.experimentalWebSockets).toBe(true)
  86. expect(umbrella.experimentalWebSockets).toBe(false)
  87. }),
  88. )
  89. it.effect("layer accepts partial test overrides and fills defaults from Config definitions", () =>
  90. Effect.gen(function* () {
  91. const flags = yield* readFlags.pipe(
  92. Effect.provide(RuntimeFlags.layer({ disableDefaultPlugins: true, bashDefaultTimeoutMs: 1_000 })),
  93. )
  94. expect(flags.pure).toBe(false)
  95. expect(flags.autoShare).toBe(false)
  96. expect(flags.disableDefaultPlugins).toBe(true)
  97. expect(flags.disableEmbeddedWebUi).toBe(false)
  98. expect(flags.disableExternalSkills).toBe(false)
  99. expect(flags.disableLspDownload).toBe(false)
  100. expect(flags.disableClaudeCodePrompt).toBe(false)
  101. expect(flags.disableClaudeCodeSkills).toBe(false)
  102. expect(flags.enableExa).toBe(false)
  103. expect(flags.experimentalIconDiscovery).toBe(false)
  104. expect(flags.experimentalOxfmt).toBe(false)
  105. expect(flags.outputTokenMax).toBeUndefined()
  106. expect(flags.bashDefaultTimeoutMs).toBe(1_000)
  107. expect(flags.enableExperimentalModels).toBe(false)
  108. expect(flags.client).toBe("cli")
  109. }),
  110. )
  111. it.effect("experimentalIconDiscovery defaults to false", () =>
  112. Effect.gen(function* () {
  113. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
  114. expect(flags.experimentalIconDiscovery).toBe(false)
  115. }),
  116. )
  117. it.effect("disableExternalSkills defaults to false", () =>
  118. Effect.gen(function* () {
  119. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
  120. expect(flags.disableExternalSkills).toBe(false)
  121. }),
  122. )
  123. it.effect("disableExternalSkills reads KIRINCODE_DISABLE_EXTERNAL_SKILLS", () =>
  124. Effect.gen(function* () {
  125. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_DISABLE_EXTERNAL_SKILLS: "true" })))
  126. expect(flags.disableExternalSkills).toBe(true)
  127. }),
  128. )
  129. it.effect("disableLspDownload defaults to false", () =>
  130. Effect.gen(function* () {
  131. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
  132. expect(flags.disableLspDownload).toBe(false)
  133. }),
  134. )
  135. it.effect("disableLspDownload reads KIRINCODE_DISABLE_LSP_DOWNLOAD", () =>
  136. Effect.gen(function* () {
  137. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_DISABLE_LSP_DOWNLOAD: "true" })))
  138. expect(flags.disableLspDownload).toBe(true)
  139. }),
  140. )
  141. it.effect("disableClaudeCodePrompt defaults to false", () =>
  142. Effect.gen(function* () {
  143. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
  144. expect(flags.disableClaudeCodePrompt).toBe(false)
  145. }),
  146. )
  147. it.effect("disableClaudeCodePrompt reads KIRINCODE_DISABLE_CLAUDE_CODE_PROMPT", () =>
  148. Effect.gen(function* () {
  149. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_DISABLE_CLAUDE_CODE_PROMPT: "true" })))
  150. expect(flags.disableClaudeCodePrompt).toBe(true)
  151. }),
  152. )
  153. it.effect("disableClaudeCodePrompt inherits KIRINCODE_DISABLE_CLAUDE_CODE", () =>
  154. Effect.gen(function* () {
  155. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_DISABLE_CLAUDE_CODE: "true" })))
  156. expect(flags.disableClaudeCodePrompt).toBe(true)
  157. }),
  158. )
  159. it.effect("experimentalIconDiscovery reads KIRINCODE_EXPERIMENTAL_ICON_DISCOVERY", () =>
  160. Effect.gen(function* () {
  161. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_EXPERIMENTAL_ICON_DISCOVERY: "true" })))
  162. expect(flags.experimentalIconDiscovery).toBe(true)
  163. }),
  164. )
  165. it.effect("experimentalIconDiscovery inherits KIRINCODE_EXPERIMENTAL", () =>
  166. Effect.gen(function* () {
  167. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_EXPERIMENTAL: "true" })))
  168. expect(flags.experimentalIconDiscovery).toBe(true)
  169. }),
  170. )
  171. it.effect("specific experimental flags override KIRINCODE_EXPERIMENTAL", () =>
  172. Effect.gen(function* () {
  173. const flags = yield* readFlags.pipe(
  174. Effect.provide(
  175. fromConfig({
  176. KIRINCODE_EXPERIMENTAL: "true",
  177. KIRINCODE_EXPERIMENTAL_ICON_DISCOVERY: "false",
  178. }),
  179. ),
  180. )
  181. expect(flags.experimentalIconDiscovery).toBe(false)
  182. }),
  183. )
  184. it.effect("experimentalOxfmt defaults to false", () =>
  185. Effect.gen(function* () {
  186. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
  187. expect(flags.experimentalOxfmt).toBe(false)
  188. }),
  189. )
  190. it.effect("experimentalOxfmt is enabled by KIRINCODE_EXPERIMENTAL_OXFMT", () =>
  191. Effect.gen(function* () {
  192. const flags = yield* readFlags.pipe(
  193. Effect.provide(
  194. fromConfig({
  195. KIRINCODE_EXPERIMENTAL_OXFMT: "true",
  196. }),
  197. ),
  198. )
  199. expect(flags.experimentalOxfmt).toBe(true)
  200. }),
  201. )
  202. it.effect("experimentalOxfmt inherits KIRINCODE_EXPERIMENTAL", () =>
  203. Effect.gen(function* () {
  204. const flags = yield* readFlags.pipe(
  205. Effect.provide(
  206. fromConfig({
  207. KIRINCODE_EXPERIMENTAL: "true",
  208. }),
  209. ),
  210. )
  211. expect(flags.experimentalOxfmt).toBe(true)
  212. }),
  213. )
  214. for (const input of [
  215. { name: "absent", config: {}, expected: undefined },
  216. {
  217. name: "valid positive integer",
  218. config: { KIRINCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "1234" },
  219. expected: 1234,
  220. },
  221. {
  222. name: "invalid string",
  223. config: { KIRINCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "nope" },
  224. expected: undefined,
  225. },
  226. { name: "zero", config: { KIRINCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "0" }, expected: undefined },
  227. { name: "negative", config: { KIRINCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "-1" }, expected: undefined },
  228. {
  229. name: "non-integer",
  230. config: { KIRINCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "1.5" },
  231. expected: undefined,
  232. },
  233. ]) {
  234. it.effect(`parses bashDefaultTimeoutMs from config: ${input.name}`, () =>
  235. Effect.gen(function* () {
  236. const flags = yield* readFlags.pipe(Effect.provide(fromConfig(input.config)))
  237. expect(flags.bashDefaultTimeoutMs).toBe(input.expected)
  238. }),
  239. )
  240. }
  241. for (const input of [
  242. { name: "absent", config: {}, expected: undefined },
  243. {
  244. name: "valid positive integer",
  245. config: { KIRINCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "1234" },
  246. expected: 1234,
  247. },
  248. {
  249. name: "invalid string",
  250. config: { KIRINCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "nope" },
  251. expected: undefined,
  252. },
  253. { name: "zero", config: { KIRINCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "0" }, expected: undefined },
  254. { name: "negative", config: { KIRINCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "-1" }, expected: undefined },
  255. {
  256. name: "non-integer",
  257. config: { KIRINCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX: "1.5" },
  258. expected: undefined,
  259. },
  260. ]) {
  261. it.effect(`parses outputTokenMax from config: ${input.name}`, () =>
  262. Effect.gen(function* () {
  263. const flags = yield* readFlags.pipe(Effect.provide(fromConfig(input.config)))
  264. expect(flags.outputTokenMax).toBe(input.expected)
  265. }),
  266. )
  267. }
  268. it.effect("layer ignores the active ConfigProvider for omitted test overrides", () =>
  269. Effect.gen(function* () {
  270. const flags = yield* readFlags.pipe(
  271. Effect.provide(RuntimeFlags.layer()),
  272. Effect.provide(
  273. ConfigProvider.layer(
  274. ConfigProvider.fromUnknown({
  275. KIRINCODE_PURE: "true",
  276. KIRINCODE_DISABLE_DEFAULT_PLUGINS: "true",
  277. KIRINCODE_DISABLE_EXTERNAL_SKILLS: "true",
  278. KIRINCODE_DISABLE_LSP_DOWNLOAD: "true",
  279. KIRINCODE_EXPERIMENTAL: "true",
  280. KIRINCODE_ENABLE_EXA: "true",
  281. KIRINCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS: "1234",
  282. KIRINCODE_CLIENT: "desktop",
  283. }),
  284. ),
  285. ),
  286. )
  287. expect(flags.pure).toBe(false)
  288. expect(flags.disableDefaultPlugins).toBe(false)
  289. expect(flags.disableEmbeddedWebUi).toBe(false)
  290. expect(flags.disableExternalSkills).toBe(false)
  291. expect(flags.disableLspDownload).toBe(false)
  292. expect(flags.disableClaudeCodePrompt).toBe(false)
  293. expect(flags.disableClaudeCodeSkills).toBe(false)
  294. expect(flags.enableExa).toBe(false)
  295. expect(flags.experimentalIconDiscovery).toBe(false)
  296. expect(flags.experimentalOxfmt).toBe(false)
  297. expect(flags.outputTokenMax).toBeUndefined()
  298. expect(flags.bashDefaultTimeoutMs).toBeUndefined()
  299. expect(flags.client).toBe("cli")
  300. }),
  301. )
  302. it.effect("disableClaudeCodeSkills defaults to false", () =>
  303. Effect.gen(function* () {
  304. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({})))
  305. expect(flags.disableClaudeCodeSkills).toBe(false)
  306. }),
  307. )
  308. it.effect("disableClaudeCodeSkills reads KIRINCODE_DISABLE_CLAUDE_CODE_SKILLS", () =>
  309. Effect.gen(function* () {
  310. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_DISABLE_CLAUDE_CODE_SKILLS: "true" })))
  311. expect(flags.disableClaudeCodeSkills).toBe(true)
  312. }),
  313. )
  314. it.effect("disableClaudeCodeSkills inherits KIRINCODE_DISABLE_CLAUDE_CODE", () =>
  315. Effect.gen(function* () {
  316. const flags = yield* readFlags.pipe(Effect.provide(fromConfig({ KIRINCODE_DISABLE_CLAUDE_CODE: "true" })))
  317. expect(flags.disableClaudeCodeSkills).toBe(true)
  318. }),
  319. )
  320. })