mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:00:42 +00:00
fix: avoid duplicate gateway config loads
This commit is contained in:
@@ -27,6 +27,10 @@ const configState = vi.hoisted(() => ({
|
||||
cfg: {} as Record<string, unknown>,
|
||||
snapshot: { exists: false } as Record<string, unknown>,
|
||||
}));
|
||||
const readBestEffortConfig = vi.fn(async () => configState.cfg);
|
||||
const readConfigFileSnapshotWithPluginMetadata = vi.fn(async () => ({
|
||||
snapshot: configState.snapshot,
|
||||
}));
|
||||
const recoverConfigFromLastKnownGood = vi.fn<(params?: unknown) => Promise<boolean>>(
|
||||
async (_params?: unknown) => false,
|
||||
);
|
||||
@@ -52,8 +56,9 @@ const { runtimeErrors, defaultRuntime, resetRuntimeCapture } = createCliRuntimeC
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
getConfigPath: () => "/tmp/openclaw-test-missing-config.json",
|
||||
readBestEffortConfig: async () => configState.cfg,
|
||||
readBestEffortConfig: () => readBestEffortConfig(),
|
||||
readConfigFileSnapshot: async () => configState.snapshot,
|
||||
readConfigFileSnapshotWithPluginMetadata: () => readConfigFileSnapshotWithPluginMetadata(),
|
||||
recoverConfigFromLastKnownGood: (params: unknown) => recoverConfigFromLastKnownGood(params),
|
||||
recoverConfigFromJsonRootSuffix: (snapshot: unknown) => recoverConfigFromJsonRootSuffix(snapshot),
|
||||
}));
|
||||
@@ -186,6 +191,8 @@ describe("gateway run option collisions", () => {
|
||||
resetRuntimeCapture();
|
||||
configState.cfg = {};
|
||||
configState.snapshot = { exists: false };
|
||||
readBestEffortConfig.mockClear();
|
||||
readConfigFileSnapshotWithPluginMetadata.mockClear();
|
||||
controlUiState.root = "/tmp/openclaw-control-ui";
|
||||
gatewayLogMessages.length = 0;
|
||||
recoverConfigFromLastKnownGood.mockReset();
|
||||
@@ -306,10 +313,15 @@ describe("gateway run option collisions", () => {
|
||||
it("starts gateway when token mode has no configured token (startup bootstrap path)", async () => {
|
||||
await runGatewayCli(["gateway", "run", "--allow-unconfigured"]);
|
||||
|
||||
expect(readConfigFileSnapshotWithPluginMetadata).toHaveBeenCalledTimes(1);
|
||||
expect(readBestEffortConfig).not.toHaveBeenCalled();
|
||||
expect(startGatewayServer).toHaveBeenCalledWith(
|
||||
18789,
|
||||
expect.objectContaining({
|
||||
bind: "loopback",
|
||||
startupConfigSnapshotRead: {
|
||||
snapshot: configState.snapshot,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -339,7 +351,7 @@ describe("gateway run option collisions", () => {
|
||||
expect(writeDiagnosticStabilityBundleForFailureSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("blocks startup when the observed snapshot loses gateway.mode even if loadConfig still says local", async () => {
|
||||
it("blocks startup when the observed snapshot loses gateway.mode", async () => {
|
||||
configState.cfg = {
|
||||
gateway: {
|
||||
mode: "local",
|
||||
@@ -365,6 +377,7 @@ describe("gateway run option collisions", () => {
|
||||
`Config write audit: ${path.join("/tmp", "logs", "config-audit.jsonl")}`,
|
||||
);
|
||||
expect(startGatewayServer).not.toHaveBeenCalled();
|
||||
expect(readBestEffortConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("restores last-known-good config before startup when the effective config is invalid", async () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
GatewayAuthMode,
|
||||
GatewayBindMode,
|
||||
GatewayTailscaleMode,
|
||||
ReadConfigFileSnapshotWithPluginMetadataResult,
|
||||
} from "../../config/config.js";
|
||||
import { formatConfigIssueSummary } from "../../config/issue-format.js";
|
||||
import { CONFIG_PATH, resolveGatewayPort, resolveStateDir } from "../../config/paths.js";
|
||||
@@ -271,18 +272,21 @@ function getGatewayStartGuardErrors(params: {
|
||||
|
||||
async function readGatewayStartupConfig(params: {
|
||||
startupTrace: ReturnType<typeof createGatewayCliStartupTrace>;
|
||||
}): Promise<{ cfg: OpenClawConfig; snapshot: ConfigFileSnapshot | null }> {
|
||||
}): Promise<{
|
||||
cfg: OpenClawConfig;
|
||||
snapshot: ConfigFileSnapshot | null;
|
||||
startupConfigSnapshotRead?: ReadConfigFileSnapshotWithPluginMetadataResult;
|
||||
}> {
|
||||
const {
|
||||
readBestEffortConfig,
|
||||
readConfigFileSnapshot,
|
||||
readConfigFileSnapshotWithPluginMetadata,
|
||||
recoverConfigFromLastKnownGood,
|
||||
recoverConfigFromJsonRootSuffix,
|
||||
} = await import("../../config/config.js");
|
||||
let cfg = await params.startupTrace.measure("cli.config-load", () => readBestEffortConfig());
|
||||
let snapshot: ConfigFileSnapshot | null = await params.startupTrace.measure(
|
||||
"cli.config-snapshot",
|
||||
() => readConfigFileSnapshot().catch(() => null),
|
||||
);
|
||||
let snapshotRead: ReadConfigFileSnapshotWithPluginMetadataResult | null =
|
||||
await params.startupTrace.measure("cli.config-snapshot", () =>
|
||||
readConfigFileSnapshotWithPluginMetadata().catch(() => null),
|
||||
);
|
||||
let snapshot: ConfigFileSnapshot | null = snapshotRead?.snapshot ?? null;
|
||||
if (snapshot?.exists && !snapshot.valid) {
|
||||
const invalidSnapshot = snapshot;
|
||||
const recovered = await params.startupTrace.measure("cli.config-recovery", () =>
|
||||
@@ -317,9 +321,10 @@ async function readGatewayStartupConfig(params: {
|
||||
`gateway: failed to persist config auto-recovery notice: ${formatErrorMessage(err)}`,
|
||||
);
|
||||
}
|
||||
snapshot = await params.startupTrace.measure("cli.config-snapshot-reload", () =>
|
||||
readConfigFileSnapshot().catch(() => null),
|
||||
snapshotRead = await params.startupTrace.measure("cli.config-snapshot-reload", () =>
|
||||
readConfigFileSnapshotWithPluginMetadata().catch(() => null),
|
||||
);
|
||||
snapshot = snapshotRead?.snapshot ?? null;
|
||||
} else {
|
||||
const repaired = await params.startupTrace.measure("cli.config-prefix-recovery", () =>
|
||||
recoverConfigFromJsonRootSuffix(invalidSnapshot),
|
||||
@@ -328,16 +333,19 @@ async function readGatewayStartupConfig(params: {
|
||||
gatewayLog.warn(
|
||||
`gateway: repaired invalid effective config by stripping a non-JSON prefix: ${invalidSnapshot.path}`,
|
||||
);
|
||||
snapshot = await params.startupTrace.measure("cli.config-snapshot-reload", () =>
|
||||
readConfigFileSnapshot().catch(() => null),
|
||||
snapshotRead = await params.startupTrace.measure("cli.config-snapshot-reload", () =>
|
||||
readConfigFileSnapshotWithPluginMetadata().catch(() => null),
|
||||
);
|
||||
snapshot = snapshotRead?.snapshot ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (snapshot?.valid) {
|
||||
cfg = snapshot.config;
|
||||
}
|
||||
return { cfg, snapshot };
|
||||
const cfg = snapshot?.config ?? {};
|
||||
return {
|
||||
cfg,
|
||||
snapshot,
|
||||
...(snapshotRead ? { startupConfigSnapshotRead: snapshotRead } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveGatewayRunOptions(opts: GatewayRunOpts, command?: Command): GatewayRunOpts {
|
||||
@@ -563,7 +571,9 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
}
|
||||
|
||||
gatewayLog.info("loading configuration…");
|
||||
const { cfg, snapshot } = await readGatewayStartupConfig({ startupTrace });
|
||||
const { cfg, snapshot, startupConfigSnapshotRead } = await readGatewayStartupConfig({
|
||||
startupTrace,
|
||||
});
|
||||
void maybeLogPendingControlUiBuild(cfg).catch((err) => {
|
||||
gatewayLog.warn(`Control UI asset check failed: ${String(err)}`);
|
||||
});
|
||||
@@ -842,6 +852,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
auth: authOverride,
|
||||
tailscale: tailscaleOverride,
|
||||
startupStartedAt,
|
||||
...(startupConfigSnapshotRead ? { startupConfigSnapshotRead } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ export type {
|
||||
ConfigWriteFollowUp,
|
||||
RuntimeConfigSnapshotMetadata,
|
||||
} from "./runtime-snapshot.js";
|
||||
export type { ConfigWriteNotification } from "./io.js";
|
||||
export type {
|
||||
ConfigWriteNotification,
|
||||
ReadConfigFileSnapshotWithPluginMetadataResult,
|
||||
} from "./io.js";
|
||||
export { ConfigMutationConflictError, mutateConfigFile, replaceConfigFile } from "./mutate.js";
|
||||
export * from "./paths.js";
|
||||
export * from "./recovery-policy.js";
|
||||
|
||||
@@ -244,6 +244,42 @@ describe("gateway startup config recovery", () => {
|
||||
expect(log.info).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reuses a CLI preflight snapshot without rereading config", async () => {
|
||||
const snapshot = buildTestConfigSnapshot({
|
||||
path: configPath,
|
||||
exists: true,
|
||||
raw: `${JSON.stringify(validConfig)}\n`,
|
||||
parsed: validConfig,
|
||||
valid: true,
|
||||
config: validConfig,
|
||||
issues: [],
|
||||
legacyIssues: [],
|
||||
});
|
||||
const log = { info: vi.fn(), warn: vi.fn() };
|
||||
|
||||
await expect(
|
||||
loadGatewayStartupConfigSnapshot({
|
||||
minimalTestGateway: false,
|
||||
log,
|
||||
initialSnapshotRead: {
|
||||
snapshot,
|
||||
pluginMetadataSnapshot,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
snapshot,
|
||||
wroteConfig: false,
|
||||
pluginMetadataSnapshot,
|
||||
});
|
||||
|
||||
expect(configIo.readConfigFileSnapshotWithPluginMetadata).not.toHaveBeenCalled();
|
||||
expect(applyPluginAutoEnable).toHaveBeenCalledWith({
|
||||
config: validConfig,
|
||||
env: process.env,
|
||||
manifestRegistry: pluginManifestRegistry,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves empty model allowlist entries through startup auto-enable writes", async () => {
|
||||
const sourceConfig = {
|
||||
agents: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { loadAuthProfileStoreWithoutExternalProfiles } from "../agents/auth-profiles.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import {
|
||||
type ReadConfigFileSnapshotWithPluginMetadataResult,
|
||||
readConfigFileSnapshotWithPluginMetadata,
|
||||
recoverConfigFromLastKnownGood,
|
||||
recoverConfigFromJsonRootSuffix,
|
||||
@@ -202,11 +203,14 @@ export async function loadGatewayStartupConfigSnapshot(params: {
|
||||
minimalTestGateway: boolean;
|
||||
log: GatewayStartupLog;
|
||||
measure?: GatewayStartupConfigMeasure;
|
||||
initialSnapshotRead?: ReadConfigFileSnapshotWithPluginMetadataResult;
|
||||
}): Promise<GatewayStartupConfigSnapshotLoadResult> {
|
||||
const measure = params.measure ?? (async (_name, run) => await run());
|
||||
let snapshotRead = await measure("config.snapshot.read", () =>
|
||||
readConfigFileSnapshotWithPluginMetadata({ measure }),
|
||||
);
|
||||
let snapshotRead =
|
||||
params.initialSnapshotRead ??
|
||||
(await measure("config.snapshot.read", () =>
|
||||
readConfigFileSnapshotWithPluginMetadata({ measure }),
|
||||
));
|
||||
let configSnapshot = snapshotRead.snapshot;
|
||||
let pluginMetadataSnapshot = snapshotRead.pluginMetadataSnapshot;
|
||||
let wroteConfig = false;
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
readConfigFileSnapshot,
|
||||
recoverConfigFromLastKnownGood,
|
||||
registerConfigWriteListener,
|
||||
setRuntimeConfigSnapshot,
|
||||
type ReadConfigFileSnapshotWithPluginMetadataResult,
|
||||
} from "../config/io.js";
|
||||
import { replaceConfigFile } from "../config/mutate.js";
|
||||
import { isNixMode } from "../config/paths.js";
|
||||
@@ -456,6 +458,11 @@ export type GatewayServerOptions = {
|
||||
* Optional startup timestamp used for concise readiness logging.
|
||||
*/
|
||||
startupStartedAt?: number;
|
||||
/**
|
||||
* Config snapshot already read by the CLI gateway preflight. Passing it avoids
|
||||
* reparsing openclaw.json during server startup.
|
||||
*/
|
||||
startupConfigSnapshotRead?: ReadConfigFileSnapshotWithPluginMetadataResult;
|
||||
};
|
||||
|
||||
type SetupWizardRunner = NonNullable<GatewayServerOptions["wizardRunner"]>;
|
||||
@@ -491,6 +498,9 @@ export async function startGatewayServer(
|
||||
minimalTestGateway,
|
||||
log,
|
||||
measure: (name, run) => startupTrace.measure(name, run),
|
||||
...(opts.startupConfigSnapshotRead
|
||||
? { initialSnapshotRead: opts.startupConfigSnapshotRead }
|
||||
: {}),
|
||||
}),
|
||||
);
|
||||
const configSnapshot = startupConfigLoad.snapshot;
|
||||
@@ -585,6 +595,7 @@ export async function startGatewayServer(
|
||||
startupInternalWriteHash = startupSnapshot.hash ?? null;
|
||||
startupLastGoodSnapshot = startupSnapshot;
|
||||
}
|
||||
setRuntimeConfigSnapshot(cfgAtStart, startupLastGoodSnapshot.sourceConfig);
|
||||
const pluginBootstrap = await startupTrace.measure("plugins.bootstrap", () =>
|
||||
prepareGatewayPluginBootstrap({
|
||||
cfgAtStart,
|
||||
|
||||
Reference in New Issue
Block a user