mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-18 10:07:58 +00:00
test(server): cover port fallback and mDNS lifecycle
Add the previously-untested edge cases: - port: 0 prefers 4096 when free - port: 0 falls back to a random port when 4096 is taken - mDNS is skipped on loopback hostnames - mDNS publishes on non-loopback hostnames and unpublishes on stop(true) - mDNS unpublishes on graceful stop() via the scope finalizer
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { afterEach, describe, expect, test } from "bun:test"
|
||||
import net from "node:net"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { Server } from "../../src/server/server"
|
||||
@@ -302,6 +303,32 @@ describe("HttpApi Server.listen", () => {
|
||||
expect(output).not.toContain("Sent HTTP response")
|
||||
})
|
||||
|
||||
test("port 0 prefers 4096 when free", async () => {
|
||||
if (!(await isPortFree(4096))) return
|
||||
const listener = await startListener()
|
||||
try {
|
||||
expect(listener.port).toBe(4096)
|
||||
} finally {
|
||||
await stop(listener, "timed out cleaning up port-0 prefers-4096 listener")
|
||||
}
|
||||
})
|
||||
|
||||
test("port 0 falls back when 4096 is taken", async () => {
|
||||
const blocker = await occupyPort(4096)
|
||||
if (!blocker) return
|
||||
try {
|
||||
const listener = await startListener()
|
||||
try {
|
||||
expect(listener.port).not.toBe(4096)
|
||||
expect(listener.port).toBeGreaterThan(0)
|
||||
} finally {
|
||||
await stop(listener, "timed out cleaning up port-0 fallback listener")
|
||||
}
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => blocker.close(() => resolve()))
|
||||
}
|
||||
})
|
||||
|
||||
testPty("rejects unsafe PTY ticket mint and connect requests", async () => {
|
||||
await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } })
|
||||
const listener = await startListener()
|
||||
@@ -389,3 +416,20 @@ describe("HttpApi Server.listen", () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function isPortFree(port: number) {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const probe = net.createServer()
|
||||
probe.once("error", () => resolve(false))
|
||||
probe.once("listening", () => probe.close(() => resolve(true)))
|
||||
probe.listen(port, "127.0.0.1")
|
||||
})
|
||||
}
|
||||
|
||||
function occupyPort(port: number) {
|
||||
return new Promise<net.Server | undefined>((resolve) => {
|
||||
const server = net.createServer()
|
||||
server.once("error", () => resolve(undefined))
|
||||
server.listen(port, "127.0.0.1", () => resolve(server))
|
||||
})
|
||||
}
|
||||
|
||||
82
packages/opencode/test/server/httpapi-mdns.test.ts
Normal file
82
packages/opencode/test/server/httpapi-mdns.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { afterEach, describe, expect, mock, test } from "bun:test"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import * as Log from "@opencode-ai/core/util/log"
|
||||
import { withTimeout } from "../../src/util/timeout"
|
||||
import { resetDatabase } from "../fixture/db"
|
||||
import { disposeAllInstances } from "../fixture/fixture"
|
||||
|
||||
void Log.init({ print: false })
|
||||
|
||||
type Event = { kind: "publish"; port: number; name: string } | { kind: "unpublishAll" } | { kind: "destroy" }
|
||||
const events: Event[] = []
|
||||
|
||||
void mock.module("bonjour-service", () => ({
|
||||
Bonjour: class {
|
||||
publish(opts: { port: number; name: string }) {
|
||||
events.push({ kind: "publish", port: opts.port, name: opts.name })
|
||||
return { on: () => {} }
|
||||
}
|
||||
unpublishAll() {
|
||||
events.push({ kind: "unpublishAll" })
|
||||
}
|
||||
destroy() {
|
||||
events.push({ kind: "destroy" })
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
// Import Server AFTER the mock so the MDNS module picks up the stub.
|
||||
const { Server } = await import("../../src/server/server")
|
||||
|
||||
const original = {
|
||||
OPENCODE_SERVER_PASSWORD: Flag.OPENCODE_SERVER_PASSWORD,
|
||||
OPENCODE_SERVER_USERNAME: Flag.OPENCODE_SERVER_USERNAME,
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
events.length = 0
|
||||
Flag.OPENCODE_SERVER_PASSWORD = original.OPENCODE_SERVER_PASSWORD
|
||||
Flag.OPENCODE_SERVER_USERNAME = original.OPENCODE_SERVER_USERNAME
|
||||
await disposeAllInstances()
|
||||
await resetDatabase()
|
||||
})
|
||||
|
||||
describe("HttpApi Server.listen mDNS", () => {
|
||||
test("skips publish for loopback hostnames", async () => {
|
||||
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
|
||||
Flag.OPENCODE_SERVER_USERNAME = "opencode"
|
||||
const listener = await Server.listen({ hostname: "127.0.0.1", port: 0, mdns: true })
|
||||
try {
|
||||
expect(events.filter((e) => e.kind === "publish")).toEqual([])
|
||||
} finally {
|
||||
await withTimeout(listener.stop(true), 10_000, "timed out stopping loopback mdns listener")
|
||||
}
|
||||
expect(events.filter((e) => e.kind === "publish")).toEqual([])
|
||||
})
|
||||
|
||||
test("publishes for non-loopback hostnames and unpublishes on stop", async () => {
|
||||
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
|
||||
Flag.OPENCODE_SERVER_USERNAME = "opencode"
|
||||
const listener = await Server.listen({ hostname: "0.0.0.0", port: 0, mdns: true })
|
||||
try {
|
||||
const published = events.filter((e) => e.kind === "publish")
|
||||
expect(published.length).toBe(1)
|
||||
expect(published[0]!.port).toBe(listener.port)
|
||||
expect(published[0]!.name).toBe(`opencode-${listener.port}`)
|
||||
} finally {
|
||||
await withTimeout(listener.stop(true), 10_000, "timed out stopping mdns listener")
|
||||
}
|
||||
expect(events.some((e) => e.kind === "unpublishAll")).toBe(true)
|
||||
expect(events.some((e) => e.kind === "destroy")).toBe(true)
|
||||
})
|
||||
|
||||
test("scope finalizer unpublishes even if stop() is not called for force-close", async () => {
|
||||
Flag.OPENCODE_SERVER_PASSWORD = "mdns-secret"
|
||||
Flag.OPENCODE_SERVER_USERNAME = "opencode"
|
||||
const listener = await Server.listen({ hostname: "0.0.0.0", port: 0, mdns: true })
|
||||
expect(events.filter((e) => e.kind === "publish").length).toBe(1)
|
||||
// Plain (graceful) stop without close=true should still unpublish.
|
||||
await withTimeout(listener.stop(), 10_000, "timed out stopping graceful mdns listener")
|
||||
expect(events.some((e) => e.kind === "unpublishAll")).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user