mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:18:37 +00:00
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:
@@ -25,7 +25,7 @@ import {
|
|||||||
} from "../../agents/model-selection.js";
|
} from "../../agents/model-selection.js";
|
||||||
import { formatCliCommand } from "../../cli/command-format.js";
|
import { formatCliCommand } from "../../cli/command-format.js";
|
||||||
import { withProgressTotals } from "../../cli/progress.js";
|
import { withProgressTotals } from "../../cli/progress.js";
|
||||||
import { CONFIG_PATH } from "../../config/config.js";
|
import { createConfigIO } from "../../config/config.js";
|
||||||
import {
|
import {
|
||||||
resolveAgentModelFallbackValues,
|
resolveAgentModelFallbackValues,
|
||||||
resolveAgentModelPrimaryValue,
|
resolveAgentModelPrimaryValue,
|
||||||
@@ -77,6 +77,7 @@ export async function modelsStatusCommand(
|
|||||||
if (opts.plain && opts.probe) {
|
if (opts.plain && opts.probe) {
|
||||||
throw new Error("--probe cannot be used with --plain output.");
|
throw new Error("--probe cannot be used with --plain output.");
|
||||||
}
|
}
|
||||||
|
const configPath = createConfigIO().configPath;
|
||||||
const cfg = await loadModelsConfig({ commandName: "models status", runtime });
|
const cfg = await loadModelsConfig({ commandName: "models status", runtime });
|
||||||
const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent });
|
const agentId = resolveKnownAgentId({ cfg, rawAgentId: opts.agent });
|
||||||
const agentDir = agentId ? resolveAgentDir(cfg, agentId) : resolveOpenClawAgentDir();
|
const agentDir = agentId ? resolveAgentDir(cfg, agentId) : resolveOpenClawAgentDir();
|
||||||
@@ -326,7 +327,7 @@ export async function modelsStatusCommand(
|
|||||||
runtime.log(
|
runtime.log(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
configPath: CONFIG_PATH,
|
configPath,
|
||||||
...(agentId ? { agentId } : {}),
|
...(agentId ? { agentId } : {}),
|
||||||
agentDir,
|
agentDir,
|
||||||
defaultModel: defaultLabel,
|
defaultModel: defaultLabel,
|
||||||
@@ -389,7 +390,7 @@ export async function modelsStatusCommand(
|
|||||||
rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel;
|
rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel;
|
||||||
|
|
||||||
runtime.log(
|
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(
|
runtime.log(
|
||||||
`${label("Agent dir")}${colorize(rich, theme.muted, ":")} ${colorize(
|
`${label("Agent dir")}${colorize(rich, theme.muted, ":")} ${colorize(
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ const mocks = vi.hoisted(() => {
|
|||||||
getCustomProviderApiKey: vi.fn().mockReturnValue(undefined),
|
getCustomProviderApiKey: vi.fn().mockReturnValue(undefined),
|
||||||
getShellEnvAppliedKeys: vi.fn().mockReturnValue(["OPENAI_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]),
|
getShellEnvAppliedKeys: vi.fn().mockReturnValue(["OPENAI_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]),
|
||||||
shouldEnableShellEnvFallback: vi.fn().mockReturnValue(true),
|
shouldEnableShellEnvFallback: vi.fn().mockReturnValue(true),
|
||||||
|
createConfigIO: vi.fn().mockReturnValue({
|
||||||
|
configPath: "/tmp/openclaw-dev/openclaw.json",
|
||||||
|
}),
|
||||||
loadConfig: vi.fn().mockReturnValue({
|
loadConfig: vi.fn().mockReturnValue({
|
||||||
agents: {
|
agents: {
|
||||||
defaults: {
|
defaults: {
|
||||||
@@ -115,6 +118,7 @@ vi.mock("../../config/config.js", async (importOriginal) => {
|
|||||||
const actual = await importOriginal<typeof import("../../config/config.js")>();
|
const actual = await importOriginal<typeof import("../../config/config.js")>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
|
createConfigIO: mocks.createConfigIO,
|
||||||
loadConfig: mocks.loadConfig,
|
loadConfig: mocks.loadConfig,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -200,6 +204,7 @@ describe("modelsStatusCommand auth overview", () => {
|
|||||||
|
|
||||||
expect(mocks.resolveOpenClawAgentDir).toHaveBeenCalled();
|
expect(mocks.resolveOpenClawAgentDir).toHaveBeenCalled();
|
||||||
expect(payload.defaultModel).toBe("anthropic/claude-opus-4-5");
|
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.storePath).toBe("/tmp/openclaw-agent/auth-profiles.json");
|
||||||
expect(payload.auth.shellEnvFallback.enabled).toBe(true);
|
expect(payload.auth.shellEnvFallback.enabled).toBe(true);
|
||||||
expect(payload.auth.shellEnvFallback.appliedKeys).toContain("OPENAI_API_KEY");
|
expect(payload.auth.shellEnvFallback.appliedKeys).toContain("OPENAI_API_KEY");
|
||||||
|
|||||||
25
src/config/logging.test.ts
Normal file
25
src/config/logging.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { displayPath } from "../utils.js";
|
import { displayPath } from "../utils.js";
|
||||||
import { CONFIG_PATH } from "./paths.js";
|
import { createConfigIO } from "./io.js";
|
||||||
|
|
||||||
type LogConfigUpdatedOptions = {
|
type LogConfigUpdatedOptions = {
|
||||||
path?: string;
|
path?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function formatConfigPath(path: string = CONFIG_PATH): string {
|
export function formatConfigPath(path: string = createConfigIO().configPath): string {
|
||||||
return displayPath(path);
|
return displayPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logConfigUpdated(runtime: RuntimeEnv, opts: LogConfigUpdatedOptions = {}): void {
|
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}` : "";
|
const suffix = opts.suffix ? ` ${opts.suffix}` : "";
|
||||||
runtime.log(`Updated ${path}${suffix}`);
|
runtime.log(`Updated ${path}${suffix}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||||
import {
|
import {
|
||||||
CONFIG_PATH,
|
createConfigIO,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
parseConfigJson5,
|
parseConfigJson5,
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
@@ -197,6 +197,7 @@ function buildConfigRestartSentinelPayload(params: {
|
|||||||
threadId: ReturnType<typeof extractDeliveryInfo>["threadId"];
|
threadId: ReturnType<typeof extractDeliveryInfo>["threadId"];
|
||||||
note: string | undefined;
|
note: string | undefined;
|
||||||
}): RestartSentinelPayload {
|
}): RestartSentinelPayload {
|
||||||
|
const configPath = createConfigIO().configPath;
|
||||||
return {
|
return {
|
||||||
kind: params.kind,
|
kind: params.kind,
|
||||||
status: "ok",
|
status: "ok",
|
||||||
@@ -208,7 +209,7 @@ function buildConfigRestartSentinelPayload(params: {
|
|||||||
doctorHint: formatDoctorNonInteractiveHint(),
|
doctorHint: formatDoctorNonInteractiveHint(),
|
||||||
stats: {
|
stats: {
|
||||||
mode: params.mode,
|
mode: params.mode,
|
||||||
root: CONFIG_PATH,
|
root: configPath,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -323,7 +324,7 @@ export const configHandlers: GatewayRequestHandlers = {
|
|||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
ok: true,
|
ok: true,
|
||||||
path: CONFIG_PATH,
|
path: createConfigIO().configPath,
|
||||||
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
|
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
|
||||||
},
|
},
|
||||||
undefined,
|
undefined,
|
||||||
@@ -440,7 +441,7 @@ export const configHandlers: GatewayRequestHandlers = {
|
|||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
ok: true,
|
ok: true,
|
||||||
path: CONFIG_PATH,
|
path: createConfigIO().configPath,
|
||||||
config: redactConfigObject(validated.config, schemaPatch.uiHints),
|
config: redactConfigObject(validated.config, schemaPatch.uiHints),
|
||||||
restart,
|
restart,
|
||||||
sentinel: {
|
sentinel: {
|
||||||
@@ -500,7 +501,7 @@ export const configHandlers: GatewayRequestHandlers = {
|
|||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
ok: true,
|
ok: true,
|
||||||
path: CONFIG_PATH,
|
path: createConfigIO().configPath,
|
||||||
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
|
config: redactConfigObject(parsed.config, parsed.schema.uiHints),
|
||||||
restart,
|
restart,
|
||||||
sentinel: {
|
sentinel: {
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export function registerDefaultAuthTokenSuite(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("connect (req) handshake returns hello-ok payload", async () => {
|
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 ws = await openWs(port);
|
||||||
|
|
||||||
const res = await connectReq(ws);
|
const res = await connectReq(ws);
|
||||||
@@ -106,7 +106,7 @@ export function registerDefaultAuthTokenSuite(): void {
|
|||||||
}
|
}
|
||||||
| undefined;
|
| undefined;
|
||||||
expect(payload?.type).toBe("hello-ok");
|
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);
|
expect(payload?.snapshot?.stateDir).toBe(STATE_DIR);
|
||||||
|
|
||||||
ws.close();
|
ws.close();
|
||||||
|
|||||||
@@ -47,6 +47,31 @@ async function resetTempDir(name: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("gateway config methods", () => {
|
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 () => {
|
it("returns a path-scoped config schema lookup", async () => {
|
||||||
const res = await rpcReq<{
|
const res = await rpcReq<{
|
||||||
path: string;
|
path: string;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
import { resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||||
import { getHealthSnapshot, type HealthSummary } from "../../commands/health.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 { resolveMainSessionKey } from "../../config/sessions.js";
|
||||||
import { listSystemPresence } from "../../infra/system-presence.js";
|
import { listSystemPresence } from "../../infra/system-presence.js";
|
||||||
import { getUpdateAvailable } from "../../infra/update-startup.js";
|
import { getUpdateAvailable } from "../../infra/update-startup.js";
|
||||||
@@ -16,6 +16,7 @@ let broadcastHealthUpdate: ((snap: HealthSummary) => void) | null = null;
|
|||||||
|
|
||||||
export function buildGatewaySnapshot(): Snapshot {
|
export function buildGatewaySnapshot(): Snapshot {
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
|
const configPath = createConfigIO().configPath;
|
||||||
const defaultAgentId = resolveDefaultAgentId(cfg);
|
const defaultAgentId = resolveDefaultAgentId(cfg);
|
||||||
const mainKey = normalizeMainKey(cfg.session?.mainKey);
|
const mainKey = normalizeMainKey(cfg.session?.mainKey);
|
||||||
const mainSessionKey = resolveMainSessionKey(cfg);
|
const mainSessionKey = resolveMainSessionKey(cfg);
|
||||||
@@ -32,7 +33,7 @@ export function buildGatewaySnapshot(): Snapshot {
|
|||||||
stateVersion: { presence: presenceVersion, health: healthVersion },
|
stateVersion: { presence: presenceVersion, health: healthVersion },
|
||||||
uptimeMs,
|
uptimeMs,
|
||||||
// Surface resolved paths so UIs can display the true config location.
|
// Surface resolved paths so UIs can display the true config location.
|
||||||
configPath: CONFIG_PATH,
|
configPath,
|
||||||
stateDir: STATE_DIR,
|
stateDir: STATE_DIR,
|
||||||
sessionDefaults: {
|
sessionDefaults: {
|
||||||
defaultAgentId,
|
defaultAgentId,
|
||||||
|
|||||||
Reference in New Issue
Block a user