mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
fix(onboard): preserve named-agent profiles during config resets (#127716)
This commit is contained in:
committed by
GitHub
parent
01cd0a48db
commit
0a704810e4
@@ -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
@@ -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,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user