Files
openclaw/src/plugins/channel-plugin-ids.test.ts
Peter Steinberger a0f35574d0 Remove codex-cli backend and migrate to Codex runtime
Remove the bundled codex-cli backend, migrate legacy codex-cli refs and runtime pins to the Codex app-server runtime, and update live/backend workflow coverage for the supported CLI lanes.
2026-05-14 10:07:18 +01:00

2419 lines
67 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.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 (
!!value &&
typeof value === "object" &&
!Array.isArray(value) &&
(value as { enabled?: unknown }).enabled === false
);
})
.map(([channelId]) => channelId.toLowerCase());
}),
);
const listPotentialConfiguredChannelPresenceSignals = vi.hoisted(() => vi.fn());
const hasPotentialConfiguredChannels = vi.hoisted(() => vi.fn());
const hasMeaningfulChannelConfig = vi.hoisted(() =>
vi.fn((value: unknown) => {
return (
!!value &&
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,
hasPotentialConfiguredChannels,
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,
resolveConfiguredChannelPresencePolicy,
resolveConfiguredDeferredChannelPluginIdsFromRegistry,
resolveConfiguredChannelPluginIds,
resolveGatewayStartupPluginIds,
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 {
return {
plugins: [
{
id: "demo-channel",
channels: ["demo-channel"],
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "demo-other-channel",
channels: ["demo-other-channel"],
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "browser",
channels: [],
activation: {
onStartup: true,
onConfigPaths: ["browser"],
},
origin: "bundled",
enabledByDefault: true,
providers: [],
cliBackends: [],
},
{
id: "demo-provider-plugin",
channels: [],
origin: "bundled",
enabledByDefault: undefined,
providers: ["demo-provider"],
cliBackends: ["demo-cli"],
},
{
id: "microsoft",
channels: [],
origin: "bundled",
enabledByDefault: true,
providers: [],
cliBackends: [],
contracts: { speechProviders: ["microsoft"] },
},
{
id: "tts-local-cli",
channels: [],
origin: "bundled",
enabledByDefault: true,
providers: [],
cliBackends: [],
contracts: { speechProviders: ["tts-local-cli", "cli"] },
},
{
id: "anthropic",
channels: [],
origin: "bundled",
enabledByDefault: true,
providers: ["anthropic"],
cliBackends: ["claude-cli"],
},
{
id: "openai",
channels: [],
origin: "bundled",
enabledByDefault: true,
providers: ["openai", "openai-codex"],
cliBackends: [],
contracts: {
imageGenerationProviders: ["openai"],
videoGenerationProviders: ["openai"],
},
},
{
id: "google",
channels: [],
origin: "bundled",
enabledByDefault: true,
providers: ["google", "google-gemini-cli"],
cliBackends: ["google-gemini-cli"],
contracts: {
imageGenerationProviders: ["google"],
videoGenerationProviders: ["google"],
musicGenerationProviders: ["google"],
},
},
{
id: "codex",
channels: [],
activation: {
onAgentHarnesses: ["codex"],
},
origin: "bundled",
enabledByDefault: undefined,
providers: ["codex"],
cliBackends: [],
},
{
id: "activation-only-channel-plugin",
channels: [],
activation: {
onChannels: ["activation-only-channel"],
},
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "workspace-activation-channel-plugin",
channels: [],
activation: {
onChannels: ["workspace-activation-channel"],
},
origin: "workspace",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "global-activation-channel-plugin",
channels: [],
activation: {
onChannels: ["global-activation-channel"],
},
origin: "global",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "external-env-channel-plugin",
channels: ["external-env-channel"],
channelEnvVars: {
"external-env-channel": ["EXTERNAL_ENV_CHANNEL_TOKEN"],
},
origin: "config",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "ambient-env-channel-plugin",
channels: ["ambient-env-channel"],
channelEnvVars: {
"ambient-env-channel": ["HOME", "PATH"],
},
origin: "config",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "voice-call",
channels: [],
activation: {
onStartup: true,
},
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "memory-core",
kind: "memory",
channels: [],
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "memory-lancedb",
kind: "memory",
channels: [],
origin: "bundled",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "demo-global-sidecar",
channels: [],
activation: {
onStartup: true,
},
origin: "global",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "demo-global-startup-opt-out",
channels: [],
activation: {
onStartup: false,
},
origin: "global",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "demo-global-explicit-startup",
channels: [],
activation: {
onStartup: true,
},
origin: "global",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "external-hook-capability",
channels: [],
activation: {
onCapabilities: ["hook"],
},
origin: "global",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "external-hook-policy",
channels: [],
origin: "global",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
{
id: "lossless-claw",
kind: "context-engine",
channels: [],
// No activation.onStartup — this is the bug scenario (#76576):
// external context-engine plugins do not set onStartup but must be
// included in gateway startup when selected via plugins.slots.contextEngine.
origin: "installed",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
},
].map(withManifestLoadPaths) 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),
},
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 {
OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY: "1",
...overrides,
};
}
function useManifestRegistryFixture(
registry: PluginManifestRegistry = createManifestRegistryFixture(),
) {
const index = createInstalledPluginIndexFixture(registry);
loadPluginManifestRegistry.mockReset().mockReturnValue(registry);
loadPluginRegistrySnapshot.mockReset().mockReturnValue(index);
return { registry, index };
}
function expectStartupPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
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,
}),
).toEqual(params.expected);
}
function expectStartupPluginIdsCase(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
expected: readonly string[];
}) {
expectStartupPluginIds(params);
}
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;
return {
...(params.noConfiguredChannels
? {
channels: {},
}
: params.channelIds?.length
? {
channels: Object.fromEntries(
params.channelIds.map((channelId) => [channelId, { enabled: true }]),
),
}
: {}),
...(params.enabledPluginIds?.length
? {
plugins: {
...(params.allowPluginIds?.length ? { allow: params.allowPluginIds } : {}),
...(hasSlots ? { slots: slotsConfig } : {}),
entries: Object.fromEntries(
params.enabledPluginIds.map((pluginId) => [pluginId, { enabled: true }]),
),
},
}
: params.allowPluginIds?.length
? {
plugins: {
allow: params.allowPluginIds,
},
}
: hasSlots
? {
plugins: {
slots: slotsConfig,
},
}
: {}),
...(params.providerIds?.length
? {
models: {
providers: Object.fromEntries(
params.providerIds.map((providerId) => [
providerId,
{
baseUrl: "https://example.com",
models: [],
},
]),
),
},
}
: {}),
...(params.modelId
? {
agents: {
defaults: {
model: { primary: params.modelId },
...(params.agentRuntimeId
? {
agentRuntime: {
id: params.agentRuntimeId,
fallback: "none",
},
}
: {}),
models: {
[params.modelId]: {},
},
},
...(params.agentRuntimeIds?.length
? {
list: params.agentRuntimeIds.map((runtime, index) => ({
id: `agent-${index + 1}`,
agentRuntime: { id: runtime },
})),
}
: {}),
},
}
: params.agentRuntimeId || params.agentRuntimeIds?.length
? {
agents: {
defaults: params.agentRuntimeId
? {
agentRuntime: {
id: params.agentRuntimeId,
fallback: "none",
},
}
: {},
...(params.agentRuntimeIds?.length
? {
list: params.agentRuntimeIds.map((runtime, index) => ({
id: `agent-${index + 1}`,
agentRuntime: { id: runtime },
})),
}
: {}),
},
}
: {}),
} as OpenClawConfig;
}
describe("resolveGatewayStartupPluginIds", () => {
beforeEach(() => {
listPotentialConfiguredChannelIds.mockReset().mockImplementation((config: OpenClawConfig) => {
if (Object.prototype.hasOwnProperty.call(config, "channels")) {
return Object.keys(config.channels ?? {});
}
return ["demo-channel"];
});
listPotentialConfiguredChannelPresenceSignals
.mockReset()
.mockImplementation((config: OpenClawConfig) => {
return listPotentialConfiguredChannelIds(config).map((channelId: string) => ({
channelId,
source: "config",
}));
});
hasPotentialConfiguredChannels.mockReset().mockImplementation((config: OpenClawConfig) => {
if (Object.prototype.hasOwnProperty.call(config, "channels")) {
return Object.keys(config.channels ?? {}).length > 0;
}
return true;
});
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 configured bundled speech providers at startup",
{
channels: {},
messages: { tts: { provider: "microsoft" } },
} as OpenClawConfig,
["browser", "microsoft", "memory-core"],
],
[
"includes bundled speech providers configured by provider block",
{
channels: {},
messages: { 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: {},
messages: { tts: { provider: "edge" } },
} as OpenClawConfig,
["browser", "microsoft", "memory-core"],
],
[
"includes active persona speech providers at startup",
{
channels: {},
messages: {
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: {},
messages: {
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" } },
},
messages: {
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" } },
},
},
},
messages: {
tts: {
personas: {
narrator: {
label: "Narrator",
provider: "microsoft",
},
},
},
},
} as OpenClawConfig,
["demo-channel", "browser", "microsoft", "memory-core"],
],
[
"honors disabled speech provider config blocks at startup",
{
channels: {},
messages: {
tts: {
provider: "microsoft",
providers: { microsoft: { enabled: false } },
},
},
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"honors explicit plugin disablement for configured speech providers",
{
channels: {},
messages: { 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: {
imageGenerationModel: {
primary: "openai/gpt-image-2",
fallbacks: ["google/gemini-3-pro-image-preview"],
},
videoGenerationModel: {
primary: "google/veo-3.1-fast-generate-preview",
},
musicGenerationModel: {
primary: "google/lyria-3-clip-preview",
},
},
},
} as OpenClawConfig,
["browser", "openai", "google", "memory-core"],
],
[
"honors explicit plugin disablement for configured generation providers",
{
channels: {},
agents: {
defaults: {
imageGenerationModel: { primary: "google/gemini-3-pro-image-preview" },
},
},
plugins: { entries: { google: { enabled: false } } },
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"keeps configured generation providers behind restrictive allowlists",
{
channels: {},
agents: {
defaults: {
imageGenerationModel: { 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"],
],
[
"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) => {
expectStartupPluginIdsCase({ config, expected });
});
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;
expectStartupPluginIdsCase({
config: effectiveConfig,
activationSourceConfig: rawConfig,
expected: ["browser"],
});
});
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;
expectStartupPluginIdsCase({
config: runtimeConfig,
activationSourceConfig,
expected: [],
});
});
it("skips startup when activation.onStartup is false", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
enabledPluginIds: ["demo-global-explicit-startup"],
allowPluginIds: ["demo-global-explicit-startup"],
noConfiguredChannels: true,
memorySlot: "none",
}),
expected: ["demo-global-explicit-startup"],
});
});
it("loads explicit hook-capability plugins at startup", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
noConfiguredChannels: true,
memorySlot: "none",
}),
expected: ["browser"],
});
});
it("blocks hook-capability plugins when plugins are globally disabled", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
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) => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
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;
expectStartupPluginIdsCase({
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;
expectStartupPluginIdsCase({
config: effectiveConfig,
activationSourceConfig: rawConfig,
expected: ["browser", "memory-core"],
});
});
it("lets bundled root config activation paths bypass restrictive allowlists", () => {
expectStartupPluginIdsCase({
config: {
browser: {
enabled: true,
},
channels: {},
plugins: {
allow: ["telegram"],
},
},
expected: ["browser"],
});
});
it("does not bypass restrictive allowlists for disabled root config activation paths", () => {
expectStartupPluginIdsCase({
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;
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
allow: ["browser"],
},
} as OpenClawConfig,
env: createPluginPlanningTestEnv(),
expected: ["demo-channel", "browser"],
});
});
it("does not treat explicitly disabled stale channel config as startup intent", () => {
expectStartupPluginIdsCase({
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(
(
_config: OpenClawConfig,
_env: NodeJS.ProcessEnv,
options?: { includePersistedAuthState?: boolean },
) => (options?.includePersistedAuthState === false ? [] : ["demo-channel"]),
);
expectStartupPluginIdsCase({
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(
(
_config: 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("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", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
enabledPluginIds: ["memory-lancedb"],
memorySlot: "memory-lancedb",
}),
expected: ["demo-channel", "browser", "memory-lancedb"],
});
});
it("normalizes the raw memory slot id before startup filtering", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
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)", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
contextEngine: "legacy",
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("normalizes the context-engine slot id before startup filtering", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
agentRuntimeId: "codex",
enabledPluginIds: ["codex"],
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("includes required agent harness owner plugins for model runtime policy", () => {
expectStartupPluginIdsCase({
config: {
agents: {
defaults: {
models: {
"openai/gpt-5.5": { agentRuntime: { id: "codex" } },
},
},
},
plugins: {
entries: {
codex: { enabled: true },
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "codex", "memory-core"],
});
});
it("includes Codex when an OpenAI agent model uses the implicit runtime default", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
modelId: "openai/gpt-5.5",
}),
expected: ["demo-channel", "browser", "codex", "memory-core"],
});
});
it("includes Codex when OpenAI is a selectable default agent model", () => {
expectStartupPluginIdsCase({
config: {
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-6" },
models: {
"openai/gpt-5.5": {},
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "codex", "memory-core"],
});
});
it("does not include Codex when an OpenAI model is manually pinned to PI", () => {
expectStartupPluginIdsCase({
config: {
agents: {
defaults: {
model: { primary: "openai/gpt-5.5" },
models: {
"openai/gpt-5.5": { agentRuntime: { id: "pi" } },
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("ignores legacy per-agent runtime during startup planning", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
agentRuntimeIds: ["codex"],
enabledPluginIds: ["codex"],
}),
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("ignores env runtime overrides during startup planning", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
enabledPluginIds: ["codex"],
}),
env: { OPENCLAW_AGENT_RUNTIME: "codex" },
expected: ["demo-channel", "browser", "memory-core"],
});
});
it("ignores legacy CLI backend runtime during startup planning", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
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) => {
expectStartupPluginIdsCase({
config: createStartupConfig({
agentRuntimeId: runtime,
}),
expected: ["demo-channel", "browser", "memory-core"],
});
},
);
it("does not include required CLI backend owner plugins when they are explicitly disabled", () => {
expectStartupPluginIdsCase({
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", () => {
expectStartupPluginIdsCase({
config: {
agents: {
defaults: {
models: {
"openai/gpt-5.5": { agentRuntime: { id: "codex" } },
},
},
},
plugins: {
entries: {
codex: {
enabled: false,
},
},
},
} as OpenClawConfig,
expected: ["demo-channel", "browser", "memory-core"],
});
});
});
describe("resolveConfiguredChannelPluginIds", () => {
beforeEach(() => {
listPotentialConfiguredChannelIds.mockReset().mockImplementation((config: OpenClawConfig) => {
if (Object.prototype.hasOwnProperty.call(config, "channels")) {
return Object.keys(config.channels ?? {});
}
return [];
});
listPotentialConfiguredChannelPresenceSignals
.mockReset()
.mockImplementation((config: OpenClawConfig) => {
return listPotentialConfiguredChannelIds(config).map((channelId: string) => ({
channelId,
source: "config",
}));
});
hasPotentialConfiguredChannels.mockReset().mockImplementation((config: OpenClawConfig) => {
if (Object.prototype.hasOwnProperty.call(config, "channels")) {
return Object.keys(config.channels ?? {}).length > 0;
}
return false;
});
useManifestRegistryFixture();
});
it("uses manifest activation channel ownership before falling back to direct channel lists", () => {
expect(
resolveConfiguredChannelPluginIds({
config: createStartupConfig({
channelIds: ["activation-only-channel"],
}),
workspaceDir: "/tmp",
env: process.env,
}),
).toEqual(["activation-only-channel-plugin"]);
});
it("keeps bundled activation owners behind restrictive allowlists", () => {
expect(
resolveConfiguredChannelPluginIds({
config: createStartupConfig({
channelIds: ["activation-only-channel"],
allowPluginIds: ["browser"],
}),
workspaceDir: "/tmp",
env: process.env,
}),
).toStrictEqual([]);
});
it("keeps explicitly configured bundled channel owners under restrictive allowlists", () => {
expect(
resolveConfiguredChannelPluginIds({
config: {
channels: {
"demo-channel": {
token: "configured",
},
},
plugins: {
allow: ["browser"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {},
}),
).toEqual(["demo-channel"]);
});
it("blocks bundled activation owners when explicitly denied", () => {
expect(
resolveConfiguredChannelPluginIds({
config: {
channels: {
"activation-only-channel": { enabled: true },
},
plugins: {
deny: ["activation-only-channel-plugin"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: process.env,
}),
).toStrictEqual([]);
});
it("blocks bundled activation owners when plugins are globally disabled", () => {
expect(
resolveConfiguredChannelPluginIds({
config: {
channels: {
"activation-only-channel": { enabled: true },
},
plugins: {
enabled: false,
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: process.env,
}),
).toStrictEqual([]);
});
it("filters untrusted workspace activation owners from configured-channel runtime planning", () => {
expect(
resolveConfiguredChannelPluginIds({
config: createStartupConfig({
channelIds: ["workspace-activation-channel"],
}),
workspaceDir: "/tmp",
env: process.env,
}),
).toStrictEqual([]);
});
it("filters untrusted global activation owners from configured-channel runtime planning", () => {
expect(
resolveConfiguredChannelPluginIds({
config: createStartupConfig({
channelIds: ["global-activation-channel"],
}),
workspaceDir: "/tmp",
env: process.env,
}),
).toStrictEqual([]);
});
it("keeps explicitly enabled global activation owners eligible for configured-channel runtime planning", () => {
expect(
resolveConfiguredChannelPluginIds({
config: createStartupConfig({
channelIds: ["global-activation-channel"],
enabledPluginIds: ["global-activation-channel-plugin"],
}),
workspaceDir: "/tmp",
env: process.env,
}),
).toEqual(["global-activation-channel-plugin"]);
});
it("does not treat auto-enabled non-bundled channel owners as explicitly trusted", () => {
expect(
resolveConfiguredChannelPluginIds({
config: createStartupConfig({
channelIds: ["global-activation-channel"],
enabledPluginIds: ["global-activation-channel-plugin"],
}),
activationSourceConfig: createStartupConfig({
channelIds: ["global-activation-channel"],
}),
workspaceDir: "/tmp",
env: process.env,
}),
).toStrictEqual([]);
});
it("includes trusted external channel owners configured only by manifest env vars", () => {
expect(
resolveConfiguredChannelPluginIds({
config: {
plugins: {
allow: ["external-env-channel-plugin"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_TOKEN: "token",
} as NodeJS.ProcessEnv,
}),
).toEqual(["external-env-channel-plugin"]);
});
it("blocks bundled activation owners when explicitly disabled", () => {
expect(
resolveConfiguredChannelPluginIds({
config: {
channels: {
"activation-only-channel": { enabled: true },
},
plugins: {
entries: {
"activation-only-channel-plugin": {
enabled: false,
},
},
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: process.env,
}),
).toStrictEqual([]);
});
});
describe("listConfiguredChannelIdsForReadOnlyScope", () => {
beforeEach(() => {
listPotentialConfiguredChannelIds.mockReset().mockReturnValue([]);
listPotentialConfiguredChannelPresenceSignals.mockReset().mockReturnValue([]);
hasPotentialConfiguredChannels.mockReset().mockReturnValue(false);
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_CHANNEL_TOKEN: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
expect(
hasConfiguredChannelsForReadOnlyScope({
config: {
plugins: {
allow: ["memory-core"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
DEMO_CHANNEL_TOKEN: "token",
} 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_CHANNEL_TOKEN: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toEqual([
{
channelId: "demo-channel",
sources: ["env"],
effective: false,
pluginIds: [],
blockedReasons: ["not-in-allowlist"],
},
]);
});
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_CHANNEL_TOKEN: "token",
} 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_CHANNEL_TOKEN: "ambient",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
expect(
listConfiguredChannelIdsForReadOnlyScope({
config,
workspaceDir: "/tmp",
env: {
DEMO_CHANNEL_TOKEN: "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: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
});
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_CHANNEL_TOKEN: "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_CHANNEL_TOKEN: "ambient",
} as NodeJS.ProcessEnv,
}),
).toEqual(["demo-other-channel"]);
});
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"],
},
]);
});
it("uses manifest env vars as read-only configured channel triggers", () => {
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
plugins: {
allow: ["external-env-channel-plugin"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_TOKEN: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toEqual(["external-env-channel"]);
});
it("ignores manifest env vars from untrusted external plugins", () => {
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_TOKEN: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
expect(
hasConfiguredChannelsForReadOnlyScope({
config: {} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_TOKEN: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toBe(false);
});
it("ignores ambient or malformed manifest env vars as read-only configured channel triggers", () => {
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
plugins: {
allow: ["ambient-env-channel-plugin"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
HOME: "/tmp/user",
PATH: "/usr/bin",
lowercase_token: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toStrictEqual([]);
});
it("accepts lowercase or mixed-case manifest env vars as read-only configured channel triggers", () => {
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
plugins: {
allow: ["external-env-channel-plugin"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
external_env_channel_token: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
manifestRecords: [
{
id: "external-env-channel-plugin",
channels: ["external-env-channel"],
channelEnvVars: {
"external-env-channel": ["external_env_channel_token"],
},
origin: "config",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
} as never,
],
}),
).toEqual(["external-env-channel"]);
});
it("matches uppercase process env entries for lowercase manifest env var declarations", () => {
expect(
listConfiguredChannelIdsForReadOnlyScope({
config: {
plugins: {
allow: ["external-env-channel-plugin"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_TOKEN: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
manifestRecords: [
{
id: "external-env-channel-plugin",
channels: ["external-env-channel"],
channelEnvVars: {
"external-env-channel": ["external_env_channel_token"],
},
origin: "config",
enabledByDefault: undefined,
providers: [],
cliBackends: [],
} as never,
],
}),
).toEqual(["external-env-channel"]);
});
it("uses manifest env vars for read-only channel presence checks", () => {
listPotentialConfiguredChannelIds.mockReturnValue([]);
listPotentialConfiguredChannelPresenceSignals.mockReturnValue([]);
hasPotentialConfiguredChannels.mockReturnValue(false);
expect(
hasConfiguredChannelsForReadOnlyScope({
config: {
plugins: {
allow: ["external-env-channel-plugin"],
},
} as OpenClawConfig,
workspaceDir: "/tmp",
env: {
EXTERNAL_ENV_CHANNEL_TOKEN: "token",
} as NodeJS.ProcessEnv,
includePersistedAuthState: false,
}),
).toBe(true);
});
});