mirror of https://github.com/openclaw/openclaw.git
311 lines
9.1 KiB
TypeScript
311 lines
9.1 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { createWizardPrompter as buildWizardPrompter } from "../../test/helpers/wizard-prompter.js";
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
|
|
const runTui = vi.hoisted(() => vi.fn(async () => {}));
|
|
const probeGatewayReachable = vi.hoisted(() => vi.fn(async () => ({ ok: true })));
|
|
const setupOnboardingShellCompletion = vi.hoisted(() => vi.fn(async () => {}));
|
|
const buildGatewayInstallPlan = vi.hoisted(() =>
|
|
vi.fn(async () => ({
|
|
programArguments: [],
|
|
workingDirectory: "/tmp",
|
|
environment: {},
|
|
})),
|
|
);
|
|
const gatewayServiceInstall = vi.hoisted(() => vi.fn(async () => {}));
|
|
const gatewayServiceRestart = vi.hoisted(() =>
|
|
vi.fn<() => Promise<{ outcome: "completed" } | { outcome: "scheduled" }>>(async () => ({
|
|
outcome: "completed",
|
|
})),
|
|
);
|
|
const gatewayServiceUninstall = vi.hoisted(() => vi.fn(async () => {}));
|
|
const gatewayServiceIsLoaded = vi.hoisted(() => vi.fn(async () => false));
|
|
const resolveGatewayInstallToken = vi.hoisted(() =>
|
|
vi.fn(async () => ({
|
|
token: undefined,
|
|
tokenRefConfigured: true,
|
|
warnings: [],
|
|
})),
|
|
);
|
|
const isSystemdUserServiceAvailable = vi.hoisted(() => vi.fn(async () => true));
|
|
|
|
vi.mock("../commands/onboard-helpers.js", () => ({
|
|
detectBrowserOpenSupport: vi.fn(async () => ({ ok: false })),
|
|
formatControlUiSshHint: vi.fn(() => "ssh hint"),
|
|
openUrl: vi.fn(async () => false),
|
|
probeGatewayReachable,
|
|
resolveControlUiLinks: vi.fn(() => ({
|
|
httpUrl: "http://127.0.0.1:18789",
|
|
wsUrl: "ws://127.0.0.1:18789",
|
|
})),
|
|
waitForGatewayReachable: vi.fn(async () => {}),
|
|
}));
|
|
|
|
vi.mock("../commands/daemon-install-helpers.js", () => ({
|
|
buildGatewayInstallPlan,
|
|
gatewayInstallErrorHint: vi.fn(() => "hint"),
|
|
}));
|
|
|
|
vi.mock("../commands/gateway-install-token.js", () => ({
|
|
resolveGatewayInstallToken,
|
|
}));
|
|
|
|
vi.mock("../commands/daemon-runtime.js", () => ({
|
|
DEFAULT_GATEWAY_DAEMON_RUNTIME: "node",
|
|
GATEWAY_DAEMON_RUNTIME_OPTIONS: [{ value: "node", label: "Node" }],
|
|
}));
|
|
|
|
vi.mock("../commands/health-format.js", () => ({
|
|
formatHealthCheckFailure: vi.fn(() => "health failed"),
|
|
}));
|
|
|
|
vi.mock("../commands/health.js", () => ({
|
|
healthCommand: vi.fn(async () => {}),
|
|
}));
|
|
|
|
vi.mock("../daemon/service.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../daemon/service.js")>();
|
|
return {
|
|
...actual,
|
|
resolveGatewayService: vi.fn(() => ({
|
|
isLoaded: gatewayServiceIsLoaded,
|
|
restart: gatewayServiceRestart,
|
|
uninstall: gatewayServiceUninstall,
|
|
install: gatewayServiceInstall,
|
|
})),
|
|
};
|
|
});
|
|
|
|
vi.mock("../daemon/systemd.js", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../daemon/systemd.js")>();
|
|
return {
|
|
...actual,
|
|
isSystemdUserServiceAvailable,
|
|
};
|
|
});
|
|
|
|
vi.mock("../infra/control-ui-assets.js", () => ({
|
|
ensureControlUiAssetsBuilt: vi.fn(async () => ({ ok: true })),
|
|
}));
|
|
|
|
vi.mock("../terminal/restore.js", () => ({
|
|
restoreTerminalState: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../tui/tui.js", () => ({
|
|
runTui,
|
|
}));
|
|
|
|
vi.mock("./onboarding.completion.js", () => ({
|
|
setupOnboardingShellCompletion,
|
|
}));
|
|
|
|
import { finalizeOnboardingWizard } from "./onboarding.finalize.js";
|
|
|
|
function createRuntime(): RuntimeEnv {
|
|
return {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn(),
|
|
};
|
|
}
|
|
|
|
function expectFirstOnboardingInstallPlanCallOmitsToken() {
|
|
const [firstArg] =
|
|
(buildGatewayInstallPlan.mock.calls.at(0) as [Record<string, unknown>] | undefined) ?? [];
|
|
expect(firstArg).toBeDefined();
|
|
expect(firstArg && "token" in firstArg).toBe(false);
|
|
}
|
|
|
|
describe("finalizeOnboardingWizard", () => {
|
|
beforeEach(() => {
|
|
runTui.mockClear();
|
|
probeGatewayReachable.mockClear();
|
|
setupOnboardingShellCompletion.mockClear();
|
|
buildGatewayInstallPlan.mockClear();
|
|
gatewayServiceInstall.mockClear();
|
|
gatewayServiceIsLoaded.mockReset();
|
|
gatewayServiceIsLoaded.mockResolvedValue(false);
|
|
gatewayServiceRestart.mockReset();
|
|
gatewayServiceRestart.mockResolvedValue({ outcome: "completed" });
|
|
gatewayServiceUninstall.mockReset();
|
|
resolveGatewayInstallToken.mockClear();
|
|
isSystemdUserServiceAvailable.mockReset();
|
|
isSystemdUserServiceAvailable.mockResolvedValue(true);
|
|
});
|
|
|
|
it("resolves gateway password SecretRef for probe and TUI", async () => {
|
|
const previous = process.env.OPENCLAW_GATEWAY_PASSWORD;
|
|
process.env.OPENCLAW_GATEWAY_PASSWORD = "resolved-gateway-password"; // pragma: allowlist secret
|
|
const select = vi.fn(async (params: { message: string }) => {
|
|
if (params.message === "How do you want to hatch your bot?") {
|
|
return "tui";
|
|
}
|
|
return "later";
|
|
});
|
|
const prompter = buildWizardPrompter({
|
|
select: select as never,
|
|
confirm: vi.fn(async () => false),
|
|
});
|
|
const runtime = createRuntime();
|
|
|
|
try {
|
|
await finalizeOnboardingWizard({
|
|
flow: "quickstart",
|
|
opts: {
|
|
acceptRisk: true,
|
|
authChoice: "skip",
|
|
installDaemon: false,
|
|
skipHealth: true,
|
|
skipUi: false,
|
|
},
|
|
baseConfig: {},
|
|
nextConfig: {
|
|
gateway: {
|
|
auth: {
|
|
mode: "password",
|
|
password: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "OPENCLAW_GATEWAY_PASSWORD",
|
|
},
|
|
},
|
|
},
|
|
tools: {
|
|
web: {
|
|
search: {
|
|
apiKey: "",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
workspaceDir: "/tmp",
|
|
settings: {
|
|
port: 18789,
|
|
bind: "loopback",
|
|
authMode: "password",
|
|
gatewayToken: undefined,
|
|
tailscaleMode: "off",
|
|
tailscaleResetOnExit: false,
|
|
},
|
|
prompter,
|
|
runtime,
|
|
});
|
|
} finally {
|
|
if (previous === undefined) {
|
|
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
|
} else {
|
|
process.env.OPENCLAW_GATEWAY_PASSWORD = previous;
|
|
}
|
|
}
|
|
|
|
expect(probeGatewayReachable).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
url: "ws://127.0.0.1:18789",
|
|
password: "resolved-gateway-password", // pragma: allowlist secret
|
|
}),
|
|
);
|
|
expect(runTui).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
url: "ws://127.0.0.1:18789",
|
|
password: "resolved-gateway-password", // pragma: allowlist secret
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("does not persist resolved SecretRef token in daemon install plan", async () => {
|
|
const prompter = buildWizardPrompter({
|
|
select: vi.fn(async () => "later") as never,
|
|
confirm: vi.fn(async () => false),
|
|
});
|
|
const runtime = createRuntime();
|
|
|
|
await finalizeOnboardingWizard({
|
|
flow: "advanced",
|
|
opts: {
|
|
acceptRisk: true,
|
|
authChoice: "skip",
|
|
installDaemon: true,
|
|
skipHealth: true,
|
|
skipUi: true,
|
|
},
|
|
baseConfig: {},
|
|
nextConfig: {
|
|
gateway: {
|
|
auth: {
|
|
mode: "token",
|
|
token: {
|
|
source: "env",
|
|
provider: "default",
|
|
id: "OPENCLAW_GATEWAY_TOKEN",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
workspaceDir: "/tmp",
|
|
settings: {
|
|
port: 18789,
|
|
bind: "loopback",
|
|
authMode: "token",
|
|
gatewayToken: "session-token",
|
|
tailscaleMode: "off",
|
|
tailscaleResetOnExit: false,
|
|
},
|
|
prompter,
|
|
runtime,
|
|
});
|
|
|
|
expect(resolveGatewayInstallToken).toHaveBeenCalledTimes(1);
|
|
expect(buildGatewayInstallPlan).toHaveBeenCalledTimes(1);
|
|
expectFirstOnboardingInstallPlanCallOmitsToken();
|
|
expect(gatewayServiceInstall).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("stops after a scheduled restart instead of reinstalling the service", async () => {
|
|
const progressUpdate = vi.fn();
|
|
const progressStop = vi.fn();
|
|
gatewayServiceIsLoaded.mockResolvedValue(true);
|
|
gatewayServiceRestart.mockResolvedValueOnce({ outcome: "scheduled" });
|
|
const prompter = buildWizardPrompter({
|
|
select: vi.fn(async (params: { message: string }) => {
|
|
if (params.message === "Gateway service already installed") {
|
|
return "restart";
|
|
}
|
|
return "later";
|
|
}) as never,
|
|
confirm: vi.fn(async () => false),
|
|
progress: vi.fn(() => ({ update: progressUpdate, stop: progressStop })),
|
|
});
|
|
|
|
await finalizeOnboardingWizard({
|
|
flow: "advanced",
|
|
opts: {
|
|
acceptRisk: true,
|
|
authChoice: "skip",
|
|
installDaemon: true,
|
|
skipHealth: true,
|
|
skipUi: true,
|
|
},
|
|
baseConfig: {},
|
|
nextConfig: {},
|
|
workspaceDir: "/tmp",
|
|
settings: {
|
|
port: 18789,
|
|
bind: "loopback",
|
|
authMode: "token",
|
|
gatewayToken: undefined,
|
|
tailscaleMode: "off",
|
|
tailscaleResetOnExit: false,
|
|
},
|
|
prompter,
|
|
runtime: createRuntime(),
|
|
});
|
|
|
|
expect(gatewayServiceRestart).toHaveBeenCalledTimes(1);
|
|
expect(gatewayServiceInstall).not.toHaveBeenCalled();
|
|
expect(gatewayServiceUninstall).not.toHaveBeenCalled();
|
|
expect(progressUpdate).toHaveBeenCalledWith("Restarting Gateway service…");
|
|
expect(progressStop).toHaveBeenCalledWith("Gateway service restart scheduled.");
|
|
});
|
|
});
|