mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 12:00:44 +00:00
docs(plugins): define config ownership contract
* fix(plugins): flag channel config metadata gaps * docs(plugins): clarify config ownership
This commit is contained in:
@@ -49,7 +49,24 @@ function collectSchemaPaths(schema: unknown, prefix = ""): string[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
expect(value && typeof value === "object" && !Array.isArray(value)).toBe(true);
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
describe("config footprint guardrails", () => {
|
||||
it("keeps plugin entry config generic in the generated base schema", () => {
|
||||
const root = asRecord(GENERATED_BASE_CONFIG_SCHEMA.schema);
|
||||
const plugins = asRecord(asRecord(root.properties).plugins);
|
||||
const entries = asRecord(asRecord(plugins.properties).entries);
|
||||
const entry = asRecord(entries.additionalProperties);
|
||||
const pluginConfig = asRecord(asRecord(entry.properties).config);
|
||||
|
||||
expect(pluginConfig.type).toBe("object");
|
||||
expect(pluginConfig.additionalProperties).toEqual({});
|
||||
expect(pluginConfig.properties).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps retired legacy paths out of the generated base config schema", () => {
|
||||
const basePaths = new Set(collectSchemaPaths(GENERATED_BASE_CONFIG_SCHEMA.schema));
|
||||
|
||||
@@ -144,4 +161,32 @@ describe("config footprint guardrails", () => {
|
||||
"return ssrfPolicyFromDangerouslyAllowPrivateNetwork(allowPrivateNetwork);",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps bundled channel schemas as a fixed legacy SDK compatibility surface", () => {
|
||||
const source = readSource("src/plugin-sdk/channel-config-schema.ts");
|
||||
const providersCoreExports = source.match(
|
||||
/Legacy bundled channel schema exports[\s\S]*?export \{(?<exports>[\s\S]*?)\} from "\.\.\/config\/zod-schema\.providers-core\.js";/,
|
||||
)?.groups?.exports;
|
||||
expect(providersCoreExports).toBeDefined();
|
||||
const exportedSchemaNames = Array.from(
|
||||
`${providersCoreExports ?? ""}\nWhatsAppConfigSchema`.matchAll(
|
||||
/\b([A-Z][A-Za-z0-9]+ConfigSchema)\b/g,
|
||||
),
|
||||
)
|
||||
.map((match) => match[1])
|
||||
.filter((name): name is string => Boolean(name))
|
||||
.toSorted((left, right) => left.localeCompare(right));
|
||||
|
||||
expect(exportedSchemaNames).toEqual([
|
||||
"DiscordConfigSchema",
|
||||
"GoogleChatConfigSchema",
|
||||
"IMessageConfigSchema",
|
||||
"MSTeamsConfigSchema",
|
||||
"SignalConfigSchema",
|
||||
"SlackConfigSchema",
|
||||
"TelegramConfigSchema",
|
||||
"WhatsAppConfigSchema",
|
||||
]);
|
||||
expect(source).toContain("Legacy bundled channel schema exports");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -510,6 +510,67 @@ describe("loadPluginManifestRegistry", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reports non-bundled channel manifests without channel config descriptors", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
id: "external-chat",
|
||||
channels: ["external-chat"],
|
||||
configSchema: { type: "object" },
|
||||
});
|
||||
|
||||
const registry = loadSingleCandidateRegistry({
|
||||
idHint: "external-chat",
|
||||
rootDir: dir,
|
||||
origin: "global",
|
||||
});
|
||||
|
||||
expect(registry.plugins[0]?.channels).toEqual(["external-chat"]);
|
||||
expect(registry.diagnostics).toContainEqual(
|
||||
expect.objectContaining({
|
||||
level: "warn",
|
||||
pluginId: "external-chat",
|
||||
source: path.join(dir, "openclaw.plugin.json"),
|
||||
message: expect.stringContaining("without channelConfigs metadata"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts non-bundled channel manifests with channel config descriptors", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
id: "external-chat",
|
||||
channels: ["external-chat"],
|
||||
configSchema: { type: "object" },
|
||||
channelConfigs: {
|
||||
"external-chat": {
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
token: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registry = loadSingleCandidateRegistry({
|
||||
idHint: "external-chat",
|
||||
rootDir: dir,
|
||||
origin: "global",
|
||||
});
|
||||
|
||||
expect(registry.plugins[0]?.channelConfigs?.["external-chat"]?.schema).toMatchObject({
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
});
|
||||
expect(
|
||||
registry.diagnostics.some((diagnostic) =>
|
||||
diagnostic.message.includes("without channelConfigs metadata"),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back providerDiscoverySource from .ts to emitted .js files", () => {
|
||||
const dir = makeTempDir();
|
||||
writeManifest(dir, {
|
||||
|
||||
@@ -474,6 +474,40 @@ function pushProviderAuthEnvVarsCompatDiagnostic(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function pushNonBundledChannelConfigDescriptorDiagnostic(params: {
|
||||
record: PluginManifestRecord;
|
||||
diagnostics: PluginDiagnostic[];
|
||||
}): void {
|
||||
if (params.record.origin === "bundled" || params.record.format === "bundle") {
|
||||
return;
|
||||
}
|
||||
const declaredChannels = params.record.channels
|
||||
.map((channelId) => channelId.trim())
|
||||
.filter((channelId) => channelId.length > 0);
|
||||
if (declaredChannels.length === 0) {
|
||||
return;
|
||||
}
|
||||
const channelConfigs = params.record.channelConfigs ?? {};
|
||||
const missingChannels = declaredChannels.filter((channelId) => !channelConfigs[channelId]);
|
||||
if (missingChannels.length === 0) {
|
||||
return;
|
||||
}
|
||||
params.diagnostics.push({
|
||||
level: "warn",
|
||||
pluginId: params.record.id,
|
||||
source: params.record.manifestPath,
|
||||
message: `channel plugin manifest declares ${missingChannels.join(", ")} without channelConfigs metadata; add openclaw.plugin.json#channelConfigs so config schema and setup surfaces work before runtime loads`,
|
||||
});
|
||||
}
|
||||
|
||||
function pushManifestCompatibilityDiagnostics(params: {
|
||||
record: PluginManifestRecord;
|
||||
diagnostics: PluginDiagnostic[];
|
||||
}): void {
|
||||
pushProviderAuthEnvVarsCompatDiagnostic(params);
|
||||
pushNonBundledChannelConfigDescriptorDiagnostic(params);
|
||||
}
|
||||
|
||||
function matchesInstalledPluginRecord(params: {
|
||||
pluginId: string;
|
||||
candidate: PluginCandidate;
|
||||
@@ -666,7 +700,7 @@ export function loadPluginManifestRegistry(
|
||||
if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) {
|
||||
records[existing.recordIndex] = record;
|
||||
seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex });
|
||||
pushProviderAuthEnvVarsCompatDiagnostic({ record, diagnostics });
|
||||
pushManifestCompatibilityDiagnostics({ record, diagnostics });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -689,7 +723,7 @@ export function loadPluginManifestRegistry(
|
||||
if (candidateWins) {
|
||||
records[existing.recordIndex] = record;
|
||||
seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex });
|
||||
pushProviderAuthEnvVarsCompatDiagnostic({ record, diagnostics });
|
||||
pushManifestCompatibilityDiagnostics({ record, diagnostics });
|
||||
}
|
||||
diagnostics.push({
|
||||
level: "warn",
|
||||
@@ -702,7 +736,7 @@ export function loadPluginManifestRegistry(
|
||||
|
||||
seenIds.set(manifest.id, { candidate, recordIndex: records.length });
|
||||
records.push(record);
|
||||
pushProviderAuthEnvVarsCompatDiagnostic({ record, diagnostics });
|
||||
pushManifestCompatibilityDiagnostics({ record, diagnostics });
|
||||
}
|
||||
|
||||
const registry = { plugins: records, diagnostics };
|
||||
|
||||
Reference in New Issue
Block a user