Skip to content

Commit d9e8e8a

Browse files
authored
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
1 parent da3cccb commit d9e8e8a

File tree

8 files changed

+73
-15
lines changed

8 files changed

+73
-15
lines changed

src/commands/models/list.status-command.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
} from "../../agents/model-selection.js";
2626
import { formatCliCommand } from "../../cli/command-format.js";
2727
import { withProgressTotals } from "../../cli/progress.js";
28-
import { CONFIG_PATH } from "../../config/config.js";
28+
import { createConfigIO } from "../../config/config.js";
2929
import {
3030
resolveAgentModelFallbackValues,
3131
resolveAgentModelPrimaryValue,
@@ -77,6 +77,7 @@ export async function modelsStatusCommand(
7777
if (opts.plain && opts.probe) {
7878
throw new Error("--probe cannot be used with --plain output.");
7979
}
80+
const configPath = createConfigIO().configPath;
8081
const cfg = await loadModelsConfig({ commandName: "models status", runtime });
8182
const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent });
8283
const agentDir = agentId ? resolveAgentDir(cfg, agentId) : resolveOpenClawAgentDir();
@@ -326,7 +327,7 @@ export async function modelsStatusCommand(
326327
runtime.log(
327328
JSON.stringify(
328329
{
329-
configPath: CONFIG_PATH,
330+
configPath,
330331
...(agentId ? { agentId } : {}),
331332
agentDir,
332333
defaultModel: defaultLabel,
@@ -389,7 +390,7 @@ export async function modelsStatusCommand(
389390
rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel;
390391

391392
runtime.log(
392-
`${label("Config")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, shortenHomePath(CONFIG_PATH))}`,
393+
`${label("Config")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, shortenHomePath(configPath))}`,
393394
);
394395
runtime.log(
395396
`${label("Agent dir")}${colorize(rich, theme.muted, ":")} ${colorize(

src/commands/models/list.status.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ const mocks = vi.hoisted(() => {
6464
getCustomProviderApiKey: vi.fn().mockReturnValue(undefined),
6565
getShellEnvAppliedKeys: vi.fn().mockReturnValue(["OPENAI_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]),
6666
shouldEnableShellEnvFallback: vi.fn().mockReturnValue(true),
67+
createConfigIO: vi.fn().mockReturnValue({
68+
configPath: "/tmp/openclaw-dev/openclaw.json",
69+
}),
6770
loadConfig: vi.fn().mockReturnValue({
6871
agents: {
6972
defaults: {
@@ -115,6 +118,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
115118
const actual = await importOriginal<typeof import("../../config/config.js")>();
116119
return {
117120
...actual,
121+
createConfigIO: mocks.createConfigIO,
118122
loadConfig: mocks.loadConfig,
119123
};
120124
});
@@ -200,6 +204,7 @@ describe("modelsStatusCommand auth overview", () => {
200204

201205
expect(mocks.resolveOpenClawAgentDir).toHaveBeenCalled();
202206
expect(payload.defaultModel).toBe("anthropic/claude-opus-4-5");
207+
expect(payload.configPath).toBe("/tmp/openclaw-dev/openclaw.json");
203208
expect(payload.auth.storePath).toBe("/tmp/openclaw-agent/auth-profiles.json");
204209
expect(payload.auth.shellEnvFallback.enabled).toBe(true);
205210
expect(payload.auth.shellEnvFallback.appliedKeys).toContain("OPENAI_API_KEY");

src/config/logging.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
const mocks = vi.hoisted(() => ({
4+
createConfigIO: vi.fn().mockReturnValue({
5+
configPath: "/tmp/openclaw-dev/openclaw.json",
6+
}),
7+
}));
8+
9+
vi.mock("./io.js", () => ({
10+
createConfigIO: mocks.createConfigIO,
11+
}));
12+
13+
import { formatConfigPath, logConfigUpdated } from "./logging.js";
14+
15+
describe("config logging", () => {
16+
it("formats the live config path when no explicit path is provided", () => {
17+
expect(formatConfigPath()).toBe("/tmp/openclaw-dev/openclaw.json");
18+
});
19+
20+
it("logs the live config path when no explicit path is provided", () => {
21+
const runtime = { log: vi.fn() };
22+
logConfigUpdated(runtime as never);
23+
expect(runtime.log).toHaveBeenCalledWith("Updated /tmp/openclaw-dev/openclaw.json");
24+
});
25+
});

src/config/logging.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import type { RuntimeEnv } from "../runtime.js";
22
import { displayPath } from "../utils.js";
3-
import { CONFIG_PATH } from "./paths.js";
3+
import { createConfigIO } from "./io.js";
44

55
type LogConfigUpdatedOptions = {
66
path?: string;
77
suffix?: string;
88
};
99

10-
export function formatConfigPath(path: string = CONFIG_PATH): string {
10+
export function formatConfigPath(path: string = createConfigIO().configPath): string {
1111
return displayPath(path);
1212
}
1313

1414
export function logConfigUpdated(runtime: RuntimeEnv, opts: LogConfigUpdatedOptions = {}): void {
15-
const path = formatConfigPath(opts.path ?? CONFIG_PATH);
15+
const path = formatConfigPath(opts.path ?? createConfigIO().configPath);
1616
const suffix = opts.suffix ? ` ${opts.suffix}` : "";
1717
runtime.log(`Updated ${path}${suffix}`);
1818
}

src/gateway/server-methods/config.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
22
import { listChannelPlugins } from "../../channels/plugins/index.js";
33
import {
4-
CONFIG_PATH,
4+
createConfigIO,
55
loadConfig,
66
parseConfigJson5,
77
readConfigFileSnapshot,
@@ -197,6 +197,7 @@ function buildConfigRestartSentinelPayload(params: {
197197
threadId: ReturnType<typeof extractDeliveryInfo>["threadId"];
198198
note: string | undefined;
199199
}): RestartSentinelPayload {
200+
const configPath = createConfigIO().configPath;
200201
return {
201202
kind: params.kind,
202203
status: "ok",
@@ -208,7 +209,7 @@ function buildConfigRestartSentinelPayload(params: {
208209
doctorHint: formatDoctorNonInteractiveHint(),
209210
stats: {
210211
mode: params.mode,
211-
root: CONFIG_PATH,
212+
root: configPath,
212213
},
213214
};
214215
}
@@ -323,7 +324,7 @@ export const configHandlers: GatewayRequestHandlers = {
323324
true,
324325
{
325326
ok: true,
326-
path: CONFIG_PATH,
327+
path: createConfigIO().configPath,
327328
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
328329
},
329330
undefined,
@@ -440,7 +441,7 @@ export const configHandlers: GatewayRequestHandlers = {
440441
true,
441442
{
442443
ok: true,
443-
path: CONFIG_PATH,
444+
path: createConfigIO().configPath,
444445
config: redactConfigObject(validated.config, schemaPatch.uiHints),
445446
restart,
446447
sentinel: {
@@ -500,7 +501,7 @@ export const configHandlers: GatewayRequestHandlers = {
500501
true,
501502
{
502503
ok: true,
503-
path: CONFIG_PATH,
504+
path: createConfigIO().configPath,
504505
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
505506
restart,
506507
sentinel: {

src/gateway/server.auth.default-token.suite.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function registerDefaultAuthTokenSuite(): void {
9494
});
9595

9696
test("connect (req) handshake returns hello-ok payload", async () => {
97-
const { CONFIG_PATH, STATE_DIR } = await import("../config/config.js");
97+
const { STATE_DIR, createConfigIO } = await import("../config/config.js");
9898
const ws = await openWs(port);
9999

100100
const res = await connectReq(ws);
@@ -106,7 +106,7 @@ export function registerDefaultAuthTokenSuite(): void {
106106
}
107107
| undefined;
108108
expect(payload?.type).toBe("hello-ok");
109-
expect(payload?.snapshot?.configPath).toBe(CONFIG_PATH);
109+
expect(payload?.snapshot?.configPath).toBe(createConfigIO().configPath);
110110
expect(payload?.snapshot?.stateDir).toBe(STATE_DIR);
111111

112112
ws.close();

src/gateway/server.config-patch.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,31 @@ async function resetTempDir(name: string): Promise<string> {
4747
}
4848

4949
describe("gateway config methods", () => {
50+
it("round-trips config.set and returns the live config path", async () => {
51+
const { createConfigIO } = await import("../config/config.js");
52+
const current = await rpcReq<{
53+
raw?: unknown;
54+
hash?: string;
55+
config?: Record<string, unknown>;
56+
}>(requireWs(), "config.get", {});
57+
expect(current.ok).toBe(true);
58+
expect(typeof current.payload?.hash).toBe("string");
59+
expect(current.payload?.config).toBeTruthy();
60+
61+
const res = await rpcReq<{
62+
ok?: boolean;
63+
path?: string;
64+
config?: Record<string, unknown>;
65+
}>(requireWs(), "config.set", {
66+
raw: JSON.stringify(current.payload?.config ?? {}, null, 2),
67+
baseHash: current.payload?.hash,
68+
});
69+
70+
expect(res.ok).toBe(true);
71+
expect(res.payload?.path).toBe(createConfigIO().configPath);
72+
expect(res.payload?.config).toBeTruthy();
73+
});
74+
5075
it("returns a path-scoped config schema lookup", async () => {
5176
const res = await rpcReq<{
5277
path: string;

src/gateway/server/health-state.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
22
import { getHealthSnapshot, type HealthSummary } from "../../commands/health.js";
3-
import { CONFIG_PATH, STATE_DIR, loadConfig } from "../../config/config.js";
3+
import { STATE_DIR, createConfigIO, loadConfig } from "../../config/config.js";
44
import { resolveMainSessionKey } from "../../config/sessions.js";
55
import { listSystemPresence } from "../../infra/system-presence.js";
66
import { getUpdateAvailable } from "../../infra/update-startup.js";
@@ -16,6 +16,7 @@ let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null;
1616

1717
export function buildGatewaySnapshot(): Snapshot {
1818
const cfg = loadConfig();
19+
const configPath = createConfigIO().configPath;
1920
const defaultAgentId = resolveDefaultAgentId(cfg);
2021
const mainKey = normalizeMainKey(cfg.session?.mainKey);
2122
const mainSessionKey = resolveMainSessionKey(cfg);
@@ -32,7 +33,7 @@ export function buildGatewaySnapshot(): Snapshot {
3233
stateVersion: { presence: presenceVersion, health: healthVersion },
3334
uptimeMs,
3435
// Surface resolved paths so UIs can display the true config location.
35-
configPath: CONFIG_PATH,
36+
configPath,
3637
stateDir: STATE_DIR,
3738
sessionDefaults: {
3839
defaultAgentId,

0 commit comments

Comments
 (0)