diff --git a/extensions/discord/src/monitor/reply-delivery.ts b/extensions/discord/src/monitor/reply-delivery.ts index b51e92f0de0..3d1ac384d98 100644 --- a/extensions/discord/src/monitor/reply-delivery.ts +++ b/extensions/discord/src/monitor/reply-delivery.ts @@ -117,10 +117,10 @@ async function sendDiscordMediaOnly(params: { target: string; cfg: OpenClawConfig; token: string; - rest: RequestClient; + rest?: RequestClient; mediaUrl: string; - accountId: string; - mediaLocalRoots?: string[]; + accountId?: string; + mediaLocalRoots?: readonly string[]; replyTo?: string; retryConfig: ResolvedRetryConfig; }): Promise { @@ -143,10 +143,10 @@ async function sendDiscordMediaBatch(params: { target: string; cfg: OpenClawConfig; token: string; - rest: RequestClient; + rest?: RequestClient; mediaUrls: string[]; - accountId: string; - mediaLocalRoots?: string[]; + accountId?: string; + mediaLocalRoots?: readonly string[]; replyTo: () => string | undefined; retryConfig: ResolvedRetryConfig; }): Promise { diff --git a/extensions/memory-core/src/memory/manager-cache.test.ts b/extensions/memory-core/src/memory/manager-cache.test.ts index e38622075ab..435b4e9b366 100644 --- a/extensions/memory-core/src/memory/manager-cache.test.ts +++ b/extensions/memory-core/src/memory/manager-cache.test.ts @@ -8,7 +8,7 @@ import { type TestEntry = { id: string; - close: ReturnType; + close: () => Promise; }; function createTestCache(): ManagedCache { diff --git a/extensions/qqbot/src/channel-config-shared.ts b/extensions/qqbot/src/channel-config-shared.ts index 1d5cdb4b6fe..afd6f6b4c18 100644 --- a/extensions/qqbot/src/channel-config-shared.ts +++ b/extensions/qqbot/src/channel-config-shared.ts @@ -168,7 +168,7 @@ export const qqbotSetupAdapterShared = { }: { cfg: OpenClawConfig; accountId: string; - name: string; + name?: string; }) => applyAccountNameToChannelSection({ cfg, diff --git a/extensions/telegram/contract-api.ts b/extensions/telegram/contract-api.ts index 7bf8b522c73..145554fd685 100644 --- a/extensions/telegram/contract-api.ts +++ b/extensions/telegram/contract-api.ts @@ -15,6 +15,7 @@ export { buildCommandsPaginationKeyboard, buildTelegramModelsProviderChannelData, } from "./src/command-ui.js"; +export { createTelegramThreadBindingManager } from "./src/thread-bindings.js"; export type { TelegramInteractiveHandlerContext, TelegramInteractiveHandlerRegistration, diff --git a/src/channels/plugins/bundled.ts b/src/channels/plugins/bundled.ts index 43e61cbf9d4..d4270ad987b 100644 --- a/src/channels/plugins/bundled.ts +++ b/src/channels/plugins/bundled.ts @@ -105,19 +105,13 @@ function resolveGeneratedBundledChannelModulePath(params: { if (!params.entry) { return null; } - const candidateRoots = [ - path.resolve(OPENCLAW_PACKAGE_ROOT, "dist", "extensions", params.metadata.dirName), - path.resolve(OPENCLAW_PACKAGE_ROOT, "extensions", params.metadata.dirName), - ]; - for (const rootDir of candidateRoots) { - const resolved = resolveBundledChannelGeneratedPath( - rootDir, - params.entry, - params.metadata.dirName, - ); - if (resolved) { - return resolved; - } + const resolved = resolveBundledChannelGeneratedPath( + OPENCLAW_PACKAGE_ROOT, + params.entry, + params.metadata.dirName, + ); + if (resolved) { + return resolved; } return null; } diff --git a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts index fbe4bc96e73..e82df933046 100644 --- a/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts +++ b/src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts @@ -169,8 +169,7 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { 'export * from "./src/send.js";', 'export * from "./src/session.js";', 'export { setWhatsAppRuntime } from "./src/runtime.js";', - "export async function startWebLoginWithQr( ...args: Parameters ): ReturnType { const { startWebLoginWithQr } = await loadLoginQrModule(); return await startWebLoginWithQr(...args); }", - "export async function waitForWebLogin( ...args: Parameters ): ReturnType { const { waitForWebLogin } = await loadLoginQrModule(); return await waitForWebLogin(...args); }", + 'export { startWebLoginWithQr, waitForWebLogin } from "./login-qr-runtime.js";', ], } as const; diff --git a/src/plugins/contracts/runtime-seams.contract.test.ts b/src/plugins/contracts/runtime-seams.contract.test.ts index e3d3b5e81d0..28dce380bb8 100644 --- a/src/plugins/contracts/runtime-seams.contract.test.ts +++ b/src/plugins/contracts/runtime-seams.contract.test.ts @@ -8,58 +8,82 @@ import { } from "../../config/runtime-snapshot.js"; import { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js"; import { TEST_UNDICI_RUNTIME_DEPS_KEY } from "../../infra/net/undici-runtime.js"; -import type { PluginManifestRecord } from "../manifest-registry.js"; - -const loadPluginManifestRegistryMock = vi.fn(); +import { clearPluginDiscoveryCache } from "../discovery.js"; +import { clearPluginManifestRegistryCache } from "../manifest-registry.js"; const originalBundledPluginsDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; +const originalStateDir = process.env.OPENCLAW_STATE_DIR; const originalGlobalFetch = globalThis.fetch; const tempDirs: string[] = []; -function createRuntimePluginDir(pluginId: string, marker: string): string { - const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), `openclaw-runtime-contract-${pluginId}-`)); - tempDirs.push(rootDir); - const pluginRoot = path.join(rootDir, pluginId); +function createInstalledRuntimePluginDir( + pluginId: string, + marker: string, +): { + bundledDir: string; + stateDir: string; + pluginRoot: string; +} { + const bundledDir = fs.mkdtempSync( + path.join(os.tmpdir(), `openclaw-runtime-contract-bundled-${pluginId}-`), + ); + const stateDir = fs.mkdtempSync( + path.join(os.tmpdir(), `openclaw-runtime-contract-state-${pluginId}-`), + ); + tempDirs.push(bundledDir, stateDir); + const pluginRoot = path.join(stateDir, "extensions", pluginId); fs.mkdirSync(pluginRoot, { recursive: true }); fs.writeFileSync( path.join(pluginRoot, "runtime-api.js"), `export const marker = ${JSON.stringify(marker)};\n`, "utf8", ); - return pluginRoot; -} - -function buildPluginManifestRecord(params: { - id: string; - origin: PluginManifestRecord["origin"]; - rootDir: string; -}): PluginManifestRecord { + fs.writeFileSync( + path.join(pluginRoot, "package.json"), + JSON.stringify({ + name: `@openclaw/${pluginId}`, + version: "0.0.0", + openclaw: { + extensions: ["./runtime-api.js"], + channel: { id: pluginId }, + }, + }), + "utf8", + ); + fs.writeFileSync( + path.join(pluginRoot, "openclaw.plugin.json"), + JSON.stringify({ + id: pluginId, + channels: [pluginId], + configSchema: { type: "object", additionalProperties: false, properties: {} }, + }), + "utf8", + ); return { - id: params.id, - origin: params.origin, - rootDir: params.rootDir, - source: params.rootDir, - manifestPath: path.join(params.rootDir, "openclaw.plugin.json"), - channels: [params.id], - providers: [], - cliBackends: [], - skills: [], - hooks: [], + bundledDir, + stateDir, + pluginRoot, }; } afterEach(() => { - loadPluginManifestRegistryMock.mockReset(); clearRuntimeConfigSnapshot(); vi.restoreAllMocks(); vi.resetModules(); - vi.doUnmock("../manifest-registry.js"); + vi.doUnmock("../../config/plugin-auto-enable.js"); + clearPluginDiscoveryCache(); + clearPluginManifestRegistryCache(); Reflect.deleteProperty(globalThis as object, TEST_UNDICI_RUNTIME_DEPS_KEY); if (originalBundledPluginsDir === undefined) { delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; } else { process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = originalBundledPluginsDir; } + if (originalStateDir === undefined) { + delete process.env.OPENCLAW_STATE_DIR; + } else { + process.env.OPENCLAW_STATE_DIR = originalStateDir; + } if (originalGlobalFetch) { (globalThis as Record).fetch = originalGlobalFetch; } else { @@ -72,48 +96,45 @@ afterEach(() => { describe("shared runtime seam contracts", () => { it("allows activated runtime facades when the resolved plugin root matches an installed-style manifest record", async () => { - const pluginRoot = createRuntimePluginDir("line", "line-ok"); - process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = path.dirname(pluginRoot); + const pluginId = "line-contract-fixture"; + const { bundledDir, stateDir } = createInstalledRuntimePluginDir(pluginId, "line-ok"); + process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir; + process.env.OPENCLAW_STATE_DIR = stateDir; setRuntimeConfigSnapshot({ plugins: { entries: { - line: { + [pluginId]: { enabled: true, }, }, }, }); - loadPluginManifestRegistryMock.mockReturnValue({ - plugins: [buildPluginManifestRecord({ id: "line", origin: "global", rootDir: pluginRoot })], - diagnostics: [], - }); - vi.doMock("../manifest-registry.js", async () => { - const actual = - await vi.importActual("../manifest-registry.js"); - return { - ...actual, - loadPluginManifestRegistry: ( - ...args: Parameters - ) => loadPluginManifestRegistryMock(...args), - }; - }); + clearPluginDiscoveryCache(); + clearPluginManifestRegistryCache(); + vi.resetModules(); + vi.doMock("../../config/plugin-auto-enable.js", () => ({ + applyPluginAutoEnable: ({ config }: { config?: unknown }) => ({ + config: config ?? {}, + autoEnabledReasons: {}, + }), + })); const facadeRuntime = await import("../../plugin-sdk/facade-runtime.js"); facadeRuntime.resetFacadeRuntimeStateForTest(); expect( facadeRuntime.canLoadActivatedBundledPluginPublicSurface({ - dirName: "line", + dirName: pluginId, artifactBasename: "runtime-api.js", }), ).toBe(true); expect( facadeRuntime.loadActivatedBundledPluginPublicSurfaceModuleSync<{ marker: string }>({ - dirName: "line", + dirName: pluginId, artifactBasename: "runtime-api.js", }).marker, ).toBe("line-ok"); - expect(facadeRuntime.listImportedBundledPluginFacadeIds()).toEqual(["line"]); + expect(facadeRuntime.listImportedBundledPluginFacadeIds()).toEqual([pluginId]); }); it("keeps guarded fetch on mocked global fetches even when a dispatcher is attached", async () => { diff --git a/test/helpers/channels/registry-session-binding.ts b/test/helpers/channels/registry-session-binding.ts index 0b70ffa07d8..f9baf6b222f 100644 --- a/test/helpers/channels/registry-session-binding.ts +++ b/test/helpers/channels/registry-session-binding.ts @@ -203,12 +203,14 @@ const sessionBindingContractEntries: Record< const { createThreadBindingManager } = await getContractApi<{ createThreadBindingManager: (params: { accountId: string; + cfg?: OpenClawConfig; persist: boolean; enableSweeper: boolean; }) => unknown; }>("discord"); createThreadBindingManager({ accountId: "default", + cfg: baseSessionBindingCfg, persist: false, enableSweeper: false, }); @@ -221,12 +223,14 @@ const sessionBindingContractEntries: Record< const { createThreadBindingManager } = await getContractApi<{ createThreadBindingManager: (params: { accountId: string; + cfg?: OpenClawConfig; persist: boolean; enableSweeper: boolean; }) => unknown; }>("discord"); createThreadBindingManager({ accountId: "default", + cfg: baseSessionBindingCfg, persist: false, enableSweeper: false, }); @@ -238,9 +242,8 @@ const sessionBindingContractEntries: Record< channel: "discord", accountId: "default", conversationId: "channel:123456789012345678", - parentConversationId: "channel:123456789012345677", }, - placement: "child", + placement: "current", metadata: { agentId: "discord", label: "discord-child", @@ -250,7 +253,6 @@ const sessionBindingContractEntries: Record< channel: "discord", accountId: "default", conversationId: "channel:123456789012345678", - parentConversationId: "channel:123456789012345677", targetSessionKey: "agent:discord:child:thread-1", }); return binding; @@ -269,20 +271,18 @@ const sessionBindingContractEntries: Record< adapterAvailable: true, bindSupported: true, unbindSupported: true, - placements: ["current", "child"], + placements: ["current"], }, getCapabilities: async () => { const { createFeishuThreadBindingManager } = await getContractApi<{ createFeishuThreadBindingManager: (params: { - accountId: string; - persist: boolean; - enableSweeper: boolean; + accountId?: string; + cfg: OpenClawConfig; }) => unknown; }>("feishu"); createFeishuThreadBindingManager({ accountId: "default", - persist: false, - enableSweeper: false, + cfg: baseSessionBindingCfg, }); return getSessionBindingService().getCapabilities({ channel: "feishu", @@ -292,15 +292,13 @@ const sessionBindingContractEntries: Record< bindAndResolve: async () => { const { createFeishuThreadBindingManager } = await getContractApi<{ createFeishuThreadBindingManager: (params: { - accountId: string; - persist: boolean; - enableSweeper: boolean; + accountId?: string; + cfg: OpenClawConfig; }) => unknown; }>("feishu"); createFeishuThreadBindingManager({ accountId: "default", - persist: false, - enableSweeper: false, + cfg: baseSessionBindingCfg, }); const service = getSessionBindingService(); const binding = await service.bind({ @@ -309,10 +307,10 @@ const sessionBindingContractEntries: Record< conversation: { channel: "feishu", accountId: "default", - conversationId: "chat:123456", - parentConversationId: "chat:123455", + conversationId: "oc_group_chat:topic:om_topic_root", + parentConversationId: "oc_group_chat", }, - placement: "child", + placement: "current", metadata: { agentId: "feishu", label: "feishu-child", @@ -321,8 +319,8 @@ const sessionBindingContractEntries: Record< expectResolvedSessionBinding({ channel: "feishu", accountId: "default", - conversationId: "chat:123456", - parentConversationId: "chat:123455", + conversationId: "oc_group_chat:topic:om_topic_root", + parentConversationId: "oc_group_chat", targetSessionKey: "agent:feishu:child:thread-1", }); return binding; @@ -332,7 +330,7 @@ const sessionBindingContractEntries: Record< expectClearedSessionBinding({ channel: "feishu", accountId: "default", - conversationId: "chat:123456", + conversationId: "oc_group_chat:topic:om_topic_root", }); }, }, @@ -421,10 +419,10 @@ const sessionBindingContractEntries: Record< conversation: { channel: "matrix", accountId: matrixSessionBindingAuth.accountId, - conversationId: "!room:example.org::$thread", + conversationId: "$thread", parentConversationId: "!room:example.org", }, - placement: "child", + placement: "current", metadata: { agentId: "matrix", label: "matrix-thread", @@ -433,7 +431,7 @@ const sessionBindingContractEntries: Record< expectResolvedSessionBinding({ channel: "matrix", accountId: matrixSessionBindingAuth.accountId, - conversationId: "!room:example.org::$thread", + conversationId: "$thread", parentConversationId: "!room:example.org", targetSessionKey: "agent:matrix:thread", }); @@ -444,7 +442,7 @@ const sessionBindingContractEntries: Record< expectClearedSessionBinding({ channel: "matrix", accountId: matrixSessionBindingAuth.accountId, - conversationId: "!room:example.org::$thread", + conversationId: "$thread", }); }, }, @@ -493,10 +491,9 @@ const sessionBindingContractEntries: Record< conversation: { channel: "telegram", accountId: "default", - conversationId: "chat:1234:topic:5678", - parentConversationId: "chat:1234", + conversationId: "-100200300:topic:77", }, - placement: "child", + placement: "current", metadata: { agentId: "telegram", label: "telegram-topic", @@ -505,8 +502,7 @@ const sessionBindingContractEntries: Record< expectResolvedSessionBinding({ channel: "telegram", accountId: "default", - conversationId: "chat:1234:topic:5678", - parentConversationId: "chat:1234", + conversationId: "-100200300:topic:77", targetSessionKey: "agent:telegram:child:thread-1", }); return binding; @@ -516,7 +512,7 @@ const sessionBindingContractEntries: Record< expectClearedSessionBinding({ channel: "telegram", accountId: "default", - conversationId: "chat:1234:topic:5678", + conversationId: "-100200300:topic:77", }); }, },