httpapi-mdns.test.ts 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. import { afterEach, describe, expect, mock, test } from "bun:test"
  2. import { Flag } from "@opencode-ai/core/flag/flag"
  3. import * as Log from "@opencode-ai/core/util/log"
  4. import { withTimeout } from "../../src/util/timeout"
  5. import { resetDatabase } from "../fixture/db"
  6. import { disposeAllInstances } from "../fixture/fixture"
  7. void Log.init({ print: false })
  8. type Event = { kind: "publish"; port: number; name: string } | { kind: "unpublishAll" } | { kind: "destroy" }
  9. const events: Event[] = []
  10. void mock.module("bonjour-service", () => ({
  11. Bonjour: class {
  12. publish(opts: { port: number; name: string }) {
  13. events.push({ kind: "publish", port: opts.port, name: opts.name })
  14. return { on: () => {} }
  15. }
  16. unpublishAll() {
  17. events.push({ kind: "unpublishAll" })
  18. }
  19. destroy() {
  20. events.push({ kind: "destroy" })
  21. }
  22. },
  23. }))
  24. // Import Server AFTER the mock so the MDNS module picks up the stub.
  25. const { Server } = await import("../../src/server/server")
  26. const original = {
  27. OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
  28. OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
  29. }
  30. afterEach(async () => {
  31. events.length = 0
  32. Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
  33. Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
  34. await disposeAllInstances()
  35. await resetDatabase()
  36. })
  37. describe("HttpApi Server.listen mDNS", () => {
  38. test("skips publish for loopback hostnames", async () => {
  39. Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
  40. Flag.OPENCODE_SERVER_USERNAME = "opencode"
  41. const listener = await Server.listen({ hostname: "127.0.0.1", port: 0, mdns: true })
  42. try {
  43. expect(events.filter((e) => e.kind === "publish")).toEqual([])
  44. } finally {
  45. await withTimeout(listener.stop(true), 10_000, "timed out stopping loopback mdns listener")
  46. }
  47. expect(events.filter((e) => e.kind === "publish")).toEqual([])
  48. })
  49. test("publishes for non-loopback hostnames and unpublishes on stop", async () => {
  50. Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
  51. Flag.OPENCODE_SERVER_USERNAME = "opencode"
  52. const listener = await Server.listen({ hostname: "0.0.0.0", port: 0, mdns: true })
  53. try {
  54. const published = events.filter((e) => e.kind === "publish")
  55. expect(published.length).toBe(1)
  56. expect(published[0]!.port).toBe(listener.port)
  57. expect(published[0]!.name).toBe(`opencode-${listener.port}`)
  58. } finally {
  59. await withTimeout(listener.stop(true), 10_000, "timed out stopping mdns listener")
  60. }
  61. expect(events.some((e) => e.kind === "unpublishAll")).toBe(true)
  62. expect(events.some((e) => e.kind === "destroy")).toBe(true)
  63. })
  64. test("scope finalizer unpublishes even if stop() is not called for force-close", async () => {
  65. Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
  66. Flag.OPENCODE_SERVER_USERNAME = "opencode"
  67. const listener = await Server.listen({ hostname: "0.0.0.0", port: 0, mdns: true })
  68. expect(events.filter((e) => e.kind === "publish").length).toBe(1)
  69. // Plain (graceful) stop without close=true should still unpublish.
  70. await withTimeout(listener.stop(), 10_000, "timed out stopping graceful mdns listener")
  71. expect(events.some((e) => e.kind === "unpublishAll")).toBe(true)
  72. })
  73. })