httpapi-mdns.test.ts 3.1 KB

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