mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 21:11:40 +00:00
fix(onboard): preflight TTY before reset (#116491)
This commit is contained in:
committed by
GitHub
parent
3b355879b8
commit
7f7534253f
@@ -22,6 +22,7 @@ const mocks = vi.hoisted(() => ({
|
||||
runInteractiveSetup: vi.fn(async () => {}),
|
||||
runGuidedOnboarding: vi.fn(async () => {}),
|
||||
runNonInteractiveSetup: vi.fn(async () => {}),
|
||||
hasInteractiveOnboardingTty: vi.fn(() => true),
|
||||
resolvePluginProviders: vi.fn((): ProviderPlugin[] => [
|
||||
{
|
||||
id: "anthropic",
|
||||
@@ -90,6 +91,10 @@ vi.mock("./onboard-non-interactive.js", () => ({
|
||||
runNonInteractiveSetup: mocks.runNonInteractiveSetup,
|
||||
}));
|
||||
|
||||
vi.mock("./onboard-interactive-runner.js", () => ({
|
||||
hasInteractiveOnboardingTty: mocks.hasInteractiveOnboardingTty,
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
readConfigFileSnapshot: mocks.readConfigFileSnapshot,
|
||||
resolveGatewayPort: () => 18_789,
|
||||
@@ -173,6 +178,7 @@ describe("setupWizardCommand", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.clearAllMocks();
|
||||
mocks.hasInteractiveOnboardingTty.mockReturnValue(true);
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({ exists: false, valid: false, config: {} });
|
||||
});
|
||||
|
||||
@@ -228,6 +234,42 @@ describe("setupWizardCommand", () => {
|
||||
expectResetCall({ scope: "config+creds+sessions", runtime });
|
||||
});
|
||||
|
||||
it.each([
|
||||
["guided", { reset: true }],
|
||||
["classic", { reset: true, classic: true }],
|
||||
] as const)("rejects headless %s onboarding before reset", async (_label, options) => {
|
||||
const runtime = makeRuntime();
|
||||
mocks.hasInteractiveOnboardingTty.mockReturnValue(false);
|
||||
|
||||
await setupWizardCommand(options, runtime);
|
||||
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
"Onboarding needs an interactive TTY. Use `openclaw onboard --non-interactive --accept-risk ...` for automation.",
|
||||
);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
expect(mocks.readConfigFileSnapshot).not.toHaveBeenCalled();
|
||||
expect(mocks.handleReset).not.toHaveBeenCalled();
|
||||
expect(mocks.runGuidedOnboarding).not.toHaveBeenCalled();
|
||||
expect(mocks.runInteractiveSetup).not.toHaveBeenCalled();
|
||||
expect(mocks.runNonInteractiveSetup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps non-interactive reset ordering without a TTY", async () => {
|
||||
const runtime = makeRuntime();
|
||||
mocks.hasInteractiveOnboardingTty.mockReturnValue(false);
|
||||
|
||||
await setupWizardCommand({ reset: true, nonInteractive: true, acceptRisk: true }, runtime);
|
||||
|
||||
expect(mocks.handleReset).toHaveBeenCalledOnce();
|
||||
expect(mocks.runNonInteractiveSetup).toHaveBeenCalledOnce();
|
||||
const resetOrder = mocks.handleReset.mock.invocationCallOrder[0];
|
||||
const setupOrder = mocks.runNonInteractiveSetup.mock.invocationCallOrder[0];
|
||||
if (resetOrder === undefined || setupOrder === undefined) {
|
||||
throw new Error("expected reset and non-interactive setup calls");
|
||||
}
|
||||
expect(resetOrder).toBeLessThan(setupOrder);
|
||||
});
|
||||
|
||||
it("uses configured default workspace for --reset when --workspace is not provided", async () => {
|
||||
const runtime = makeRuntime();
|
||||
mocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
|
||||
@@ -22,6 +22,7 @@ import { resolveProviderInstallCatalogEntries } from "../plugins/provider-instal
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { t } from "../wizard/i18n/index.js";
|
||||
import {
|
||||
formatDeprecatedNonInteractiveAuthChoiceError,
|
||||
isDeprecatedAuthChoice,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
} from "./onboard-custom-config.js";
|
||||
import { runGuidedOnboarding } from "./onboard-guided.js";
|
||||
import { DEFAULT_WORKSPACE, handleReset } from "./onboard-helpers.js";
|
||||
import { hasInteractiveOnboardingTty } from "./onboard-interactive-runner.js";
|
||||
import { runInteractiveSetup } from "./onboard-interactive.js";
|
||||
import { runNonInteractiveSetup } from "./onboard-non-interactive.js";
|
||||
import { resolveNonInteractiveApiKey as resolveNonInteractiveCredential } from "./onboard-non-interactive/api-keys.js";
|
||||
@@ -536,6 +538,14 @@ export async function setupWizardCommand(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!normalizedOpts.nonInteractive && !hasInteractiveOnboardingTty()) {
|
||||
// Reset is destructive, so prove the selected interactive surface can run
|
||||
// before reading or moving any operator state.
|
||||
runtime.error(t("wizard.guided.ttyRequired"));
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
runtime.log(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user