fix: resolve live config paths in status and gateway metadata (#39952)

* fix: resolve live config paths in status and gateway metadata

* fix: resolve remaining runtime config path references

* test: cover gateway config.set config path response
This commit is contained in:
Tak Hoffman
2026-03-08 09:59:32 -05:00
committed by GitHub
parent da3cccb212
commit d9e8e8ac15
8 changed files with 73 additions and 15 deletions

View File

@@ -25,7 +25,7 @@ import {
} from "../../agents/model-selection.js";
import { formatCliCommand } from "../../cli/command-format.js";
import { withProgressTotals } from "../../cli/progress.js";
import { CONFIG_PATH } from "../../config/config.js";
import { createConfigIO } from "../../config/config.js";
import {
resolveAgentModelFallbackValues,
resolveAgentModelPrimaryValue,
@@ -77,6 +77,7 @@ export async function modelsStatusCommand(
if (opts.plain && opts.probe) {
throw new Error("--probe cannot be used with --plain output.");
}
const configPath = createConfigIO().configPath;
const cfg = await loadModelsConfig({ commandName: "models status", runtime });
const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent });
const agentDir = agentId ? resolveAgentDir(cfg, agentId) : resolveOpenClawAgentDir();
@@ -326,7 +327,7 @@ export async function modelsStatusCommand(
runtime.log(
JSON.stringify(
{
configPath: CONFIG_PATH,
configPath,
...(agentId ? { agentId } : {}),
agentDir,
defaultModel: defaultLabel,
@@ -389,7 +390,7 @@ export async function modelsStatusCommand(
rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel;
runtime.log(
`${label("Config")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, shortenHomePath(CONFIG_PATH))}`,
`${label("Config")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, shortenHomePath(configPath))}`,
);
runtime.log(
`${label("Agent dir")}${colorize(rich, theme.muted, ":")} ${colorize(

View File

@@ -64,6 +64,9 @@ const mocks = vi.hoisted(() => {
getCustomProviderApiKey: vi.fn().mockReturnValue(undefined),
getShellEnvAppliedKeys: vi.fn().mockReturnValue(["OPENAI_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]),
shouldEnableShellEnvFallback: vi.fn().mockReturnValue(true),
createConfigIO: vi.fn().mockReturnValue({
configPath: "/tmp/openclaw-dev/openclaw.json",
}),
loadConfig: vi.fn().mockReturnValue({
agents: {
defaults: {
@@ -115,6 +118,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../config/config.js")>();
return {
...actual,
createConfigIO: mocks.createConfigIO,
loadConfig: mocks.loadConfig,
};
});
@@ -200,6 +204,7 @@ describe("modelsStatusCommand auth overview", () => {
expect(mocks.resolveOpenClawAgentDir).toHaveBeenCalled();
expect(payload.defaultModel).toBe("anthropic/claude-opus-4-5");
expect(payload.configPath).toBe("/tmp/openclaw-dev/openclaw.json");
expect(payload.auth.storePath).toBe("/tmp/openclaw-agent/auth-profiles.json");
expect(payload.auth.shellEnvFallback.enabled).toBe(true);
expect(payload.auth.shellEnvFallback.appliedKeys).toContain("OPENAI_API_KEY");

View File

@@ -0,0 +1,25 @@
import { describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
createConfigIO: vi.fn().mockReturnValue({
configPath: "/tmp/openclaw-dev/openclaw.json",
}),
}));
vi.mock("./io.js", () => ({
createConfigIO: mocks.createConfigIO,
}));
import { formatConfigPath, logConfigUpdated } from "./logging.js";
describe("config logging", () => {
it("formats the live config path when no explicit path is provided", () => {
expect(formatConfigPath()).toBe("/tmp/openclaw-dev/openclaw.json");
});
it("logs the live config path when no explicit path is provided", () => {
const runtime = { log: vi.fn() };
logConfigUpdated(runtime as never);
expect(runtime.log).toHaveBeenCalledWith("Updated /tmp/openclaw-dev/openclaw.json");
});
});

View File

@@ -1,18 +1,18 @@
import type { RuntimeEnv } from "../runtime.js";
import { displayPath } from "../utils.js";
import { CONFIG_PATH } from "./paths.js";
import { createConfigIO } from "./io.js";
type LogConfigUpdatedOptions = {
path?: string;
suffix?: string;
};
export function formatConfigPath(path: string = CONFIG_PATH): string {
export function formatConfigPath(path: string = createConfigIO().configPath): string {
return displayPath(path);
}
export function logConfigUpdated(runtime: RuntimeEnv, opts: LogConfigUpdatedOptions = {}): void {
const path = formatConfigPath(opts.path ?? CONFIG_PATH);
const path = formatConfigPath(opts.path ?? createConfigIO().configPath);
const suffix = opts.suffix ? ` ${opts.suffix}` : "";
runtime.log(`Updated ${path}${suffix}`);
}

View File

@@ -1,7 +1,7 @@
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { listChannelPlugins } from "../../channels/plugins/index.js";
import {
CONFIG_PATH,
createConfigIO,
loadConfig,
parseConfigJson5,
readConfigFileSnapshot,
@@ -197,6 +197,7 @@ function buildConfigRestartSentinelPayload(params: {
threadId: ReturnType<typeof extractDeliveryInfo>["threadId"];
note: string | undefined;
}): RestartSentinelPayload {
const configPath = createConfigIO().configPath;
return {
kind: params.kind,
status: "ok",
@@ -208,7 +209,7 @@ function buildConfigRestartSentinelPayload(params: {
doctorHint: formatDoctorNonInteractiveHint(),
stats: {
mode: params.mode,
root: CONFIG_PATH,
root: configPath,
},
};
}
@@ -323,7 +324,7 @@ export const configHandlers: GatewayRequestHandlers = {
true,
{
ok: true,
path: CONFIG_PATH,
path: createConfigIO().configPath,
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
},
undefined,
@@ -440,7 +441,7 @@ export const configHandlers: GatewayRequestHandlers = {
true,
{
ok: true,
path: CONFIG_PATH,
path: createConfigIO().configPath,
config: redactConfigObject(validated.config, schemaPatch.uiHints),
restart,
sentinel: {
@@ -500,7 +501,7 @@ export const configHandlers: GatewayRequestHandlers = {
true,
{
ok: true,
path: CONFIG_PATH,
path: createConfigIO().configPath,
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
restart,
sentinel: {

View File

@@ -94,7 +94,7 @@ export function registerDefaultAuthTokenSuite(): void {
});
test("connect (req) handshake returns hello-ok payload", async () => {
const { CONFIG_PATH, STATE_DIR } = await import("../config/config.js");
const { STATE_DIR, createConfigIO } = await import("../config/config.js");
const ws = await openWs(port);
const res = await connectReq(ws);
@@ -106,7 +106,7 @@ export function registerDefaultAuthTokenSuite(): void {
}
| undefined;
expect(payload?.type).toBe("hello-ok");
expect(payload?.snapshot?.configPath).toBe(CONFIG_PATH);
expect(payload?.snapshot?.configPath).toBe(createConfigIO().configPath);
expect(payload?.snapshot?.stateDir).toBe(STATE_DIR);
ws.close();

View File

@@ -47,6 +47,31 @@ async function resetTempDir(name: string): Promise<string> {
}
describe("gateway config methods", () => {
it("round-trips config.set and returns the live config path", async () => {
const { createConfigIO } = await import("../config/config.js");
const current = await rpcReq<{
raw?: unknown;
hash?: string;
config?: Record<string, unknown>;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
expect(typeof current.payload?.hash).toBe("string");
expect(current.payload?.config).toBeTruthy();
const res = await rpcReq<{
ok?: boolean;
path?: string;
config?: Record<string, unknown>;
}>(requireWs(), "config.set", {
raw: JSON.stringify(current.payload?.config ?? {}, null, 2),
baseHash: current.payload?.hash,
});
expect(res.ok).toBe(true);
expect(res.payload?.path).toBe(createConfigIO().configPath);
expect(res.payload?.config).toBeTruthy();
});
it("returns a path-scoped config schema lookup", async () => {
const res = await rpcReq<{
path: string;

View File

@@ -1,6 +1,6 @@
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
import { getHealthSnapshot, type HealthSummary } from "../../commands/health.js";
import { CONFIG_PATH, STATE_DIR, loadConfig } from "../../config/config.js";
import { STATE_DIR, createConfigIO, loadConfig } from "../../config/config.js";
import { resolveMainSessionKey } from "../../config/sessions.js";
import { listSystemPresence } from "../../infra/system-presence.js";
import { getUpdateAvailable } from "../../infra/update-startup.js";
@@ -16,6 +16,7 @@ let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null;
export function buildGatewaySnapshot(): Snapshot {
const cfg = loadConfig();
const configPath = createConfigIO().configPath;
const defaultAgentId = resolveDefaultAgentId(cfg);
const mainKey = normalizeMainKey(cfg.session?.mainKey);
const mainSessionKey = resolveMainSessionKey(cfg);
@@ -32,7 +33,7 @@ export function buildGatewaySnapshot(): Snapshot {
stateVersion: { presence: presenceVersion, health: healthVersion },
uptimeMs,
// Surface resolved paths so UIs can display the true config location.
configPath: CONFIG_PATH,
configPath,
stateDir: STATE_DIR,
sessionDefaults: {
defaultAgentId,