fix: clarify no-daemon onboarding gateway checks

This commit is contained in:
Shakker
2026-03-27 09:32:40 +00:00
committed by Shakker
parent e765daaed3
commit 37538072e6
2 changed files with 96 additions and 12 deletions

View File

@@ -5,7 +5,12 @@ import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
import type { RuntimeEnv } from "../runtime.js";
const runTui = vi.hoisted(() => vi.fn(async () => {}));
const probeGatewayReachable = vi.hoisted(() => vi.fn(async () => ({ ok: true })));
const probeGatewayReachable = vi.hoisted(() =>
vi.fn<() => Promise<{ ok: boolean; detail?: string }>>(async () => ({ ok: true })),
);
const waitForGatewayReachable = vi.hoisted(() =>
vi.fn<() => Promise<{ ok: boolean; detail?: string }>>(async () => ({ ok: true })),
);
const setupWizardShellCompletion = vi.hoisted(() => vi.fn(async () => {}));
const buildGatewayInstallPlan = vi.hoisted(() =>
vi.fn(async () => ({
@@ -58,7 +63,7 @@ vi.mock("../commands/onboard-helpers.js", () => ({
httpUrl: "http://127.0.0.1:18789",
wsUrl: "ws://127.0.0.1:18789",
})),
waitForGatewayReachable: vi.fn(async () => {}),
waitForGatewayReachable,
}));
vi.mock("../commands/daemon-install-helpers.js", () => ({
@@ -233,6 +238,8 @@ describe("finalizeSetupWizard", () => {
beforeEach(() => {
runTui.mockClear();
probeGatewayReachable.mockClear();
waitForGatewayReachable.mockReset();
waitForGatewayReachable.mockResolvedValue({ ok: true });
setupWizardShellCompletion.mockClear();
buildGatewayInstallPlan.mockClear();
gatewayServiceInstall.mockClear();
@@ -505,4 +512,48 @@ describe("finalizeSetupWizard", () => {
"Web search",
);
});
it("shows actionable gateway guidance instead of a hard error in no-daemon onboarding", async () => {
waitForGatewayReachable.mockResolvedValue({
ok: false,
detail: "gateway closed (1006 abnormal closure (no close frame)): no close reason",
});
probeGatewayReachable.mockResolvedValue({
ok: false,
detail: "gateway closed (1006 abnormal closure (no close frame)): no close reason",
});
const prompter = createLaterPrompter();
const runtime = createRuntime();
await finalizeSetupWizard({
flow: "quickstart",
opts: {
acceptRisk: true,
authChoice: "skip",
installDaemon: false,
skipHealth: false,
skipUi: false,
},
baseConfig: {},
nextConfig: {},
workspaceDir: "/tmp",
settings: {
port: 18789,
bind: "loopback",
authMode: "token",
gatewayToken: "test-token",
tailscaleMode: "off",
tailscaleResetOnExit: false,
},
prompter,
runtime,
});
expect(runtime.error).not.toHaveBeenCalledWith("health failed");
expect(prompter.note).toHaveBeenCalledWith(
expect.stringContaining("Setup was run without Gateway service install"),
"Gateway",
);
expect(prompter.note).not.toHaveBeenCalledWith(expect.any(String), "Dashboard ready");
});
});

View File

@@ -51,6 +51,7 @@ export async function finalizeSetupWizard(
options: FinalizeOnboardingOptions,
): Promise<{ launchedTui: boolean }> {
const { flow, opts, baseConfig, nextConfig, settings, prompter, runtime } = options;
let gatewayProbe: { ok: boolean; detail?: string } = { ok: true };
const withWizardProgress = async <T>(
label: string,
@@ -232,15 +233,33 @@ export async function finalizeSetupWizard(
basePath: undefined,
});
// Daemon install/restart can briefly flap the WS; wait a bit so health check doesn't false-fail.
await waitForGatewayReachable({
gatewayProbe = await waitForGatewayReachable({
url: probeLinks.wsUrl,
token: settings.gatewayToken,
deadlineMs: 15_000,
});
try {
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
} catch (err) {
runtime.error(formatHealthCheckFailure(err));
if (gatewayProbe.ok) {
try {
await healthCommand({ json: false, timeoutMs: 10_000 }, runtime);
} catch (err) {
runtime.error(formatHealthCheckFailure(err));
await prompter.note(
[
"Docs:",
"https://docs.openclaw.ai/gateway/health",
"https://docs.openclaw.ai/gateway/troubleshooting",
].join("\n"),
"Health check help",
);
}
} else if (installDaemon) {
runtime.error(
formatHealthCheckFailure(
new Error(
gatewayProbe.detail ?? `gateway did not become reachable at ${probeLinks.wsUrl}`,
),
),
);
await prompter.note(
[
"Docs:",
@@ -249,6 +268,17 @@ export async function finalizeSetupWizard(
].join("\n"),
"Health check help",
);
} else {
await prompter.note(
[
"Gateway not detected yet.",
"Setup was run without Gateway service install, so no background gateway is expected.",
`Start now: ${formatCliCommand("openclaw gateway run")}`,
`Or rerun with: ${formatCliCommand("openclaw onboard --install-daemon")}`,
`Or skip this probe next time: ${formatCliCommand("openclaw onboard --skip-health")}`,
].join("\n"),
"Gateway",
);
}
}
@@ -304,11 +334,13 @@ export async function finalizeSetupWizard(
}
}
const gatewayProbe = await probeGatewayReachable({
url: links.wsUrl,
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
password: settings.authMode === "password" ? resolvedGatewayPassword : "",
});
if (opts.skipHealth || !gatewayProbe.ok) {
gatewayProbe = await probeGatewayReachable({
url: links.wsUrl,
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
password: settings.authMode === "password" ? resolvedGatewayPassword : "",
});
}
const gatewayStatusLine = gatewayProbe.ok
? "Gateway: reachable"
: `Gateway: not detected${gatewayProbe.detail ? ` (${gatewayProbe.detail})` : ""}`;
@@ -446,6 +478,7 @@ export async function finalizeSetupWizard(
const shouldOpenControlUi =
!opts.skipUi &&
gatewayProbe.ok &&
settings.authMode === "token" &&
Boolean(settings.gatewayToken) &&
hatchChoice === null;