import { afterAll, afterEach, beforeAll } from "vitest" ;
import type {
ChannelId,
ChannelOutboundAdapter,
ChannelPlugin,
} from "../src/channels/plugins/types.js" ;
import type { OpenClawConfig } from "../src/config/config.js" ;
import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js" ;
import type { PluginRegistry } from "../src/plugins/registry.js" ;
import { installSharedTestSetup } from "./setup.shared.js" ;
installSharedTestSetup();
const WORKER_RUNTIME_STATE = Symbol.for ("openclaw.testSetupRuntimeState" );
const WORKER_RUNTIME_HELPERS = Symbol.for ("openclaw.testSetupRuntimeHelpers" );
type WorkerRuntimeState = {
defaultPluginRegistry: PluginRegistry | null ;
materializedDefaultPluginRegistry: PluginRegistry | null ;
};
type WorkerRuntimeHelpers = {
clearPluginDiscoveryCache: typeof import ("../src/plugins/discovery.js" ).clearPluginDiscoveryCache;
clearPluginManifestRegistryCache: typeof import ("../src/plugins/manifest-registry-state.js" ).clearPluginManifestRegistryCache;
clearSessionStoreCaches: typeof import ("../src/config/sessions/store-cache.js" ).clearSessionStoreCaches;
drainFileLockStateForTest: typeof import ("../src/infra/file-lock.js" ).drainFileLockStateForTest;
drainSessionStoreLockQueuesForTest: typeof import ("../src/config/sessions/store-lock-state.js" ).drainSessionStoreLockQueuesForTest;
drainSessionWriteLockStateForTest: typeof import ("../src/agents/session-write-lock.js" ).drainSessionWriteLockStateForTest;
resetContextWindowCacheForTest: typeof import ("../src/agents/context-runtime-state.js" ).resetContextWindowCacheForTest;
resetFileLockStateForTest: typeof import ("../src/infra/file-lock.js" ).resetFileLockStateForTest;
resetModelsJsonReadyCacheForTest: typeof import ("../src/agents/models-config-state.js" ).resetModelsJsonReadyCacheForTest;
resetPluginRuntimeStateForTest: typeof import ("../src/plugins/runtime.js" ).resetPluginRuntimeStateForTest;
resetSessionWriteLockStateForTest: typeof import ("../src/agents/session-write-lock.js" ).resetSessionWriteLockStateForTest;
setActivePluginRegistry: typeof import ("../src/plugins/runtime.js" ).setActivePluginRegistry;
};
type ReplyToModeResolver = NonNullable<
NonNullable<ChannelPlugin["threading" ]>["resolveReplyToMode" ]
>;
const workerRuntimeState = (() => {
const globalState = globalThis as typeof globalThis & {
[WORKER_RUNTIME_STATE]?: WorkerRuntimeState;
};
if (!globalState[WORKER_RUNTIME_STATE]) {
globalState[WORKER_RUNTIME_STATE] = {
defaultPluginRegistry: null ,
materializedDefaultPluginRegistry: null ,
};
}
return globalState[WORKER_RUNTIME_STATE];
})();
function loadWorkerRuntimeHelpers(): Promise<WorkerRuntimeHelpers> {
const globalState = globalThis as typeof globalThis & {
[WORKER_RUNTIME_HELPERS]?: Promise<WorkerRuntimeHelpers>;
};
globalState[WORKER_RUNTIME_HELPERS] ??= Promise.all([
import ("../src/agents/context-runtime-state.js" ),
import ("../src/agents/models-config-state.js" ),
import ("../src/agents/session-write-lock.js" ),
import ("../src/config/sessions/store-cache.js" ),
import ("../src/config/sessions/store-lock-state.js" ),
import ("../src/infra/file-lock.js" ),
import ("../src/plugins/discovery.js" ),
import ("../src/plugins/manifest-registry-state.js" ),
import ("../src/plugins/runtime.js" ),
]).then(
([
contextRuntimeState,
modelsConfigState,
sessionWriteLock,
sessionStoreCache,
sessionStoreLockState,
fileLock,
discovery,
manifestRegistryState,
pluginRuntime,
]) => ({
clearPluginDiscoveryCache: discovery.clearPluginDiscoveryCache,
clearPluginManifestRegistryCache: manifestRegistryState.clearPluginManifestRegistryCache,
clearSessionStoreCaches: sessionStoreCache.clearSessionStoreCaches,
drainFileLockStateForTest: fileLock.drainFileLockStateForTest,
drainSessionStoreLockQueuesForTest: sessionStoreLockState.drainSessionStoreLockQueuesForTest,
drainSessionWriteLockStateForTest: sessionWriteLock.drainSessionWriteLockStateForTest,
resetContextWindowCacheForTest: contextRuntimeState.resetContextWindowCacheForTest,
resetFileLockStateForTest: fileLock.resetFileLockStateForTest,
resetModelsJsonReadyCacheForTest: modelsConfigState.resetModelsJsonReadyCacheForTest,
resetPluginRuntimeStateForTest: pluginRuntime.resetPluginRuntimeStateForTest,
resetSessionWriteLockStateForTest: sessionWriteLock.resetSessionWriteLockStateForTest,
setActivePluginRegistry: pluginRuntime.setActivePluginRegistry,
}),
);
return globalState[WORKER_RUNTIME_HELPERS];
}
const pickSendFn = (id: ChannelId, deps?: OutboundSendDeps) => {
return deps?.[id] as ((...args: unknown[]) => Promise<unknown>) | undefined;
};
function createTopLevelChannelReplyToModeResolverForTest(channelId: string): ReplyToModeResolver {
return ({ cfg }) => {
const channelConfig = (
cfg.channels as Record<string, { replyToMode?: "off" | "first" | "all" }> | undefined
)?.[channelId];
return channelConfig?.replyToMode ?? "off" ;
};
}
function createTestRegistryForSetup(
channels: Array<{ pluginId: string; plugin: ChannelPlugin; source: string }> = [],
): PluginRegistry {
return {
plugins: [],
tools: [],
hooks: [],
typedHooks: [],
channels: channels as unknown as PluginRegistry["channels" ],
channelSetups: channels.map((entry) => ({
pluginId: entry.pluginId,
plugin: entry.plugin,
source: entry.source,
enabled: true ,
})),
providers: [],
speechProviders: [],
realtimeTranscriptionProviders: [],
realtimeVoiceProviders: [],
mediaUnderstandingProviders: [],
imageGenerationProviders: [],
videoGenerationProviders: [],
webFetchProviders: [],
webSearchProviders: [],
memoryEmbeddingProviders: [],
gatewayHandlers: {},
gatewayMethodScopes: {},
httpRoutes: [],
cliRegistrars: [],
reloads: [],
nodeHostCommands: [],
securityAuditCollectors: [],
services: [],
gatewayDiscoveryServices: [],
commands: [],
conversationBindingResolvedHandlers: [],
diagnostics: [],
};
}
function resolveSlackStubReplyToMode(params: {
cfg: OpenClawConfig;
chatType?: string | null ;
}): "off" | "first" | "all" {
const entry = (
params.cfg.channels as
| Record<
string,
{
replyToMode?: "off" | "first" | "all" ;
replyToModeByChatType?: Partial<
Record<"direct" | "group" | "channel" , "off" | "first" | "all" >
>;
dm?: { replyToMode?: "off" | "first" | "all" };
}
>
| undefined
)?.slack;
const normalizedChatType = params.chatType?.trim().toLowerCase();
if (
normalizedChatType === "direct" ||
normalizedChatType === "group" ||
normalizedChatType === "channel"
) {
const byChatType = entry?.replyToModeByChatType?.[normalizedChatType];
if (byChatType) {
return byChatType;
}
if (normalizedChatType === "direct" && entry?.dm?.replyToMode) {
return entry.dm.replyToMode;
}
}
return entry?.replyToMode ?? "off" ;
}
const createStubOutbound = (
id: ChannelId,
deliveryMode: ChannelOutboundAdapter["deliveryMode" ] = "direct" ,
): ChannelOutboundAdapter => ({
deliveryMode,
sendText: async ({ deps, to, text }) => {
const send = pickSendFn(id, deps);
if (send) {
// oxlint-disable-next-line typescript/no-explicit-any
const result = (await send(to, text, { verbose: false } as any)) as {
messageId: string;
};
return { channel: id, ...result };
}
return { channel: id, messageId: "test" };
},
sendMedia: async ({ deps, to, text, mediaUrl }) => {
const send = pickSendFn(id, deps);
if (send) {
// oxlint-disable-next-line typescript/no-explicit-any
const result = (await send(to, text, { verbose: false , mediaUrl } as any)) as {
messageId: string;
};
return { channel: id, ...result };
}
return { channel: id, messageId: "test" };
},
});
const createStubPlugin = (params: {
id: ChannelId;
label?: string;
aliases?: string[];
deliveryMode?: ChannelOutboundAdapter["deliveryMode" ];
preferSessionLookupForAnnounceTarget?: boolean ;
resolveReplyToMode?: (params: {
cfg: OpenClawConfig;
accountId?: string | null ;
chatType?: string | null ;
}) => "off" | "first" | "all" ;
}): ChannelPlugin => ({
id: params.id,
meta: {
id: params.id,
label: params.label ?? String(params.id),
selectionLabel: params.label ?? String(params.id),
docsPath: `/channels/${params.id}`,
blurb: "test stub." ,
aliases: params.aliases,
preferSessionLookupForAnnounceTarget: params.preferSessionLookupForAnnounceTarget,
},
capabilities: { chatTypes: ["direct" , "group" ] },
threading: params.resolveReplyToMode
? {
resolveReplyToMode: params.resolveReplyToMode,
}
: undefined,
config: {
listAccountIds: (cfg: OpenClawConfig) => {
const channels = cfg.channels as Record<string, unknown> | undefined;
const entry = channels?.[params.id];
if (!entry || typeof entry !== "object" ) {
return [];
}
const accounts = (entry as { accounts?: Record<string, unknown> }).accounts;
const ids = accounts ? Object.keys(accounts).filter(Boolean ) : [];
return ids.length > 0 ? ids : ["default" ];
},
resolveAccount: (cfg: OpenClawConfig, accountId?: string | null ) => {
const channels = cfg.channels as Record<string, unknown> | undefined;
const entry = channels?.[params.id];
if (!entry || typeof entry !== "object" ) {
return {};
}
const accounts = (entry as { accounts?: Record<string, unknown> }).accounts;
const match = accountId ? accounts?.[accountId] : undefined;
return (match && typeof match === "object" ) || typeof match === "string" ? match : entry;
},
isConfigured: async (_account, cfg: OpenClawConfig) => {
const channels = cfg.channels as Record<string, unknown> | undefined;
return Boolean (channels?.[params.id]);
},
},
outbound: createStubOutbound(params.id, params.deliveryMode),
});
const createDefaultRegistry = () =>
createTestRegistryForSetup([
{
pluginId: "discord" ,
plugin: createStubPlugin({
id: "discord" ,
label: "Discord" ,
resolveReplyToMode: createTopLevelChannelReplyToModeResolverForTest("discord" ),
}),
source: "test" ,
},
{
pluginId: "slack" ,
plugin: createStubPlugin({
id: "slack" ,
label: "Slack" ,
resolveReplyToMode: ({ cfg, chatType }) => resolveSlackStubReplyToMode({ cfg, chatType }),
}),
source: "test" ,
},
{
pluginId: "telegram" ,
plugin: {
...createStubPlugin({
id: "telegram" ,
label: "Telegram" ,
resolveReplyToMode: createTopLevelChannelReplyToModeResolverForTest("telegram" ),
}),
status: {
buildChannelSummary: async () => ({
configured: false ,
tokenSource: process.env.TELEGRAM_BOT_TOKEN ? "env" : "none" ,
}),
},
},
source: "test" ,
},
{
pluginId: "whatsapp" ,
plugin: createStubPlugin({
id: "whatsapp" ,
label: "WhatsApp" ,
deliveryMode: "gateway" ,
preferSessionLookupForAnnounceTarget: true ,
}),
source: "test" ,
},
{
pluginId: "signal" ,
plugin: createStubPlugin({ id: "signal" , label: "Signal" }),
source: "test" ,
},
{
pluginId: "imessage" ,
plugin: createStubPlugin({ id: "imessage" , label: "iMessage" , aliases: ["imsg" ] }),
source: "test" ,
},
]);
function getDefaultPluginRegistry(): PluginRegistry {
workerRuntimeState.materializedDefaultPluginRegistry ??= createDefaultRegistry();
return workerRuntimeState.materializedDefaultPluginRegistry;
}
function resolveDefaultPluginRegistryProxy(): PluginRegistry {
workerRuntimeState.defaultPluginRegistry ??= new Proxy({} as PluginRegistry, {
defineProperty(_target, property, attributes) {
return Reflect.defineProperty(getDefaultPluginRegistry() as object, property, attributes);
},
deleteProperty(_target, property) {
return Reflect.deleteProperty(getDefaultPluginRegistry() as object, property);
},
get(_target, property, receiver) {
return Reflect.get(getDefaultPluginRegistry() as object, property, receiver);
},
getOwnPropertyDescriptor(_target, property) {
return Reflect.getOwnPropertyDescriptor(getDefaultPluginRegistry() as object, property);
},
has(_target, property) {
return Reflect.has(getDefaultPluginRegistry() as object, property);
},
ownKeys() {
return Reflect.ownKeys(getDefaultPluginRegistry() as object);
},
set(_target, property, value, receiver) {
return Reflect.set(getDefaultPluginRegistry() as object, property, value, receiver);
},
});
return workerRuntimeState.defaultPluginRegistry;
}
async function installDefaultPluginRegistry(): Promise<void > {
const { resetPluginRuntimeStateForTest, setActivePluginRegistry } =
await loadWorkerRuntimeHelpers();
workerRuntimeState.materializedDefaultPluginRegistry = null ;
resetPluginRuntimeStateForTest();
setActivePluginRegistry(resolveDefaultPluginRegistryProxy());
}
// Some suites import channel/plugin consumers at module top level, before
// Vitest runs hooks. Seed the lazy registry during setup module evaluation so
// import-time lookups still see the default test registry.
await installDefaultPluginRegistry();
beforeAll(async () => {
await installDefaultPluginRegistry();
});
afterEach(async () => {
const {
clearPluginDiscoveryCache,
clearPluginManifestRegistryCache,
clearSessionStoreCaches,
drainFileLockStateForTest,
drainSessionStoreLockQueuesForTest,
drainSessionWriteLockStateForTest,
resetContextWindowCacheForTest,
resetFileLockStateForTest,
resetModelsJsonReadyCacheForTest,
resetSessionWriteLockStateForTest,
} = await loadWorkerRuntimeHelpers();
await drainSessionStoreLockQueuesForTest();
clearSessionStoreCaches();
await drainFileLockStateForTest();
await drainSessionWriteLockStateForTest();
resetFileLockStateForTest();
resetContextWindowCacheForTest();
resetModelsJsonReadyCacheForTest();
resetSessionWriteLockStateForTest();
clearPluginDiscoveryCache();
clearPluginManifestRegistryCache();
await installDefaultPluginRegistry();
});
afterAll(async () => {
const {
clearPluginDiscoveryCache,
clearPluginManifestRegistryCache,
clearSessionStoreCaches,
drainFileLockStateForTest,
drainSessionWriteLockStateForTest,
} = await loadWorkerRuntimeHelpers();
clearSessionStoreCaches();
await drainFileLockStateForTest();
await drainSessionWriteLockStateForTest();
clearPluginDiscoveryCache();
clearPluginManifestRegistryCache();
});
Messung V0.5 in Prozent C=100 H=97 G=98
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland