diff --git a/src/commands/models/list.status-command.ts b/src/commands/models/list.status-command.ts index 612dbcb66..59614e3f8 100644 --- a/src/commands/models/list.status-command.ts +++ b/src/commands/models/list.status-command.ts @@ -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( diff --git a/src/commands/models/list.status.test.ts b/src/commands/models/list.status.test.ts index 7a792ac04..6f06e63f4 100644 --- a/src/commands/models/list.status.test.ts +++ b/src/commands/models/list.status.test.ts @@ -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(); 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"); diff --git a/src/config/logging.test.ts b/src/config/logging.test.ts new file mode 100644 index 000000000..6c55961d8 --- /dev/null +++ b/src/config/logging.test.ts @@ -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"); + }); +}); diff --git a/src/config/logging.ts b/src/config/logging.ts index 1dd4ee896..cb039c1b1 100644 --- a/src/config/logging.ts +++ b/src/config/logging.ts @@ -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}`); } diff --git a/src/gateway/server-methods/config.ts b/src/gateway/server-methods/config.ts index 5faf83ec4..9b57a126e 100644 --- a/src/gateway/server-methods/config.ts +++ b/src/gateway/server-methods/config.ts @@ -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["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: { diff --git a/src/gateway/server.auth.default-token.suite.ts b/src/gateway/server.auth.default-token.suite.ts index 98bbbbe60..532ec88b4 100644 --- a/src/gateway/server.auth.default-token.suite.ts +++ b/src/gateway/server.auth.default-token.suite.ts @@ -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(); diff --git a/src/gateway/server.config-patch.test.ts b/src/gateway/server.config-patch.test.ts index 44daced16..1f2d465b4 100644 --- a/src/gateway/server.config-patch.test.ts +++ b/src/gateway/server.config-patch.test.ts @@ -47,6 +47,31 @@ async function resetTempDir(name: string): Promise { } 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; + }>(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; + }>(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; diff --git a/src/gateway/server/health-state.ts b/src/gateway/server/health-state.ts index b3a9c1f33..0c14d6e0a 100644 --- a/src/gateway/server/health-state.ts +++ b/src/gateway/server/health-state.ts @@ -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,