plugin-toggle.test.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import { expect, spyOn, test } from "bun:test"
  2. import fs from "fs/promises"
  3. import path from "path"
  4. import { pathToFileURL } from "url"
  5. import { tmpdir } from "../../fixture/fixture"
  6. import { createTuiPluginApi } from "../../fixture/tui-plugin"
  7. import { createTuiResolvedConfig } from "../../fixture/tui-runtime"
  8. import { TuiConfig } from "../../../src/config/tui"
  9. const { TuiPluginRuntime } = await import("../../../src/plugin/tui/runtime")
  10. test("toggles plugin runtime state by exported id", async () => {
  11. await using tmp = await tmpdir({
  12. init: async (dir) => {
  13. const file = path.join(dir, "toggle-plugin.ts")
  14. const spec = pathToFileURL(file).href
  15. const marker = path.join(dir, "toggle.txt")
  16. await Bun.write(
  17. file,
  18. `export default {
  19. id: "demo.toggle",
  20. tui: async (api, options) => {
  21. const text = await Bun.file(options.marker).text().catch(() => "")
  22. await Bun.write(options.marker, text + "start\\n")
  23. api.lifecycle.onDispose(async () => {
  24. const next = await Bun.file(options.marker).text().catch(() => "")
  25. await Bun.write(options.marker, next + "stop\\n")
  26. })
  27. },
  28. }
  29. `,
  30. )
  31. return {
  32. spec,
  33. marker,
  34. }
  35. },
  36. })
  37. process.env.KIRINCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
  38. const config = createTuiResolvedConfig({
  39. plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
  40. plugin_enabled: {
  41. "demo.toggle": false,
  42. },
  43. plugin_origins: [
  44. {
  45. spec: [tmp.extra.spec, { marker: tmp.extra.marker }],
  46. scope: "local",
  47. source: path.join(tmp.path, "tui.json"),
  48. },
  49. ],
  50. })
  51. const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
  52. const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
  53. const api = createTuiPluginApi()
  54. try {
  55. await TuiPluginRuntime.init({ api, config })
  56. await expect(fs.readFile(tmp.extra.marker, "utf8")).rejects.toThrow()
  57. expect(TuiPluginRuntime.list().find((item) => item.id === "demo.toggle")).toEqual({
  58. id: "demo.toggle",
  59. source: "file",
  60. spec: tmp.extra.spec,
  61. target: tmp.extra.spec,
  62. enabled: false,
  63. active: false,
  64. })
  65. await expect(TuiPluginRuntime.activatePlugin("demo.toggle")).resolves.toBe(true)
  66. await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("start\n")
  67. expect(api.kv.get("plugin_enabled", {})).toEqual({
  68. "demo.toggle": true,
  69. })
  70. await expect(TuiPluginRuntime.deactivatePlugin("demo.toggle")).resolves.toBe(true)
  71. await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("start\nstop\n")
  72. expect(api.kv.get("plugin_enabled", {})).toEqual({
  73. "demo.toggle": false,
  74. })
  75. await expect(TuiPluginRuntime.activatePlugin("missing.id")).resolves.toBe(false)
  76. } finally {
  77. await TuiPluginRuntime.dispose()
  78. cwd.mockRestore()
  79. wait.mockRestore()
  80. delete process.env.KIRINCODE_PLUGIN_META_FILE
  81. }
  82. })
  83. test("deactivating plugin pops pushed mode", async () => {
  84. await using tmp = await tmpdir({
  85. init: async (dir) => {
  86. const file = path.join(dir, "mode-plugin.ts")
  87. const spec = pathToFileURL(file).href
  88. await Bun.write(
  89. file,
  90. `export default {
  91. id: "demo.mode",
  92. tui: async (api) => {
  93. api.mode.push("demo.mode")
  94. },
  95. }
  96. `,
  97. )
  98. return { spec }
  99. },
  100. })
  101. const stack: { id: symbol; mode: string }[] = []
  102. let popCount = 0
  103. const api = createTuiPluginApi({
  104. mode: {
  105. current: () => stack.at(-1)?.mode ?? "base",
  106. push(mode) {
  107. const id = Symbol(mode)
  108. let active = true
  109. stack.push({ id, mode })
  110. return () => {
  111. if (!active) return
  112. active = false
  113. popCount += 1
  114. const index = stack.findIndex((item) => item.id === id)
  115. if (index !== -1) stack.splice(index, 1)
  116. }
  117. },
  118. },
  119. })
  120. const config = createTuiResolvedConfig({
  121. plugin: [tmp.extra.spec],
  122. plugin_origins: [{ spec: tmp.extra.spec, scope: "local", source: path.join(tmp.path, "tui.json") }],
  123. })
  124. const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
  125. const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
  126. try {
  127. await TuiPluginRuntime.init({ api, config })
  128. expect(api.mode.current()).toBe("demo.mode")
  129. expect(popCount).toBe(0)
  130. await expect(TuiPluginRuntime.deactivatePlugin("demo.mode")).resolves.toBe(true)
  131. expect(api.mode.current()).toBe("base")
  132. expect(popCount).toBe(1)
  133. } finally {
  134. await TuiPluginRuntime.dispose()
  135. cwd.mockRestore()
  136. wait.mockRestore()
  137. }
  138. })
  139. test("kv plugin_enabled overrides tui config on startup", async () => {
  140. await using tmp = await tmpdir({
  141. init: async (dir) => {
  142. const file = path.join(dir, "startup-plugin.ts")
  143. const spec = pathToFileURL(file).href
  144. const marker = path.join(dir, "startup.txt")
  145. await Bun.write(
  146. file,
  147. `export default {
  148. id: "demo.startup",
  149. tui: async (_api, options) => {
  150. await Bun.write(options.marker, "on")
  151. },
  152. }
  153. `,
  154. )
  155. return {
  156. spec,
  157. marker,
  158. }
  159. },
  160. })
  161. process.env.KIRINCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
  162. const config = createTuiResolvedConfig({
  163. plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
  164. plugin_enabled: {
  165. "demo.startup": false,
  166. },
  167. plugin_origins: [
  168. {
  169. spec: [tmp.extra.spec, { marker: tmp.extra.marker }],
  170. scope: "local",
  171. source: path.join(tmp.path, "tui.json"),
  172. },
  173. ],
  174. })
  175. const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
  176. const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
  177. const api = createTuiPluginApi()
  178. api.kv.set("plugin_enabled", {
  179. "demo.startup": true,
  180. })
  181. try {
  182. await TuiPluginRuntime.init({ api, config })
  183. await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("on")
  184. expect(TuiPluginRuntime.list().find((item) => item.id === "demo.startup")).toEqual({
  185. id: "demo.startup",
  186. source: "file",
  187. spec: tmp.extra.spec,
  188. target: tmp.extra.spec,
  189. enabled: true,
  190. active: true,
  191. })
  192. } finally {
  193. await TuiPluginRuntime.dispose()
  194. cwd.mockRestore()
  195. wait.mockRestore()
  196. delete process.env.KIRINCODE_PLUGIN_META_FILE
  197. }
  198. })
  199. test("loads disabled-by-default internal plugin inactive and activates on demand", async () => {
  200. await using tmp = await tmpdir()
  201. const config = createTuiResolvedConfig()
  202. const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
  203. const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
  204. const api = createTuiPluginApi()
  205. try {
  206. await TuiPluginRuntime.init({ api, config })
  207. expect(TuiPluginRuntime.list().find((item) => item.id === "internal:plugin-manager")).toMatchObject({
  208. enabled: true,
  209. active: true,
  210. })
  211. expect(TuiPluginRuntime.list().find((item) => item.id === "which-key")).toEqual({
  212. id: "which-key",
  213. source: "internal",
  214. spec: "which-key",
  215. target: "which-key",
  216. enabled: false,
  217. active: false,
  218. })
  219. await expect(TuiPluginRuntime.activatePlugin("which-key")).resolves.toBe(true)
  220. expect(TuiPluginRuntime.list().find((item) => item.id === "which-key")).toEqual({
  221. id: "which-key",
  222. source: "internal",
  223. spec: "which-key",
  224. target: "which-key",
  225. enabled: true,
  226. active: true,
  227. })
  228. expect(api.kv.get("plugin_enabled", {})).toEqual({
  229. "which-key": true,
  230. })
  231. } finally {
  232. await TuiPluginRuntime.dispose()
  233. cwd.mockRestore()
  234. wait.mockRestore()
  235. }
  236. })