mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 07:00:22 +00:00
feat(cli): add json schema to cli tool (#54523)
Merged via squash.
Prepared head SHA: 39c15ee70d
Co-authored-by: kvokka <15954013+kvokka@users.noreply.github.com>
Co-authored-by: altaywtf <9790196+altaywtf@users.noreply.github.com>
Reviewed-by: @altaywtf
This commit is contained in:
281
src/config/runtime-schema.test.ts
Normal file
281
src/config/runtime-schema.test.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConfigFileSnapshot, OpenClawConfig } from "./types.js";
|
||||
|
||||
const mockLoadConfig = vi.hoisted(() => vi.fn<() => OpenClawConfig>());
|
||||
const mockReadConfigFileSnapshot = vi.hoisted(() => vi.fn<() => Promise<ConfigFileSnapshot>>());
|
||||
const mockLoadOpenClawPlugins = vi.hoisted(() => vi.fn());
|
||||
const mockListChannelPlugins = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./config.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./config.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadConfig: () => mockLoadConfig(),
|
||||
readConfigFileSnapshot: () => mockReadConfigFileSnapshot(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../plugins/loader.js", () => ({
|
||||
loadOpenClawPlugins: (...args: unknown[]) => mockLoadOpenClawPlugins(...args),
|
||||
}));
|
||||
|
||||
vi.mock("../channels/plugins/index.js", () => ({
|
||||
listChannelPlugins: (...args: unknown[]) => mockListChannelPlugins(...args),
|
||||
}));
|
||||
|
||||
function makeSnapshot(params: { valid: boolean; config?: OpenClawConfig }): ConfigFileSnapshot {
|
||||
return {
|
||||
path: "/tmp/openclaw.json",
|
||||
exists: true,
|
||||
raw: "{}",
|
||||
parsed: params.config ?? {},
|
||||
resolved: params.config ?? {},
|
||||
valid: params.valid,
|
||||
config: params.config ?? {},
|
||||
issues: params.valid ? [] : [{ path: "gateway", message: "invalid" }],
|
||||
warnings: [],
|
||||
legacyIssues: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("readBestEffortRuntimeConfigSchema", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLoadConfig.mockReturnValue({});
|
||||
mockListChannelPlugins.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("uses scoped plugin registry channels for valid configs", async () => {
|
||||
mockReadConfigFileSnapshot.mockResolvedValueOnce(
|
||||
makeSnapshot({
|
||||
valid: true,
|
||||
config: { plugins: { entries: { demo: { enabled: true } } } },
|
||||
}),
|
||||
);
|
||||
mockLoadOpenClawPlugins.mockReturnValueOnce({
|
||||
plugins: [
|
||||
{
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
description: "Demo plugin",
|
||||
configUiHints: {},
|
||||
configJsonSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
channels: [
|
||||
{
|
||||
pluginId: "telegram",
|
||||
pluginName: "Telegram",
|
||||
source: "bundled",
|
||||
plugin: {
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram", blurb: "Telegram channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
channelSetups: [],
|
||||
});
|
||||
|
||||
const { readBestEffortRuntimeConfigSchema } = await import("./runtime-schema.js");
|
||||
const result = await readBestEffortRuntimeConfigSchema();
|
||||
const schema = result.schema as { properties?: Record<string, unknown> };
|
||||
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
|
||||
const channelProps = channelsNode?.properties as Record<string, unknown> | undefined;
|
||||
const pluginsNode = schema.properties?.plugins as Record<string, unknown> | undefined;
|
||||
const pluginProps = pluginsNode?.properties as Record<string, unknown> | undefined;
|
||||
const entriesNode = pluginProps?.entries as Record<string, unknown> | undefined;
|
||||
const entryProps = entriesNode?.properties as Record<string, unknown> | undefined;
|
||||
|
||||
expect(mockLoadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: { plugins: { entries: { demo: { enabled: true } } } },
|
||||
activate: false,
|
||||
cache: false,
|
||||
}),
|
||||
);
|
||||
expect(channelProps?.telegram).toBeTruthy();
|
||||
expect(entryProps?.demo).toBeTruthy();
|
||||
});
|
||||
|
||||
it("falls back to channel-only schema when config is invalid", async () => {
|
||||
mockReadConfigFileSnapshot.mockResolvedValueOnce(makeSnapshot({ valid: false }));
|
||||
mockLoadOpenClawPlugins.mockReturnValueOnce({
|
||||
plugins: [],
|
||||
channels: [
|
||||
{
|
||||
pluginId: "slack",
|
||||
pluginName: "Slack",
|
||||
source: "bundled",
|
||||
plugin: {
|
||||
id: "slack",
|
||||
meta: { label: "Slack", blurb: "Slack channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
channelSetups: [
|
||||
{
|
||||
pluginId: "telegram",
|
||||
pluginName: "Telegram",
|
||||
source: "bundled",
|
||||
plugin: {
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram", blurb: "Telegram channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { readBestEffortRuntimeConfigSchema } = await import("./runtime-schema.js");
|
||||
const result = await readBestEffortRuntimeConfigSchema();
|
||||
const schema = result.schema as { properties?: Record<string, unknown> };
|
||||
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
|
||||
const channelProps = channelsNode?.properties as Record<string, unknown> | undefined;
|
||||
const pluginsNode = schema.properties?.plugins as Record<string, unknown> | undefined;
|
||||
const pluginProps = pluginsNode?.properties as Record<string, unknown> | undefined;
|
||||
const entriesNode = pluginProps?.entries as Record<string, unknown> | undefined;
|
||||
const entryProps = entriesNode?.properties as Record<string, unknown> | undefined;
|
||||
|
||||
expect(mockLoadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: { plugins: { enabled: true } },
|
||||
activate: false,
|
||||
cache: false,
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
}),
|
||||
);
|
||||
expect(channelProps?.telegram).toBeTruthy();
|
||||
expect(channelProps?.slack).toBeTruthy();
|
||||
expect(entryProps?.demo).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not fall back to active registry channels when invalid fallback load throws", async () => {
|
||||
mockReadConfigFileSnapshot.mockResolvedValueOnce(makeSnapshot({ valid: false }));
|
||||
mockLoadOpenClawPlugins.mockImplementationOnce(() => {
|
||||
throw new Error("plugin load failed");
|
||||
});
|
||||
mockListChannelPlugins.mockReturnValueOnce([
|
||||
{
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram", blurb: "Telegram channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { readBestEffortRuntimeConfigSchema } = await import("./runtime-schema.js");
|
||||
const result = await readBestEffortRuntimeConfigSchema();
|
||||
const schema = result.schema as { properties?: Record<string, unknown> };
|
||||
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
|
||||
const channelProps = channelsNode?.properties as Record<string, unknown> | undefined;
|
||||
|
||||
expect(channelProps?.telegram).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadGatewayRuntimeConfigSchema", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockLoadConfig.mockReturnValue({ plugins: { entries: { demo: { enabled: true } } } });
|
||||
});
|
||||
|
||||
it("preserves gateway channel source and loader options", async () => {
|
||||
mockLoadOpenClawPlugins.mockReturnValueOnce({
|
||||
plugins: [
|
||||
{
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
description: "Demo plugin",
|
||||
configUiHints: {},
|
||||
configJsonSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
mode: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
channels: [
|
||||
{
|
||||
pluginId: "scoped-only",
|
||||
pluginName: "Scoped Only",
|
||||
source: "bundled",
|
||||
plugin: {
|
||||
id: "scoped-only",
|
||||
meta: { label: "Scoped Only" },
|
||||
},
|
||||
},
|
||||
],
|
||||
channelSetups: [],
|
||||
});
|
||||
mockListChannelPlugins.mockReturnValueOnce([
|
||||
{
|
||||
id: "telegram",
|
||||
meta: { label: "Telegram", blurb: "Telegram channel" },
|
||||
configSchema: {
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
botToken: { type: "string" },
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
const { loadGatewayRuntimeConfigSchema } = await import("./runtime-schema.js");
|
||||
const result = loadGatewayRuntimeConfigSchema();
|
||||
const schema = result.schema as { properties?: Record<string, unknown> };
|
||||
const channelsNode = schema.properties?.channels as Record<string, unknown> | undefined;
|
||||
const channelProps = channelsNode?.properties as Record<string, unknown> | undefined;
|
||||
|
||||
expect(mockLoadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config: { plugins: { entries: { demo: { enabled: true } } } },
|
||||
cache: true,
|
||||
}),
|
||||
);
|
||||
expect(mockLoadOpenClawPlugins.mock.calls[0]?.[0]?.activate).toBeUndefined();
|
||||
expect(channelProps?.telegram).toBeTruthy();
|
||||
expect(channelProps?.["scoped-only"]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user