Files
openclaw/src/plugins/channel-plugin-ids.test.ts
Vincent Koc fa570d8ed3 test(plugins): include dreaming sidecar in startup expectation (#116018)
(cherry picked from commit cbb3c420db)

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-30 01:52:25 +08:00

3715 lines
104 KiB
TypeScript

/** Tests channel plugin id resolution from config, manifests, and installed state. */
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
import type { InstalledPluginIndex, InstalledPluginIndexRecord } from "./installed-plugin-index.js";
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
const listPotentialConfiguredChannelIds = vi.hoisted(() => vi.fn());
const listExplicitlyDisabledChannelIdsForConfig = vi.hoisted(() =>
vi.fn((config: OpenClawConfig) => {
return Object.entries(config.channels ?? {})
.filter(([, value]) => {
return (
Boolean(value) &&
typeof value === "object" &&
!Array.isArray(value) &&
(value as { enabled?: unknown }).enabled === false
);
})
.map(([channelId]) => channelId.toLowerCase());
}),
);
const listPotentialConfiguredChannelPresenceSignals = vi.hoisted(() => vi.fn());
const hasMeaningfulChannelConfig = vi.hoisted(() =>
vi.fn((value: unknown) => {
return (
value !== null &&
typeof value === "object" &&
!Array.isArray(value) &&
Object.keys(value).some((key) => key !== "enabled")
);
}),
);
const loadPluginManifestRegistry = vi.hoisted(() => vi.fn());
const loadPluginManifestRegistryForInstalledIndex = vi.hoisted(() => vi.fn());
const loadPluginManifestRegistryForPluginRegistry = vi.hoisted(() => vi.fn());
const loadPluginRegistrySnapshot = vi.hoisted(() => vi.fn());
vi.mock("../channels/config-presence.js", () => ({
listPotentialConfiguredChannelIds,
listExplicitlyDisabledChannelIdsForConfig,
listPotentialConfiguredChannelPresenceSignals,
hasMeaningfulChannelConfig,
}));
vi.mock("./manifest-registry-installed.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./manifest-registry-installed.js")>();
return {
...actual,
loadPluginManifestRegistryForInstalledIndex,
};
});
vi.mock("./plugin-registry-snapshot.js", () => ({
loadPluginRegistrySnapshot,
loadPluginRegistrySnapshotWithMetadata: (params: unknown) => ({
snapshot: loadPluginRegistrySnapshot(params),
diagnostics: [],
}),
}));
vi.mock("./plugin-registry-contributions.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./plugin-registry-contributions.js")>();
return {
...actual,
loadPluginManifestRegistryForPluginRegistry,
};
});
import {
hasConfiguredChannelsForReadOnlyScope,
listConfiguredAnnounceChannelIdsForConfig,
listConfiguredChannelIdsForReadOnlyScope,
listExplicitConfiguredChannelIdsForConfig,
resolveConfiguredChannelPluginIds,
resolveConfiguredChannelPresencePolicy,
resolveConfiguredDeferredChannelPluginIdsFromRegistry,
resolveConfigValidationMetadataPluginIds,
resolveGatewayStartupMetadataPluginIds,
resolveGatewayStartupPluginIdsFromRegistry,
resolveGatewayStartupPluginPlanFromRegistry,
} from "./channel-plugin-ids.js";
function withManifestLoadPaths<T extends { id: string }>(
plugin: T,
): T & Pick<PluginManifestRecord, "rootDir" | "source" | "manifestPath" | "skills" | "hooks"> {
return {
rootDir: `/tmp/plugins/${plugin.id}`,
source: `/tmp/plugins/${plugin.id}/index.ts`,
manifestPath: `/tmp/plugins/${plugin.id}/openclaw.plugin.json`,
skills: [],
hooks: [],
...plugin,
};
}
function createManifestRegistryFixture(): PluginManifestRegistry {
const plugins = [
{ id: "demo-channel", channels: ["demo-channel"] },
{ id: "demo-other-channel", channels: ["demo-other-channel"] },
{
id: "browser",
activation: { onStartup: true, onConfigPaths: ["browser"] },
enabledByDefault: true,
},
{
id: "demo-provider-plugin",
providers: ["demo-provider"],
cliBackends: ["demo-cli"],
},
{
id: "microsoft",
enabledByDefault: true,
contracts: { speechProviders: ["microsoft"] },
},
{
id: "tts-local-cli",
enabledByDefault: true,
contracts: { speechProviders: ["tts-local-cli", "cli"] },
},
{ id: "gradium", origin: "global", contracts: { speechProviders: ["gradium"] } },
{
id: "anthropic",
enabledByDefault: true,
providers: ["anthropic"],
modelSupport: { modelPrefixes: ["claude-"] },
cliBackends: ["claude-cli"],
},
{
id: "openai",
enabledByDefault: true,
providers: ["openai", "openai-codex"],
modelSupport: { modelPrefixes: ["gpt-"] },
contracts: {
speechProviders: ["openai"],
realtimeTranscriptionProviders: ["openai"],
realtimeVoiceProviders: ["openai"],
imageGenerationProviders: ["openai"],
videoGenerationProviders: ["openai"],
memoryEmbeddingProviders: ["openai"],
},
},
{
id: "ollama",
enabledByDefault: true,
providers: ["ollama"],
contracts: { memoryEmbeddingProviders: ["ollama"] },
},
{
id: "generic-embedding",
enabledByDefault: true,
contracts: { embeddingProviders: ["generic-embed"] },
},
{
id: "llama-cpp",
origin: "global",
enabledByDefault: true,
contracts: { embeddingProviders: ["local"] },
},
{
id: "google",
enabledByDefault: true,
providers: ["google", "google-gemini-cli"],
cliBackends: ["google-gemini-cli"],
contracts: {
realtimeVoiceProviders: ["google"],
imageGenerationProviders: ["google"],
videoGenerationProviders: ["google"],
musicGenerationProviders: ["google"],
},
},
{ id: "amazon-bedrock", enabledByDefault: true, providers: ["amazon-bedrock"] },
{ id: "brave", origin: "global", contracts: { webSearchProviders: ["brave"] } },
{ id: "codex", providers: ["codex"], activation: { onAgentHarnesses: ["codex"] } },
{
id: "activation-only-channel-plugin",
activation: { onChannels: ["activation-only-channel"] },
},
{
id: "workspace-activation-channel-plugin",
origin: "workspace",
activation: { onChannels: ["workspace-activation-channel"] },
},
{
id: "global-activation-channel-plugin",
origin: "global",
activation: { onChannels: ["global-activation-channel"] },
},
{
id: "external-env-channel-plugin",
origin: "config",
channels: ["external-env-channel"],
packageChannel: {
id: "external-env-channel",
configuredState: {
env: { allOf: ["EXTERNAL_ENV_CHANNEL_HOST", "EXTERNAL_ENV_CHANNEL_NICK"] },
},
},
},
{ id: "voice-call", activation: { onStartup: true } },
{ id: "memory-core", kind: "memory" },
{ id: "memory-lancedb", kind: "memory" },
{ id: "demo-global-sidecar", origin: "global", activation: { onStartup: true } },
{
id: "demo-global-startup-opt-out",
origin: "global",
activation: { onStartup: false },
},
{
id: "demo-global-explicit-startup",
origin: "global",
activation: { onStartup: true },
},
{
id: "demo-config-startup",
enabledByDefault: true,
activation: {
onStartup: false,
onConfigPaths: ["plugins.entries.demo-config-startup.config.autoStart"],
},
},
{
id: "external-config-startup",
origin: "global",
activation: {
onStartup: false,
onConfigPaths: ["plugins.entries.external-config-startup.config.autoStart"],
},
},
{
id: "external-hook-capability",
origin: "global",
activation: { onCapabilities: ["hook"] },
},
{ id: "external-hook-policy", origin: "global" },
{
id: "external-trusted-policy",
origin: "global",
contracts: { trustedToolPolicies: ["workflow-budget"] },
},
// Keep the legacy installed-index origin: #76576 must exercise the original
// context-engine regression even though current manifest origins are narrower.
{
id: "lossless-claw",
kind: "context-engine",
origin: "installed" as PluginManifestRecord["origin"],
},
{
id: "qa-lab",
activation: { onStartup: false },
contracts: { workerProviders: ["static-ssh"] },
},
{
id: "external-worker-provider",
origin: "global",
contracts: { workerProviders: ["external-ssh"] },
},
] satisfies Array<Pick<PluginManifestRecord, "id"> & Partial<PluginManifestRecord>>;
return {
plugins: plugins.map((plugin) =>
withManifestLoadPaths({
channels: [],
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
...plugin,
}),
) as PluginManifestRecord[],
diagnostics: [],
};
}
function createManifestRegistryFixtureWithWorkspaceDemoChannel(): PluginManifestRegistry {
const fixture = createManifestRegistryFixture();
return {
...fixture,
plugins: [
...fixture.plugins,
withManifestLoadPaths({
id: "workspace-demo-channel-plugin",
channels: ["demo-channel"],
startupDeferConfiguredChannelFullLoadUntilAfterListen: true,
origin: "workspace",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
}),
],
};
}
function normalizeStartupAgentHarnesses(record: PluginManifestRecord): readonly string[] {
return [
...new Set([...(record.activation?.onAgentHarnesses ?? []), ...(record.cliBackends ?? [])]),
].toSorted((left, right) => left.localeCompare(right));
}
function hasPluginKind(record: PluginManifestRecord, kind: string): boolean {
return Array.isArray(record.kind) ? record.kind.includes(kind as never) : record.kind === kind;
}
function createInstalledPluginRecordFixture(
record: PluginManifestRecord,
): InstalledPluginIndexRecord {
const memory = hasPluginKind(record, "memory");
return {
pluginId: record.id,
manifestPath: record.manifestPath,
manifestHash: `test-${record.id}`,
source: record.source,
rootDir: record.rootDir,
origin: record.origin,
enabled: true,
...(record.enabledByDefault === true ? { enabledByDefault: true } : {}),
startup: {
sidecar: record.activation?.onStartup === true,
memory,
deferConfiguredChannelFullLoadUntilAfterListen:
record.startupDeferConfiguredChannelFullLoadUntilAfterListen === true,
agentHarnesses: normalizeStartupAgentHarnesses(record),
configPaths: record.activation?.onConfigPaths ?? [],
},
contributions: {
channels: record.channels,
channelConfigs: Object.keys(record.channelConfigs ?? {}),
providers: record.providers,
modelCatalogProviders: [
...Object.keys(record.modelCatalog?.providers ?? {}),
...Object.keys(record.modelCatalog?.aliases ?? {}),
...(record.modelCatalog?.suppressions ?? []).map((entry) => entry.provider),
],
modelSupportPrefixes: record.modelSupport?.modelPrefixes ?? [],
modelSupportPatterns: record.modelSupport?.modelPatterns ?? [],
autoEnableProviderIds: record.autoEnableWhenConfiguredProviders ?? [],
commandAliases: record.commandAliases?.map((alias) => alias.name) ?? [],
contracts: Object.fromEntries(Object.entries(record.contracts ?? {})),
},
compat: [],
};
}
function createInstalledPluginIndexFixture(
registry: PluginManifestRegistry = loadPluginManifestRegistry(),
): InstalledPluginIndex {
return {
version: 1,
hostContractVersion: "test",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash: "test",
generatedAtMs: 0,
installRecords: {},
plugins: registry.plugins.map(createInstalledPluginRecordFixture),
diagnostics: registry.diagnostics,
};
}
function filterManifestRegistryForInstalledIndex(params: {
pluginIds?: readonly string[];
includeDisabled?: boolean;
}): PluginManifestRegistry {
const registry = loadPluginManifestRegistry() as PluginManifestRegistry;
const pluginIdSet = params.pluginIds?.length ? new Set(params.pluginIds) : null;
return {
...registry,
plugins: pluginIdSet
? registry.plugins.filter((plugin) => pluginIdSet.has(plugin.id))
: registry.plugins,
};
}
function createPluginPlanningTestEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
return {
...overrides,
};
}
function useManifestRegistryFixture(
registry: PluginManifestRegistry = createManifestRegistryFixture(),
) {
const index = createInstalledPluginIndexFixture(registry);
loadPluginManifestRegistry.mockReset().mockReturnValue(registry);
loadPluginManifestRegistryForPluginRegistry
.mockReset()
.mockImplementation(() => loadPluginManifestRegistry());
loadPluginRegistrySnapshot.mockReset().mockReturnValue(index);
return { registry, index };
}
function expectStartupPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
workerProviderIds?: readonly string[];
expected: readonly string[];
}) {
const manifestRegistry = loadPluginManifestRegistry() as PluginManifestRegistry;
expect(
resolveGatewayStartupPluginIdsFromRegistry({
config: params.config,
...(params.activationSourceConfig !== undefined
? { activationSourceConfig: params.activationSourceConfig }
: {}),
env: createPluginPlanningTestEnv(params.env),
index: createInstalledPluginIndexFixture(manifestRegistry),
manifestRegistry,
...(params.workerProviderIds !== undefined
? { workerProviderIds: params.workerProviderIds }
: {}),
}),
).toEqual(params.expected);
}
function resolveConfiguredDeferredChannelPluginIdsForFixture(params: {
config: OpenClawConfig;
env?: NodeJS.ProcessEnv;
}): string[] {
const manifestRegistry = loadPluginManifestRegistry() as PluginManifestRegistry;
return resolveConfiguredDeferredChannelPluginIdsFromRegistry({
config: params.config,
env: createPluginPlanningTestEnv(params.env),
index: createInstalledPluginIndexFixture(manifestRegistry),
manifestRegistry,
});
}
function createStartupConfig(params: {
enabledPluginIds?: string[];
providerIds?: string[];
modelId?: string;
agentRuntimeId?: string;
agentRuntimeIds?: string[];
channelIds?: string[];
allowPluginIds?: string[];
noConfiguredChannels?: boolean;
memorySlot?: string;
contextEngine?: string;
}) {
const slotsConfig = {
...(params.memorySlot ? { memory: params.memorySlot } : {}),
...(params.contextEngine ? { contextEngine: params.contextEngine } : {}),
};
const hasSlots = Object.keys(slotsConfig).length > 0;
const includeSlots =
hasSlots && (!params.allowPluginIds?.length || Boolean(params.enabledPluginIds?.length));
const config: Record<string, unknown> = {};
if (params.noConfiguredChannels) {
config.channels = {};
} else if (params.channelIds?.length) {
config.channels = Object.fromEntries(
params.channelIds.map((channelId) => [channelId, { enabled: true }]),
);
}
if (params.enabledPluginIds?.length || params.allowPluginIds?.length || hasSlots) {
config.plugins = {
...(params.allowPluginIds?.length ? { allow: params.allowPluginIds } : {}),
...(includeSlots ? { slots: slotsConfig } : {}),
...(params.enabledPluginIds?.length
? {
entries: Object.fromEntries(
params.enabledPluginIds.map((pluginId) => [pluginId, { enabled: true }]),
),
}
: {}),
};
}
if (params.providerIds?.length) {
config.models = {
providers: Object.fromEntries(
params.providerIds.map((providerId) => [
providerId,
{ baseUrl: "https://example.com", models: [] },
]),
),
};
}
if (params.modelId || params.agentRuntimeId || params.agentRuntimeIds?.length) {
config.agents = {
defaults: {
...(params.modelId
? { model: { primary: params.modelId }, models: { [params.modelId]: {} } }
: {}),
...(params.agentRuntimeId
? { agentRuntime: { id: params.agentRuntimeId, fallback: "none" } }
: {}),
},
...(params.agentRuntimeIds?.length
? {
list: params.agentRuntimeIds.map((runtime, index) => ({
id: `agent-${index + 1}`,
agentRuntime: { id: runtime },
})),
}
: {}),
};
}
return config as OpenClawConfig;
}
describe("resolveGatewayStartupPluginIds", () => {
beforeEach(() => {
listPotentialConfiguredChannelIds.mockReset().mockImplementation((config: OpenClawConfig) => {
if (Object.hasOwn(config, "channels")) {
return Object.keys(config.channels ?? {});
}
return ["demo-channel"];
});
listPotentialConfiguredChannelPresenceSignals
.mockReset()
.mockImplementation((config: OpenClawConfig) => {
return listPotentialConfiguredChannelIds(config).map((channelId: string) => ({
channelId,
source: "config",
}));
});
useManifestRegistryFixture();
loadPluginManifestRegistryForInstalledIndex
.mockReset()
.mockImplementation(filterManifestRegistryForInstalledIndex);
loadPluginManifestRegistryForPluginRegistry
.mockReset()
.mockImplementation(() => loadPluginManifestRegistry());
});
it.each([
[
"includes only configured channel plugins at idle startup",
createStartupConfig({
enabledPluginIds: ["voice-call"],
modelId: "demo-cli/demo-model",
}),
["demo-channel", "browser", "voice-call", "memory-core"],
],
[
"keeps bundled startup sidecars with enabledByDefault at idle startup",
{} as OpenClawConfig,
["demo-channel", "browser", "memory-core"],
],
[
"keeps provider plugins out of idle startup when only provider config references them",
createStartupConfig({
providerIds: ["demo-provider"],
}),
["demo-channel", "browser", "memory-core"],
],
[
"includes bundled model providers selected by agent defaults at startup",
createStartupConfig({
modelId: "amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0",
}),
["demo-channel", "browser", "amazon-bedrock", "memory-core"],
],
[
"includes bundled model providers selected only as agent fallbacks at startup",
{
agents: {
defaults: {
model: {
fallbacks: ["amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0"],
},
},
},
} as OpenClawConfig,
["demo-channel", "browser", "amazon-bedrock", "memory-core"],
],
[
"honors explicit plugin disablement for selected model providers",
{
agents: {
defaults: {
model: { primary: "amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0" },
},
},
plugins: { entries: { "amazon-bedrock": { enabled: false } } },
} as OpenClawConfig,
["demo-channel", "browser", "memory-core"],
],
[
"includes configured bundled speech providers at startup",
{
channels: {},
tts: { provider: "microsoft" },
} as OpenClawConfig,
["browser", "microsoft", "memory-core"],
],
[
"includes bundled speech providers configured by provider block",
{
channels: {},
tts: { providers: { "tts-local-cli": { command: "say" } } },
} as OpenClawConfig,
["browser", "tts-local-cli", "memory-core"],
],
[
"maps legacy edge TTS selection to the Microsoft speech plugin",
{
channels: {},
tts: { provider: "edge" },
} as OpenClawConfig,
["browser", "microsoft", "memory-core"],
],
[
"includes explicitly enabled external speech providers at startup",
{
channels: {},
tts: { provider: "gradium" },
plugins: { entries: { gradium: { enabled: true } } },
} as OpenClawConfig,
["browser", "gradium", "memory-core"],
],
[
"includes active persona speech providers at startup",
{
channels: {},
tts: {
persona: "narrator",
personas: {
narrator: {
label: "Narrator",
provider: "microsoft",
},
},
},
} as OpenClawConfig,
["browser", "microsoft", "memory-core"],
],
[
"includes agent-inherited active persona speech providers at startup",
{
channels: {},
tts: {
personas: {
narrator: {
label: "Narrator",
provider: "microsoft",
},
},
},
agents: {
list: [{ id: "reader", tts: { persona: "narrator" } }],
},
} as OpenClawConfig,
["browser", "microsoft", "memory-core"],
],
[
"includes channel-inherited active persona speech providers at startup",
{
channels: {
"demo-channel": { tts: { persona: "narrator" } },
},
tts: {
personas: {
narrator: {
label: "Narrator",
provider: "microsoft",
},
},
},
} as OpenClawConfig,
["demo-channel", "browser", "microsoft", "memory-core"],
],
[
"includes account-inherited active persona speech providers at startup",
{
channels: {
"demo-channel": {
accounts: {
primary: { tts: { persona: "narrator" } },
},
},
},
tts: {
personas: {
narrator: {
label: "Narrator",
provider: "microsoft",
},
},
},
} as OpenClawConfig,
["demo-channel", "browser", "microsoft", "memory-core"],
],
[
"honors disabled speech provider config blocks at startup",
{
channels: {},
tts: {
provider: "microsoft",
providers: { microsoft: { enabled: false } },
},
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"honors explicit plugin disablement for configured speech providers",
{
channels: {},
tts: { provider: "microsoft" },
plugins: { entries: { microsoft: { enabled: false } } },
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"includes bundled generation providers configured by media defaults at startup",
{
channels: {},
agents: {
defaults: {
mediaModels: {
image: {
primary: "openai/gpt-image-2",
fallbacks: ["google/gemini-3-pro-image-preview"],
},
video: { primary: "google/veo-3.1-fast-generate-preview" },
music: { primary: "google/lyria-3-clip-preview" },
},
},
},
} as OpenClawConfig,
["browser", "openai", "google", "memory-core"],
],
[
"honors explicit plugin disablement for configured generation providers",
{
channels: {},
agents: {
defaults: {
mediaModels: {
image: { primary: "google/gemini-3-pro-image-preview" },
},
},
},
plugins: { entries: { google: { enabled: false } } },
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"includes bundled voice providers configured by voice defaults at startup",
{
channels: {},
agents: {
defaults: {
voiceModel: {
primary: "openai/gpt-4o-mini-tts",
fallbacks: ["google/gemini-live-2.5-flash-preview"],
},
},
},
} as OpenClawConfig,
["browser", "openai", "google", "memory-core"],
],
[
"honors explicit plugin disablement for configured voice providers",
{
channels: {},
agents: {
defaults: {
voiceModel: { primary: "openai/gpt-4o-mini-tts" },
},
},
plugins: { entries: { openai: { enabled: false } } },
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"includes the owning plugin for a configured memory embedding provider at startup",
{
channels: {},
memory: { search: { provider: "openai" } },
agents: {
defaults: {},
},
} as OpenClawConfig,
["browser", "openai", "memory-core"],
],
[
"includes the owning plugin for a configured memory embedding fallback at startup",
{
channels: {},
memory: { search: { provider: "ollama", fallback: "openai" } },
agents: {
defaults: {},
},
} as OpenClawConfig,
["browser", "openai", "ollama", "memory-core"],
],
[
"includes the owning plugin for a per-agent memory embedding provider at startup",
{
channels: {},
agents: {
list: [{ id: "researcher", memory: { search: { provider: "openai" } } }],
},
} as OpenClawConfig,
["browser", "openai", "memory-core"],
],
[
"includes the api-owner plugin for a custom models.providers memory embedding provider at startup",
{
channels: {},
memory: { search: { provider: "ollama-5080" } },
agents: {
defaults: {},
},
models: {
providers: {
"ollama-5080": {
api: "ollama",
baseUrl: "http://gpu-box.local:11435",
models: [],
},
},
},
} as OpenClawConfig,
["browser", "ollama", "memory-core"],
],
[
"includes the api-owner plugin for a custom models.providers memory embedding fallback at startup",
{
channels: {},
memory: { search: { provider: "openai", fallback: "ollama-5080" } },
agents: {
defaults: {},
},
models: {
providers: {
"ollama-5080": {
api: "ollama",
baseUrl: "http://gpu-box.local:11435",
models: [],
},
},
},
} as OpenClawConfig,
["browser", "openai", "ollama", "memory-core"],
],
[
"includes generic embedding provider owners for configured memory search at startup",
{
channels: {},
memory: { search: { provider: "generic-embed" } },
agents: {
defaults: {},
},
} as OpenClawConfig,
["browser", "generic-embedding", "memory-core"],
],
[
"does not load plugin owners for core generic memory embedding providers",
{
channels: {},
memory: { search: { provider: "openai-compatible" } },
agents: {
defaults: {},
},
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"does not load plugin owners for custom providers backed by core generic embeddings",
{
channels: {},
memory: { search: { provider: "tenant-embeddings" } },
agents: {
defaults: {},
},
models: {
providers: {
"tenant-embeddings": {
api: "openai-responses",
baseUrl: "http://127.0.0.1:11434/v1",
models: [],
},
},
},
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"does not load memory embedding provider owners when the memory slot is disabled",
{
channels: {},
memory: { search: { provider: "openai", fallback: "ollama" } },
agents: {
defaults: {},
},
plugins: {
slots: { memory: "none" },
},
} as OpenClawConfig,
["browser"],
],
[
"ignores memory embedding fallbacks when primary provider is fts-only",
{
channels: {},
memory: { search: { provider: "none", fallback: "openai" } },
agents: {
defaults: {},
},
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"includes the llama.cpp provider for configured local memory embeddings",
{
channels: {},
memory: { search: { provider: "local", fallback: "auto" } },
agents: {
defaults: {},
},
} as OpenClawConfig,
["browser", "llama-cpp", "memory-core"],
],
[
"skips memory embedding providers from disabled memory search blocks",
{
channels: {},
memory: { search: { enabled: false, provider: "openai", fallback: "ollama" } },
agents: {
defaults: {},
},
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"honors explicit plugin disablement for configured memory embedding providers",
{
channels: {},
memory: { search: { provider: "openai" } },
agents: {
defaults: {},
},
plugins: { entries: { openai: { enabled: false } } },
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"honors denied plugins for configured memory embedding providers",
{
channels: {},
memory: { search: { provider: "openai" } },
agents: {
defaults: {},
},
plugins: { deny: ["openai"] },
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"skips a per-agent memory embedding provider when memory search is disabled by inherited defaults",
{
channels: {},
memory: { search: { enabled: false } },
agents: {
defaults: {},
list: [
{ id: "researcher", memory: { search: { provider: "openai", fallback: "ollama" } } },
],
},
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"includes the inherited default provider when a per-agent override re-enables memory search",
{
channels: {},
memory: { search: { enabled: false, provider: "openai", fallback: "ollama" } },
agents: {
defaults: {},
list: [{ id: "researcher", memory: { search: { enabled: true } } }],
},
} as OpenClawConfig,
["browser", "openai", "ollama", "memory-core"],
],
[
"includes default memory embedding providers for unlisted agents even when listed agents override memory search",
{
channels: {},
memory: { search: { provider: "openai" } },
agents: {
defaults: {},
list: [
{ id: "muted", memory: { search: { enabled: false } } },
{ id: "researcher", memory: { search: { provider: "ollama" } } },
],
},
} as OpenClawConfig,
["browser", "openai", "ollama", "memory-core"],
],
[
"includes default memory embedding providers for listed agents that inherit defaults",
{
channels: {},
memory: { search: { provider: "openai" } },
agents: {
defaults: {},
list: [{ id: "researcher" }],
},
} as OpenClawConfig,
["browser", "openai", "memory-core"],
],
[
"includes explicitly selected external web search providers at startup",
{
channels: {},
tools: {
web: {
search: {
enabled: true,
provider: "brave",
},
},
},
plugins: {
allow: ["brave"],
entries: {
brave: {
enabled: true,
},
},
},
} as OpenClawConfig,
["brave"],
],
[
"honors disabled web search when selecting startup providers",
{
channels: {},
tools: {
web: {
search: {
enabled: false,
provider: "brave",
},
},
},
plugins: {
allow: ["brave"],
entries: {
brave: {
enabled: true,
},
},
},
} as OpenClawConfig,
[],
],
[
"honors explicit plugin disablement for configured web search providers",
{
channels: {},
tools: {
web: {
search: {
enabled: true,
provider: "brave",
},
},
},
plugins: {
allow: ["brave"],
entries: {
brave: {
enabled: false,
},
},
},
} as OpenClawConfig,
[],
],
[
"keeps configured generation providers behind restrictive allowlists",
{
channels: {},
agents: {
defaults: {
mediaModels: {
image: { primary: "google/gemini-3-pro-image-preview" },
},
},
},
plugins: { allow: ["browser"] },
} as OpenClawConfig,
["browser"],
],
[
"includes explicitly enabled non-channel sidecars in startup scope",
createStartupConfig({
enabledPluginIds: ["demo-global-sidecar", "voice-call"],
}),
["demo-channel", "browser", "voice-call", "memory-core", "demo-global-sidecar"],
],
[
"includes explicitly enabled external channel plugins without channel config",
{
channels: {},
plugins: {
entries: {
"external-env-channel-plugin": { enabled: true },
},
},
} as OpenClawConfig,
["browser", "external-env-channel-plugin", "memory-core"],
],
[
"does not start explicitly enabled external channel plugins when every channel is disabled",
{
channels: {
"external-env-channel": { enabled: false },
},
plugins: {
entries: {
"external-env-channel-plugin": { enabled: true },
},
},
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"keeps default-enabled startup sidecars when a restrictive allowlist permits them",
createStartupConfig({
allowPluginIds: ["browser"],
noConfiguredChannels: true,
}),
["browser"],
],
[
"includes every configured channel plugin and excludes other channels",
createStartupConfig({
channelIds: ["demo-channel", "demo-other-channel"],
}),
["demo-channel", "demo-other-channel", "browser", "memory-core"],
],
] as const)("%s", (_name, config, expected) => {
expectStartupPluginIds({ config, expected });
});
it("matches explicitly disabled channel ids case-insensitively", () => {
const registry = createManifestRegistryFixture();
useManifestRegistryFixture({
...registry,
plugins: registry.plugins.map((plugin) =>
plugin.id === "external-env-channel-plugin"
? Object.assign({}, plugin, { channels: ["External-Env-Channel"] })
: plugin,
),
});
expectStartupPluginIds({
config: {
channels: {
"external-env-channel": { enabled: false },
},
plugins: {
entries: {
"external-env-channel-plugin": { enabled: true },
},
},
} as OpenClawConfig,
expected: ["browser", "memory-core"],
});
});
it("loads configured worker-provider owners from the activation source", () => {
const activationSourceConfig = {
channels: {},
cloudWorkers: {
profiles: {
development: { provider: " Static-SSH " },
secondary: { provider: "STATIC-SSH" },
},
},
} as OpenClawConfig;
expectStartupPluginIds({
config: activationSourceConfig,
activationSourceConfig,
expected: ["browser", "memory-core", "qa-lab"],
});
});
it("keeps an auto-enabled worker provider in a restrictive reload plan", () => {
const authoredConfig = {
channels: {},
cloudWorkers: { profiles: { development: { provider: "static-ssh" } } },
plugins: { allow: ["browser"] },
} as OpenClawConfig;
const effectiveConfig = applyPluginAutoEnable({
config: authoredConfig,
env: createPluginPlanningTestEnv(),
manifestRegistry: createManifestRegistryFixture(),
}).config;
expectStartupPluginIds({
config: effectiveConfig,
activationSourceConfig: authoredConfig,
expected: ["browser", "qa-lab"],
});
});
it("loads bundled worker-provider owners required by durable environments", () => {
expectStartupPluginIds({
config: { channels: {} } as OpenClawConfig,
workerProviderIds: [" Static-SSH ", "STATIC-SSH"],
expected: ["browser", "memory-core", "qa-lab"],
});
});
it("keeps durable external worker-provider owners behind explicit enablement", () => {
expectStartupPluginIds({
config: { channels: {} } as OpenClawConfig,
workerProviderIds: ["external-ssh"],
expected: ["browser", "memory-core"],
});
expectStartupPluginIds({
config: {
channels: {},
plugins: { entries: { "external-worker-provider": { enabled: true } } },
} as OpenClawConfig,
workerProviderIds: ["external-ssh"],
expected: ["browser", "memory-core", "external-worker-provider"],
});
});
it("honors explicit disablement of configured worker-provider owners", () => {
const config = {
channels: {},
cloudWorkers: {
profiles: {
development: { provider: "static-ssh" },
},
},
plugins: { entries: { "qa-lab": { enabled: false } } },
} as OpenClawConfig;
expectStartupPluginIds({
config,
activationSourceConfig: config,
expected: ["browser", "memory-core"],
});
});
it("keeps configured worker-provider owners behind restrictive allowlists", () => {
const config = {
channels: {},
cloudWorkers: {
profiles: {
development: { provider: "static-ssh" },
},
},
plugins: { allow: ["browser"] },
} as OpenClawConfig;
expectStartupPluginIds({
config,
activationSourceConfig: config,
expected: ["browser"],
});
});
it("keeps durable worker-provider owners behind disable and allowlist gates", () => {
expectStartupPluginIds({
config: { channels: {}, plugins: { enabled: false } } as OpenClawConfig,
workerProviderIds: ["static-ssh"],
expected: [],
});
expectStartupPluginIds({
config: {
channels: {},
plugins: { entries: { "qa-lab": { enabled: false } } },
} as OpenClawConfig,
workerProviderIds: ["static-ssh"],
expected: ["browser", "memory-core"],
});
expectStartupPluginIds({
config: { channels: {}, plugins: { deny: ["qa-lab"] } } as OpenClawConfig,
workerProviderIds: ["static-ssh"],
expected: ["browser", "memory-core"],
});
expectStartupPluginIds({
config: { channels: {}, plugins: { allow: ["browser"] } } as OpenClawConfig,
workerProviderIds: ["static-ssh"],
expected: ["browser"],
});
});
it("keeps effective-only bundled sidecars behind restrictive allowlists", () => {
const rawConfig = createStartupConfig({
allowPluginIds: ["browser"],
});
const effectiveConfig = {
...rawConfig,
plugins: {
allow: ["browser"],
entries: {
"voice-call": {
enabled: true,
},
"memory-core": {
enabled: true,
},
},
},
} as OpenClawConfig;
expectStartupPluginIds({
config: effectiveConfig,
activationSourceConfig: rawConfig,
expected: ["browser"],
});
});
it("includes auto-enabled external web search providers at startup", () => {
const rawConfig = {
channels: {},
tools: {
web: {
search: {
enabled: true,
provider: "brave",
},
},
},
plugins: {
allow: ["browser"],
},
} as OpenClawConfig;
const effectiveConfig = {
...rawConfig,
plugins: {
allow: ["browser", "brave"],
entries: {
brave: {
enabled: true,
},
},
},
} as OpenClawConfig;
expectStartupPluginIds({
config: effectiveConfig,
activationSourceConfig: rawConfig,
expected: ["browser", "brave"],
});
});
it("does not let runtime-default plugin entries bypass the authored startup allowlist", () => {
const activationSourceConfig = {
channels: {},
plugins: {
allow: ["bench-plugin"],
entries: {
browser: {
enabled: false,
},
},
},
} as OpenClawConfig;
const runtimeConfig = {
...activationSourceConfig,
plugins: {
...activationSourceConfig.plugins,
entries: {
...activationSourceConfig.plugins?.entries,
"memory-core": {
config: {
dreaming: {
enabled: false,
},
},
},
},
},
} as OpenClawConfig;
expectStartupPluginIds({
config: runtimeConfig,
activationSourceConfig,
expected: [],
});
});
it("skips startup when activation.onStartup is false", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["demo-global-startup-opt-out"],
allowPluginIds: ["demo-global-startup-opt-out"],
noConfiguredChannels: true,
memorySlot: "none",
}),
expected: [],
});
});
it("loads explicit startup plugins when activation.onStartup is true", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["demo-global-explicit-startup"],
allowPluginIds: ["demo-global-explicit-startup"],
noConfiguredChannels: true,
memorySlot: "none",
}),
expected: ["demo-global-explicit-startup"],
});
});
it("loads explicit trusted policy plugins at startup", () => {
expectStartupPluginIds({
config: createStartupConfig({
allowPluginIds: ["external-trusted-policy"],
noConfiguredChannels: true,
memorySlot: "none",
}),
expected: ["external-trusted-policy"],
});
});
it("loads startup-lazy bundled plugins only when their activation config is present", () => {
expectStartupPluginIds({
config: createStartupConfig({
noConfiguredChannels: true,
memorySlot: "none",
}),
expected: ["browser"],
});
expectStartupPluginIds({
config: {
channels: {},
plugins: {
slots: { memory: "none" },
entries: {
"demo-config-startup": {
enabled: true,
config: {
autoStart: [{ providerId: "demo" }],
},
},
},
},
} as OpenClawConfig,
expected: ["browser", "demo-config-startup"],
});
});
it("loads startup-lazy external plugins from config only when explicitly enabled", () => {
expectStartupPluginIds({
config: {
channels: {},
plugins: {
slots: { memory: "none" },
entries: {
"external-config-startup": {
enabled: true,
config: { autoStart: { enabled: true } },
},
},
},
} as OpenClawConfig,
expected: ["browser", "external-config-startup"],
});
expectStartupPluginIds({
config: {
channels: {},
plugins: {
slots: { memory: "none" },
entries: {
"external-config-startup": {
config: { autoStart: { enabled: true } },
},
},
},
} as OpenClawConfig,
expected: ["browser"],
});
});
it("keeps startup-lazy external plugins behind config and activation policy", () => {
const externalEntry = {
enabled: true,
config: { autoStart: { enabled: true } },
};
const cases: Array<{ plugins: OpenClawConfig["plugins"]; expected: readonly string[] }> = [
{
plugins: {
slots: { memory: "none" },
entries: {
"external-config-startup": {
enabled: true,
config: { autoStart: { enabled: false } },
},
},
},
expected: ["browser"],
},
{
plugins: {
slots: { memory: "none" },
deny: ["external-config-startup"],
entries: { "external-config-startup": externalEntry },
},
expected: ["browser"],
},
{
plugins: {
slots: { memory: "none" },
allow: ["browser"],
entries: { "external-config-startup": externalEntry },
},
expected: ["browser"],
},
{
plugins: {
enabled: false,
slots: { memory: "none" },
entries: { "external-config-startup": externalEntry },
},
expected: [],
},
];
for (const testCase of cases) {
expectStartupPluginIds({
config: { channels: {}, plugins: testCase.plugins } as OpenClawConfig,
expected: testCase.expected,
});
}
});
it("does not let effective config broaden authored external config-path activation", () => {
const activationSourceConfig = {
channels: {},
plugins: {
allow: ["browser"],
slots: { memory: "none" },
entries: {
"external-config-startup": {
enabled: true,
config: { autoStart: { enabled: true } },
},
},
},
} as OpenClawConfig;
const runtimeConfig = {
...activationSourceConfig,
plugins: {
...activationSourceConfig.plugins,
allow: ["browser", "external-config-startup"],
},
} as OpenClawConfig;
expectStartupPluginIds({
config: runtimeConfig,
activationSourceConfig,
expected: ["browser"],
});
});
it("loads explicit hook-capability plugins at startup", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["external-hook-capability"],
allowPluginIds: ["external-hook-capability"],
noConfiguredChannels: true,
memorySlot: "none",
}),
expected: ["external-hook-capability"],
});
});
it("does not ambient-load hook-capability plugins at startup", () => {
expectStartupPluginIds({
config: createStartupConfig({
noConfiguredChannels: true,
memorySlot: "none",
}),
expected: ["browser"],
});
});
it("blocks hook-capability plugins when plugins are globally disabled", () => {
expectStartupPluginIds({
config: {
channels: {},
plugins: {
enabled: false,
allow: ["external-hook-capability"],
slots: { memory: "none" },
entries: {
"external-hook-capability": {
enabled: true,
},
},
},
},
expected: [],
});
});
it("blocks hook-capability plugins when explicitly denied", () => {
expectStartupPluginIds({
config: {
channels: {},
plugins: {
allow: ["external-hook-capability"],
deny: ["external-hook-capability"],
slots: { memory: "none" },
entries: {
"external-hook-capability": {
enabled: true,
},
},
},
},
expected: [],
});
});
it("loads explicit hook-policy plugins at startup", () => {
expectStartupPluginIds({
config: {
channels: {},
plugins: {
slots: { memory: "none" },
entries: {
browser: {
enabled: false,
},
"external-hook-policy": {
hooks: {
allowConversationAccess: true,
allowPromptInjection: true,
},
},
},
},
},
expected: ["external-hook-policy"],
});
});
it.each([
["conversation access", { allowConversationAccess: true }],
["prompt injection", { allowPromptInjection: true }],
] as const)("loads hook-policy plugins with only %s enabled", (_name, hooks) => {
expectStartupPluginIds({
config: {
channels: {},
plugins: {
slots: { memory: "none" },
entries: {
browser: {
enabled: false,
},
"external-hook-policy": {
hooks,
},
},
},
},
expected: ["external-hook-policy"],
});
});
it("keeps hook-policy plugins behind restrictive allowlists", () => {
expectStartupPluginIds({
config: {
channels: {},
plugins: {
allow: ["browser"],
slots: { memory: "none" },
entries: {
browser: {
enabled: false,
},
"external-hook-policy": {
hooks: {
allowPromptInjection: true,
},
},
},
},
},
expected: [],
});
});
it("does not let effective-only hook policy bypass the authored startup allowlist", () => {
const activationSourceConfig = {
channels: {},
plugins: {
allow: ["browser"],
slots: { memory: "none" },
entries: {
browser: {
enabled: false,
},
},
},
} as OpenClawConfig;
const runtimeConfig = {
channels: {},
plugins: {
allow: ["browser", "external-hook-policy"],
slots: { memory: "none" },
entries: {
browser: {
enabled: false,
},
"external-hook-policy": {
hooks: {
allowPromptInjection: true,
},
},
},
},
} as OpenClawConfig;
expectStartupPluginIds({
config: runtimeConfig,
activationSourceConfig,
expected: [],
});
});
it("starts bundled sidecars selected by root config activation paths", () => {
const rawConfig = {
browser: {
enabled: true,
defaultProfile: "docker-cdp",
},
channels: {},
} satisfies OpenClawConfig;
const effectiveConfig = {
...rawConfig,
plugins: {
entries: {
browser: {
enabled: true,
},
},
},
} satisfies OpenClawConfig;
expectStartupPluginIds({
config: effectiveConfig,
activationSourceConfig: rawConfig,
expected: ["browser", "memory-core"],
});
});
it("lets bundled root config activation paths bypass restrictive allowlists", () => {
expectStartupPluginIds({
config: {
browser: {
enabled: true,
},
channels: {},
plugins: {
allow: ["telegram"],
},
},
expected: ["browser"],
});
});
it("does not bypass restrictive allowlists for disabled root config activation paths", () => {
expectStartupPluginIds({
config: {
browser: {
enabled: false,
},
channels: {},
plugins: {
allow: ["telegram"],
},
},
expected: [],
});
});
it("does not let weak channel presence start untrusted workspace channel owners", () => {
useManifestRegistryFixture(createManifestRegistryFixtureWithWorkspaceDemoChannel());
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
const config = {} as OpenClawConfig;
expectStartupPluginIds({
config,
env: createPluginPlanningTestEnv({
DEMO_CHANNEL_ANYTHING: "1",
}),
expected: ["demo-channel", "browser", "memory-core"],
});
expect(
resolveConfiguredDeferredChannelPluginIdsForFixture({
config,
env: createPluginPlanningTestEnv({
DEMO_CHANNEL_ANYTHING: "1",
}),
}),
).toStrictEqual([]);
});
it("keeps explicitly trusted deferred channel owners eligible at startup", () => {
useManifestRegistryFixture(createManifestRegistryFixtureWithWorkspaceDemoChannel());
expect(
resolveConfiguredDeferredChannelPluginIdsForFixture({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
allow: ["workspace-demo-channel-plugin"],
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
}),
).toEqual(["workspace-demo-channel-plugin"]);
});
it("preserves explicit bundled channel config under restrictive allowlists", () => {
expectStartupPluginIds({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
allow: ["browser"],
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
expected: ["demo-channel", "browser"],
});
});
it("derives a conservative metadata manifest scope for restrictive startup allowlists", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
allow: ["browser"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "demo-channel"]);
});
it("keeps config-path activation owners in restrictive startup metadata scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
browser: {
enabled: true,
},
channels: {},
plugins: {
allow: ["openai"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "openai"]);
});
it("keeps configured agent model providers in restrictive startup metadata scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
agents: {
defaults: {
model: "amazon-bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0",
},
entries: { ops: { utilityModel: "openai/gpt-5.5-nano" } },
},
channels: {},
plugins: {
allow: ["browser"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["amazon-bedrock", "browser", "openai"]);
});
it("keeps configured memory embedding providers in restrictive startup metadata scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
memory: { search: { provider: "openai", fallback: "ollama" } },
agents: {
defaults: {},
},
channels: {},
plugins: {
allow: ["browser", "memory-core"],
slots: {
memory: "memory-core",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "memory-core", "ollama", "openai"]);
});
it("keeps durable worker-provider owners in restrictive startup metadata scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
channels: {},
plugins: { allow: ["browser"], slots: { memory: "none" } },
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
workerProviderIds: ["static-ssh"],
}),
).toEqual(["browser", "qa-lab"]);
});
it("keeps configured worker-provider owners in restrictive startup metadata scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
channels: {},
cloudWorkers: { profiles: { development: { provider: "static-ssh" } } },
plugins: { allow: ["browser"], slots: { memory: "none" } },
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "qa-lab"]);
});
it("uses installed-index model support for restrictive startup shorthand model scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
agents: {
defaults: {
model: "gpt-5.4@work",
},
},
channels: {},
plugins: {
allow: ["browser"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "openai"]);
});
it("does not use unsafe installed-index model support patterns for startup scopes", () => {
const registry = {
plugins: [
...createManifestRegistryFixture().plugins,
withManifestLoadPaths({
id: "unsafe-model-support",
channels: [],
origin: "bundled" as const,
enabledByDefault: true,
providers: [],
cliBackends: [],
modelSupport: {
modelPatterns: ["^(a+)+$"],
},
}),
],
diagnostics: [],
};
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
agents: {
defaults: {
model: "aaaaaaaaaaaaaaaaaaaaaaaa!",
},
},
channels: {},
plugins: {
allow: ["browser"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toBeUndefined();
});
it("falls back to unscoped metadata for legacy indexes without config-path activation metadata", () => {
const registry = createManifestRegistryFixture();
const legacyIndex = createInstalledPluginIndexFixture(registry);
const legacyPlugins = [...legacyIndex.plugins];
const browserPluginIndex = legacyPlugins.findIndex((plugin) => plugin.pluginId === "browser");
const browserPlugin = legacyPlugins[browserPluginIndex];
if (!browserPlugin) {
throw new Error("Expected browser plugin fixture");
}
legacyPlugins[browserPluginIndex] = {
...browserPlugin,
startup: {
sidecar: browserPlugin.startup.sidecar,
memory: browserPlugin.startup.memory,
deferConfiguredChannelFullLoadUntilAfterListen:
browserPlugin.startup.deferConfiguredChannelFullLoadUntilAfterListen,
agentHarnesses: browserPlugin.startup.agentHarnesses,
},
compat: ["activation-config-path-hint"],
};
const index = {
...legacyIndex,
plugins: legacyPlugins,
};
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
browser: {
enabled: true,
},
channels: {},
plugins: {
allow: ["openai"],
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toBeUndefined();
});
it("does not scope metadata manifests when bundled discovery compat can widen allowlists", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
plugins: {
allow: ["browser"],
bundledDiscovery: "compat",
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toBeUndefined();
});
it("falls back to unscoped metadata when a configured provider cannot be mapped before manifests", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
agents: {
defaults: {
mediaModels: { image: { primary: "unknown-provider/model" } },
},
},
plugins: {
allow: ["browser"],
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toBeUndefined();
});
it("scopes config-validation metadata to explicit plugin and configured channel owners", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveConfigValidationMetadataPluginIds({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
allow: ["openai"],
entries: {
browser: {
enabled: false,
config: {
profile: "default",
},
},
},
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "demo-channel", "openai"]);
});
it("uses installed-index provider contracts to scope config-validation provider owners", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveConfigValidationMetadataPluginIds({
config: {
channels: {},
tools: {
web: {
search: {
provider: "brave",
},
},
},
plugins: {
allow: ["browser"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["brave", "browser"]);
});
it("uses installed-index model support to scope shorthand model owners", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveConfigValidationMetadataPluginIds({
config: {
channels: {},
agents: {
defaults: {
model: "gpt-5.4@work",
},
},
plugins: {
allow: ["browser"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "openai"]);
});
it("uses heartbeat target channel ids for config-validation channel owner scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveConfigValidationMetadataPluginIds({
config: {
channels: {},
agents: {
defaults: {
heartbeat: {
target: "demo-channel",
},
},
},
plugins: {
allow: ["browser"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "demo-channel"]);
});
it("keeps disabled channel config owners in config-validation scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveConfigValidationMetadataPluginIds({
config: {
channels: {
"demo-channel": {
enabled: false,
},
},
plugins: {
allow: ["browser"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "demo-channel"]);
});
it("falls back to full validation metadata for unmapped shorthand models", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveConfigValidationMetadataPluginIds({
config: {
channels: {},
agents: {
defaults: {
model: "unknown-shorthand-model",
},
},
plugins: {
allow: ["browser"],
slots: {
memory: "none",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toBeUndefined();
});
it("does not add default startup-only plugins to config-validation scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveConfigValidationMetadataPluginIds({
config: {
channels: {},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual([]);
});
it("still scopes explicit validation metadata when runtime plugins are disabled", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveConfigValidationMetadataPluginIds({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
enabled: false,
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["demo-channel"]);
});
it("falls back to full validation metadata when disabled plugins use load paths", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveConfigValidationMetadataPluginIds({
config: {
channels: {},
plugins: {
enabled: false,
load: {
paths: ["/tmp/plugins/custom"],
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toBeUndefined();
});
it("does not treat explicitly disabled stale channel config as startup intent", () => {
expectStartupPluginIds({
config: {
channels: {
"demo-channel": {
enabled: false,
token: "stale",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
expected: ["browser", "memory-core"],
});
});
it("does not treat persisted auth alone as gateway startup intent", () => {
listPotentialConfiguredChannelIds.mockImplementation(
(
configForTest: OpenClawConfig,
_env: NodeJS.ProcessEnv,
options?: { includePersistedAuthState?: boolean },
) => (options?.includePersistedAuthState === false ? [] : ["demo-channel"]),
);
expectStartupPluginIds({
config: {} as OpenClawConfig,
env: createPluginPlanningTestEnv({
OPENCLAW_STATE_DIR: "/tmp/openclaw-with-persisted-demo-channel",
}),
expected: ["browser", "memory-core"],
});
});
it("does not treat persisted auth alone as deferred channel startup intent", () => {
useManifestRegistryFixture(createManifestRegistryFixtureWithWorkspaceDemoChannel());
listPotentialConfiguredChannelIds.mockImplementation(
(
configForTest: OpenClawConfig,
_env: NodeJS.ProcessEnv,
options?: { includePersistedAuthState?: boolean },
) => (options?.includePersistedAuthState === false ? [] : ["demo-channel"]),
);
expect(
resolveConfiguredDeferredChannelPluginIdsForFixture({
config: {
plugins: {
allow: ["workspace-demo-channel-plugin"],
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv({
OPENCLAW_STATE_DIR: "/tmp/openclaw-with-persisted-demo-channel",
}),
}),
).toStrictEqual([]);
});
it("resolves channel, deferred, and startup plugin ids from one manifest registry", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
const plan = resolveGatewayStartupPluginPlanFromRegistry({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
manifestRegistry: registry,
});
expect(plan.channelPluginIds).toContain("demo-channel");
expect(plan.pluginIds).toContain("demo-channel");
expect(plan.configuredDeferredChannelPluginIds).toStrictEqual([]);
});
it("carries deferred configured channel ids through the startup plan", () => {
const registry = createManifestRegistryFixtureWithWorkspaceDemoChannel();
const index = createInstalledPluginIndexFixture(registry);
const plan = resolveGatewayStartupPluginPlanFromRegistry({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
allow: ["workspace-demo-channel-plugin"],
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
manifestRegistry: registry,
});
expect(plan.pluginIds).toContain("workspace-demo-channel-plugin");
expect(plan.configuredDeferredChannelPluginIds).toStrictEqual([
"workspace-demo-channel-plugin",
]);
});
it("does not treat explicitly disabled stale channel config as deferred startup intent", () => {
useManifestRegistryFixture(createManifestRegistryFixtureWithWorkspaceDemoChannel());
expect(
resolveConfiguredDeferredChannelPluginIdsForFixture({
config: {
channels: {
"demo-channel": {
enabled: false,
token: "stale",
},
},
plugins: {
allow: ["workspace-demo-channel-plugin"],
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
}),
).toStrictEqual([]);
});
it("includes the explicitly selected memory slot plugin in startup scope", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["memory-lancedb"],
memorySlot: "memory-lancedb",
}),
expected: ["demo-channel", "browser", "memory-core", "memory-lancedb"],
});
});
it("includes memory-core as a dreaming sidecar for restrictive selected-memory allowlists", () => {
expectStartupPluginIds({
config: {
channels: {},
plugins: {
allow: ["browser", "memory-lancedb"],
slots: { memory: "memory-lancedb" },
entries: {
"memory-lancedb": { enabled: true, config: { dreaming: { enabled: true } } },
},
},
} as OpenClawConfig,
expected: ["browser", "memory-core", "memory-lancedb"],
});
});
it("includes memory-core in restrictive dreaming startup metadata scopes", () => {
const registry = createManifestRegistryFixture();
const index = createInstalledPluginIndexFixture(registry);
expect(
resolveGatewayStartupMetadataPluginIds({
config: {
channels: {},
plugins: {
allow: ["browser", "memory-lancedb"],
slots: { memory: "memory-lancedb" },
entries: {
"memory-lancedb": { enabled: true, config: { dreaming: { enabled: true } } },
},
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
index,
}),
).toEqual(["browser", "memory-core", "memory-lancedb"]);
});
it("does not include denied memory-core as a restrictive dreaming startup sidecar", () => {
expectStartupPluginIds({
config: {
channels: {},
plugins: {
allow: ["browser", "memory-lancedb"],
deny: ["memory-core"],
slots: { memory: "memory-lancedb" },
entries: {
"memory-lancedb": { enabled: true, config: { dreaming: { enabled: true } } },
},
},
} as OpenClawConfig,
expected: ["browser", "memory-lancedb"],
});
});
it("does not include explicitly disabled memory-core as a restrictive dreaming startup sidecar", () => {
expectStartupPluginIds({
config: {
channels: {},
plugins: {
allow: ["browser", "memory-lancedb"],
slots: { memory: "memory-lancedb" },
entries: {
"memory-core": { enabled: false },
"memory-lancedb": { enabled: true, config: { dreaming: { enabled: true } } },
},
},
} as OpenClawConfig,
expected: ["browser", "memory-lancedb"],
});
});
it("normalizes the raw memory slot id before startup filtering", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["memory-core"],
memorySlot: "Memory-Core",
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("includes the default memory slot plugin when the allowlist permits it", () => {
expectStartupPluginIds({
config: createStartupConfig({
allowPluginIds: ["browser", "memory-core"],
noConfiguredChannels: true,
}),
expected: ["browser", "memory-core"],
});
});
it("does not include non-selected memory plugins only because they are enabled", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["memory-lancedb"],
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("includes the selected context-engine slot plugin in startup scope even without activation.onStartup (#76576)", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["lossless-claw"],
contextEngine: "lossless-claw",
}),
expected: ["demo-channel", "browser", "memory-core", "lossless-claw"],
});
});
it("does not include context-engine plugins not selected via the slot", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["lossless-claw"],
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("does not include the context-engine slot plugin when it is the built-in legacy engine", () => {
expectStartupPluginIds({
config: createStartupConfig({
contextEngine: "legacy",
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("normalizes the context-engine slot id before startup filtering", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["lossless-claw"],
contextEngine: "Lossless-Claw",
}),
expected: ["demo-channel", "browser", "memory-core", "lossless-claw"],
});
});
it("ignores legacy default agent runtime during startup planning", () => {
expectStartupPluginIds({
config: createStartupConfig({
agentRuntimeId: "codex",
enabledPluginIds: ["codex"],
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("includes required agent harness owner plugins for model runtime policy", () => {
expectStartupPluginIds({
config: {
agents: {
defaults: {
models: {
"openai/gpt-5.5": { agentRuntime: { id: "codex" } },
},
},
},
plugins: {
entries: {
codex: { enabled: true },
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "openai", "codex", "memory-core"],
});
});
it("includes Codex when an OpenAI agent model uses the implicit runtime default", () => {
expectStartupPluginIds({
config: createStartupConfig({
modelId: "openai/gpt-5.5",
}),
expected: ["demo-channel", "browser", "openai", "codex", "memory-core"],
});
});
it("includes Codex when OpenAI is a selectable default agent model", () => {
expectStartupPluginIds({
config: {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
models: {
"openai/gpt-5.5": {},
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "anthropic", "openai", "codex", "memory-core"],
});
});
it("does not include Codex when an OpenAI model is manually pinned to OpenClaw", () => {
expectStartupPluginIds({
config: {
agents: {
defaults: {
model: { primary: "openai/gpt-5.5" },
models: {
"openai/gpt-5.5": { agentRuntime: { id: "openclaw" } },
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "openai", "memory-core"],
});
});
it("ignores legacy per-agent runtime during startup planning", () => {
expectStartupPluginIds({
config: createStartupConfig({
agentRuntimeIds: ["codex"],
enabledPluginIds: ["codex"],
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("ignores env runtime overrides during startup planning", () => {
expectStartupPluginIds({
config: createStartupConfig({
enabledPluginIds: ["codex"],
}),
env: { OPENCLAW_AGENT_RUNTIME: "codex" },
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("ignores legacy CLI backend runtime during startup planning", () => {
expectStartupPluginIds({
config: createStartupConfig({
agentRuntimeId: "demo-cli",
enabledPluginIds: ["demo-provider-plugin"],
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("includes required CLI backend owner plugins for provider runtime policy", () => {
expectStartupPluginIds({
config: {
models: {
providers: {
"demo-provider": {
baseUrl: "https://example.com",
models: [],
agentRuntime: { id: "demo-cli" },
},
},
},
plugins: {
entries: {
"demo-provider-plugin": { enabled: true },
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "demo-provider-plugin", "memory-core"],
});
});
it("includes required CLI backend owner plugins for model runtime policy", () => {
expectStartupPluginIds({
config: {
agents: {
defaults: {
models: {
"anthropic/claude-opus-4-6": { agentRuntime: { id: "claude-cli" } },
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "anthropic", "memory-core"],
});
});
it.each(["claude-cli", "codex-cli", "google-gemini-cli"] as const)(
"ignores legacy bundled %s runtime at startup",
(runtime) => {
expectStartupPluginIds({
config: createStartupConfig({
agentRuntimeId: runtime,
}),
expected: ["demo-channel", "browser", "memory-core"],
});
},
);
it("does not include required CLI backend owner plugins when they are explicitly disabled", () => {
expectStartupPluginIds({
config: {
models: {
providers: {
"demo-provider": {
baseUrl: "https://example.com",
models: [],
agentRuntime: { id: "demo-cli" },
},
},
},
plugins: {
entries: {
"demo-provider-plugin": {
enabled: false,
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("does not include required agent harness owner plugins when they are explicitly disabled", () => {
expectStartupPluginIds({
config: {
agents: {
defaults: {
models: {
"openai/gpt-5.5": { agentRuntime: { id: "codex" } },
},
},
},
plugins: {
entries: {
codex: {
enabled: false,
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "openai", "memory-core"],
});
});
});
describe("resolveConfiguredChannelPluginIds", () => {
beforeEach(() => {
listPotentialConfiguredChannelIds.mockReset().mockImplementation((config: OpenClawConfig) => {
if (Object.hasOwn(config, "channels")) {
return Object.keys(config.channels ?? {});
}
return [];
});
listPotentialConfiguredChannelPresenceSignals
.mockReset()
.mockImplementation((config: OpenClawConfig) => {
return listPotentialConfiguredChannelIds(config).map((channelId: string) => ({
channelId,
source: "config",
}));
});
useManifestRegistryFixture();
});
it.each([
{
name: "uses manifest activation channel ownership before falling back to direct channel lists",
config: createStartupConfig({ channelIds: ["activation-only-channel"] }),
expected: ["activation-only-channel-plugin"],
},
{
name: "keeps bundled activation owners behind restrictive allowlists",
config: createStartupConfig({
channelIds: ["activation-only-channel"],
allowPluginIds: ["browser"],
}),
expected: [],
},
{
name: "keeps explicitly configured bundled channel owners under restrictive allowlists",
config: {
channels: { "demo-channel": { token: "configured" } },
plugins: { allow: ["browser"] },
} as OpenClawConfig,
env: {},
expected: ["demo-channel"],
},
{
name: "blocks bundled activation owners when explicitly denied",
config: {
channels: { "activation-only-channel": { enabled: true } },
plugins: { deny: ["activation-only-channel-plugin"] },
} as OpenClawConfig,
expected: [],
},
{
name: "blocks bundled activation owners when plugins are globally disabled",
config: {
channels: { "activation-only-channel": { enabled: true } },
plugins: { enabled: false },
} as OpenClawConfig,
expected: [],
},
{
name: "filters untrusted workspace activation owners from configured-channel runtime planning",
config: createStartupConfig({ channelIds: ["workspace-activation-channel"] }),
expected: [],
},
{
name: "filters untrusted global activation owners from configured-channel runtime planning",
config: createStartupConfig({ channelIds: ["global-activation-channel"] }),
expected: [],
},
{
name: "keeps explicitly enabled global activation owners eligible for configured-channel runtime planning",
config: createStartupConfig({
channelIds: ["global-activation-channel"],
enabledPluginIds: ["global-activation-channel-plugin"],
}),
expected: ["global-activation-channel-plugin"],
},
{
name: "does not treat auto-enabled non-bundled channel owners as explicitly trusted",
config: createStartupConfig({
channelIds: ["global-activation-channel"],
enabledPluginIds: ["global-activation-channel-plugin"],
}),
activationSourceConfig: createStartupConfig({
channelIds: ["global-activation-channel"],
}),
expected: [],
},
{
name: "blocks bundled activation owners when explicitly disabled",
config: {
channels: { "activation-only-channel": { enabled: true } },
plugins: { entries: { "activation-only-channel-plugin": { enabled: false } } },
} as OpenClawConfig,
expected: [],
},
] satisfies Array<{
name: string;
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
expected: string[];
}>)("$name", ({ config, activationSourceConfig, env, expected }) => {
expect(
resolveConfiguredChannelPluginIds({
config,
...(activationSourceConfig ? { activationSourceConfig } : {}),
workspaceDir: "/tmp",
env: env ?? process.env,
}),
).toStrictEqual(expected);
});
});
describe("listConfiguredChannelIdsForReadOnlyScope", () => {
beforeEach(() => {
listPotentialConfiguredChannelIds.mockReset().mockReturnValue([]);
listPotentialConfiguredChannelPresenceSignals.mockReset().mockReturnValue([]);
hasMeaningfulChannelConfig.mockClear();
useManifestRegistryFixture();
});
it("filters bundled ambient channel triggers through effective activation", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
plugins: {
allow: ["memory-core"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
DEMO_FAKE_TEST_TRIGGER: "present",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
expect(
hasConfiguredChannelsForReadOnlyScope({
config: {
plugins: {
allow: ["memory-core"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
DEMO_FAKE_TEST_TRIGGER: "present",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toBe(false);
});
it("returns reason-rich policy entries for blocked ambient channel triggers", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
resolveConfiguredChannelPresencePolicy({
config: {
plugins: {
allow: ["memory-core"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
DEMO_FAKE_TEST_TRIGGER: "present",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toEqual([
{
channelId: "demo-channel",
sources: ["env"],
effective: false,
pluginIds: [],
blockedReasons: ["not-in-allowlist"],
},
]);
});
it("suppresses env-only presence when ambient triggers are disabled", () => {
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
resolveConfiguredChannelPresencePolicy({
config: {},
workspaceDir: "/tmp",
env: { DEMO_FAKE_TEST_TRIGGER: "present" } as NodeJS.ProcessEnv,
includePersistedAuthState: false,
ambientEnvTriggers: "suppress",
}),
).toStrictEqual([]);
});
it("suppresses manifest-env-only presence when ambient triggers are disabled", () => {
expect(
resolveConfiguredChannelPresencePolicy({
config: {
plugins: {
allow: ["external-env-channel-plugin"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_HOST: "irc.example.com",
EXTERNAL_ENV_CHANNEL_NICK: "openclaw",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
ambientEnvTriggers: "suppress",
}),
).toStrictEqual([]);
});
it("retains mixed explicit-config and env presence under suppression", () => {
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
resolveConfiguredChannelPresencePolicy({
config: {
channels: {
"demo-channel": { enabled: true },
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: { DEMO_FAKE_TEST_TRIGGER: "present" } as NodeJS.ProcessEnv,
includePersistedAuthState: false,
ambientEnvTriggers: "suppress",
}),
).toEqual([
{
channelId: "demo-channel",
sources: ["env", "explicit-config"],
effective: true,
pluginIds: ["demo-channel"],
blockedReasons: [],
},
]);
});
it("keeps explicitly enabled bundled ambient channel triggers", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
plugins: {
entries: {
"demo-channel": {
enabled: true,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
DEMO_FAKE_TEST_TRIGGER: "present",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toEqual(["demo-channel"]);
});
it("treats enabled-only channel config as explicit read-only intent", () => {
expect(
resolveConfiguredChannelPresencePolicy({
config: {
channels: {
"demo-channel": {
enabled: true,
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual([
{
channelId: "demo-channel",
sources: ["explicit-config"],
effective: true,
pluginIds: ["demo-channel"],
blockedReasons: [],
},
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
channels: {
"demo-channel": {
enabled: true,
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual(["demo-channel"]);
});
it("does not treat disabled stale channel config as explicit read-only intent", () => {
const config = {
channels: {
"demo-channel": {
enabled: false,
token: "stale-token",
},
},
} as OpenClawConfig;
expect(listExplicitConfiguredChannelIdsForConfig(config)).toStrictEqual([]);
expect(
resolveConfiguredChannelPresencePolicy({
config,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toStrictEqual([]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toStrictEqual([]);
});
it("treats disabled channel config as a hard read-only env suppressor", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
const config = {
channels: {
"Demo-Channel": {
enabled: false,
token: "stale-token",
},
},
plugins: {
entries: {
"demo-channel": {
enabled: true,
},
},
},
} as OpenClawConfig;
expect(
resolveConfiguredChannelPresencePolicy({
config,
workspaceDir: "/tmp",
env: {
DEMO_FAKE_TEST_TRIGGER: "ambient",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config,
workspaceDir: "/tmp",
env: {
DEMO_FAKE_TEST_TRIGGER: "ambient",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
});
it("treats disabled channel config as a hard persisted-auth suppressor", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "persisted-auth" },
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
channels: {
"demo-channel": {
enabled: false,
},
},
plugins: {
entries: {
"demo-channel": {
enabled: true,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toStrictEqual([]);
});
it("treats disabled channel config as a hard manifest-env suppressor", () => {
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
channels: {
"external-env-channel": {
enabled: false,
},
},
plugins: {
allow: ["external-env-channel-plugin"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_TOKEN: "present",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
});
it("requires every package manifest allOf env variable", () => {
const config = {
plugins: {
allow: ["external-env-channel-plugin"],
},
} as OpenClawConfig;
expect(
listConfiguredChannelIdsForReadOnlyScope({
config,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_HOST: "irc.example.com",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_HOST: "irc.example.com",
EXTERNAL_ENV_CHANNEL_NICK: "openclaw",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toContain("external-env-channel");
});
it("lets explicit bundled channel config bypass restrictive allowlists", () => {
const config = {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
allow: ["browser"],
},
} as OpenClawConfig;
expect(
resolveConfiguredChannelPresencePolicy({
config,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual([
{
channelId: "demo-channel",
sources: ["explicit-config"],
effective: true,
pluginIds: ["demo-channel"],
blockedReasons: [],
},
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual(["demo-channel"]);
});
it("keeps explicitly configured bundled channels discovered from potential ids", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "config" },
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toEqual(["demo-channel"]);
});
it("blocks explicitly configured bundled channels when plugins are disabled or denied", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "config" },
]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
enabled: false,
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toStrictEqual([]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
deny: ["demo-channel"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
includePersistedAuthState: false,
}),
).toStrictEqual([]);
});
it("lists explicit configured channels without ambient env triggers", () => {
expect(
listExplicitConfiguredChannelIdsForConfig({
channels: {
defaults: {
model: "sonnet-4.6",
},
"demo-channel": {
token: "configured",
},
"demo-other-channel": {
enabled: false,
},
},
} as OpenClawConfig),
).toEqual(["demo-channel"]);
});
it("does not let disabled mixed-case channel config announce ambient matches", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
]);
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
channels: {
"Demo-Channel": {
enabled: false,
token: "stale-token",
},
},
plugins: {
entries: {
"demo-channel": {
enabled: true,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
DEMO_FAKE_TEST_TRIGGER: "ambient",
} as NodeJS.ProcessEnv,
}),
).toStrictEqual([]);
});
it("uses effective read-only channel policy for announce channels", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["demo-channel", "demo-other-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "demo-channel", source: "env" },
{ channelId: "demo-other-channel", source: "config" },
]);
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
channels: {
"demo-other-channel": {
token: "configured",
},
},
plugins: {
allow: ["demo-other-channel"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
DEMO_FAKE_TEST_TRIGGER: "ambient",
} as NodeJS.ProcessEnv,
}),
).toEqual(["demo-other-channel"]);
});
it("announces explicit configured channels without installed owners", () => {
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
channels: {
clickclack: {
token: "configured",
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toStrictEqual(["clickclack"]);
});
it("does not announce ownerless explicit channels suppressed by plugin policy", () => {
const ownerlessChannelConfig = {
channels: {
clickclack: {
token: "configured",
},
},
} as OpenClawConfig;
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
...ownerlessChannelConfig,
plugins: {
enabled: false,
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toStrictEqual([]);
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
...ownerlessChannelConfig,
plugins: {
deny: ["clickclack"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toStrictEqual([]);
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
...ownerlessChannelConfig,
plugins: {
entries: {
clickclack: {
enabled: false,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toStrictEqual([]);
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
...ownerlessChannelConfig,
plugins: {
allow: ["slack"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toStrictEqual([]);
});
it("does not announce explicit channels suppressed by plugin policy", () => {
const baseConfig = {
channels: {
"demo-channel": {
token: "configured",
},
},
} as OpenClawConfig;
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
...baseConfig,
plugins: {
enabled: false,
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toStrictEqual([]);
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
...baseConfig,
plugins: {
deny: ["demo-channel"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toStrictEqual([]);
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
...baseConfig,
plugins: {
entries: {
"demo-channel": {
enabled: false,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toStrictEqual([]);
});
it("keeps announce channels with another effective owner", () => {
expect(
listConfiguredAnnounceChannelIdsForConfig({
config: {
channels: {
shared: {
token: "configured",
},
},
plugins: {
entries: {
"shared-good": {
enabled: true,
},
"shared-disabled": {
enabled: false,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
manifestRecords: [
{
id: "shared-good",
channels: ["shared"],
origin: "config",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
} as never,
{
id: "shared-disabled",
channels: ["shared"],
origin: "config",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
} as never,
],
}),
).toStrictEqual(["shared"]);
});
it("does not treat activation-only declarations as channel ownership", () => {
listPotentialConfiguredChannelIds.mockReturnValue(["activation-only-channel"]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([
{ channelId: "activation-only-channel", source: "env" },
]);
expect(
resolveConfiguredChannelPresencePolicy({
config: {
plugins: {
entries: {
"activation-only-channel-plugin": {
enabled: true,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
ACTIVATION_ONLY_CHANNEL_TOKEN: "ambient",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toEqual([
{
channelId: "activation-only-channel",
sources: ["env"],
effective: false,
pluginIds: [],
blockedReasons: ["no-channel-owner"],
},
]);
});
});
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */