mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 09:51:22 +00:00
This PR includes three main improvements:
1. Tailscale Binary Detection with Fallback Strategies
- Added findTailscaleBinary() with multi-strategy detection:
* PATH lookup via 'which' command
* Known macOS app path (/Applications/Tailscale.app/Contents/MacOS/Tailscale)
* find /Applications for Tailscale.app
* locate database lookup
- Added getTailscaleBinary() with caching
- Updated all Tailscale operations to use detected binary
- Added TUI warning when Tailscale binary not found for serve/funnel modes
2. Custom Gateway IP Binding with Fallback
- New bind mode "custom" allowing user-specified IP with fallback to 0.0.0.0
- Removed "tailnet" mode (folded into "auto")
- All modes now support graceful fallback: custom (if fail → 0.0.0.0), loopback (127.0.0.1 → 0.0.0.0), auto (tailnet → 0.0.0.0), lan (0.0.0.0)
- Added customBindHost config option for custom bind mode
- Added canBindTo() helper to test IP availability before binding
- Updated configure and onboarding wizards with new bind mode options
3. Health Probe Password Auth Fix
- Gateway probe now tries both new and old passwords
- Fixes issue where password change fails health check if gateway hasn't restarted yet
- Uses nextConfig password first, falls back to baseConfig password if needed
Files changed:
- src/infra/tailscale.ts: Binary detection + caching
- src/gateway/net.ts: IP binding with fallback logic
- src/config/types.ts: BridgeBindMode type + customBindHost field
- src/commands/configure.ts: Health probe dual-password try + Tailscale detection warning + bind mode UI
- src/wizard/onboarding.ts: Tailscale detection warning + bind mode UI
- src/gateway/server.ts: Use new resolveGatewayBindHost
- src/gateway/call.ts: Updated preferTailnet logic (removed "tailnet" mode)
- src/commands/onboard-types.ts: Updated GatewayBind type
- src/commands/onboard-helpers.ts: resolveControlUiLinks updated
- src/cli/*.ts: Updated bind mode casts
- src/gateway/call.test.ts: Removed "tailnet" mode test
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
openUrl,
|
|
resolveBrowserOpenCommand,
|
|
resolveControlUiLinks,
|
|
} from "./onboard-helpers.js";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
runCommandWithTimeout: vi.fn(async () => ({
|
|
stdout: "",
|
|
stderr: "",
|
|
code: 0,
|
|
signal: null,
|
|
killed: false,
|
|
})),
|
|
pickPrimaryTailnetIPv4: vi.fn(() => undefined),
|
|
}));
|
|
|
|
vi.mock("../process/exec.js", () => ({
|
|
runCommandWithTimeout: mocks.runCommandWithTimeout,
|
|
}));
|
|
|
|
vi.mock("../infra/tailnet.js", () => ({
|
|
pickPrimaryTailnetIPv4: mocks.pickPrimaryTailnetIPv4,
|
|
}));
|
|
|
|
describe("openUrl", () => {
|
|
it("quotes URLs on win32 so '&' is not treated as cmd separator", async () => {
|
|
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
|
|
|
const url =
|
|
"https://accounts.google.com/o/oauth2/v2/auth?client_id=abc&response_type=code&redirect_uri=http%3A%2F%2Flocalhost";
|
|
|
|
const ok = await openUrl(url);
|
|
expect(ok).toBe(true);
|
|
|
|
expect(mocks.runCommandWithTimeout).toHaveBeenCalledTimes(1);
|
|
const [argv, options] = mocks.runCommandWithTimeout.mock.calls[0] ?? [];
|
|
expect(argv?.slice(0, 4)).toEqual(["cmd", "/c", "start", '""']);
|
|
expect(argv?.at(-1)).toBe(`"${url}"`);
|
|
expect(options).toMatchObject({
|
|
timeoutMs: 5_000,
|
|
windowsVerbatimArguments: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("resolveBrowserOpenCommand", () => {
|
|
it("marks win32 commands as quoteUrl=true", async () => {
|
|
vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
|
const resolved = await resolveBrowserOpenCommand();
|
|
expect(resolved.argv).toEqual(["cmd", "/c", "start", ""]);
|
|
expect(resolved.quoteUrl).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("resolveControlUiLinks", () => {
|
|
it("uses customBindHost for custom bind", () => {
|
|
const links = resolveControlUiLinks({
|
|
port: 18789,
|
|
bind: "custom",
|
|
customBindHost: "192.168.1.100",
|
|
});
|
|
expect(links.httpUrl).toBe("http://192.168.1.100:18789/");
|
|
expect(links.wsUrl).toBe("ws://192.168.1.100:18789");
|
|
});
|
|
|
|
it("falls back to loopback for invalid customBindHost", () => {
|
|
const links = resolveControlUiLinks({
|
|
port: 18789,
|
|
bind: "custom",
|
|
customBindHost: "192.168.001.100",
|
|
});
|
|
expect(links.httpUrl).toBe("http://127.0.0.1:18789/");
|
|
expect(links.wsUrl).toBe("ws://127.0.0.1:18789");
|
|
});
|
|
});
|