refactor(config): use source snapshots for config writes

This commit is contained in:
Peter Steinberger
2026-03-30 00:39:21 +01:00
parent c5baf63fa5
commit a27ccee5d9
18 changed files with 154 additions and 72 deletions

View File

@@ -3,12 +3,12 @@ import type { OpenClawConfig } from "../../config/config.js";
const mocks = vi.hoisted(() => ({
readConfigFileSnapshot: vi.fn(),
writeConfigFile: vi.fn(),
replaceConfigFile: vi.fn(),
}));
vi.mock("../../config/config.js", () => ({
readConfigFileSnapshot: (...args: unknown[]) => mocks.readConfigFileSnapshot(...args),
writeConfigFile: (...args: unknown[]) => mocks.writeConfigFile(...args),
replaceConfigFile: (...args: unknown[]) => mocks.replaceConfigFile(...args),
}));
let loadValidConfigOrThrow: typeof import("./shared.js").loadValidConfigOrThrow;
@@ -18,7 +18,7 @@ describe("models/shared", () => {
beforeEach(async () => {
vi.resetModules();
mocks.readConfigFileSnapshot.mockClear();
mocks.writeConfigFile.mockClear();
mocks.replaceConfigFile.mockClear();
({ loadValidConfigOrThrow, updateConfig } = await import("./shared.js"));
});
@@ -26,6 +26,7 @@ describe("models/shared", () => {
const cfg = { providers: {} } as unknown as OpenClawConfig;
mocks.readConfigFileSnapshot.mockResolvedValue({
valid: true,
runtimeConfig: cfg,
config: cfg,
});
@@ -48,19 +49,22 @@ describe("models/shared", () => {
const cfg = { update: { channel: "stable" } } as unknown as OpenClawConfig;
mocks.readConfigFileSnapshot.mockResolvedValue({
valid: true,
hash: "config-1",
sourceConfig: cfg,
config: cfg,
});
mocks.writeConfigFile.mockResolvedValue(undefined);
mocks.replaceConfigFile.mockResolvedValue(undefined);
await updateConfig((current) => ({
...current,
update: { channel: "beta" },
}));
expect(mocks.writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
expect(mocks.replaceConfigFile).toHaveBeenCalledWith({
nextConfig: expect.objectContaining({
update: { channel: "beta" },
}),
);
baseHash: "config-1",
});
});
});

View File

@@ -11,7 +11,7 @@ import { formatCliCommand } from "../../cli/command-format.js";
import {
type OpenClawConfig,
readConfigFileSnapshot,
writeConfigFile,
replaceConfigFile,
} from "../../config/config.js";
import { formatConfigIssueLines } from "../../config/issue-format.js";
import { toAgentModelListLike } from "../../config/model-input.js";
@@ -70,15 +70,22 @@ export async function loadValidConfigOrThrow(): Promise<OpenClawConfig> {
const issues = formatConfigIssueLines(snapshot.issues, "-").join("\n");
throw new Error(`Invalid config at ${snapshot.path}\n${issues}`);
}
return snapshot.config;
return snapshot.runtimeConfig ?? snapshot.config;
}
export async function updateConfig(
mutator: (cfg: OpenClawConfig) => OpenClawConfig,
): Promise<OpenClawConfig> {
const config = await loadValidConfigOrThrow();
const next = mutator(config);
await writeConfigFile(next);
const snapshot = await readConfigFileSnapshot();
if (!snapshot.valid) {
const issues = formatConfigIssueLines(snapshot.issues, "-").join("\n");
throw new Error(`Invalid config at ${snapshot.path}\n${issues}`);
}
const next = mutator(structuredClone(snapshot.sourceConfig ?? snapshot.config));
await replaceConfigFile({
nextConfig: next,
baseHash: snapshot.hash,
});
return next;
}