mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 04:16:25 +00:00
refactor: modularize plugin runtime and test hooks
This commit is contained in:
244
extensions/test-utils/plugin-runtime-mock.ts
Normal file
244
extensions/test-utils/plugin-runtime-mock.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
||||
import { removeAckReactionAfterReply, shouldAckReaction } from "openclaw/plugin-sdk";
|
||||
import { vi } from "vitest";
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[K in keyof T]?: T[K] extends (...args: never[]) => unknown
|
||||
? T[K]
|
||||
: T[K] extends ReadonlyArray<unknown>
|
||||
? T[K]
|
||||
: T[K] extends object
|
||||
? DeepPartial<T[K]>
|
||||
: T[K];
|
||||
};
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function mergeDeep<T>(base: T, overrides: DeepPartial<T>): T {
|
||||
const result: Record<string, unknown> = { ...(base as Record<string, unknown>) };
|
||||
for (const [key, overrideValue] of Object.entries(overrides as Record<string, unknown>)) {
|
||||
if (overrideValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
const baseValue = result[key];
|
||||
if (isObject(baseValue) && isObject(overrideValue)) {
|
||||
result[key] = mergeDeep(baseValue, overrideValue);
|
||||
continue;
|
||||
}
|
||||
result[key] = overrideValue;
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
|
||||
export function createPluginRuntimeMock(overrides: DeepPartial<PluginRuntime> = {}): PluginRuntime {
|
||||
const base: PluginRuntime = {
|
||||
version: "1.0.0-test",
|
||||
config: {
|
||||
loadConfig: vi.fn(() => ({})) as unknown as PluginRuntime["config"]["loadConfig"],
|
||||
writeConfigFile: vi.fn() as unknown as PluginRuntime["config"]["writeConfigFile"],
|
||||
},
|
||||
system: {
|
||||
enqueueSystemEvent: vi.fn() as unknown as PluginRuntime["system"]["enqueueSystemEvent"],
|
||||
requestHeartbeatNow: vi.fn() as unknown as PluginRuntime["system"]["requestHeartbeatNow"],
|
||||
runCommandWithTimeout: vi.fn() as unknown as PluginRuntime["system"]["runCommandWithTimeout"],
|
||||
formatNativeDependencyHint: vi.fn(
|
||||
() => "",
|
||||
) as unknown as PluginRuntime["system"]["formatNativeDependencyHint"],
|
||||
},
|
||||
media: {
|
||||
loadWebMedia: vi.fn() as unknown as PluginRuntime["media"]["loadWebMedia"],
|
||||
detectMime: vi.fn() as unknown as PluginRuntime["media"]["detectMime"],
|
||||
mediaKindFromMime: vi.fn() as unknown as PluginRuntime["media"]["mediaKindFromMime"],
|
||||
isVoiceCompatibleAudio:
|
||||
vi.fn() as unknown as PluginRuntime["media"]["isVoiceCompatibleAudio"],
|
||||
getImageMetadata: vi.fn() as unknown as PluginRuntime["media"]["getImageMetadata"],
|
||||
resizeToJpeg: vi.fn() as unknown as PluginRuntime["media"]["resizeToJpeg"],
|
||||
},
|
||||
tts: {
|
||||
textToSpeechTelephony: vi.fn() as unknown as PluginRuntime["tts"]["textToSpeechTelephony"],
|
||||
},
|
||||
stt: {
|
||||
transcribeAudioFile: vi.fn() as unknown as PluginRuntime["stt"]["transcribeAudioFile"],
|
||||
},
|
||||
tools: {
|
||||
createMemoryGetTool: vi.fn() as unknown as PluginRuntime["tools"]["createMemoryGetTool"],
|
||||
createMemorySearchTool:
|
||||
vi.fn() as unknown as PluginRuntime["tools"]["createMemorySearchTool"],
|
||||
registerMemoryCli: vi.fn() as unknown as PluginRuntime["tools"]["registerMemoryCli"],
|
||||
},
|
||||
channel: {
|
||||
text: {
|
||||
chunkByNewline: vi.fn((text: string) => (text ? [text] : [])),
|
||||
chunkMarkdownText: vi.fn((text: string) => [text]),
|
||||
chunkMarkdownTextWithMode: vi.fn((text: string) => (text ? [text] : [])),
|
||||
chunkText: vi.fn((text: string) => (text ? [text] : [])),
|
||||
chunkTextWithMode: vi.fn((text: string) => (text ? [text] : [])),
|
||||
resolveChunkMode: vi.fn(() => "length"),
|
||||
resolveTextChunkLimit: vi.fn(() => 4000),
|
||||
hasControlCommand: vi.fn(() => false),
|
||||
resolveMarkdownTableMode: vi.fn(() => "code"),
|
||||
convertMarkdownTables: vi.fn((text: string) => text),
|
||||
},
|
||||
reply: {
|
||||
dispatchReplyWithBufferedBlockDispatcher: vi.fn(
|
||||
async () => undefined,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyWithBufferedBlockDispatcher"],
|
||||
createReplyDispatcherWithTyping:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["createReplyDispatcherWithTyping"],
|
||||
resolveEffectiveMessagesConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveEffectiveMessagesConfig"],
|
||||
resolveHumanDelayConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["resolveHumanDelayConfig"],
|
||||
dispatchReplyFromConfig:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["reply"]["dispatchReplyFromConfig"],
|
||||
withReplyDispatcher: vi.fn(async ({ dispatcher, run, onSettled }) => {
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
dispatcher.markComplete();
|
||||
try {
|
||||
await dispatcher.waitForIdle();
|
||||
} finally {
|
||||
await onSettled?.();
|
||||
}
|
||||
}
|
||||
}) as unknown as PluginRuntime["channel"]["reply"]["withReplyDispatcher"],
|
||||
finalizeInboundContext: vi.fn(
|
||||
(ctx: Record<string, unknown>) => ctx,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["finalizeInboundContext"],
|
||||
formatAgentEnvelope: vi.fn(
|
||||
(opts: { body: string }) => opts.body,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["formatAgentEnvelope"],
|
||||
formatInboundEnvelope: vi.fn(
|
||||
(opts: { body: string }) => opts.body,
|
||||
) as unknown as PluginRuntime["channel"]["reply"]["formatInboundEnvelope"],
|
||||
resolveEnvelopeFormatOptions: vi.fn(() => ({
|
||||
template: "channel+name+time",
|
||||
})) as unknown as PluginRuntime["channel"]["reply"]["resolveEnvelopeFormatOptions"],
|
||||
},
|
||||
routing: {
|
||||
resolveAgentRoute: vi.fn(() => ({
|
||||
agentId: "main",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:test:dm:peer",
|
||||
})) as unknown as PluginRuntime["channel"]["routing"]["resolveAgentRoute"],
|
||||
},
|
||||
pairing: {
|
||||
buildPairingReply: vi.fn(
|
||||
() => "Pairing code: TESTCODE",
|
||||
) as unknown as PluginRuntime["channel"]["pairing"]["buildPairingReply"],
|
||||
readAllowFromStore: vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
[],
|
||||
) as unknown as PluginRuntime["channel"]["pairing"]["readAllowFromStore"],
|
||||
upsertPairingRequest: vi.fn().mockResolvedValue({
|
||||
code: "TESTCODE",
|
||||
created: true,
|
||||
}) as unknown as PluginRuntime["channel"]["pairing"]["upsertPairingRequest"],
|
||||
},
|
||||
media: {
|
||||
fetchRemoteMedia:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["media"]["fetchRemoteMedia"],
|
||||
saveMediaBuffer: vi.fn().mockResolvedValue({
|
||||
path: "/tmp/test-media.jpg",
|
||||
contentType: "image/jpeg",
|
||||
}) as unknown as PluginRuntime["channel"]["media"]["saveMediaBuffer"],
|
||||
},
|
||||
session: {
|
||||
resolveStorePath: vi.fn(
|
||||
() => "/tmp/sessions.json",
|
||||
) as unknown as PluginRuntime["channel"]["session"]["resolveStorePath"],
|
||||
readSessionUpdatedAt: vi.fn(
|
||||
() => undefined,
|
||||
) as unknown as PluginRuntime["channel"]["session"]["readSessionUpdatedAt"],
|
||||
recordSessionMetaFromInbound:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordSessionMetaFromInbound"],
|
||||
recordInboundSession:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["recordInboundSession"],
|
||||
updateLastRoute:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["session"]["updateLastRoute"],
|
||||
},
|
||||
mentions: {
|
||||
buildMentionRegexes: vi.fn(() => [
|
||||
/\bbert\b/i,
|
||||
]) as unknown as PluginRuntime["channel"]["mentions"]["buildMentionRegexes"],
|
||||
matchesMentionPatterns: vi.fn((text: string, regexes: RegExp[]) =>
|
||||
regexes.some((regex) => regex.test(text)),
|
||||
) as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionPatterns"],
|
||||
matchesMentionWithExplicit: vi.fn(
|
||||
(params: { text: string; mentionRegexes: RegExp[]; explicitWasMentioned?: boolean }) =>
|
||||
params.explicitWasMentioned === true
|
||||
? true
|
||||
: params.mentionRegexes.some((regex) => regex.test(params.text)),
|
||||
) as unknown as PluginRuntime["channel"]["mentions"]["matchesMentionWithExplicit"],
|
||||
},
|
||||
reactions: {
|
||||
shouldAckReaction,
|
||||
removeAckReactionAfterReply,
|
||||
},
|
||||
groups: {
|
||||
resolveGroupPolicy: vi.fn(
|
||||
() => "open",
|
||||
) as unknown as PluginRuntime["channel"]["groups"]["resolveGroupPolicy"],
|
||||
resolveRequireMention: vi.fn(
|
||||
() => false,
|
||||
) as unknown as PluginRuntime["channel"]["groups"]["resolveRequireMention"],
|
||||
},
|
||||
debounce: {
|
||||
createInboundDebouncer: vi.fn(
|
||||
(params: { onFlush: (items: unknown[]) => Promise<void> }) => ({
|
||||
enqueue: async (item: unknown) => {
|
||||
await params.onFlush([item]);
|
||||
},
|
||||
flushKey: vi.fn(),
|
||||
}),
|
||||
) as unknown as PluginRuntime["channel"]["debounce"]["createInboundDebouncer"],
|
||||
resolveInboundDebounceMs: vi.fn(
|
||||
() => 0,
|
||||
) as unknown as PluginRuntime["channel"]["debounce"]["resolveInboundDebounceMs"],
|
||||
},
|
||||
commands: {
|
||||
resolveCommandAuthorizedFromAuthorizers: vi.fn(
|
||||
() => false,
|
||||
) as unknown as PluginRuntime["channel"]["commands"]["resolveCommandAuthorizedFromAuthorizers"],
|
||||
isControlCommandMessage:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["isControlCommandMessage"],
|
||||
shouldComputeCommandAuthorized:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"],
|
||||
shouldHandleTextCommands:
|
||||
vi.fn() as unknown as PluginRuntime["channel"]["commands"]["shouldHandleTextCommands"],
|
||||
},
|
||||
discord: {} as PluginRuntime["channel"]["discord"],
|
||||
activity: {} as PluginRuntime["channel"]["activity"],
|
||||
line: {} as PluginRuntime["channel"]["line"],
|
||||
slack: {} as PluginRuntime["channel"]["slack"],
|
||||
telegram: {} as PluginRuntime["channel"]["telegram"],
|
||||
signal: {} as PluginRuntime["channel"]["signal"],
|
||||
imessage: {} as PluginRuntime["channel"]["imessage"],
|
||||
whatsapp: {} as PluginRuntime["channel"]["whatsapp"],
|
||||
},
|
||||
events: {
|
||||
onAgentEvent: vi.fn(() => () => {}) as unknown as PluginRuntime["events"]["onAgentEvent"],
|
||||
onSessionTranscriptUpdate: vi.fn(
|
||||
() => () => {},
|
||||
) as unknown as PluginRuntime["events"]["onSessionTranscriptUpdate"],
|
||||
},
|
||||
logging: {
|
||||
shouldLogVerbose: vi.fn(() => false),
|
||||
getChildLogger: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
})),
|
||||
},
|
||||
state: {
|
||||
resolveStateDir: vi.fn(() => "/tmp/openclaw"),
|
||||
},
|
||||
};
|
||||
|
||||
return mergeDeep(base, overrides);
|
||||
}
|
||||
Reference in New Issue
Block a user