mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:30:44 +00:00
Fix setup TUI hatch terminal handoff (#69524)
* fix: relaunch setup tui in a fresh process
* fix: harden setup tui handoff
* fix: preserve tui hatch exit flow
* Revert "fix: preserve tui hatch exit flow"
This reverts commit f4f119a5a3.
* fix: let setup tui resolve gateway auth
* fix: support packaged tui relaunch
* fix: pin setup tui gateway target
* fix: preserve setup tui auth source
This commit is contained in:
@@ -145,6 +145,7 @@ export async function resolveGatewayInteractiveSurfaceAuth(params: {
|
|||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
explicitAuth?: ExplicitGatewayAuth;
|
explicitAuth?: ExplicitGatewayAuth;
|
||||||
|
suppressEnvAuthFallback?: boolean;
|
||||||
surface: "local" | "remote";
|
surface: "local" | "remote";
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
token?: string;
|
token?: string;
|
||||||
@@ -155,8 +156,12 @@ export async function resolveGatewayInteractiveSurfaceAuth(params: {
|
|||||||
const diagnostics: string[] = [];
|
const diagnostics: string[] = [];
|
||||||
const explicitToken = trimToUndefined(params.explicitAuth?.token);
|
const explicitToken = trimToUndefined(params.explicitAuth?.token);
|
||||||
const explicitPassword = trimToUndefined(params.explicitAuth?.password);
|
const explicitPassword = trimToUndefined(params.explicitAuth?.password);
|
||||||
const envToken = trimToUndefined(env.OPENCLAW_GATEWAY_TOKEN);
|
const envToken = params.suppressEnvAuthFallback
|
||||||
const envPassword = trimToUndefined(env.OPENCLAW_GATEWAY_PASSWORD);
|
? undefined
|
||||||
|
: trimToUndefined(env.OPENCLAW_GATEWAY_TOKEN);
|
||||||
|
const envPassword = params.suppressEnvAuthFallback
|
||||||
|
? undefined
|
||||||
|
: trimToUndefined(env.OPENCLAW_GATEWAY_PASSWORD);
|
||||||
|
|
||||||
if (params.surface === "remote") {
|
if (params.surface === "remote") {
|
||||||
const remoteToken = explicitToken
|
const remoteToken = explicitToken
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ describe("resolveGatewayConnection", () => {
|
|||||||
"OPENCLAW_GATEWAY_URL",
|
"OPENCLAW_GATEWAY_URL",
|
||||||
"OPENCLAW_GATEWAY_TOKEN",
|
"OPENCLAW_GATEWAY_TOKEN",
|
||||||
"OPENCLAW_GATEWAY_PASSWORD",
|
"OPENCLAW_GATEWAY_PASSWORD",
|
||||||
|
"OPENCLAW_TUI_SETUP_AUTH_SOURCE",
|
||||||
]);
|
]);
|
||||||
loadConfig.mockReset();
|
loadConfig.mockReset();
|
||||||
resolveGatewayPort.mockReset();
|
resolveGatewayPort.mockReset();
|
||||||
@@ -126,6 +127,7 @@ describe("resolveGatewayConnection", () => {
|
|||||||
delete process.env.OPENCLAW_GATEWAY_URL;
|
delete process.env.OPENCLAW_GATEWAY_URL;
|
||||||
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||||
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||||
|
delete process.env.OPENCLAW_TUI_SETUP_AUTH_SOURCE;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -199,6 +201,74 @@ describe("resolveGatewayConnection", () => {
|
|||||||
expect(result.token).toBeUndefined();
|
expect(result.token).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps normal TUI local password mode env precedence by default", async () => {
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
gateway: {
|
||||||
|
mode: "local",
|
||||||
|
auth: {
|
||||||
|
mode: "password",
|
||||||
|
password: "config-password", // pragma: allowlist secret
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await withEnvAsync({ OPENCLAW_GATEWAY_PASSWORD: "env-password" }, async () => {
|
||||||
|
const result = await resolveGatewayConnection({});
|
||||||
|
expect(result.password).toBe("env-password");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses configured local password for setup-launched TUI despite stale gateway password env", async () => {
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
gateway: {
|
||||||
|
mode: "local",
|
||||||
|
auth: {
|
||||||
|
mode: "password",
|
||||||
|
password: "config-password", // pragma: allowlist secret
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await withEnvAsync(
|
||||||
|
{
|
||||||
|
OPENCLAW_GATEWAY_PASSWORD: "stale-env-password", // pragma: allowlist secret
|
||||||
|
OPENCLAW_TUI_SETUP_AUTH_SOURCE: "config",
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const result = await resolveGatewayConnection({});
|
||||||
|
expect(result.password).toBe("config-password");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still resolves env SecretRefs for setup-launched TUI config auth", async () => {
|
||||||
|
loadConfig.mockReturnValue({
|
||||||
|
secrets: {
|
||||||
|
providers: {
|
||||||
|
default: { source: "env" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gateway: {
|
||||||
|
mode: "local",
|
||||||
|
auth: {
|
||||||
|
mode: "password",
|
||||||
|
password: { source: "env", provider: "default", id: "OPENCLAW_GATEWAY_PASSWORD" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await withEnvAsync(
|
||||||
|
{
|
||||||
|
OPENCLAW_GATEWAY_PASSWORD: "resolved-ref-password", // pragma: allowlist secret
|
||||||
|
OPENCLAW_TUI_SETUP_AUTH_SOURCE: "config",
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const result = await resolveGatewayConnection({});
|
||||||
|
expect(result.password).toBe("resolved-ref-password");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("fails when both local token and password are configured but gateway.auth.mode is unset", async () => {
|
it("fails when both local token and password are configured but gateway.auth.mode is unset", async () => {
|
||||||
loadConfig.mockReturnValue({
|
loadConfig.mockReturnValue({
|
||||||
gateway: {
|
gateway: {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from "../gateway/protocol/index.js";
|
} from "../gateway/protocol/index.js";
|
||||||
import { formatErrorMessage } from "../infra/errors.js";
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
|
import { TUI_SETUP_AUTH_SOURCE_CONFIG, TUI_SETUP_AUTH_SOURCE_ENV } from "./setup-launch-env.js";
|
||||||
import type { ResponseUsageMode, SessionInfo, SessionScope } from "./tui-types.js";
|
import type { ResponseUsageMode, SessionInfo, SessionScope } from "./tui-types.js";
|
||||||
|
|
||||||
export type GatewayConnectionOptions = {
|
export type GatewayConnectionOptions = {
|
||||||
@@ -317,6 +318,7 @@ export async function resolveGatewayConnection(
|
|||||||
const env = process.env;
|
const env = process.env;
|
||||||
const gatewayAuthMode = config.gateway?.auth?.mode;
|
const gatewayAuthMode = config.gateway?.auth?.mode;
|
||||||
const isRemoteMode = config.gateway?.mode === "remote";
|
const isRemoteMode = config.gateway?.mode === "remote";
|
||||||
|
const preferConfiguredAuth = env[TUI_SETUP_AUTH_SOURCE_ENV] === TUI_SETUP_AUTH_SOURCE_CONFIG;
|
||||||
|
|
||||||
const urlOverride =
|
const urlOverride =
|
||||||
typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : undefined;
|
typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : undefined;
|
||||||
@@ -394,6 +396,7 @@ export async function resolveGatewayConnection(
|
|||||||
config,
|
config,
|
||||||
env,
|
env,
|
||||||
explicitAuth,
|
explicitAuth,
|
||||||
|
suppressEnvAuthFallback: preferConfiguredAuth,
|
||||||
surface: "local",
|
surface: "local",
|
||||||
});
|
});
|
||||||
if (resolved.failureReason) {
|
if (resolved.failureReason) {
|
||||||
|
|||||||
2
src/tui/setup-launch-env.ts
Normal file
2
src/tui/setup-launch-env.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const TUI_SETUP_AUTH_SOURCE_ENV = "OPENCLAW_TUI_SETUP_AUTH_SOURCE";
|
||||||
|
export const TUI_SETUP_AUTH_SOURCE_CONFIG = "config";
|
||||||
129
src/tui/tui-launch.test.ts
Normal file
129
src/tui/tui-launch.test.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
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:9231",
|
||||||
|
"--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",
|
||||||
|
password: "test-password",
|
||||||
|
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",
|
||||||
|
"--password",
|
||||||
|
"test-password",
|
||||||
|
],
|
||||||
|
expect.objectContaining({ stdio: "inherit" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("launches compiled CLI shapes without repeating the current command", async () => {
|
||||||
|
process.argv[1] = "setup";
|
||||||
|
const child = createChildProcess();
|
||||||
|
spawnMock.mockImplementation((_cmd: string, _args: string[], _opts: SpawnOptions) => {
|
||||||
|
queueMicrotask(() => child.emit("exit", 0, null));
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
|
||||||
|
await launchTuiCli({ deliver: false });
|
||||||
|
|
||||||
|
expect(spawnMock).toHaveBeenCalledWith(
|
||||||
|
process.execPath,
|
||||||
|
["tui"],
|
||||||
|
expect.objectContaining({ stdio: "inherit" }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pins the child gateway URL and config auth source through env without adding url argv", async () => {
|
||||||
|
const child = createChildProcess();
|
||||||
|
spawnMock.mockImplementation((_cmd: string, _args: string[], _opts: SpawnOptions) => {
|
||||||
|
queueMicrotask(() => child.emit("exit", 0, null));
|
||||||
|
return child;
|
||||||
|
});
|
||||||
|
|
||||||
|
await launchTuiCli(
|
||||||
|
{ deliver: false },
|
||||||
|
{ authSource: "config", gatewayUrl: "ws://127.0.0.1:18789" },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(spawnMock).toHaveBeenCalledWith(
|
||||||
|
process.execPath,
|
||||||
|
["/repo/openclaw.mjs", "tui"],
|
||||||
|
expect.objectContaining({
|
||||||
|
env: expect.objectContaining({
|
||||||
|
OPENCLAW_GATEWAY_URL: "ws://127.0.0.1:18789",
|
||||||
|
OPENCLAW_TUI_SETUP_AUTH_SOURCE: "config",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
126
src/tui/tui-launch.ts
Normal file
126
src/tui/tui-launch.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import path from "node:path";
|
||||||
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
|
import { attachChildProcessBridge } from "../process/child-process-bridge.js";
|
||||||
|
import { TUI_SETUP_AUTH_SOURCE_CONFIG, TUI_SETUP_AUTH_SOURCE_ENV } from "./setup-launch-env.js";
|
||||||
|
import type { TuiOptions } from "./tui.js";
|
||||||
|
|
||||||
|
type TuiLaunchOptions = {
|
||||||
|
authSource?: "config";
|
||||||
|
gatewayUrl?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function appendOption(args: string[], flag: string, value: string | number | undefined): void {
|
||||||
|
if (value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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=")
|
||||||
|
) {
|
||||||
|
const next = execArgv[index + 1];
|
||||||
|
if (!arg.includes("=") && typeof next === "string" && !next.startsWith("-")) {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
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 buildCurrentCliEntryArgs(): string[] {
|
||||||
|
const entry = process.argv[1]?.trim();
|
||||||
|
if (!entry) {
|
||||||
|
throw new Error("unable to relaunch TUI: current CLI entry path is unavailable");
|
||||||
|
}
|
||||||
|
return path.isAbsolute(entry) ? [entry] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTuiCliArgs(opts: TuiOptions): string[] {
|
||||||
|
const args = [...filterTuiExecArgv(process.execArgv), ...buildCurrentCliEntryArgs(), "tui"];
|
||||||
|
appendOption(args, "--url", opts.url);
|
||||||
|
appendOption(args, "--token", opts.token);
|
||||||
|
appendOption(args, "--password", opts.password);
|
||||||
|
appendOption(args, "--session", opts.session);
|
||||||
|
appendOption(args, "--thinking", opts.thinking);
|
||||||
|
appendOption(args, "--message", opts.message);
|
||||||
|
appendOption(args, "--timeout-ms", opts.timeoutMs);
|
||||||
|
appendOption(args, "--history-limit", opts.historyLimit);
|
||||||
|
if (opts.deliver) {
|
||||||
|
args.push("--deliver");
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function launchTuiCli(
|
||||||
|
opts: TuiOptions,
|
||||||
|
launchOptions: TuiLaunchOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const args = buildTuiCliArgs(opts);
|
||||||
|
const env =
|
||||||
|
launchOptions.gatewayUrl || launchOptions.authSource
|
||||||
|
? {
|
||||||
|
...process.env,
|
||||||
|
...(launchOptions.gatewayUrl ? { OPENCLAW_GATEWAY_URL: launchOptions.gatewayUrl } : {}),
|
||||||
|
...(launchOptions.authSource === "config"
|
||||||
|
? { [TUI_SETUP_AUTH_SOURCE_ENV]: TUI_SETUP_AUTH_SOURCE_CONFIG }
|
||||||
|
: {}),
|
||||||
|
}
|
||||||
|
: process.env;
|
||||||
|
const stdinWasPaused =
|
||||||
|
typeof process.stdin.isPaused === "function" ? process.stdin.isPaused() : false;
|
||||||
|
|
||||||
|
process.stdin.pause();
|
||||||
|
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
const child = spawn(process.execPath, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
const { detach } = attachChildProcessBridge(child);
|
||||||
|
|
||||||
|
child.once("error", (error) => {
|
||||||
|
detach();
|
||||||
|
reject(new Error(`failed to launch TUI: ${formatErrorMessage(error)}`));
|
||||||
|
});
|
||||||
|
|
||||||
|
child.once("exit", (code, signal) => {
|
||||||
|
detach();
|
||||||
|
if (signal) {
|
||||||
|
reject(new Error(`TUI exited from signal ${signal}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((code ?? 0) !== 0) {
|
||||||
|
reject(new Error(`TUI exited with code ${code ?? 1}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}).finally(() => {
|
||||||
|
if (!stdinWasPaused) {
|
||||||
|
process.stdin.resume();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -4,7 +4,8 @@ import type { OpenClawConfig } from "../config/config.js";
|
|||||||
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
|
import type { PluginWebSearchProviderEntry } from "../plugins/types.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
|
||||||
const runTui = vi.hoisted(() => vi.fn(async () => {}));
|
const launchTuiCli = vi.hoisted(() => vi.fn(async () => {}));
|
||||||
|
const restoreTerminalState = vi.hoisted(() => vi.fn());
|
||||||
const probeGatewayReachable = vi.hoisted(() =>
|
const probeGatewayReachable = vi.hoisted(() =>
|
||||||
vi.fn<() => Promise<{ ok: boolean; detail?: string }>>(async () => ({ ok: true })),
|
vi.fn<() => Promise<{ ok: boolean; detail?: string }>>(async () => ({ ok: true })),
|
||||||
);
|
);
|
||||||
@@ -134,11 +135,11 @@ vi.mock("../infra/control-ui-assets.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../terminal/restore.js", () => ({
|
vi.mock("../terminal/restore.js", () => ({
|
||||||
restoreTerminalState: vi.fn(),
|
restoreTerminalState,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../tui/tui.js", () => ({
|
vi.mock("../tui/tui-launch.js", () => ({
|
||||||
runTui,
|
launchTuiCli,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./setup.secret-input.js", () => ({
|
vi.mock("./setup.secret-input.js", () => ({
|
||||||
@@ -236,7 +237,8 @@ function createAdvancedFinalizeArgs(params: AdvancedFinalizeArgs = {}) {
|
|||||||
|
|
||||||
describe("finalizeSetupWizard", () => {
|
describe("finalizeSetupWizard", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
runTui.mockClear();
|
launchTuiCli.mockClear();
|
||||||
|
restoreTerminalState.mockClear();
|
||||||
probeGatewayReachable.mockClear();
|
probeGatewayReachable.mockClear();
|
||||||
waitForGatewayReachable.mockReset();
|
waitForGatewayReachable.mockReset();
|
||||||
waitForGatewayReachable.mockResolvedValue({ ok: true });
|
waitForGatewayReachable.mockResolvedValue({ ok: true });
|
||||||
@@ -265,7 +267,7 @@ describe("finalizeSetupWizard", () => {
|
|||||||
listConfiguredWebSearchProviders.mockReturnValue([]);
|
listConfiguredWebSearchProviders.mockReturnValue([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resolves gateway password SecretRef for probe and TUI", async () => {
|
it("resolves gateway password SecretRef for probe but omits auth from TUI hatch", async () => {
|
||||||
const previous = process.env.OPENCLAW_GATEWAY_PASSWORD;
|
const previous = process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||||
process.env.OPENCLAW_GATEWAY_PASSWORD = "resolved-gateway-password"; // pragma: allowlist secret
|
process.env.OPENCLAW_GATEWAY_PASSWORD = "resolved-gateway-password"; // pragma: allowlist secret
|
||||||
resolveSetupSecretInputString.mockResolvedValueOnce("resolved-gateway-password");
|
resolveSetupSecretInputString.mockResolvedValueOnce("resolved-gateway-password");
|
||||||
@@ -337,14 +339,62 @@ describe("finalizeSetupWizard", () => {
|
|||||||
password: "resolved-gateway-password", // pragma: allowlist secret
|
password: "resolved-gateway-password", // pragma: allowlist secret
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(runTui).toHaveBeenCalledWith(
|
expect(launchTuiCli).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
{
|
||||||
url: "ws://127.0.0.1:18789",
|
deliver: false,
|
||||||
password: "resolved-gateway-password", // pragma: allowlist secret
|
message: undefined,
|
||||||
}),
|
},
|
||||||
|
{
|
||||||
|
authSource: "config",
|
||||||
|
gatewayUrl: "ws://127.0.0.1:18789",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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 () => {
|
it("does not persist resolved SecretRef token in daemon install plan", async () => {
|
||||||
const prompter = buildWizardPrompter({
|
const prompter = buildWizardPrompter({
|
||||||
select: vi.fn(async () => "later") as never,
|
select: vi.fn(async () => "later") as never,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { ensureControlUiAssetsBuilt } from "../infra/control-ui-assets.js";
|
|||||||
import { formatErrorMessage } from "../infra/errors.js";
|
import { formatErrorMessage } from "../infra/errors.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { restoreTerminalState } from "../terminal/restore.js";
|
import { restoreTerminalState } from "../terminal/restore.js";
|
||||||
import { runTui } from "../tui/tui.js";
|
import { launchTuiCli } from "../tui/tui-launch.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { listConfiguredWebSearchProviders } from "../web-search/runtime.js";
|
import { listConfiguredWebSearchProviders } from "../web-search/runtime.js";
|
||||||
import type { WizardPrompter } from "./prompts.js";
|
import type { WizardPrompter } from "./prompts.js";
|
||||||
@@ -423,14 +423,21 @@ export async function finalizeSetupWizard(
|
|||||||
|
|
||||||
if (hatchChoice === "tui") {
|
if (hatchChoice === "tui") {
|
||||||
restoreTerminalState("pre-setup tui", { resumeStdinIfPaused: true });
|
restoreTerminalState("pre-setup tui", { resumeStdinIfPaused: true });
|
||||||
await runTui({
|
try {
|
||||||
url: links.wsUrl,
|
await launchTuiCli(
|
||||||
token: settings.authMode === "token" ? settings.gatewayToken : undefined,
|
{
|
||||||
password: settings.authMode === "password" ? resolvedGatewayPassword : "",
|
// Safety: setup TUI should not auto-deliver to lastProvider/lastTo.
|
||||||
// Safety: setup TUI should not auto-deliver to lastProvider/lastTo.
|
deliver: false,
|
||||||
deliver: false,
|
message: hasBootstrap ? "Wake up, my friend!" : undefined,
|
||||||
message: hasBootstrap ? "Wake up, my friend!" : undefined,
|
},
|
||||||
});
|
{
|
||||||
|
authSource: "config",
|
||||||
|
gatewayUrl: links.wsUrl,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
restoreTerminalState("post-setup tui", { resumeStdinIfPaused: true });
|
||||||
|
}
|
||||||
launchedTui = true;
|
launchedTui = true;
|
||||||
} else if (hatchChoice === "web") {
|
} else if (hatchChoice === "web") {
|
||||||
const browserSupport = await detectBrowserOpenSupport();
|
const browserSupport = await detectBrowserOpenSupport();
|
||||||
|
|||||||
Reference in New Issue
Block a user