service.test.ts 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. import { NodeFileSystem } from "@effect/platform-node"
  2. import { Service } from "@opencode-ai/client/effect"
  3. import { Global } from "@opencode-ai/core/global"
  4. import { expect, test } from "bun:test"
  5. import { Effect, Schema } from "effect"
  6. import fs from "node:fs/promises"
  7. import os from "node:os"
  8. import path from "node:path"
  9. import { ServiceConfig } from "../src/services/service-config"
  10. test("local channel stores service config with the local service filename", async () => {
  11. const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-"))
  12. try {
  13. await Effect.runPromise(
  14. ServiceConfig.set("hostname", "127.0.0.2").pipe(
  15. Effect.provide(Global.layerWith({ config: path.join(root, "config"), state: path.join(root, "state") })),
  16. Effect.provide(NodeFileSystem.layer),
  17. ),
  18. )
  19. expect(await Bun.file(path.join(root, "config", "service-local.json")).json()).toEqual({
  20. hostname: "127.0.0.2",
  21. })
  22. expect(await Bun.file(path.join(root, "config", "service.json")).exists()).toBe(false)
  23. } finally {
  24. await fs.rm(root, { recursive: true, force: true })
  25. }
  26. })
  27. test("concurrent service processes elect one server", async () => {
  28. const root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-service-election-"))
  29. const env = {
  30. ...process.env,
  31. HOME: root,
  32. OPENCODE_DB: path.join(root, "opencode.db"),
  33. OPENCODE_TEST_HOME: root,
  34. XDG_CACHE_HOME: path.join(root, "cache"),
  35. XDG_CONFIG_HOME: path.join(root, "config"),
  36. XDG_DATA_HOME: path.join(root, "data"),
  37. XDG_STATE_HOME: path.join(root, "state"),
  38. }
  39. const command = [process.execPath, path.join(import.meta.dir, "../src/index.ts"), "serve", "--service"]
  40. const first = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })
  41. const second = Bun.spawn(command, { env, stderr: "pipe", stdout: "ignore" })
  42. try {
  43. const registration = path.join(root, "state", "opencode", "service-local.json")
  44. const info = await waitForInfo(registration)
  45. const winner = info.pid === first.pid ? first : second
  46. const loser = info.pid === first.pid ? second : first
  47. const exited = await Promise.race([loser.exited.then(() => true), Bun.sleep(10_000).then(() => false)])
  48. expect(exited).toBe(true)
  49. expect(winner.exitCode).toBe(null)
  50. } finally {
  51. first.kill("SIGTERM")
  52. second.kill("SIGTERM")
  53. await Promise.all([first.exited, second.exited])
  54. await fs.rm(root, { recursive: true, force: true })
  55. }
  56. })
  57. async function waitForInfo(file: string) {
  58. for (let attempt = 0; attempt < 200; attempt++) {
  59. const value = await Bun.file(file)
  60. .json()
  61. .catch(() => undefined)
  62. if (value !== undefined) return Schema.decodeUnknownPromise(Service.Info)(value)
  63. await Bun.sleep(50)
  64. }
  65. throw new Error("Timed out waiting for service registration")
  66. }