fix: avoid duplicate gateway config loads

This commit is contained in:
Peter Steinberger
2026-05-02 15:49:05 +01:00
parent 10ebcbdb99
commit 5980040894
6 changed files with 101 additions and 23 deletions

View File

@@ -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 () => {

View File

@@ -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 } : {}),
}),
});