mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-11 09:11:13 +00:00
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
282 lines
9.2 KiB
TypeScript
282 lines
9.2 KiB
TypeScript
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();
|
|
});
|
|
});
|