mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:20:43 +00:00
fix: harden setup tui handoff
This commit is contained in:
83
src/tui/tui-launch.test.ts
Normal file
83
src/tui/tui-launch.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { ChildProcess, SpawnOptions } from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const spawnMock = vi.hoisted(() => vi.fn());
|
||||
const detachMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("node:child_process", () => ({
|
||||
spawn: spawnMock,
|
||||
}));
|
||||
|
||||
vi.mock("../process/child-process-bridge.js", () => ({
|
||||
attachChildProcessBridge: vi.fn(() => ({ detach: detachMock })),
|
||||
}));
|
||||
|
||||
import { launchTuiCli } from "./tui-launch.js";
|
||||
|
||||
const originalArgv = [...process.argv];
|
||||
const originalExecArgv = [...process.execArgv];
|
||||
|
||||
function createChildProcess(): ChildProcess {
|
||||
return new EventEmitter() as ChildProcess;
|
||||
}
|
||||
|
||||
describe("launchTuiCli", () => {
|
||||
beforeEach(() => {
|
||||
process.argv = [...originalArgv];
|
||||
process.argv[1] = "/repo/openclaw.mjs";
|
||||
process.execArgv.length = 0;
|
||||
spawnMock.mockReset();
|
||||
detachMock.mockReset();
|
||||
vi.spyOn(process.stdin, "pause").mockImplementation(() => process.stdin);
|
||||
vi.spyOn(process.stdin, "resume").mockImplementation(() => process.stdin);
|
||||
vi.spyOn(process.stdin, "isPaused").mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.argv = [...originalArgv];
|
||||
process.execArgv.length = 0;
|
||||
process.execArgv.push(...originalExecArgv);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("filters inherited inspector flags when relaunching TUI", async () => {
|
||||
process.execArgv.push(
|
||||
"--import",
|
||||
"tsx",
|
||||
"--inspect=127.0.0.1:9229",
|
||||
"--inspect-brk",
|
||||
"--inspect-wait=0",
|
||||
"--inspect-port",
|
||||
"9230",
|
||||
"--no-warnings",
|
||||
);
|
||||
const child = createChildProcess();
|
||||
spawnMock.mockImplementation((_cmd: string, _args: string[], _opts: SpawnOptions) => {
|
||||
queueMicrotask(() => child.emit("exit", 0, null));
|
||||
return child;
|
||||
});
|
||||
|
||||
await launchTuiCli({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
token: "test-token",
|
||||
deliver: false,
|
||||
});
|
||||
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
"--no-warnings",
|
||||
"/repo/openclaw.mjs",
|
||||
"tui",
|
||||
"--url",
|
||||
"ws://127.0.0.1:18789",
|
||||
"--token",
|
||||
"test-token",
|
||||
],
|
||||
expect.objectContaining({ stdio: "inherit" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -10,13 +10,42 @@ function appendOption(args: string[], flag: string, value: string | number | und
|
||||
args.push(flag, String(value));
|
||||
}
|
||||
|
||||
function filterTuiExecArgv(execArgv: readonly string[]): string[] {
|
||||
const filtered: string[] = [];
|
||||
for (let index = 0; index < execArgv.length; index += 1) {
|
||||
const arg = execArgv[index] ?? "";
|
||||
if (
|
||||
arg === "--inspect" ||
|
||||
arg.startsWith("--inspect=") ||
|
||||
arg === "--inspect-brk" ||
|
||||
arg.startsWith("--inspect-brk=") ||
|
||||
arg === "--inspect-wait" ||
|
||||
arg.startsWith("--inspect-wait=")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (arg === "--inspect-port") {
|
||||
const next = execArgv[index + 1];
|
||||
if (typeof next === "string" && !next.startsWith("-")) {
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--inspect-port=")) {
|
||||
continue;
|
||||
}
|
||||
filtered.push(arg);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function buildTuiCliArgs(opts: TuiOptions): string[] {
|
||||
const entry = process.argv[1]?.trim();
|
||||
if (!entry) {
|
||||
throw new Error("unable to relaunch TUI: current CLI entry path is unavailable");
|
||||
}
|
||||
|
||||
const args = [...process.execArgv, entry, "tui"];
|
||||
const args = [...filterTuiExecArgv(process.execArgv), entry, "tui"];
|
||||
appendOption(args, "--url", opts.url);
|
||||
appendOption(args, "--token", opts.token);
|
||||
appendOption(args, "--password", opts.password);
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
|
||||
const launchTuiCli = vi.hoisted(() => vi.fn(async () => {}));
|
||||
const restoreTerminalState = vi.hoisted(() => vi.fn());
|
||||
const probeGatewayReachable = vi.hoisted(() =>
|
||||
vi.fn<() => Promise<{ ok: boolean; detail?: string }>>(async () => ({ ok: true })),
|
||||
);
|
||||
@@ -134,7 +135,7 @@ vi.mock("../infra/control-ui-assets.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../terminal/restore.js", () => ({
|
||||
restoreTerminalState: vi.fn(),
|
||||
restoreTerminalState,
|
||||
}));
|
||||
|
||||
vi.mock("../tui/tui-launch.js", () => ({
|
||||
@@ -237,6 +238,7 @@ function createAdvancedFinalizeArgs(params: AdvancedFinalizeArgs = {}) {
|
||||
describe("finalizeSetupWizard", () => {
|
||||
beforeEach(() => {
|
||||
launchTuiCli.mockClear();
|
||||
restoreTerminalState.mockClear();
|
||||
probeGatewayReachable.mockClear();
|
||||
waitForGatewayReachable.mockReset();
|
||||
waitForGatewayReachable.mockResolvedValue({ ok: true });
|
||||
@@ -345,6 +347,50 @@ describe("finalizeSetupWizard", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("restores terminal state after failed TUI hatch", async () => {
|
||||
launchTuiCli.mockRejectedValueOnce(new Error("TUI exited with code 1"));
|
||||
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 });
|
||||
|
||||
await expect(
|
||||
finalizeSetupWizard({
|
||||
flow: "advanced",
|
||||
opts: {
|
||||
acceptRisk: true,
|
||||
authChoice: "skip",
|
||||
installDaemon: false,
|
||||
skipHealth: true,
|
||||
skipUi: false,
|
||||
},
|
||||
baseConfig: {},
|
||||
nextConfig: {},
|
||||
workspaceDir: "/tmp",
|
||||
settings: {
|
||||
port: 18789,
|
||||
bind: "loopback",
|
||||
authMode: "token",
|
||||
gatewayToken: "test-token",
|
||||
tailscaleMode: "off",
|
||||
tailscaleResetOnExit: false,
|
||||
},
|
||||
prompter,
|
||||
runtime: createRuntime(),
|
||||
}),
|
||||
).rejects.toThrow("TUI exited with code 1");
|
||||
|
||||
expect(restoreTerminalState).toHaveBeenCalledWith("pre-setup tui", {
|
||||
resumeStdinIfPaused: true,
|
||||
});
|
||||
expect(restoreTerminalState).toHaveBeenCalledWith("post-setup tui", {
|
||||
resumeStdinIfPaused: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not persist resolved SecretRef token in daemon install plan", async () => {
|
||||
const prompter = buildWizardPrompter({
|
||||
select: vi.fn(async () => "later") as never,
|
||||
|
||||
@@ -423,15 +423,18 @@ export async function finalizeSetupWizard(
|
||||
|
||||
if (hatchChoice === "tui") {
|
||||
restoreTerminalState("pre-setup tui", { resumeStdinIfPaused: true });
|
||||
await launchTuiCli({
|
||||
url: links.wsUrl,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
password: settings.authMode === "password" ? resolvedGatewayPassword : "",
|
||||
// Safety: setup TUI should not auto-deliver to lastProvider/lastTo.
|
||||
deliver: false,
|
||||
message: hasBootstrap ? "Wake up, my friend!" : undefined,
|
||||
});
|
||||
restoreTerminalState("post-setup tui", { resumeStdinIfPaused: true });
|
||||
try {
|
||||
await launchTuiCli({
|
||||
url: links.wsUrl,
|
||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
||||
password: settings.authMode === "password" ? resolvedGatewayPassword : "",
|
||||
// Safety: setup TUI should not auto-deliver to lastProvider/lastTo.
|
||||
deliver: false,
|
||||
message: hasBootstrap ? "Wake up, my friend!" : undefined,
|
||||
});
|
||||
} finally {
|
||||
restoreTerminalState("post-setup tui", { resumeStdinIfPaused: true });
|
||||
}
|
||||
launchedTui = true;
|
||||
} else if (hatchChoice === "web") {
|
||||
const browserSupport = await detectBrowserOpenSupport();
|
||||
|
||||
Reference in New Issue
Block a user