fix(onboard): preserve named-agent profiles during config resets (#127716)

This commit is contained in:
Peter Steinberger
2026-08-21 17:42:40 -07:00
committed by GitHub
parent 01cd0a48db
commit 0a704810e4
2 changed files with 90 additions and 76 deletions
+73 -74
View File
@@ -6,6 +6,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js";
import type { RuntimeEnv } from "../runtime.js";
import { resolveUserPath } from "../utils.js";
import * as nonInteractiveApiKeys from "./onboard-non-interactive/api-keys.js";
import { setupWizardCommand } from "./onboard.js";
type ConfigSnapshotStub = {
@@ -261,12 +262,7 @@ describe("setupWizardCommand", () => {
it("defaults --reset to config+creds+sessions scope", async () => {
const runtime = makeRuntime();
await setupWizardCommand(
{
reset: true,
},
runtime,
);
await setupWizardCommand({ reset: true }, runtime);
expectResetCall({ scope: "config+creds+sessions", runtime });
});
@@ -324,12 +320,7 @@ describe("setupWizardCommand", () => {
},
});
await setupWizardCommand(
{
reset: true,
},
runtime,
);
await setupWizardCommand({ reset: true }, runtime);
expect(mocks.handleReset).toHaveBeenCalledWith(
"config+creds+sessions",
@@ -353,13 +344,7 @@ describe("setupWizardCommand", () => {
},
});
await setupWizardCommand(
{
reset: true,
resetScope: "full",
},
runtime,
);
await setupWizardCommand({ reset: true, resetScope: "full" }, runtime);
expect(mocks.handleReset).toHaveBeenCalledWith(
"full",
@@ -388,13 +373,7 @@ describe("setupWizardCommand", () => {
} as unknown as OpenClawConfig,
});
await setupWizardCommand(
{
reset: true,
resetScope: "full",
},
runtime,
);
await setupWizardCommand({ reset: true, resetScope: "full" }, runtime);
expect(runtime.error).toHaveBeenCalledWith(
"Configured workspace is invalid. Pass --workspace with the workspace to remove, or use a narrower --reset-scope.",
@@ -414,13 +393,7 @@ describe("setupWizardCommand", () => {
readError: { code: "EACCES" },
});
await setupWizardCommand(
{
reset: true,
resetScope: "full",
},
runtime,
);
await setupWizardCommand({ reset: true, resetScope: "full" }, runtime);
expect(runtime.error).toHaveBeenCalledWith(
"Cannot determine the configured workspace from an unreadable config. Pass --workspace with the workspace to remove, or use a narrower --reset-scope.",
@@ -437,13 +410,7 @@ describe("setupWizardCommand", () => {
sourceConfig: { gateway: { port: 1 } },
});
await setupWizardCommand(
{
reset: true,
resetScope: "full",
},
runtime,
);
await setupWizardCommand({ reset: true, resetScope: "full" }, runtime);
expect(mocks.handleReset).toHaveBeenCalledWith(
"full",
@@ -455,13 +422,7 @@ describe("setupWizardCommand", () => {
it("accepts explicit --reset-scope full", async () => {
const runtime = makeRuntime();
await setupWizardCommand(
{
reset: true,
resetScope: "full",
},
runtime,
);
await setupWizardCommand({ reset: true, resetScope: "full" }, runtime);
expectResetCall({ scope: "full", runtime });
});
@@ -490,12 +451,7 @@ describe("setupWizardCommand", () => {
it("rejects --reset-scope without --reset", async () => {
const runtime = makeRuntime();
await setupWizardCommand(
{
resetScope: "full",
},
runtime,
);
await setupWizardCommand({ resetScope: "full" }, runtime);
expect(runtime.error).toHaveBeenCalledWith(
"--reset-scope requires --reset. Re-run with openclaw onboard --reset --reset-scope full.",
@@ -876,6 +832,56 @@ describe("setupWizardCommand", () => {
expect(mocks.runNonInteractiveSetup).not.toHaveBeenCalled();
});
it.each([
{ agentName: "robby", agentId: "robby", scope: "config", reuseProfile: true },
{ agentName: "Robby!", agentId: "robby", scope: "config", reuseProfile: true },
{ agentName: undefined, agentId: "main", scope: "config", reuseProfile: true },
{ agentName: "robby", agentId: "robby", scope: "config+creds+sessions", reuseProfile: false },
{ agentName: "robby", agentId: "robby", scope: "full", reuseProfile: false },
] as const)(
"preflights $agentId provider profiles against reset scope $scope",
async ({ agentName, agentId, scope, reuseProfile }) => {
const runtime = makeRuntime();
const workspaceDir = "/tmp/openclaw-reset-agent-workspace";
const agentDirSuffix = path.join("agents", agentId, "agent");
const resolveApiKey = vi
.spyOn(nonInteractiveApiKeys, "resolveNonInteractiveApiKey")
.mockImplementation(async (input) =>
input.allowProfile && input.agentDir?.endsWith(agentDirSuffix)
? { key: "fixture-agent-profile-key", source: "profile" }
: null,
);
try {
await setupWizardCommand(
{
reset: true,
resetScope: scope,
nonInteractive: true,
acceptRisk: true,
agentName,
workspace: workspaceDir,
authChoice: "apiKey",
tokenProvider: "anthropic",
},
runtime,
);
expect(resolveApiKey).toHaveBeenCalledWith(
expect.objectContaining({
agentDir: expect.stringContaining(agentDirSuffix),
workspaceDir,
allowProfile: reuseProfile,
}),
);
expect(mocks.handleReset).toHaveBeenCalledTimes(reuseProfile ? 1 : 0);
expect(mocks.runNonInteractiveSetup).toHaveBeenCalledTimes(reuseProfile ? 1 : 0);
} finally {
resolveApiKey.mockRestore();
}
},
);
it.each(localResetProviderCases)(
"validates $providerId exactly once before reset and non-interactive setup",
async ({ providerId, methodId }) => {
@@ -1115,28 +1121,21 @@ describe("setupWizardCommand", () => {
expect(mocks.runInteractiveSetup).not.toHaveBeenCalled();
});
it("rejects conflicting classic and non-interactive modes", async () => {
it.each([
{
opts: { classic: true },
error:
"--classic cannot be combined with --non-interactive. Remove --non-interactive to open the classic wizard, or remove --classic for automated setup.",
},
{
opts: { tui: true },
error:
"--tui cannot be combined with --non-interactive. Remove --tui for automation, or remove --non-interactive to open the terminal hatch.",
},
])("rejects conflicting $opts and non-interactive modes", async ({ opts, error }) => {
const runtime = makeRuntime();
await setupWizardCommand({ classic: true, nonInteractive: true, acceptRisk: true }, runtime);
expect(runtime.error).toHaveBeenCalledWith(
"--classic cannot be combined with --non-interactive. Remove --non-interactive to open the classic wizard, or remove --classic for automated setup.",
);
expect(runtime.exit).toHaveBeenCalledWith(1);
expect(mocks.runNonInteractiveSetup).not.toHaveBeenCalled();
expect(mocks.runInteractiveSetup).not.toHaveBeenCalled();
expect(mocks.runGuidedOnboarding).not.toHaveBeenCalled();
});
it("rejects conflicting TUI and non-interactive modes", async () => {
const runtime = makeRuntime();
await setupWizardCommand({ tui: true, nonInteractive: true, acceptRisk: true }, runtime);
expect(runtime.error).toHaveBeenCalledWith(
"--tui cannot be combined with --non-interactive. Remove --tui for automation, or remove --non-interactive to open the terminal hatch.",
);
await setupWizardCommand({ ...opts, nonInteractive: true, acceptRisk: true }, runtime);
expect(runtime.error).toHaveBeenCalledWith(error);
expect(runtime.exit).toHaveBeenCalledWith(1);
expect(mocks.runNonInteractiveSetup).not.toHaveBeenCalled();
expect(mocks.runInteractiveSetup).not.toHaveBeenCalled();
+17 -2
View File
@@ -34,6 +34,7 @@ import {
import { formatAuthChoiceChoicesForCli } from "./auth-choice-options.js";
import { GENERIC_PROVIDER_AUTH_CHOICES } from "./auth-choice-options.static.js";
import { isGatewayDaemonRuntime } from "./daemon-runtime.js";
import { resolveOnboardingSetupTarget } from "./onboard-agent-target.js";
import {
applyCustomApiConfig,
CustomApiError,
@@ -329,7 +330,16 @@ async function validateResetAuthChoice(params: {
`Auth choice "${authChoice}" was not matched to provider "${params.opts.tokenProvider?.trim()}".`,
);
}
if (params.opts.nonInteractive && authChoice === "custom-api-key") {
if (!params.opts.nonInteractive || authChoice === "skip") {
return true;
}
const target = resolveOnboardingSetupTarget(
params.baseConfig,
params.opts.agentName
? { name: params.opts.agentName, workspaceDir: params.workspaceDir }
: undefined,
);
if (authChoice === "custom-api-key") {
try {
const custom = parseNonInteractiveCustomApiFlags({
baseUrl: params.opts.customBaseUrl,
@@ -351,6 +361,8 @@ async function validateResetAuthChoice(params: {
flagName: "--custom-api-key",
envVar: "CUSTOM_API_KEY",
runtime: params.runtime,
agentDir: target.agentDir,
workspaceDir: params.workspaceDir,
allowProfile: params.resetScope === "config",
required: false,
secretInputMode: params.opts.secretInputMode,
@@ -376,7 +388,7 @@ async function validateResetAuthChoice(params: {
return rejectOption(params.runtime, message);
}
}
if (params.opts.nonInteractive && authChoice !== "custom-api-key" && authChoice !== "skip") {
if (authChoice !== "custom-api-key") {
const runtimeProvider = providerAuthChoice
? resolveProviderMatch(
resolvePluginProviders({
@@ -412,12 +424,15 @@ async function validateResetAuthChoice(params: {
baseConfig: params.baseConfig,
opts: params.opts,
runtime: params.runtime,
agentDir: target.agentDir,
workspaceDir: params.workspaceDir,
resolveApiKey: async (input) =>
await resolveNonInteractiveCredential({
...input,
cfg: params.baseConfig,
runtime: params.runtime,
agentDir: target.agentDir,
workspaceDir: params.workspaceDir,
allowProfile: input.allowProfile === false ? false : params.resetScope === "config",
secretInputMode: params.opts.secretInputMode,
}),