Files
openclaw/src/commands/dashboard.e2e.test.ts

121 lines
3.5 KiB
TypeScript
Raw Normal View History

import { beforeEach, describe, expect, it, vi } from "vitest";
2026-01-12 19:08:29 +00:00
import { dashboardCommand } from "./dashboard.js";
const mocks = vi.hoisted(() => ({
readConfigFileSnapshot: vi.fn(),
resolveGatewayPort: vi.fn(),
resolveControlUiLinks: vi.fn(),
detectBrowserOpenSupport: vi.fn(),
openUrl: vi.fn(),
formatControlUiSshHint: vi.fn(),
copyToClipboard: vi.fn(),
}));
vi.mock("../config/config.js", () => ({
readConfigFileSnapshot: mocks.readConfigFileSnapshot,
resolveGatewayPort: mocks.resolveGatewayPort,
}));
vi.mock("./onboard-helpers.js", () => ({
resolveControlUiLinks: mocks.resolveControlUiLinks,
detectBrowserOpenSupport: mocks.detectBrowserOpenSupport,
openUrl: mocks.openUrl,
formatControlUiSshHint: mocks.formatControlUiSshHint,
}));
vi.mock("../infra/clipboard.js", () => ({
2026-01-12 19:08:29 +00:00
copyToClipboard: mocks.copyToClipboard,
}));
const runtime = {
log: vi.fn(),
error: vi.fn(),
exit: vi.fn(),
};
function resetRuntime() {
runtime.log.mockClear();
runtime.error.mockClear();
runtime.exit.mockClear();
}
function mockSnapshot(token = "abc") {
mocks.readConfigFileSnapshot.mockResolvedValue({
2026-01-30 03:15:10 +01:00
path: "/tmp/openclaw.json",
2026-01-12 19:08:29 +00:00
exists: true,
raw: "{}",
parsed: {},
valid: true,
config: { gateway: { auth: { token } } },
issues: [],
legacyIssues: [],
});
mocks.resolveGatewayPort.mockReturnValue(18789);
mocks.resolveControlUiLinks.mockReturnValue({
httpUrl: "http://127.0.0.1:18789/",
wsUrl: "ws://127.0.0.1:18789",
});
}
describe("dashboardCommand", () => {
beforeEach(() => {
resetRuntime();
mocks.readConfigFileSnapshot.mockReset();
mocks.resolveGatewayPort.mockReset();
mocks.resolveControlUiLinks.mockReset();
mocks.detectBrowserOpenSupport.mockReset();
mocks.openUrl.mockReset();
mocks.formatControlUiSshHint.mockReset();
mocks.copyToClipboard.mockReset();
});
it("opens and copies the dashboard link by default", async () => {
mockSnapshot("abc123");
mocks.copyToClipboard.mockResolvedValue(true);
mocks.detectBrowserOpenSupport.mockResolvedValue({ ok: true });
mocks.openUrl.mockResolvedValue(true);
await dashboardCommand(runtime);
expect(mocks.resolveControlUiLinks).toHaveBeenCalledWith({
port: 18789,
bind: "loopback",
feat: add Tailscale binary detection, IP binding modes, and health probe password fix 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
2026-01-11 14:13:13 -06:00
customBindHost: undefined,
2026-01-12 19:08:29 +00:00
basePath: undefined,
});
expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123");
expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/#token=abc123");
2026-01-12 19:08:29 +00:00
expect(runtime.log).toHaveBeenCalledWith(
2026-01-30 03:15:10 +01:00
"Opened in your browser. Keep that tab to control OpenClaw.",
2026-01-12 19:08:29 +00:00
);
});
it("prints SSH hint when browser cannot open", async () => {
mockSnapshot("shhhh");
mocks.copyToClipboard.mockResolvedValue(false);
mocks.detectBrowserOpenSupport.mockResolvedValue({
ok: false,
reason: "ssh",
});
2026-01-12 19:08:29 +00:00
mocks.formatControlUiSshHint.mockReturnValue("ssh hint");
await dashboardCommand(runtime);
expect(mocks.openUrl).not.toHaveBeenCalled();
expect(runtime.log).toHaveBeenCalledWith("ssh hint");
});
it("respects --no-open and skips browser attempts", async () => {
mockSnapshot();
mocks.copyToClipboard.mockResolvedValue(true);
await dashboardCommand(runtime, { noOpen: true });
expect(mocks.detectBrowserOpenSupport).not.toHaveBeenCalled();
expect(mocks.openUrl).not.toHaveBeenCalled();
expect(runtime.log).toHaveBeenCalledWith(
"Browser launch disabled (--no-open). Use the URL above.",
);
});
});