Files
openclaw/src/gateway/server-tailscale.test.ts
2026-05-11 16:22:18 +01:00

120 lines
4.1 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
enableTailscaleServe: vi.fn(async (_port: number) => undefined),
disableTailscaleServe: vi.fn(async () => undefined),
enableTailscaleFunnel: vi.fn(async (_port: number) => undefined),
disableTailscaleFunnel: vi.fn(async () => undefined),
getTailnetHostname: vi.fn(async () => null),
hasTailscaleFunnelRouteForPort: vi.fn(async (_port: number) => false),
}));
vi.mock("../infra/tailscale.js", () => ({
enableTailscaleServe: mocks.enableTailscaleServe,
disableTailscaleServe: mocks.disableTailscaleServe,
enableTailscaleFunnel: mocks.enableTailscaleFunnel,
disableTailscaleFunnel: mocks.disableTailscaleFunnel,
getTailnetHostname: mocks.getTailnetHostname,
hasTailscaleFunnelRouteForPort: mocks.hasTailscaleFunnelRouteForPort,
}));
import { startGatewayTailscaleExposure } from "./server-tailscale.js";
function createLogger() {
return { info: vi.fn(), warn: vi.fn() };
}
afterEach(() => {
for (const fn of Object.values(mocks)) {
fn.mockReset();
}
mocks.enableTailscaleServe.mockResolvedValue(undefined);
mocks.disableTailscaleServe.mockResolvedValue(undefined);
mocks.enableTailscaleFunnel.mockResolvedValue(undefined);
mocks.disableTailscaleFunnel.mockResolvedValue(undefined);
mocks.getTailnetHostname.mockResolvedValue(null);
mocks.hasTailscaleFunnelRouteForPort.mockResolvedValue(false);
});
describe("startGatewayTailscaleExposure preserveFunnel", () => {
it("calls enableTailscaleServe in serve mode when preserveFunnel is unset", async () => {
const logTailscale = createLogger();
await startGatewayTailscaleExposure({
tailscaleMode: "serve",
port: 18789,
logTailscale,
});
expect(mocks.enableTailscaleServe).toHaveBeenCalledWith(18789);
expect(mocks.hasTailscaleFunnelRouteForPort).not.toHaveBeenCalled();
});
it("skips enableTailscaleServe when preserveFunnel is true and a Funnel route covers the port", async () => {
const logTailscale = createLogger();
mocks.hasTailscaleFunnelRouteForPort.mockResolvedValue(true);
await startGatewayTailscaleExposure({
tailscaleMode: "serve",
port: 18789,
preserveFunnel: true,
logTailscale,
});
expect(mocks.hasTailscaleFunnelRouteForPort).toHaveBeenCalledWith(18789);
expect(mocks.enableTailscaleServe).not.toHaveBeenCalled();
expect(logTailscale.info.mock.calls).toEqual([
["serve skipped: preserving externally configured Tailscale Funnel for port 18789"],
]);
});
it("notes resetOnExit is a no-op when preserveFunnel skips Serve", async () => {
const logTailscale = createLogger();
mocks.hasTailscaleFunnelRouteForPort.mockResolvedValue(true);
await startGatewayTailscaleExposure({
tailscaleMode: "serve",
port: 18789,
preserveFunnel: true,
resetOnExit: true,
logTailscale,
});
expect(mocks.enableTailscaleServe).not.toHaveBeenCalled();
expect(logTailscale.info.mock.calls).toEqual([
[
"serve skipped: preserving externally configured Tailscale Funnel for port 18789; resetOnExit is a no-op because no Serve route was applied this run",
],
]);
});
it("falls back to enableTailscaleServe when preserveFunnel is true but no Funnel route exists for the port", async () => {
const logTailscale = createLogger();
mocks.hasTailscaleFunnelRouteForPort.mockResolvedValue(false);
await startGatewayTailscaleExposure({
tailscaleMode: "serve",
port: 18789,
preserveFunnel: true,
logTailscale,
});
expect(mocks.hasTailscaleFunnelRouteForPort).toHaveBeenCalledWith(18789);
expect(mocks.enableTailscaleServe).toHaveBeenCalledWith(18789);
});
it("never consults the Funnel route helper when running in funnel mode", async () => {
const logTailscale = createLogger();
await startGatewayTailscaleExposure({
tailscaleMode: "funnel",
port: 18789,
preserveFunnel: true,
logTailscale,
});
expect(mocks.hasTailscaleFunnelRouteForPort).not.toHaveBeenCalled();
expect(mocks.enableTailscaleFunnel).toHaveBeenCalledWith(18789);
});
});