plugin-discovery.test.ts 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import { mkdir, writeFile } from "node:fs/promises"
  2. import path from "node:path"
  3. import { pathToFileURL } from "node:url"
  4. import { expect, test } from "bun:test"
  5. import { discoverTuiPlugins, freshSpecifier, tuiPluginDirectories } from "../src/plugin/discovery"
  6. import { localProjectDirectory } from "../src/util/config-directories"
  7. import { tmpdir } from "./fixture/fixture"
  8. test("discovers project TUI plugin files in stable order", async () => {
  9. await using tmp = await tmpdir()
  10. const directory = path.join(tmp.path, ".opencode", "plugins", "tui")
  11. await mkdir(path.join(directory, "nested"), { recursive: true })
  12. await Promise.all([
  13. writeFile(path.join(directory, "second.tsx"), "export default {}"),
  14. writeFile(path.join(directory, "first.js"), "export default {}"),
  15. writeFile(path.join(directory, "ignored.json"), "{}"),
  16. writeFile(path.join(directory, "nested", "ignored.ts"), "export default {}"),
  17. ])
  18. expect(await discoverTuiPlugins(await tuiPluginDirectories(tmp.path, path.join(tmp.path, "config")))).toEqual([
  19. path.join(directory, "first.js"),
  20. path.join(directory, "second.tsx"),
  21. ])
  22. })
  23. test("returns no project TUI plugins when the directory is absent", async () => {
  24. await using tmp = await tmpdir()
  25. const roots = await tuiPluginDirectories(tmp.path, path.join(tmp.path, "config"))
  26. expect(await discoverTuiPlugins(roots)).toEqual([])
  27. expect(roots).toContain(path.join(tmp.path, ".opencode", "plugins", "tui"))
  28. })
  29. test("discovers global and ancestor plugin roots in precedence order", async () => {
  30. await using tmp = await tmpdir()
  31. const cwd = path.join(tmp.path, "repo", "packages", "app")
  32. const project = path.join(tmp.path, "repo")
  33. const config = path.join(tmp.path, "config")
  34. const directories = [
  35. path.join(config, "plugins", "tui"),
  36. path.join(tmp.path, "repo", ".opencode", "plugins", "tui"),
  37. path.join(tmp.path, "repo", "packages", ".opencode", "plugins", "tui"),
  38. ]
  39. const outside = path.join(tmp.path, ".opencode", "plugins", "tui")
  40. await mkdir(path.join(project, ".git"), { recursive: true })
  41. await Promise.all([...directories, outside].map((directory) => mkdir(directory, { recursive: true })))
  42. await Promise.all(
  43. directories.map((directory, index) => writeFile(path.join(directory, `${index}.ts`), "export default {}")),
  44. )
  45. await writeFile(path.join(outside, "outside.ts"), "export default {}")
  46. const roots = await tuiPluginDirectories(cwd, config)
  47. expect(await discoverTuiPlugins(roots)).toEqual(
  48. directories.map((directory, index) => path.join(directory, `${index}.ts`)),
  49. )
  50. expect(roots).not.toContain(path.join(cwd, ".opencode", "plugins", "tui"))
  51. expect(roots).not.toContain(outside)
  52. })
  53. test("uses an Hg root for a missing project plugin directory", async () => {
  54. await using tmp = await tmpdir()
  55. const project = path.join(tmp.path, "repo")
  56. const cwd = path.join(project, "package")
  57. await mkdir(path.join(project, ".hg"), { recursive: true })
  58. await mkdir(cwd, { recursive: true })
  59. expect(await tuiPluginDirectories(cwd, path.join(tmp.path, "config"))).toContain(
  60. path.join(project, ".opencode", "plugins", "tui"),
  61. )
  62. })
  63. test("truncates fractional mtimes in fresh specifiers", () => {
  64. // A dot in the query makes Bun's compiled binaries skip runtime plugin
  65. // hooks for the import, breaking JSX/solid rewriting for external plugins.
  66. const entrypoint = pathToFileURL(path.resolve("example.tsx")).href
  67. const specifier = freshSpecifier(entrypoint, 1786494961337.0317)
  68. expect(specifier.endsWith("example.tsx?mtime=1786494961337")).toBe(true)
  69. })
  70. test("propagates non-missing filesystem errors", async () => {
  71. await expect(localProjectDirectory("\0")).rejects.toBeInstanceOf(Error)
  72. await expect(discoverTuiPlugins(["\0"])).rejects.toBeInstanceOf(Error)
  73. })