diff --git a/extensions/feishu/src/bot.broadcast.test.ts b/extensions/feishu/src/bot.broadcast.test.ts index 1340fe8a83f..1eae82a61d8 100644 --- a/extensions/feishu/src/bot.broadcast.test.ts +++ b/extensions/feishu/src/bot.broadcast.test.ts @@ -89,6 +89,9 @@ describe("broadcast dispatch", () => { routing: { resolveAgentRoute: (params: unknown) => mockResolveAgentRoute(params), }, + session: { + resolveStorePath: vi.fn(() => "/tmp/feishu-session-store.json"), + }, reply: { resolveEnvelopeFormatOptions: resolveEnvelopeFormatOptionsMock, formatAgentEnvelope: vi.fn((params: { body: string }) => params.body), diff --git a/extensions/line/src/bot-handlers.test.ts b/extensions/line/src/bot-handlers.test.ts index 8713cf28400..f8431afddc1 100644 --- a/extensions/line/src/bot-handlers.test.ts +++ b/extensions/line/src/bot-handlers.test.ts @@ -5,34 +5,146 @@ import type { LineAccountConfig } from "./types.js"; // Avoid pulling in globals/pairing/media dependencies; this suite only asserts // allowlist/groupPolicy gating and message-context wiring. -vi.mock("openclaw/plugin-sdk/runtime-env", async () => { - const actual = await vi.importActual( - "openclaw/plugin-sdk/runtime-env", - ); - return { - ...actual, - danger: (text: string) => text, - logVerbose: () => {}, - shouldLogVerbose: () => false, - }; -}); +vi.mock("openclaw/plugin-sdk/channel-inbound", () => ({ + buildMentionRegexes: () => [], + matchesMentionPatterns: () => false, + resolveMentionGatingWithBypass: ({ + isGroup, + requireMention, + canDetectMention, + wasMentioned, + hasAnyMention, + allowTextCommands, + hasControlCommand, + commandAuthorized, + }: { + isGroup: boolean; + requireMention: boolean; + canDetectMention: boolean; + wasMentioned: boolean; + hasAnyMention: boolean; + allowTextCommands: boolean; + hasControlCommand: boolean; + commandAuthorized: boolean; + }) => ({ + shouldSkip: + isGroup && + requireMention && + canDetectMention && + !wasMentioned && + !(allowTextCommands && hasControlCommand && commandAuthorized && !hasAnyMention), + }), +})); +vi.mock("openclaw/plugin-sdk/channel-pairing", () => ({ + createChannelPairingChallengeIssuer: + ({ upsertPairingRequest }: { upsertPairingRequest: (args: unknown) => Promise }) => + async ({ senderId, onCreated }: { senderId: string; onCreated?: () => void }) => { + await upsertPairingRequest({ id: senderId, meta: {} }); + onCreated?.(); + }, +})); +vi.mock("openclaw/plugin-sdk/command-auth", () => ({ + hasControlCommand: (text: string) => text.trim().startsWith("!"), + resolveControlCommandGate: ({ + hasControlCommand, + authorizers, + }: { + hasControlCommand: boolean; + authorizers: Array<{ configured: boolean; allowed: boolean }>; + }) => ({ + commandAuthorized: + hasControlCommand && authorizers.some((entry) => entry.allowed || entry.configured === false), + }), +})); +vi.mock("openclaw/plugin-sdk/config-runtime", () => ({ + resolveAllowlistProviderRuntimeGroupPolicy: ({ + groupPolicy, + defaultGroupPolicy, + }: { + groupPolicy?: string; + defaultGroupPolicy: string; + }) => ({ + groupPolicy: groupPolicy ?? defaultGroupPolicy, + providerMissingFallbackApplied: false, + }), + resolveDefaultGroupPolicy: (cfg: { channels?: { line?: { groupPolicy?: string } } }) => + cfg.channels?.line?.groupPolicy ?? "open", + warnMissingProviderGroupPolicyFallbackOnce: () => {}, +})); +vi.mock("openclaw/plugin-sdk/runtime-env", () => ({ + danger: (text: string) => text, + logVerbose: () => {}, +})); +vi.mock("openclaw/plugin-sdk/group-access", () => ({ + evaluateMatchedGroupAccessForPolicy: ({ + groupPolicy, + hasMatchInput, + allowlistConfigured, + allowlistMatched, + }: { + groupPolicy: string; + hasMatchInput: boolean; + allowlistConfigured: boolean; + allowlistMatched: boolean; + }) => { + if (groupPolicy === "disabled") { + return { allowed: false, reason: "disabled" }; + } + if (groupPolicy !== "allowlist") { + return { allowed: true, reason: null }; + } + if (!hasMatchInput) { + return { allowed: false, reason: "missing_match_input" }; + } + if (!allowlistConfigured) { + return { allowed: false, reason: "empty_allowlist" }; + } + if (!allowlistMatched) { + return { allowed: false, reason: "not_allowlisted" }; + } + return { allowed: true, reason: null }; + }, +})); +vi.mock("openclaw/plugin-sdk/reply-history", () => ({ + DEFAULT_GROUP_HISTORY_LIMIT: 20, + clearHistoryEntriesIfEnabled: ({ + historyMap, + historyKey, + }: { + historyMap: Map; + historyKey: string; + }) => { + historyMap.delete(historyKey); + }, + recordPendingHistoryEntryIfEnabled: ({ + historyMap, + historyKey, + limit, + entry, + }: { + historyMap: Map; + historyKey: string; + limit: number; + entry: HistoryEntry; + }) => { + const existing = historyMap.get(historyKey) ?? []; + historyMap.set(historyKey, [...existing, entry].slice(-limit)); + }, +})); +vi.mock("openclaw/plugin-sdk/routing", () => ({ + resolveAgentRoute: () => ({ agentId: "default" }), +})); const { readAllowFromStoreMock, upsertPairingRequestMock } = vi.hoisted(() => ({ readAllowFromStoreMock: vi.fn(async () => [] as string[]), upsertPairingRequestMock: vi.fn(async () => ({ code: "CODE", created: true })), })); -vi.mock("openclaw/plugin-sdk/conversation-runtime", async () => { - const actual = await vi.importActual( - "openclaw/plugin-sdk/conversation-runtime", - ); - return { - ...actual, - resolvePairingIdLabel: () => "lineUserId", - readChannelAllowFromStore: readAllowFromStoreMock, - upsertChannelPairingRequest: upsertPairingRequestMock, - }; -}); +vi.mock("openclaw/plugin-sdk/conversation-runtime", () => ({ + resolvePairingIdLabel: () => "lineUserId", + readChannelAllowFromStore: readAllowFromStoreMock, + upsertChannelPairingRequest: upsertPairingRequestMock, +})); vi.mock("./download.js", () => ({ downloadLineMedia: async () => { diff --git a/extensions/line/src/gateway.ts b/extensions/line/src/gateway.ts index 7f83b526de7..35734c96210 100644 --- a/extensions/line/src/gateway.ts +++ b/extensions/line/src/gateway.ts @@ -10,7 +10,8 @@ import { } from "./channel-api.js"; import { getLineRuntime } from "./runtime.js"; -const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js")); +const loadLineProbeRuntime = createLazyRuntimeModule(() => import("./probe.runtime.js")); +const loadLineMonitorRuntime = createLazyRuntimeModule(() => import("./monitor.runtime.js")); export const lineGatewayAdapter: NonNullable["gateway"]> = { startAccount: async (ctx) => { @@ -30,7 +31,7 @@ export const lineGatewayAdapter: NonNullable[ let lineBotLabel = ""; try { - const probe = await (await loadLineChannelRuntime()).probeLineBot(token, 2500); + const probe = await (await loadLineProbeRuntime()).probeLineBot(token, 2500); const displayName = probe.ok ? probe.bot?.displayName?.trim() : null; if (displayName) { lineBotLabel = ` (${displayName})`; @@ -45,7 +46,7 @@ export const lineGatewayAdapter: NonNullable[ const monitorLineProvider = getLineRuntime().channel.line?.monitorLineProvider ?? - (await loadLineChannelRuntime()).monitorLineProvider; + (await loadLineMonitorRuntime()).monitorLineProvider; return await monitorLineProvider({ channelAccessToken: token, diff --git a/extensions/line/src/monitor.runtime.ts b/extensions/line/src/monitor.runtime.ts new file mode 100644 index 00000000000..4f04a8314b1 --- /dev/null +++ b/extensions/line/src/monitor.runtime.ts @@ -0,0 +1 @@ +export { monitorLineProvider } from "./monitor.js"; diff --git a/extensions/line/src/probe.runtime.ts b/extensions/line/src/probe.runtime.ts new file mode 100644 index 00000000000..5512ee4feec --- /dev/null +++ b/extensions/line/src/probe.runtime.ts @@ -0,0 +1 @@ +export { probeLineBot } from "./probe.js"; diff --git a/extensions/line/src/setup-surface.test.ts b/extensions/line/src/setup-surface.test.ts index 08c12ead43f..bac83be6168 100644 --- a/extensions/line/src/setup-surface.test.ts +++ b/extensions/line/src/setup-surface.test.ts @@ -28,7 +28,6 @@ vi.mock("@line/bot-sdk", () => ({ })); const lineConfigure = createPluginSetupWizardConfigure(linePlugin); -let probeLineBot: typeof import("./probe.js").probeLineBot; const LINE_SRC_PREFIX = `../../${bundledPluginRoot("line")}/src/`; function normalizeModuleSpecifier(specifier: string): string | null { @@ -296,10 +295,6 @@ describe("line setup wizard", () => { }); describe("probeLineBot", () => { - beforeAll(async () => { - ({ probeLineBot } = await import("./probe.js")); - }); - beforeEach(() => { getBotInfoMock.mockReset(); MessagingApiClientMock.mockReset(); @@ -315,6 +310,7 @@ describe("probeLineBot", () => { }); it("returns timeout when bot info stalls", async () => { + const { probeLineBot } = await import("./probe.js"); vi.useFakeTimers(); getBotInfoMock.mockImplementation(() => new Promise(() => {})); @@ -327,6 +323,7 @@ describe("probeLineBot", () => { }); it("returns bot info when available", async () => { + const { probeLineBot } = await import("./probe.js"); getBotInfoMock.mockResolvedValue({ displayName: "OpenClaw", userId: "U123", @@ -343,6 +340,7 @@ describe("probeLineBot", () => { describe("linePlugin status.probeAccount", () => { it("falls back to the direct probe helper when runtime is not initialized", async () => { + const { probeLineBot } = await import("./probe.js"); MessagingApiClientMock.mockReset(); MessagingApiClientMock.mockImplementation(function () { return { getBotInfo: getBotInfoMock }; diff --git a/extensions/line/src/status.ts b/extensions/line/src/status.ts index 641b51070a3..af6f42e6998 100644 --- a/extensions/line/src/status.ts +++ b/extensions/line/src/status.ts @@ -8,7 +8,7 @@ import { import { hasLineCredentials } from "./account-helpers.js"; import { DEFAULT_ACCOUNT_ID, type ChannelPlugin, type ResolvedLineAccount } from "./channel-api.js"; -const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js")); +const loadLineProbeRuntime = createLazyRuntimeModule(() => import("./probe.runtime.js")); const collectLineStatusIssues = createDependentCredentialStatusIssueCollector({ channel: "line", @@ -23,7 +23,7 @@ export const lineStatusAdapter: NonNullable[" collectStatusIssues: collectLineStatusIssues, buildChannelSummary: ({ snapshot }) => buildTokenChannelStatusSummary(snapshot), probeAccount: async ({ account, timeoutMs }) => - await (await loadLineChannelRuntime()).probeLineBot(account.channelAccessToken, timeoutMs), + await (await loadLineProbeRuntime()).probeLineBot(account.channelAccessToken, timeoutMs), resolveAccountSnapshot: ({ account }) => ({ accountId: account.accountId, name: account.name,