install-concurrency.test.ts 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import { describe, expect, test } from "bun:test"
  2. import fs from "fs/promises"
  3. import path from "path"
  4. import { Process } from "../../src/util/process"
  5. import { Filesystem } from "../../src/util/filesystem"
  6. import { tmpdir } from "../fixture/fixture"
  7. const root = path.join(import.meta.dir, "../..")
  8. const worker = path.join(import.meta.dir, "../fixture/plug-worker.ts")
  9. type Msg = {
  10. dir: string
  11. target: string
  12. mod: string
  13. holdMs?: number
  14. }
  15. function run(msg: Msg) {
  16. return Process.run([process.execPath, worker, JSON.stringify(msg)], {
  17. cwd: root,
  18. nothrow: true,
  19. })
  20. }
  21. async function plugin(dir: string, kinds: Array<"server" | "tui">) {
  22. const p = path.join(dir, "plugin")
  23. const server = kinds.includes("server")
  24. const tui = kinds.includes("tui")
  25. const exports: Record<string, string> = {}
  26. if (server) exports["./server"] = "./server.js"
  27. if (tui) exports["./tui"] = "./tui.js"
  28. await fs.mkdir(p, { recursive: true })
  29. await Bun.write(
  30. path.join(p, "package.json"),
  31. JSON.stringify(
  32. {
  33. name: "acme",
  34. version: "1.0.0",
  35. ...(server ? { main: "./server.js" } : {}),
  36. ...(Object.keys(exports).length ? { exports } : {}),
  37. },
  38. null,
  39. 2,
  40. ),
  41. )
  42. return p
  43. }
  44. async function read(file: string) {
  45. return Filesystem.readJson<{ plugin?: unknown[] }>(file)
  46. }
  47. function mods(prefix: string, n: number) {
  48. return Array.from({ length: n }, (_, i) => `${prefix}-${i}@1.0.0`)
  49. }
  50. function expectPlugins(list: unknown[] | undefined, expectMods: string[]) {
  51. expect(Array.isArray(list)).toBe(true)
  52. const hit = (list ?? []).filter((item): item is string => typeof item === "string")
  53. expect(hit.length).toBe(expectMods.length)
  54. expect(new Set(hit)).toEqual(new Set(expectMods))
  55. }
  56. describe("plugin.install.concurrent", () => {
  57. test("serializes concurrent server config updates across processes", async () => {
  58. await using tmp = await tmpdir()
  59. const target = await plugin(tmp.path, ["server"])
  60. const all = mods("mod-server", 12)
  61. const out = await Promise.all(
  62. all.map((mod) =>
  63. run({
  64. dir: tmp.path,
  65. target,
  66. mod,
  67. holdMs: 30,
  68. }),
  69. ),
  70. )
  71. expect(out.map((x) => x.code)).toEqual(Array.from({ length: all.length }, () => 0))
  72. expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([])
  73. const cfg = await read(path.join(tmp.path, ".opencode", "opencode.jsonc"))
  74. expectPlugins(cfg.plugin, all)
  75. }, 25_000)
  76. test("serializes concurrent server+tui config updates across processes", async () => {
  77. await using tmp = await tmpdir()
  78. const target = await plugin(tmp.path, ["server", "tui"])
  79. const all = mods("mod-both", 10)
  80. const out = await Promise.all(
  81. all.map((mod) =>
  82. run({
  83. dir: tmp.path,
  84. target,
  85. mod,
  86. holdMs: 30,
  87. }),
  88. ),
  89. )
  90. expect(out.map((x) => x.code)).toEqual(Array.from({ length: all.length }, () => 0))
  91. expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([])
  92. const server = await read(path.join(tmp.path, ".opencode", "opencode.jsonc"))
  93. const tui = await read(path.join(tmp.path, ".opencode", "tui.jsonc"))
  94. expectPlugins(server.plugin, all)
  95. expectPlugins(tui.plugin, all)
  96. }, 25_000)
  97. test("preserves updates when existing config uses .json", async () => {
  98. await using tmp = await tmpdir()
  99. const target = await plugin(tmp.path, ["server"])
  100. const cfg = path.join(tmp.path, ".opencode", "opencode.json")
  101. await fs.mkdir(path.dirname(cfg), { recursive: true })
  102. await Bun.write(cfg, JSON.stringify({ plugin: ["[email protected]"] }, null, 2))
  103. const next = mods("mod-json", 8)
  104. const out = await Promise.all(
  105. next.map((mod) =>
  106. run({
  107. dir: tmp.path,
  108. target,
  109. mod,
  110. holdMs: 30,
  111. }),
  112. ),
  113. )
  114. expect(out.map((x) => x.code)).toEqual(Array.from({ length: next.length }, () => 0))
  115. expect(out.map((x) => x.stderr.toString()).filter(Boolean)).toEqual([])
  116. const json = await read(cfg)
  117. expectPlugins(json.plugin, ["[email protected]", ...next])
  118. expect(await Filesystem.exists(path.join(tmp.path, ".opencode", "opencode.jsonc"))).toBe(false)
  119. }, 25_000)
  120. })