diff --git a/extensions/signal/src/monitor.tool-result.test-harness.ts b/extensions/signal/src/monitor.tool-result.test-harness.ts index 11d3d9fb19fa..f5466382d95e 100644 --- a/extensions/signal/src/monitor.tool-result.test-harness.ts +++ b/extensions/signal/src/monitor.tool-result.test-harness.ts @@ -8,6 +8,7 @@ import { createChannelIngressQueueForTests, } from "openclaw/plugin-sdk/plugin-state-test-runtime"; import type { MockFn } from "openclaw/plugin-sdk/plugin-test-runtime"; +import { closeOpenClawAgentDatabasesForTest } from "openclaw/plugin-sdk/sqlite-runtime-testing"; import { afterEach, beforeEach, vi } from "vitest"; import type { SignalDaemonHandle } from "./daemon.js"; import { setSignalRuntime } from "./runtime.js"; @@ -40,9 +41,7 @@ const streamMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; const signalCheckMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; const signalRpcRequestMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; const spawnSignalDaemonMock = vi.hoisted(() => vi.fn()) as unknown as MockFn; -const signalToolResultSessionStorePath = vi.hoisted( - () => `/tmp/openclaw-signal-tool-result-sessions-${process.pid}.json`, -); +const signalToolResultSessionStore = vi.hoisted(() => ({ path: "" })); let signalToolResultStateDir: string | undefined; let signalToolResultIngressQueue: ReturnType | undefined; @@ -149,7 +148,7 @@ vi.mock("openclaw/plugin-sdk/session-store-runtime", async () => { ); return { ...actual, - resolveStorePath: vi.fn(() => signalToolResultSessionStorePath), + resolveStorePath: vi.fn(() => signalToolResultSessionStore.path), updateLastRoute: (...args: unknown[]) => updateLastRouteMock(...args), readSessionUpdatedAt: vi.fn(() => undefined), recordSessionMetaFromInbound: vi.fn().mockResolvedValue(undefined), @@ -175,7 +174,10 @@ vi.mock("openclaw/plugin-sdk/channel-inbound", async () => { } return { ...resolved, - replyResolver: (...replyArgs: unknown[]) => replyMock(...replyArgs), + replyResolver: async (...replyArgs: unknown[]) => { + await resolved.replyOptions?.turnAdoptionLifecycle?.onAdopted(); + return await replyMock(...replyArgs); + }, } as typeof resolved; }, }, @@ -264,6 +266,7 @@ export function installSignalToolResultTestHooks() { ); const stateDir = await fs.realpath(createdStateDir); signalToolResultStateDir = stateDir; + signalToolResultSessionStore.path = path.join(stateDir, "sessions.json"); signalToolResultIngressQueue = undefined; setSignalRuntime({ logging: { @@ -294,7 +297,7 @@ export function installSignalToolResultTestHooks() { } as unknown as PluginRuntime); config = { messages: { responsePrefix: "PFX" }, - session: { store: signalToolResultSessionStorePath }, + session: { store: signalToolResultSessionStore.path }, channels: { signal: { autoStart: false, dmPolicy: "open", allowFrom: ["*"] }, }, @@ -318,10 +321,12 @@ export function installSignalToolResultTestHooks() { afterEach(async () => { clearSignalRuntimeForTest(); signalToolResultIngressQueue = undefined; + closeOpenClawAgentDatabasesForTest(); closeOpenClawStateDatabaseForTest(); if (signalToolResultStateDir) { await fs.rm(signalToolResultStateDir, { recursive: true, force: true }); signalToolResultStateDir = undefined; } + signalToolResultSessionStore.path = ""; }); } diff --git a/src/channels/plugins/target-parsing-loaded.test.ts b/src/channels/plugins/target-parsing-loaded.test.ts new file mode 100644 index 000000000000..7cf7cc6fe63a --- /dev/null +++ b/src/channels/plugins/target-parsing-loaded.test.ts @@ -0,0 +1,68 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const getLoadedChannelPluginForRead = vi.hoisted(() => vi.fn()); +const getChannelPlugin = vi.hoisted(() => vi.fn()); + +vi.mock("./index.js", () => ({ + getChannelPlugin, + normalizeChannelId: (raw?: string | null) => raw?.trim().toLowerCase() || null, +})); + +vi.mock("./registry-loaded.js", () => ({ + getLoadedChannelPluginForRead, +})); + +import { resolveExplicitDeliveryTargetCompat } from "./target-parsing-loaded.js"; + +describe("resolveExplicitDeliveryTargetCompat", () => { + beforeEach(() => { + getLoadedChannelPluginForRead.mockReset(); + getChannelPlugin.mockReset(); + }); + + it("keeps unloaded channels on generic parsing without activating a plugin", () => { + getLoadedChannelPluginForRead.mockReturnValue(undefined); + + expect( + resolveExplicitDeliveryTargetCompat({ + channel: " LegacyChat ", + rawTarget: " room-a ", + fallbackThreadId: "77", + }), + ).toEqual({ + channel: "legacychat", + rawTo: "room-a", + to: "room-a", + threadId: "77", + chatType: undefined, + }); + expect(getLoadedChannelPluginForRead).toHaveBeenCalledWith("legacychat"); + expect(getChannelPlugin).not.toHaveBeenCalled(); + }); + + it("preserves the deprecated parser contract for an already-loaded plugin", () => { + const parseExplicitTarget = vi.fn(() => ({ + to: "room-a", + threadId: 42, + chatType: "group" as const, + })); + getLoadedChannelPluginForRead.mockReturnValue({ + messaging: { parseExplicitTarget }, + }); + + expect( + resolveExplicitDeliveryTargetCompat({ + channel: "legacychat", + rawTarget: "room-a:topic:42", + }), + ).toEqual({ + channel: "legacychat", + rawTo: "room-a:topic:42", + to: "room-a", + threadId: 42, + chatType: "group", + }); + expect(parseExplicitTarget).toHaveBeenCalledWith({ raw: "room-a:topic:42" }); + expect(getChannelPlugin).not.toHaveBeenCalled(); + }); +}); diff --git a/src/channels/plugins/target-parsing-loaded.ts b/src/channels/plugins/target-parsing-loaded.ts index ca4b2154f533..1eb9b864dcf2 100644 --- a/src/channels/plugins/target-parsing-loaded.ts +++ b/src/channels/plugins/target-parsing-loaded.ts @@ -4,7 +4,7 @@ import { normalizeOptionalThreadValue, } from "@openclaw/normalization-core/string-coerce"; import type { ChannelRouteParsedTarget } from "../../plugin-sdk/channel-route.js"; -import { getChannelPlugin, normalizeChannelId } from "./index.js"; +import { normalizeChannelId } from "./index.js"; import { getLoadedChannelPluginForRead } from "./registry-loaded.js"; /** Preserves the shipped `parseExplicitTarget` SDK contract until its deprecation window ends. */ @@ -19,13 +19,10 @@ export function resolveExplicitDeliveryTargetCompat(params: { return null; } const normalizedChannel = normalizeChannelId(channel) ?? channel; - const parsed = - getLoadedChannelPluginForRead(normalizedChannel)?.messaging?.parseExplicitTarget?.({ - raw: rawTo, - }) ?? - getChannelPlugin(normalizedChannel)?.messaging?.parseExplicitTarget?.({ - raw: rawTo, - }); + // This deprecated hook belongs to the active plugin. Source-loading a bundled + // plugin here turns every target parse into broad runtime discovery. + const plugin = getLoadedChannelPluginForRead(normalizedChannel); + const parsed = plugin?.messaging?.parseExplicitTarget?.({ raw: rawTo }); return { channel, rawTo,