refactor(models): reuse validated config snapshot loader

This commit is contained in:
Peter Steinberger
2026-02-18 22:49:21 +00:00
parent 61c0c147ad
commit 8369913c7a
3 changed files with 74 additions and 13 deletions

View File

@@ -10,7 +10,6 @@ import { normalizeProviderId } from "../../agents/model-selection.js";
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { parseDurationMs } from "../../cli/parse-duration.js";
import { readConfigFileSnapshot } from "../../config/config.js";
import { logConfigUpdated } from "../../config/logging.js";
import { resolvePluginProviders } from "../../plugins/providers.js";
import type { ProviderAuthResult, ProviderPlugin } from "../../plugins/types.js";
@@ -28,7 +27,7 @@ import {
pickAuthMethod,
resolveProviderMatch,
} from "../provider-auth-helpers.js";
import { updateConfig } from "./shared.js";
import { loadValidConfigOrThrow, updateConfig } from "./shared.js";
const confirm = (params: Parameters<typeof clackConfirm>[0]) =>
clackConfirm({
@@ -278,13 +277,7 @@ export async function modelsAuthLoginCommand(opts: LoginOptions, runtime: Runtim
throw new Error("models auth login requires an interactive TTY.");
}
const snapshot = await readConfigFileSnapshot();
if (!snapshot.valid) {
const issues = snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n");
throw new Error(`Invalid config at ${snapshot.path}\n${issues}`);
}
const config = snapshot.config;
const config = await loadValidConfigOrThrow();
const defaultAgentId = resolveDefaultAgentId(config);
const agentDir = resolveAgentDir(config, defaultAgentId);
const workspaceDir =

View File

@@ -0,0 +1,63 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
const mocks = vi.hoisted(() => ({
readConfigFileSnapshot: vi.fn(),
writeConfigFile: vi.fn(),
}));
vi.mock("../../config/config.js", () => ({
readConfigFileSnapshot: (...args: unknown[]) => mocks.readConfigFileSnapshot(...args),
writeConfigFile: (...args: unknown[]) => mocks.writeConfigFile(...args),
}));
import { loadValidConfigOrThrow, updateConfig } from "./shared.js";
describe("models/shared", () => {
beforeEach(() => {
mocks.readConfigFileSnapshot.mockReset();
mocks.writeConfigFile.mockReset();
});
it("returns config when snapshot is valid", async () => {
const cfg = { providers: {} } as unknown as OpenClawConfig;
mocks.readConfigFileSnapshot.mockResolvedValue({
valid: true,
config: cfg,
});
await expect(loadValidConfigOrThrow()).resolves.toBe(cfg);
});
it("throws formatted issues when snapshot is invalid", async () => {
mocks.readConfigFileSnapshot.mockResolvedValue({
valid: false,
path: "/tmp/openclaw.json",
issues: [{ path: "providers.openai.apiKey", message: "Required" }],
});
await expect(loadValidConfigOrThrow()).rejects.toThrowError(
"Invalid config at /tmp/openclaw.json\n- providers.openai.apiKey: Required",
);
});
it("updateConfig writes mutated config", async () => {
const cfg = { update: { channel: "stable" } } as unknown as OpenClawConfig;
mocks.readConfigFileSnapshot.mockResolvedValue({
valid: true,
config: cfg,
});
mocks.writeConfigFile.mockResolvedValue(undefined);
await updateConfig((current) => ({
...current,
update: { channel: "beta" },
}));
expect(mocks.writeConfigFile).toHaveBeenCalledWith(
expect.objectContaining({
update: { channel: "beta" },
}),
);
});
});

View File

@@ -59,15 +59,20 @@ export const isLocalBaseUrl = (baseUrl: string) => {
}
};
export async function updateConfig(
mutator: (cfg: OpenClawConfig) => OpenClawConfig,
): Promise<OpenClawConfig> {
export async function loadValidConfigOrThrow(): Promise<OpenClawConfig> {
const snapshot = await readConfigFileSnapshot();
if (!snapshot.valid) {
const issues = snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n");
throw new Error(`Invalid config at ${snapshot.path}\n${issues}`);
}
const next = mutator(snapshot.config);
return snapshot.config;
}
export async function updateConfig(
mutator: (cfg: OpenClawConfig) => OpenClawConfig,
): Promise<OpenClawConfig> {
const config = await loadValidConfigOrThrow();
const next = mutator(config);
await writeConfigFile(next);
return next;
}