Skip to content

Commit 51005d2

Browse files
committed
perf(cli): skip config load for default root help
1 parent dcfc7e5 commit 51005d2

2 files changed

Lines changed: 194 additions & 1 deletion

File tree

src/cli/root-help-live-config.test.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { beforeEach, describe, expect, it, vi } from "vitest";
1+
import fs from "node:fs";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
25
import { loadRootHelpRenderOptionsForConfigSensitivePlugins } from "./root-help-live-config.js";
36

47
const readConfigFileSnapshotMock = vi.hoisted(() => vi.fn());
@@ -8,10 +11,134 @@ vi.mock("../config/config.js", () => ({
811
}));
912

1013
describe("root help live config", () => {
14+
const originalEnv = {
15+
HOME: process.env.HOME,
16+
OPENCLAW_CONFIG_PATH: process.env.OPENCLAW_CONFIG_PATH,
17+
OPENCLAW_HOME: process.env.OPENCLAW_HOME,
18+
OPENCLAW_STATE_DIR: process.env.OPENCLAW_STATE_DIR,
19+
OPENCLAW_BUNDLED_PLUGINS_DIR: process.env.OPENCLAW_BUNDLED_PLUGINS_DIR,
20+
OPENCLAW_DISABLE_BUNDLED_PLUGINS: process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS,
21+
};
22+
1123
beforeEach(() => {
1224
vi.clearAllMocks();
1325
});
1426

27+
afterEach(() => {
28+
vi.restoreAllMocks();
29+
for (const [key, value] of Object.entries(originalEnv)) {
30+
if (value === undefined) {
31+
delete process.env[key];
32+
} else {
33+
process.env[key] = value;
34+
}
35+
}
36+
});
37+
38+
it("skips the heavy config module when no plugin-sensitive config file exists", async () => {
39+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-root-help-"));
40+
process.env.OPENCLAW_STATE_DIR = path.join(tempDir, "state");
41+
process.env.OPENCLAW_CONFIG_PATH = path.join(tempDir, "missing-openclaw.json");
42+
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
43+
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
44+
45+
await expect(
46+
loadRootHelpRenderOptionsForConfigSensitivePlugins(process.env),
47+
).resolves.toBeNull();
48+
49+
expect(readConfigFileSnapshotMock).not.toHaveBeenCalled();
50+
fs.rmSync(tempDir, { recursive: true, force: true });
51+
});
52+
53+
it("checks live config when plugin-sensitive env only exists in legacy gateway env", async () => {
54+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-root-help-"));
55+
try {
56+
const cwdDir = path.join(tempDir, "cwd");
57+
const gatewayEnvPath = path.join(tempDir, ".config", "openclaw", "gateway.env");
58+
fs.mkdirSync(cwdDir, { recursive: true });
59+
fs.mkdirSync(path.dirname(gatewayEnvPath), { recursive: true });
60+
fs.writeFileSync(gatewayEnvPath, "OPENCLAW_DISABLE_BUNDLED_PLUGINS=1\n", "utf8");
61+
vi.spyOn(process, "cwd").mockReturnValue(cwdDir);
62+
process.env.HOME = tempDir;
63+
process.env.OPENCLAW_STATE_DIR = path.join(tempDir, ".openclaw");
64+
delete process.env.OPENCLAW_CONFIG_PATH;
65+
delete process.env.OPENCLAW_HOME;
66+
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
67+
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
68+
readConfigFileSnapshotMock.mockResolvedValueOnce({
69+
valid: true,
70+
sourceConfig: {},
71+
runtimeConfig: {},
72+
});
73+
74+
await expect(
75+
loadRootHelpRenderOptionsForConfigSensitivePlugins(process.env),
76+
).resolves.toBeNull();
77+
78+
expect(readConfigFileSnapshotMock).toHaveBeenCalledOnce();
79+
} finally {
80+
fs.rmSync(tempDir, { recursive: true, force: true });
81+
}
82+
});
83+
84+
it("ignores legacy gateway env during precheck for an explicit custom state dir", async () => {
85+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-root-help-"));
86+
try {
87+
const cwdDir = path.join(tempDir, "cwd");
88+
const gatewayEnvPath = path.join(tempDir, ".config", "openclaw", "gateway.env");
89+
fs.mkdirSync(cwdDir, { recursive: true });
90+
fs.mkdirSync(path.dirname(gatewayEnvPath), { recursive: true });
91+
fs.writeFileSync(gatewayEnvPath, "OPENCLAW_DISABLE_BUNDLED_PLUGINS=1\n", "utf8");
92+
vi.spyOn(process, "cwd").mockReturnValue(cwdDir);
93+
process.env.HOME = tempDir;
94+
process.env.OPENCLAW_STATE_DIR = path.join(tempDir, "custom-state");
95+
process.env.OPENCLAW_CONFIG_PATH = path.join(tempDir, "missing-openclaw.json");
96+
delete process.env.OPENCLAW_HOME;
97+
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
98+
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
99+
100+
await expect(
101+
loadRootHelpRenderOptionsForConfigSensitivePlugins(process.env),
102+
).resolves.toBeNull();
103+
104+
expect(readConfigFileSnapshotMock).not.toHaveBeenCalled();
105+
} finally {
106+
fs.rmSync(tempDir, { recursive: true, force: true });
107+
}
108+
});
109+
110+
it("checks live config when plugin-sensitive env exists beside an explicit config path", async () => {
111+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-root-help-"));
112+
try {
113+
const cwdDir = path.join(tempDir, "cwd");
114+
const configPath = path.join(tempDir, "custom", "missing-openclaw.json");
115+
const configDirEnvPath = path.join(path.dirname(configPath), ".env");
116+
fs.mkdirSync(cwdDir, { recursive: true });
117+
fs.mkdirSync(path.dirname(configDirEnvPath), { recursive: true });
118+
fs.writeFileSync(configDirEnvPath, "OPENCLAW_DISABLE_BUNDLED_PLUGINS=1\n", "utf8");
119+
vi.spyOn(process, "cwd").mockReturnValue(cwdDir);
120+
process.env.HOME = tempDir;
121+
process.env.OPENCLAW_CONFIG_PATH = configPath;
122+
delete process.env.OPENCLAW_STATE_DIR;
123+
delete process.env.OPENCLAW_HOME;
124+
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
125+
delete process.env.OPENCLAW_DISABLE_BUNDLED_PLUGINS;
126+
readConfigFileSnapshotMock.mockResolvedValueOnce({
127+
valid: true,
128+
sourceConfig: {},
129+
runtimeConfig: {},
130+
});
131+
132+
await expect(
133+
loadRootHelpRenderOptionsForConfigSensitivePlugins(process.env),
134+
).resolves.toBeNull();
135+
136+
expect(readConfigFileSnapshotMock).toHaveBeenCalledOnce();
137+
} finally {
138+
fs.rmSync(tempDir, { recursive: true, force: true });
139+
}
140+
});
141+
15142
it("uses precomputed help when plugin-sensitive config is invalid", async () => {
16143
readConfigFileSnapshotMock.mockResolvedValueOnce({
17144
valid: false,

src/cli/root-help-live-config.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
import fs from "node:fs";
2+
import os from "node:os";
3+
import path from "node:path";
4+
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
15
import type { OpenClawConfig } from "../config/types.openclaw.js";
6+
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
27
import type { RootHelpRenderOptions } from "./program/root-help.js";
38

49
function hasEntries(value: object | undefined): boolean {
@@ -32,9 +37,70 @@ export function hasPluginHelpAffectingEnv(env: NodeJS.ProcessEnv): boolean {
3237
);
3338
}
3439

40+
function hasPotentialDotEnv(env: NodeJS.ProcessEnv): boolean {
41+
try {
42+
if (fs.existsSync(path.join(process.cwd(), ".env"))) {
43+
return true;
44+
}
45+
if (fs.existsSync(path.join(resolveStateDir(env), ".env"))) {
46+
return true;
47+
}
48+
if (hasConfigPathDotEnv(env)) {
49+
return true;
50+
}
51+
return hasLegacyGatewayDotEnv(env);
52+
} catch {
53+
return true;
54+
}
55+
}
56+
57+
function hasConfigPathDotEnv(env: NodeJS.ProcessEnv): boolean {
58+
if (!env.OPENCLAW_CONFIG_PATH?.trim()) {
59+
return false;
60+
}
61+
return fs.existsSync(
62+
path.join(path.dirname(resolveConfigPath(env, resolveStateDir(env))), ".env"),
63+
);
64+
}
65+
66+
function hasExplicitNonDefaultStateDir(env: NodeJS.ProcessEnv): boolean {
67+
const override = env.OPENCLAW_STATE_DIR?.trim();
68+
if (!override) {
69+
return false;
70+
}
71+
const homeDir = resolveRequiredHomeDir(env, os.homedir);
72+
const stateDir = resolveStateDir(env, () => homeDir);
73+
const defaultStateDir = path.join(homeDir, ".openclaw");
74+
return path.resolve(stateDir) !== path.resolve(defaultStateDir);
75+
}
76+
77+
function hasLegacyGatewayDotEnv(env: NodeJS.ProcessEnv): boolean {
78+
if (hasExplicitNonDefaultStateDir(env)) {
79+
return false;
80+
}
81+
const homeDir = resolveRequiredHomeDir(env, os.homedir);
82+
return fs.existsSync(path.join(homeDir, ".config", "openclaw", "gateway.env"));
83+
}
84+
85+
function hasConfigFile(env: NodeJS.ProcessEnv): boolean {
86+
try {
87+
return fs.existsSync(resolveConfigPath(env, resolveStateDir(env)));
88+
} catch {
89+
return true;
90+
}
91+
}
92+
3593
export async function loadRootHelpRenderOptionsForConfigSensitivePlugins(
3694
env: NodeJS.ProcessEnv = process.env,
3795
): Promise<RootHelpRenderOptions | null> {
96+
if (
97+
env === process.env &&
98+
!hasPluginHelpAffectingEnv(env) &&
99+
!hasPotentialDotEnv(env) &&
100+
!hasConfigFile(env)
101+
) {
102+
return null;
103+
}
38104
const configModule = await import("../config/config.js");
39105
const snapshot = await configModule.readConfigFileSnapshot({
40106
observe: false,

0 commit comments

Comments
 (0)