From 554d786b7ba080ae205e851e019267188db9df42 Mon Sep 17 00:00:00 2001 From: Masato Hoshino Date: Sat, 1 Aug 2026 12:38:33 +0900 Subject: [PATCH] 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 --- src/commands/onboard.test.ts | 30 ++++++++++++++++++++++++++++++ src/commands/onboard.ts | 5 ++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/commands/onboard.test.ts b/src/commands/onboard.test.ts index 07adc2505a23..5ee8ec3def69 100644 --- a/src/commands/onboard.test.ts +++ b/src/commands/onboard.test.ts @@ -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(); diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts index 0c2755a00266..2e34e49d6ab2 100644 --- a/src/commands/onboard.ts +++ b/src/commands/onboard.ts @@ -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,