mirror of https://github.com/openclaw/openclaw.git
503 lines
13 KiB
TypeScript
503 lines
13 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import type { PluginOrigin } from "../plugins/types.js";
|
|
import { collectPluginConfigAssignments } from "./runtime-config-collectors-plugins.js";
|
|
import {
|
|
createResolverContext,
|
|
type ResolverContext,
|
|
type SecretDefaults,
|
|
} from "./runtime-shared.js";
|
|
|
|
function asConfig(value: unknown): OpenClawConfig {
|
|
return value as OpenClawConfig;
|
|
}
|
|
|
|
function makeContext(sourceConfig: OpenClawConfig): ResolverContext {
|
|
return createResolverContext({
|
|
sourceConfig,
|
|
env: {},
|
|
});
|
|
}
|
|
|
|
function envRef(id: string) {
|
|
return { source: "env" as const, provider: "default", id };
|
|
}
|
|
|
|
function loadablePluginOrigins(entries: Array<[string, PluginOrigin]>) {
|
|
return new Map(entries);
|
|
}
|
|
|
|
describe("collectPluginConfigAssignments", () => {
|
|
it("collects SecretRef assignments from active acpx MCP server env vars", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
entries: {
|
|
acpx: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
github: {
|
|
command: "npx",
|
|
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
env: {
|
|
GITHUB_TOKEN: envRef("GITHUB_TOKEN"),
|
|
PLAIN_VAR: "plain-value",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
const defaults: SecretDefaults = undefined;
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(1);
|
|
expect(context.assignments[0]?.path).toBe(
|
|
"plugins.entries.acpx.config.mcpServers.github.env.GITHUB_TOKEN",
|
|
);
|
|
expect(context.assignments[0]?.expected).toBe("string");
|
|
});
|
|
|
|
it("resolves assignments via apply callback", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
entries: {
|
|
acpx: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
mcp1: {
|
|
command: "node",
|
|
env: {
|
|
API_KEY: envRef("MY_API_KEY"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(1);
|
|
context.assignments[0]?.apply("resolved-key-value");
|
|
|
|
const entries = config.plugins?.entries as Record<string, Record<string, unknown>>;
|
|
const mcpServers = (entries?.acpx?.config as Record<string, unknown>)?.mcpServers as Record<
|
|
string,
|
|
Record<string, unknown>
|
|
>;
|
|
const env = mcpServers?.mcp1?.env as Record<string, unknown>;
|
|
expect(env?.API_KEY).toBe("resolved-key-value");
|
|
});
|
|
|
|
it("collects across multiple acpx servers only", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
entries: {
|
|
acpx: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
s1: { command: "a", env: { K1: envRef("K1") } },
|
|
s2: { command: "b", env: { K2: envRef("K2"), K3: envRef("K3") } },
|
|
},
|
|
},
|
|
},
|
|
other: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
s3: { command: "c", env: { K4: envRef("K4") } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([
|
|
["acpx", "bundled"],
|
|
["other", "config"],
|
|
]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(3);
|
|
const paths = context.assignments.map((a) => a.path).toSorted();
|
|
expect(paths).toEqual([
|
|
"plugins.entries.acpx.config.mcpServers.s1.env.K1",
|
|
"plugins.entries.acpx.config.mcpServers.s2.env.K2",
|
|
"plugins.entries.acpx.config.mcpServers.s2.env.K3",
|
|
]);
|
|
});
|
|
|
|
it("skips entries without config or mcpServers", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
entries: {
|
|
noConfig: {},
|
|
noMcpServers: { config: { otherKey: "value" } },
|
|
noEnv: { config: { mcpServers: { s1: { command: "x" } } } },
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
});
|
|
|
|
it("skips when no plugins.entries at all", () => {
|
|
const config = asConfig({});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
});
|
|
|
|
it("skips assignments when plugins.enabled is false", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
enabled: false,
|
|
entries: {
|
|
acpx: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
s1: { command: "node", env: { K: envRef("K") } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
expect(context.warnings.some((w) => w.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("skips assignments when entry.enabled is false", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
entries: {
|
|
acpx: {
|
|
enabled: false,
|
|
config: {
|
|
mcpServers: {
|
|
s1: { command: "node", env: { K: envRef("K") } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
expect(context.warnings.some((w) => w.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("keeps bundled acpx inactive unless explicitly enabled", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
enabled: true,
|
|
entries: {
|
|
acpx: {
|
|
config: {
|
|
mcpServers: {
|
|
s1: { command: "node", env: { K: envRef("K") } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
expect(context.warnings.some((w) => w.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("skips assignments when plugin is in denylist", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
deny: ["acpx"],
|
|
entries: {
|
|
acpx: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
s1: { command: "node", env: { K: envRef("K") } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
expect(context.warnings.some((w) => w.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("skips assignments when allowlist is set and plugin is not in it", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
allow: ["other-plugin"],
|
|
entries: {
|
|
acpx: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
s1: { command: "node", env: { K: envRef("K") } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
expect(context.warnings.some((w) => w.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE")).toBe(
|
|
true,
|
|
);
|
|
});
|
|
|
|
it("collects assignments when plugin is in allowlist", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
allow: ["acpx"],
|
|
entries: {
|
|
acpx: {
|
|
config: {
|
|
mcpServers: {
|
|
s1: { command: "node", env: { K: envRef("K") } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "config"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(1);
|
|
});
|
|
|
|
it("ignores plain string env values", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
entries: {
|
|
acpx: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
s1: {
|
|
command: "node",
|
|
env: { PLAIN: "hello", ALSO_PLAIN: "world" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
});
|
|
|
|
it("collects inline env-template refs while leaving normal strings literal", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
entries: {
|
|
acpx: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
s1: {
|
|
command: "node",
|
|
env: {
|
|
INLINE: "${INLINE_KEY}",
|
|
SECOND: "${SECOND_KEY}",
|
|
LITERAL: "hello",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["acpx", "bundled"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(2);
|
|
expect(context.assignments[0]?.path).toBe(
|
|
"plugins.entries.acpx.config.mcpServers.s1.env.INLINE",
|
|
);
|
|
expect(context.assignments[1]?.path).toBe(
|
|
"plugins.entries.acpx.config.mcpServers.s1.env.SECOND",
|
|
);
|
|
});
|
|
|
|
it("skips stale acpx entries not in loadablePluginOrigins", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
entries: {
|
|
acpx: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
s1: { command: "node", env: { K1: envRef("K1") } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
expect(
|
|
context.warnings.some(
|
|
(w) =>
|
|
w.code === "SECRETS_REF_IGNORED_INACTIVE_SURFACE" &&
|
|
w.path === "plugins.entries.acpx.config.mcpServers.s1.env.K1",
|
|
),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("ignores non-acpx plugin mcpServers surfaces", () => {
|
|
const config = asConfig({
|
|
plugins: {
|
|
entries: {
|
|
other: {
|
|
enabled: true,
|
|
config: {
|
|
mcpServers: {
|
|
s1: { command: "node", env: { K1: envRef("K1") } },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const context = makeContext(config);
|
|
|
|
collectPluginConfigAssignments({
|
|
config,
|
|
defaults: undefined,
|
|
context,
|
|
loadablePluginOrigins: loadablePluginOrigins([["other", "config"]]),
|
|
});
|
|
|
|
expect(context.assignments).toHaveLength(0);
|
|
});
|
|
});
|