mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(cli): defer profile state creation past argument validation
This commit is contained in:
@@ -111,8 +111,12 @@ async function runCliProcess(params: {
|
||||
stateEnv?: (stateDir: string) => Record<string, string>;
|
||||
timeoutMs?: number;
|
||||
expectedExitCode?: number;
|
||||
pristineHome?: boolean;
|
||||
}) {
|
||||
const fixture = await createHelpProcessFixture(params.config);
|
||||
const fixture = await createHelpProcessFixture(params.pristineHome ? undefined : params.config);
|
||||
if (params.pristineHome) {
|
||||
await fs.rm(fixture.stateDir, { force: true, recursive: true });
|
||||
}
|
||||
if (params.stateEnv) {
|
||||
const lines = Object.entries(params.stateEnv(fixture.stateDir)).map(
|
||||
([key, value]) => `${key}=${value}`,
|
||||
@@ -151,9 +155,9 @@ async function runCliProcess(params: {
|
||||
NODE_ENV: undefined,
|
||||
NODE_OPTIONS: undefined,
|
||||
NODE_USE_SYSTEM_CA: "1",
|
||||
OPENCLAW_CONFIG_PATH: fixture.configPath,
|
||||
OPENCLAW_CONFIG_PATH: params.pristineHome ? undefined : fixture.configPath,
|
||||
OPENCLAW_NO_RESPAWN: params.allowRespawn ? undefined : "1",
|
||||
OPENCLAW_STATE_DIR: fixture.stateDir,
|
||||
OPENCLAW_STATE_DIR: params.pristineHome ? undefined : fixture.stateDir,
|
||||
VITEST: undefined,
|
||||
...params.env,
|
||||
},
|
||||
@@ -219,7 +223,7 @@ async function runCliProcess(params: {
|
||||
}),
|
||||
);
|
||||
}
|
||||
return { stderr, stdout };
|
||||
return { root: fixture.root, stderr, stdout };
|
||||
}
|
||||
|
||||
function parseJsonLines(stdout: string): Array<Record<string, unknown>> {
|
||||
@@ -370,6 +374,32 @@ describe("CLI help process exit", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("rejected CLI process state isolation", () => {
|
||||
it("does not scaffold a selected profile before option validation", async () => {
|
||||
const profile = "rejected-profile";
|
||||
const result = await runCliProcess({
|
||||
args: [
|
||||
"onboard",
|
||||
"--non-interactive",
|
||||
"--accept-risk",
|
||||
"--gateway-port",
|
||||
"99999",
|
||||
"--profile",
|
||||
profile,
|
||||
],
|
||||
expectedExitCode: 1,
|
||||
pristineHome: true,
|
||||
});
|
||||
|
||||
expect(result.stderr).toContain(
|
||||
"Error: --gateway-port must be an integer between 1 and 65535.",
|
||||
);
|
||||
await expect(fs.access(path.join(result.root, `.openclaw-${profile}`))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("JSON console style process output", () => {
|
||||
const loggingConfig = {
|
||||
logging: {
|
||||
|
||||
@@ -218,7 +218,7 @@ export async function runDoctorConfigPreflight(
|
||||
const gatewayStartupCheckpointRequired = options.requireStartupMigrationCheckpoint === true;
|
||||
const migrationCheckpointRequired =
|
||||
gatewayStartupCheckpointRequired || options.requireStateMigrationCheckpoint === true;
|
||||
const migrationCheckpoint = migrationCheckpointRequired
|
||||
let migrationCheckpoint = migrationCheckpointRequired
|
||||
? await measurePreflightStep(
|
||||
"startup-checkpoint-import",
|
||||
() => import("../infra/startup-migration-checkpoint.js"),
|
||||
@@ -342,6 +342,11 @@ export async function runDoctorConfigPreflight(
|
||||
skipPristineStartupStateMigrations = pristineStatePlan.skipAllStateMigrations;
|
||||
skipPristineCoreStateMigrations ||= pristineStatePlan.skipCoreStateMigrations;
|
||||
}
|
||||
if (skipPristineStartupStateMigrations && !gatewayStartupCheckpointRequired) {
|
||||
// A pristine non-Gateway command has nothing to checkpoint. Leave the state root absent
|
||||
// until command execution reaches a real state consumer.
|
||||
migrationCheckpoint = undefined;
|
||||
}
|
||||
// The gateway uses this last-moment guard to ensure its prepared config did not change before
|
||||
// any automatic migration mutates state. A rejected guard skips every state migration stage.
|
||||
const stateMigrationsAllowed =
|
||||
|
||||
@@ -106,6 +106,25 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
describe("pristine startup state", () => {
|
||||
it("accepts a missing explicitly selected profile root", () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-pristine-profile-"));
|
||||
roots.push(root);
|
||||
const stateDir = path.join(root, ".openclaw-typo");
|
||||
fs.mkdirSync(path.join(root, ".clawdbot"));
|
||||
|
||||
expect(
|
||||
planPristineStartupStateMigrations({
|
||||
HOME: root,
|
||||
OPENCLAW_CONFIG_PATH: path.join(stateDir, "openclaw.json"),
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
}),
|
||||
).toEqual({
|
||||
skipAllStateMigrations: true,
|
||||
skipCoreStateMigrations: true,
|
||||
});
|
||||
expect(fs.existsSync(stateDir)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts the core-only Gateway benchmark config", () => {
|
||||
const env = createFixture({
|
||||
browser: { enabled: false },
|
||||
|
||||
@@ -263,16 +263,20 @@ export function planPristineStartupStateMigrations(
|
||||
if (!homeDir) {
|
||||
return { skipAllStateMigrations: false, skipCoreStateMigrations: false };
|
||||
}
|
||||
const legacyStateAbsent = resolveLegacyStateDirs(() => homeDir).every((legacyDir) => {
|
||||
if (path.resolve(legacyDir) === path.resolve(stateDir)) {
|
||||
return false;
|
||||
}
|
||||
return !fs.existsSync(legacyDir);
|
||||
});
|
||||
const explicitStateDir = env.OPENCLAW_STATE_DIR?.trim();
|
||||
const legacyStateAbsent =
|
||||
Boolean(explicitStateDir) ||
|
||||
resolveLegacyStateDirs(() => homeDir).every((legacyDir) => {
|
||||
if (path.resolve(legacyDir) === path.resolve(stateDir)) {
|
||||
return false;
|
||||
}
|
||||
return !fs.existsSync(legacyDir);
|
||||
});
|
||||
if (!legacyStateAbsent) {
|
||||
return { skipAllStateMigrations: false, skipCoreStateMigrations: false };
|
||||
}
|
||||
const configPlan = planPristineStartupConfigMigrations(tryReadJsonSync(configPath), env);
|
||||
const config = fs.existsSync(configPath) ? tryReadJsonSync(configPath) : {};
|
||||
const configPlan = planPristineStartupConfigMigrations(config, env);
|
||||
return {
|
||||
skipAllStateMigrations: configPlan.skipAllStateMigrations,
|
||||
skipCoreStateMigrations: configPlan.skipCoreStateMigrations,
|
||||
|
||||
Reference in New Issue
Block a user