Files
openclaw/src/config/runtime-schema.test.ts

209 lines
6.4 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 mockLoadPluginManifestRegistry = 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/manifest-registry.js", () => ({
loadPluginManifestRegistry: (...args: unknown[]) => mockLoadPluginManifestRegistry(...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: [],
};
}
function makeManifestRegistry() {
return {
diagnostics: [],
plugins: [
{
id: "demo",
name: "Demo",
description: "Demo plugin",
origin: "bundled",
channels: [],
configUiHints: {},
configSchema: {
type: "object",
properties: {
mode: { type: "string" },
},
},
},
{
id: "telegram",
name: "Telegram",
description: "Telegram plugin",
origin: "bundled",
channels: ["telegram"],
channelCatalogMeta: {
id: "telegram",
label: "Telegram",
blurb: "Telegram channel",
},
channelConfigs: {
telegram: {
schema: {
type: "object",
properties: {
botToken: { type: "string" },
},
},
uiHints: {},
},
},
},
{
id: "slack",
name: "Slack",
description: "Slack plugin",
origin: "bundled",
channels: ["slack"],
channelCatalogMeta: {
id: "slack",
label: "Slack",
blurb: "Slack channel",
},
channelConfigs: {
slack: {
schema: {
type: "object",
properties: {
botToken: { type: "string" },
},
},
uiHints: {},
},
},
},
{
id: "matrix",
name: "Matrix",
description: "Matrix plugin",
origin: "workspace",
channels: ["matrix"],
channelCatalogMeta: {
id: "matrix",
label: "Matrix",
blurb: "Matrix channel",
},
channelConfigs: {
matrix: {
schema: {
type: "object",
properties: {
homeserver: { type: "string" },
},
},
uiHints: {},
},
},
},
],
};
}
async function readSchemaNodes() {
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;
return { channelProps, entryProps };
}
describe("readBestEffortRuntimeConfigSchema", () => {
beforeEach(() => {
vi.clearAllMocks();
mockLoadConfig.mockReturnValue({});
mockLoadPluginManifestRegistry.mockReturnValue(makeManifestRegistry());
});
it("merges manifest plugin metadata for valid configs", async () => {
mockReadConfigFileSnapshot.mockResolvedValueOnce(
makeSnapshot({
valid: true,
config: { plugins: { entries: { demo: { enabled: true } } } },
}),
);
const { channelProps, entryProps } = await readSchemaNodes();
expect(mockLoadPluginManifestRegistry).toHaveBeenCalledWith(
expect.objectContaining({
config: { plugins: { entries: { demo: { enabled: true } } } },
cache: false,
}),
);
expect(channelProps?.telegram).toBeTruthy();
expect(channelProps?.matrix).toBeTruthy();
expect(entryProps?.demo).toBeTruthy();
});
it("falls back to bundled channel metadata when config is invalid", async () => {
mockReadConfigFileSnapshot.mockResolvedValueOnce(makeSnapshot({ valid: false }));
const { channelProps, entryProps } = await readSchemaNodes();
expect(mockLoadPluginManifestRegistry).toHaveBeenCalledWith(
expect.objectContaining({
config: { plugins: { enabled: true } },
cache: false,
}),
);
expect(channelProps?.telegram).toBeTruthy();
expect(channelProps?.slack).toBeTruthy();
expect(entryProps?.demo).toBeUndefined();
});
});
describe("loadGatewayRuntimeConfigSchema", () => {
beforeEach(() => {
vi.clearAllMocks();
mockLoadConfig.mockReturnValue({ plugins: { entries: { demo: { enabled: true } } } });
mockLoadPluginManifestRegistry.mockReturnValue(makeManifestRegistry());
});
it("uses manifest metadata instead of booting plugin runtime", async () => {
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(mockLoadPluginManifestRegistry).toHaveBeenCalledWith(
expect.objectContaining({
config: { plugins: { entries: { demo: { enabled: true } } } },
cache: false,
}),
);
expect(channelProps?.telegram).toBeTruthy();
expect(channelProps?.matrix).toBeTruthy();
});
});