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