fix(onboard): stop a full reset from resetting the default workspace when the config is unreadable (#114110)

The unreadable-config guard tested !snapshot.sourceConfig, but
ConfigFileSnapshot.sourceConfig is required and every construction site
passes an object ({} when the read failed), so the guard never fired and
--reset-scope full fell through to its DEFAULT_WORKSPACE fallback.

Test it against snapshot.readError, the field io.snapshot.ts sets only when
the config file could not be read. A readable config that simply configures
no workspace keeps using the default.

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Masato Hoshino
2026-08-01 12:38:33 +09:00
committed by GitHub
parent 45c2d5911a
commit 554d786b7b
2 changed files with 34 additions and 1 deletions
+30
View File
@@ -5,6 +5,7 @@ import { formatCliCommand } from "../cli/command-format.js";
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 { setupWizardCommand } from "./onboard.js";
type ConfigSnapshotStub = {
@@ -12,6 +13,7 @@ type ConfigSnapshotStub = {
valid: boolean;
config: OpenClawConfig;
sourceConfig?: OpenClawConfig;
readError?: { code: string | null };
};
type ProviderAuthMethodNonInteractiveValidationContext = Parameters<
@@ -364,10 +366,14 @@ describe("setupWizardCommand", () => {
it("requires an explicit workspace for a full reset when config is unreadable", async () => {
const runtime = makeRuntime();
// readConfigFileSnapshot always returns a sourceConfig object, so an
// unreadable config is only recognizable through readError.
mocks.readConfigFileSnapshot.mockResolvedValue({
exists: true,
valid: false,
config: {},
sourceConfig: {},
readError: { code: "EACCES" },
});
await setupWizardCommand(
@@ -384,6 +390,30 @@ describe("setupWizardCommand", () => {
expect(mocks.handleReset).not.toHaveBeenCalled();
});
it("uses the default workspace for a full reset when a readable config configures none", async () => {
const runtime = makeRuntime();
mocks.readConfigFileSnapshot.mockResolvedValue({
exists: true,
valid: false,
config: {},
sourceConfig: { gateway: { port: 1 } },
});
await setupWizardCommand(
{
reset: true,
resetScope: "full",
},
runtime,
);
expect(mocks.handleReset).toHaveBeenCalledWith(
"full",
resolveUserPath("~/.openclaw/workspace"),
runtime,
);
});
it("accepts explicit --reset-scope full", async () => {
const runtime = makeRuntime();
+4 -1
View File
@@ -578,7 +578,10 @@ export async function setupWizardCommand(
normalizedOpts.workspace === undefined &&
snapshot.exists &&
!snapshot.valid &&
!snapshot.sourceConfig
// A snapshot always carries a sourceConfig object (empty on failure), so
// only readError distinguishes "config could not be read" from "config
// parsed but configures no workspace", where the default is correct.
snapshot.readError !== undefined
) {
rejectOption(
runtime,