diff --git a/extensions/qa-channel/setup-entry.test.ts b/extensions/qa-channel/setup-entry.test.ts new file mode 100644 index 00000000000..047df4471a0 --- /dev/null +++ b/extensions/qa-channel/setup-entry.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "vitest"; +import setupEntry from "./setup-entry.js"; + +describe("qa-channel setup entry", () => { + it("exposes the bundled setup-entry contract", () => { + expect(setupEntry.kind).toBe("bundled-channel-setup-entry"); + expect(setupEntry.loadSetupPlugin().id).toBe("qa-channel"); + }); +}); diff --git a/extensions/qa-channel/setup-entry.ts b/extensions/qa-channel/setup-entry.ts index 98bb3e18b5b..3c27bd24cd3 100644 --- a/extensions/qa-channel/setup-entry.ts +++ b/extensions/qa-channel/setup-entry.ts @@ -1,4 +1,13 @@ -import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core"; -import { qaChannelPlugin } from "./src/channel.js"; +import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract"; -export default defineSetupPluginEntry(qaChannelPlugin); +export default defineBundledChannelSetupEntry({ + importMetaUrl: import.meta.url, + plugin: { + specifier: "./channel-plugin-api.js", + exportName: "qaChannelPlugin", + }, + runtime: { + specifier: "./api.js", + exportName: "setQaChannelRuntime", + }, +}); diff --git a/extensions/qa-channel/src/inbound.test.ts b/extensions/qa-channel/src/inbound.test.ts index eb8badfd6b2..5ae27b5829f 100644 --- a/extensions/qa-channel/src/inbound.test.ts +++ b/extensions/qa-channel/src/inbound.test.ts @@ -1,5 +1,17 @@ -import { describe, expect, it } from "vitest"; -import { isHttpMediaUrl } from "./inbound.js"; +import { createPluginRuntimeMock } from "openclaw/plugin-sdk/channel-test-helpers"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { setQaChannelRuntime } from "../api.js"; +import { handleQaInbound, isHttpMediaUrl } from "./inbound.js"; + +const dispatchInboundReplyWithBaseMock = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/inbound-reply-dispatch", () => ({ + dispatchInboundReplyWithBase: dispatchInboundReplyWithBaseMock, +})); + +beforeEach(() => { + dispatchInboundReplyWithBaseMock.mockReset(); +}); describe("isHttpMediaUrl", () => { it("accepts only http and https urls", () => { @@ -10,3 +22,45 @@ describe("isHttpMediaUrl", () => { expect(isHttpMediaUrl("data:text/plain;base64,SGVsbG8=")).toBe(false); }); }); + +describe("handleQaInbound", () => { + it("marks group messages that match configured mention patterns", async () => { + const runtime = createPluginRuntimeMock(); + vi.mocked(runtime.channel.mentions.buildMentionRegexes).mockReturnValue([/\b@?openclaw\b/i]); + setQaChannelRuntime(runtime); + + await handleQaInbound({ + channelId: "qa-channel", + channelLabel: "QA Channel", + account: { + accountId: "default", + enabled: true, + configured: true, + baseUrl: "http://127.0.0.1:43123", + botUserId: "openclaw", + botDisplayName: "OpenClaw QA", + pollTimeoutMs: 250, + config: {}, + }, + config: {}, + message: { + id: "msg-1", + accountId: "default", + direction: "inbound", + conversation: { + kind: "channel", + id: "qa-room", + title: "QA Room", + }, + senderId: "alice", + senderName: "Alice", + text: "@openclaw ping", + timestamp: 1_777_000_000_000, + reactions: [], + }, + }); + + expect(dispatchInboundReplyWithBaseMock).toHaveBeenCalledTimes(1); + expect(dispatchInboundReplyWithBaseMock.mock.calls[0]?.[0].ctxPayload.WasMentioned).toBe(true); + }); +}); diff --git a/extensions/qa-channel/src/inbound.ts b/extensions/qa-channel/src/inbound.ts index fcc1103740d..9afc823cc62 100644 --- a/extensions/qa-channel/src/inbound.ts +++ b/extensions/qa-channel/src/inbound.ts @@ -81,6 +81,16 @@ export async function handleQaInbound(params: { id: target, }, }); + const isGroup = inbound.conversation.kind !== "direct"; + const wasMentioned = isGroup + ? runtime.channel.mentions.matchesMentionPatterns( + inbound.text, + runtime.channel.mentions.buildMentionRegexes( + params.config as OpenClawConfig, + route.agentId, + ), + ) + : undefined; const storePath = runtime.channel.session.resolveStorePath(params.config.session?.store, { agentId: route.agentId, }); @@ -111,6 +121,7 @@ export async function handleQaInbound(params: { SessionKey: route.sessionKey, AccountId: route.accountId ?? params.account.accountId, ChatType: inbound.conversation.kind === "direct" ? "direct" : "group", + WasMentioned: wasMentioned, ConversationLabel: inbound.threadTitle || inbound.conversation.title || diff --git a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts index c86196a31a5..8a02caea2e9 100644 --- a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.test.ts @@ -134,6 +134,7 @@ describe("discord live qa runtime", () => { expect(next.plugins?.allow).toContain("discord"); expect(next.plugins?.entries?.discord).toEqual({ enabled: true }); + expect(next.messages?.groupChat?.visibleReplies).toBe("automatic"); expect(next.channels?.discord).toEqual({ enabled: true, defaultAccount: "sut", diff --git a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts index da007ff6555..d30bd08f8e3 100644 --- a/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/discord/discord-live.runtime.ts @@ -287,6 +287,13 @@ function buildDiscordQaConfig( allow: pluginAllow, entries: pluginEntries, }, + messages: { + ...baseCfg.messages, + groupChat: { + ...baseCfg.messages?.groupChat, + visibleReplies: "automatic", + }, + }, channels: { ...baseCfg.channels, discord: { diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts index 911248a4fb8..1a51b1cc26c 100644 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts +++ b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.test.ts @@ -165,6 +165,7 @@ describe("telegram live qa runtime", () => { expect(next.agents?.defaults?.skipBootstrap).toBe(true); expect(next.plugins?.allow).toContain("telegram"); expect(next.plugins?.entries?.telegram).toEqual({ enabled: true }); + expect(next.messages?.groupChat?.visibleReplies).toBe("automatic"); expect(next.channels?.telegram).toEqual({ enabled: true, defaultAccount: "sut", diff --git a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts index d84a13a72b0..fa4357322c5 100644 --- a/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts +++ b/extensions/qa-lab/src/live-transports/telegram/telegram-live.runtime.ts @@ -486,6 +486,13 @@ function buildTelegramQaConfig( allow: pluginAllow, entries: pluginEntries, }, + messages: { + ...baseCfg.messages, + groupChat: { + ...baseCfg.messages?.groupChat, + visibleReplies: "automatic", + }, + }, channels: { ...baseCfg.channels, telegram: { diff --git a/extensions/qa-lab/src/qa-channel-transport.test.ts b/extensions/qa-lab/src/qa-channel-transport.test.ts index 15d6fcb5d66..ee1f0a9fb3a 100644 --- a/extensions/qa-lab/src/qa-channel-transport.test.ts +++ b/extensions/qa-lab/src/qa-channel-transport.test.ts @@ -24,6 +24,7 @@ describe("qa channel transport", () => { messages: { groupChat: { mentionPatterns: ["\\b@?openclaw\\b"], + visibleReplies: "automatic", }, }, }); diff --git a/extensions/qa-lab/src/qa-channel-transport.ts b/extensions/qa-lab/src/qa-channel-transport.ts index 455f8130d3f..96cb6e645e6 100644 --- a/extensions/qa-lab/src/qa-channel-transport.ts +++ b/extensions/qa-lab/src/qa-channel-transport.ts @@ -90,6 +90,7 @@ export function createQaChannelGatewayConfig(params: { messages: { groupChat: { mentionPatterns: ["\\b@?openclaw\\b"], + visibleReplies: "automatic", }, }, }; diff --git a/extensions/qa-lab/src/qa-gateway-config.test.ts b/extensions/qa-lab/src/qa-gateway-config.test.ts index ff0b2b96872..48975eff2e4 100644 --- a/extensions/qa-lab/src/qa-gateway-config.test.ts +++ b/extensions/qa-lab/src/qa-gateway-config.test.ts @@ -23,6 +23,7 @@ function createQaChannelTransportParams(baseUrl = "http://127.0.0.1:43124") { messages: { groupChat: { mentionPatterns: ["\\b@?openclaw\\b"], + visibleReplies: "automatic", }, }, } satisfies QaTransportGatewayConfig, @@ -77,7 +78,10 @@ describe("buildQaGatewayConfig", () => { baseUrl: "http://127.0.0.1:43124", pollTimeoutMs: 250, }); - expect(cfg.messages?.groupChat?.mentionPatterns).toEqual(["\\b@?openclaw\\b"]); + expect(cfg.messages?.groupChat).toMatchObject({ + mentionPatterns: ["\\b@?openclaw\\b"], + visibleReplies: "automatic", + }); }); it("maps provider-qualified openai and anthropic refs through the mock provider lane", () => { diff --git a/extensions/qa-matrix/src/runners/contract/runtime.test.ts b/extensions/qa-matrix/src/runners/contract/runtime.test.ts index 0764582ea51..93763a769d9 100644 --- a/extensions/qa-matrix/src/runners/contract/runtime.test.ts +++ b/extensions/qa-matrix/src/runners/contract/runtime.test.ts @@ -148,6 +148,7 @@ describe("matrix live qa runtime", () => { expect(next.plugins?.allow).toContain("matrix"); expect(next.plugins?.entries?.matrix).toEqual({ enabled: true }); + expect(next.messages?.groupChat?.visibleReplies).toBe("automatic"); expect(next.channels?.matrix).toEqual({ enabled: true, defaultAccount: "sut", diff --git a/extensions/qa-matrix/src/substrate/config.test.ts b/extensions/qa-matrix/src/substrate/config.test.ts index 5ab9715766b..af3c2401b46 100644 --- a/extensions/qa-matrix/src/substrate/config.test.ts +++ b/extensions/qa-matrix/src/substrate/config.test.ts @@ -76,6 +76,7 @@ describe("matrix qa config", () => { replyToMode: "off", threadReplies: "inbound", }); + expect(next.messages?.groupChat?.visibleReplies).toBe("automatic"); }); it("applies room-keyed Matrix QA config overrides", () => { diff --git a/extensions/qa-matrix/src/substrate/config.ts b/extensions/qa-matrix/src/substrate/config.ts index abd3cbbacd7..512fe6bc4b0 100644 --- a/extensions/qa-matrix/src/substrate/config.ts +++ b/extensions/qa-matrix/src/substrate/config.ts @@ -640,6 +640,13 @@ export function buildMatrixQaConfig( matrix: { enabled: true }, }, }, + messages: { + ...baseCfg.messages, + groupChat: { + ...baseCfg.messages?.groupChat, + visibleReplies: "automatic", + }, + }, channels: { ...baseCfg.channels, matrix: {