mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
Retire the five SDK compatibility facades under the approved September 30 owner decision, and migrate in-repository callers onto their focused contracts. Keep implementations with their canonical owners and remove forwarding exports that become unused after the cutover. BREAKING CHANGE: remove openclaw/plugin-sdk/channel-lifecycle, channel-message, channel-reply-pipeline, config-runtime, and infra-runtime. Use channel-outbound/channel-inbound, config-contracts and focused configuration/infra entrypoints. The new system-event-runtime entrypoint supplies public snapshot inspection and consumption. Update affected external plugins before upgrading the host; this retirement does not certify universal external migration. Package exports, SDK entry inventories, compatibility tombstones, migration docs, and surface budgets move together. Canonical API comparison confirms exactly five removed entrypoints, 869 removed export paths, 35 focused additions, and no retained-export signature changes. Net production reduction: 1,320 lines.
410 lines
15 KiB
TypeScript
410 lines
15 KiB
TypeScript
// OpenClaw runtime test setup installs runtime mocks and cleanup.
|
|
import { afterAll, afterEach, beforeAll, vi } from "vitest";
|
|
import type {
|
|
ChannelId,
|
|
ChannelOutboundAdapter,
|
|
ChannelPlugin,
|
|
} from "../src/channels/plugins/types.public.js";
|
|
import type { OpenClawConfig } from "../src/config/config.js";
|
|
import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js";
|
|
import { createEmptyPluginRegistry } from "../src/plugins/registry-empty.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_CLEANUP_HELPERS = Symbol.for("openclaw.testSetupCleanupHelpers");
|
|
type WorkerRuntimeState = {
|
|
defaultPluginRegistry: PluginRegistry | null;
|
|
materializedDefaultPluginRegistry: PluginRegistry | null;
|
|
};
|
|
type WorkerCleanupHelpers = {
|
|
drainOpenClawAgentWriteQueuesForTest: typeof import("../src/state/openclaw-agent-write-admission.test-support.js").drainOpenClawAgentWriteQueuesForTest;
|
|
clearSessionStoreCacheForTest: typeof import("../src/config/sessions/store-writer-state.js").clearSessionStoreCacheForTest;
|
|
drainFileLockStateForTest: typeof import("../src/plugin-sdk/file-lock.js").drainFileLockStateForTest;
|
|
drainSessionStoreWriterQueuesForTest: typeof import("../src/config/sessions/store-writer-state.test-support.js").drainSessionStoreWriterQueuesForTest;
|
|
resetContextWindowCacheForTest: typeof import("../src/agents/context-runtime-state.js").resetContextWindowCacheForTest;
|
|
resetFileLockStateForTest: typeof import("../src/plugin-sdk/file-lock.js").resetFileLockStateForTest;
|
|
resetModelsJsonReadyCacheForTest: typeof import("../src/agents/models-config-state.test-support.js").resetModelsJsonReadyCacheForTest;
|
|
resetPreparedModelRuntimeSnapshotsForTest: typeof import("../src/agents/prepared-model-runtime.test-support.js").resetPreparedModelRuntimeSnapshotsForTest;
|
|
};
|
|
|
|
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 loadWorkerCleanupHelpers(): Promise<WorkerCleanupHelpers> {
|
|
const globalState = globalThis as typeof globalThis & {
|
|
[WORKER_CLEANUP_HELPERS]?: Promise<WorkerCleanupHelpers>;
|
|
};
|
|
globalState[WORKER_CLEANUP_HELPERS] ??= (async () => {
|
|
const [
|
|
contextRuntimeState,
|
|
modelsConfigState,
|
|
preparedModelRuntime,
|
|
sessionStoreWriterState,
|
|
sessionStoreWriterTestState,
|
|
agentWriteAdmission,
|
|
fileLock,
|
|
] = await Promise.all([
|
|
vi.importActual<typeof import("../src/agents/context-runtime-state.js")>(
|
|
"../src/agents/context-runtime-state.js",
|
|
),
|
|
vi.importActual<typeof import("../src/agents/models-config-state.test-support.js")>(
|
|
"../src/agents/models-config-state.test-support.js",
|
|
),
|
|
vi.importActual<typeof import("../src/agents/prepared-model-runtime.test-support.js")>(
|
|
"../src/agents/prepared-model-runtime.test-support.js",
|
|
),
|
|
vi.importActual<typeof import("../src/config/sessions/store-writer-state.js")>(
|
|
"../src/config/sessions/store-writer-state.js",
|
|
),
|
|
vi.importActual<typeof import("../src/config/sessions/store-writer-state.test-support.js")>(
|
|
"../src/config/sessions/store-writer-state.test-support.js",
|
|
),
|
|
vi.importActual<typeof import("../src/state/openclaw-agent-write-admission.test-support.js")>(
|
|
"../src/state/openclaw-agent-write-admission.test-support.js",
|
|
),
|
|
vi.importActual<typeof import("../src/plugin-sdk/file-lock.js")>(
|
|
"../src/plugin-sdk/file-lock.js",
|
|
),
|
|
]);
|
|
return {
|
|
drainOpenClawAgentWriteQueuesForTest:
|
|
agentWriteAdmission.drainOpenClawAgentWriteQueuesForTest,
|
|
clearSessionStoreCacheForTest: sessionStoreWriterState.clearSessionStoreCacheForTest,
|
|
drainFileLockStateForTest: fileLock.drainFileLockStateForTest,
|
|
drainSessionStoreWriterQueuesForTest:
|
|
sessionStoreWriterTestState.drainSessionStoreWriterQueuesForTest,
|
|
resetContextWindowCacheForTest: contextRuntimeState.resetContextWindowCacheForTest,
|
|
resetFileLockStateForTest: fileLock.resetFileLockStateForTest,
|
|
resetModelsJsonReadyCacheForTest: modelsConfigState.resetModelsJsonReadyCacheForTest,
|
|
resetPreparedModelRuntimeSnapshotsForTest:
|
|
preparedModelRuntime.resetPreparedModelRuntimeSnapshotsForTest,
|
|
};
|
|
})();
|
|
return globalState[WORKER_CLEANUP_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 {
|
|
...createEmptyPluginRegistry(),
|
|
channels: channels as unknown as PluginRegistry["channels"],
|
|
channelSetups: channels.map((entry) => ({
|
|
pluginId: entry.pluginId,
|
|
plugin: entry.plugin,
|
|
source: entry.source,
|
|
enabled: true,
|
|
})),
|
|
};
|
|
}
|
|
|
|
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) {
|
|
const result = (await send(to, text, { verbose: false })) 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) {
|
|
const result = (await send(to, text, { verbose: false, mediaUrl })) 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",
|
|
) as Parameters<typeof createStubPlugin>[0]["resolveReplyToMode"],
|
|
}),
|
|
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",
|
|
) as Parameters<typeof createStubPlugin>[0]["resolveReplyToMode"],
|
|
}),
|
|
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 settlePluginCacheRetirements(): Promise<void> {
|
|
const { waitForPluginCacheRetirement } = await vi.importActual<
|
|
typeof import("../src/plugins/plugin-cache.js")
|
|
>("../src/plugins/plugin-cache.js");
|
|
const { failures } = await waitForPluginCacheRetirement();
|
|
if (failures.length > 0) {
|
|
throw new AggregateError(failures, "Plugin cache retirement failed during test cleanup");
|
|
}
|
|
}
|
|
|
|
async function installDefaultPluginRegistry(): Promise<void> {
|
|
// Worker module resets retire the lifecycle maps. Activate through the current
|
|
// real module, never a cached closure or a suite's partial runtime mock.
|
|
const { resetPluginRuntimeStateForTest, setActivePluginRegistry } = await vi.importActual<
|
|
typeof import("../src/plugins/runtime.js")
|
|
>("../src/plugins/runtime.js");
|
|
workerRuntimeState.materializedDefaultPluginRegistry = null;
|
|
resetPluginRuntimeStateForTest();
|
|
setActivePluginRegistry(resolveDefaultPluginRegistryProxy());
|
|
await settlePluginCacheRetirements();
|
|
}
|
|
|
|
// 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 {
|
|
drainOpenClawAgentWriteQueuesForTest,
|
|
clearSessionStoreCacheForTest,
|
|
drainFileLockStateForTest,
|
|
drainSessionStoreWriterQueuesForTest,
|
|
resetContextWindowCacheForTest,
|
|
resetFileLockStateForTest,
|
|
resetModelsJsonReadyCacheForTest,
|
|
resetPreparedModelRuntimeSnapshotsForTest,
|
|
} = await loadWorkerCleanupHelpers();
|
|
await drainOpenClawAgentWriteQueuesForTest();
|
|
await drainSessionStoreWriterQueuesForTest();
|
|
clearSessionStoreCacheForTest();
|
|
await drainFileLockStateForTest();
|
|
resetFileLockStateForTest();
|
|
resetContextWindowCacheForTest();
|
|
resetModelsJsonReadyCacheForTest();
|
|
await resetPreparedModelRuntimeSnapshotsForTest();
|
|
await installDefaultPluginRegistry();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
const {
|
|
clearSessionStoreCacheForTest,
|
|
drainFileLockStateForTest,
|
|
drainOpenClawAgentWriteQueuesForTest,
|
|
drainSessionStoreWriterQueuesForTest,
|
|
} = await loadWorkerCleanupHelpers();
|
|
await drainOpenClawAgentWriteQueuesForTest();
|
|
await drainSessionStoreWriterQueuesForTest();
|
|
clearSessionStoreCacheForTest();
|
|
await drainFileLockStateForTest();
|
|
await settlePluginCacheRetirements();
|
|
});
|