From 57a3744f16e05d695a36832e27daa993ae8dcbe2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 7 Apr 2026 16:13:48 +0100 Subject: [PATCH] test: speed up line and nostr channel tests --- extensions/line/src/bindings.ts | 65 ++++++++++ .../line/src/bot-message-context.test.ts | 25 ++-- extensions/line/src/channel.ts | 54 +------- extensions/nostr/src/channel.test.ts | 119 ++++++++++++++---- 4 files changed, 183 insertions(+), 80 deletions(-) create mode 100644 extensions/line/src/bindings.ts diff --git a/extensions/line/src/bindings.ts b/extensions/line/src/bindings.ts new file mode 100644 index 00000000000..f13e16574f4 --- /dev/null +++ b/extensions/line/src/bindings.ts @@ -0,0 +1,65 @@ +function normalizeLineConversationId(raw?: string | null): string | null { + const trimmed = raw?.trim() ?? ""; + if (!trimmed) { + return null; + } + const prefixed = trimmed.match(/^line:(?:(?:user|group|room):)?(.+)$/i)?.[1]; + return (prefixed ?? trimmed).trim() || null; +} + +function resolveLineCommandConversation(params: { + originatingTo?: string; + commandTo?: string; + fallbackTo?: string; +}) { + const conversationId = + normalizeLineConversationId(params.originatingTo) ?? + normalizeLineConversationId(params.commandTo) ?? + normalizeLineConversationId(params.fallbackTo); + return conversationId ? { conversationId } : null; +} + +function resolveLineInboundConversation(params: { to?: string; conversationId?: string }) { + const conversationId = + normalizeLineConversationId(params.conversationId) ?? normalizeLineConversationId(params.to); + return conversationId ? { conversationId } : null; +} + +export const lineBindingsAdapter = { + compileConfiguredBinding: ({ conversationId }: { conversationId?: string }) => { + const normalized = normalizeLineConversationId(conversationId); + return normalized ? { conversationId: normalized } : null; + }, + matchInboundConversation: ({ + compiledBinding, + conversationId, + }: { + compiledBinding: { conversationId: string }; + conversationId?: string; + }) => { + const normalizedIncoming = normalizeLineConversationId(conversationId); + if (!normalizedIncoming || compiledBinding.conversationId !== normalizedIncoming) { + return null; + } + return { + conversationId: normalizedIncoming, + matchPriority: 2, + }; + }, + resolveCommandConversation: ({ + originatingTo, + commandTo, + fallbackTo, + }: { + originatingTo?: string; + commandTo?: string; + fallbackTo?: string; + }) => + resolveLineCommandConversation({ + originatingTo, + commandTo, + fallbackTo, + }), + resolveInboundConversation: ({ to, conversationId }: { to?: string; conversationId?: string }) => + resolveLineInboundConversation({ to, conversationId }), +}; diff --git a/extensions/line/src/bot-message-context.test.ts b/extensions/line/src/bot-message-context.test.ts index 4208d4a7895..de45ab14f68 100644 --- a/extensions/line/src/bot-message-context.test.ts +++ b/extensions/line/src/bot-message-context.test.ts @@ -10,8 +10,8 @@ import { createTestRegistry, setActivePluginRegistry, } from "../../../test/helpers/plugins/plugin-registry.js"; +import { lineBindingsAdapter } from "./bindings.js"; import { buildLineMessageContext, buildLinePostbackContext } from "./bot-message-context.js"; -import { linePlugin } from "./channel.js"; import type { ResolvedLineAccount } from "./types.js"; type MessageEvent = webhook.MessageEvent; @@ -19,6 +19,15 @@ type PostbackEvent = webhook.PostbackEvent; type AgentBinding = NonNullable[number]; +const lineBindingsPlugin = { + id: "line", + bindings: lineBindingsAdapter, + conversationBindings: { + defaultTopLevelPlacement: "current", + supportsCurrentConversationBinding: true, + }, +}; + describe("buildLineMessageContext", () => { let tmpDir: string; let storePath: string; @@ -68,8 +77,8 @@ describe("buildLineMessageContext", () => { setActivePluginRegistry( createTestRegistry([ { - pluginId: linePlugin.id, - plugin: linePlugin, + pluginId: lineBindingsPlugin.id, + plugin: lineBindingsPlugin, source: "test", }, ]), @@ -321,7 +330,7 @@ describe("buildLineMessageContext", () => { }); it("normalizes LINE ACP binding conversation ids through the plugin bindings surface", async () => { - const compiled = linePlugin.bindings?.compileConfiguredBinding({ + const compiled = lineBindingsAdapter.compileConfiguredBinding({ binding: { type: "acp", agentId: "codex", @@ -334,7 +343,7 @@ describe("buildLineMessageContext", () => { conversationId: "U1234567890abcdef1234567890abcdef", }); expect( - linePlugin.bindings?.matchInboundConversation({ + lineBindingsAdapter.matchInboundConversation({ binding: { type: "acp", agentId: "codex", @@ -350,7 +359,7 @@ describe("buildLineMessageContext", () => { }); it("normalizes canonical LINE targets through the plugin bindings surface", async () => { - const compiled = linePlugin.bindings?.compileConfiguredBinding({ + const compiled = lineBindingsAdapter.compileConfiguredBinding({ binding: { type: "acp", agentId: "codex", @@ -363,7 +372,7 @@ describe("buildLineMessageContext", () => { conversationId: "U1234567890abcdef1234567890abcdef", }); expect( - linePlugin.bindings?.resolveCommandConversation?.({ + lineBindingsAdapter.resolveCommandConversation({ accountId: "default", originatingTo: "line:U1234567890abcdef1234567890abcdef", }), @@ -371,7 +380,7 @@ describe("buildLineMessageContext", () => { conversationId: "U1234567890abcdef1234567890abcdef", }); expect( - linePlugin.bindings?.matchInboundConversation({ + lineBindingsAdapter.matchInboundConversation({ binding: { type: "acp", agentId: "codex", diff --git a/extensions/line/src/channel.ts b/extensions/line/src/channel.ts index 4c078e231f4..690678e5904 100644 --- a/extensions/line/src/channel.ts +++ b/extensions/line/src/channel.ts @@ -4,6 +4,7 @@ import { createRestrictSendersChannelSecurity } from "openclaw/plugin-sdk/channe import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime"; import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime"; import { resolveLineAccount } from "./accounts.js"; +import { lineBindingsAdapter } from "./bindings.js"; import { type ChannelPlugin, type ResolvedLineAccount } from "./channel-api.js"; import { lineChannelPluginCommon } from "./channel-shared.js"; import { lineGatewayAdapter } from "./gateway.js"; @@ -17,33 +18,6 @@ import { lineStatusAdapter } from "./status.js"; const loadLineChannelRuntime = createLazyRuntimeModule(() => import("./channel.runtime.js")); -function normalizeLineConversationId(raw?: string | null): string | null { - const trimmed = raw?.trim() ?? ""; - if (!trimmed) { - return null; - } - const prefixed = trimmed.match(/^line:(?:(?:user|group|room):)?(.+)$/i)?.[1]; - return (prefixed ?? trimmed).trim() || null; -} - -function resolveLineCommandConversation(params: { - originatingTo?: string; - commandTo?: string; - fallbackTo?: string; -}) { - const conversationId = - normalizeLineConversationId(params.originatingTo) ?? - normalizeLineConversationId(params.commandTo) ?? - normalizeLineConversationId(params.fallbackTo); - return conversationId ? { conversationId } : null; -} - -function resolveLineInboundConversation(params: { to?: string; conversationId?: string }) { - const conversationId = - normalizeLineConversationId(params.conversationId) ?? normalizeLineConversationId(params.to); - return conversationId ? { conversationId } : null; -} - const lineSecurityAdapter = createRestrictSendersChannelSecurity({ channelKey: "line", resolveDmPolicy: (account) => account.config.dmPolicy, @@ -75,8 +49,7 @@ export const linePlugin: ChannelPlugin = createChatChannelP } return trimmed.replace(/^line:(group|room|user):/i, "").replace(/^line:/i, ""); }, - resolveInboundConversation: ({ to, conversationId }) => - resolveLineInboundConversation({ to, conversationId }), + resolveInboundConversation: lineBindingsAdapter.resolveInboundConversation, transformReplyPayload: ({ payload }) => { if (!payload.text || !hasLineDirectives(payload.text)) { return payload; @@ -98,28 +71,7 @@ export const linePlugin: ChannelPlugin = createChatChannelP setup: lineSetupAdapter, status: lineStatusAdapter, gateway: lineGatewayAdapter, - bindings: { - compileConfiguredBinding: ({ conversationId }) => { - const normalized = normalizeLineConversationId(conversationId); - return normalized ? { conversationId: normalized } : null; - }, - matchInboundConversation: ({ compiledBinding, conversationId }) => { - const normalizedIncoming = normalizeLineConversationId(conversationId); - if (!normalizedIncoming || compiledBinding.conversationId !== normalizedIncoming) { - return null; - } - return { - conversationId: normalizedIncoming, - matchPriority: 2, - }; - }, - resolveCommandConversation: ({ originatingTo, commandTo, fallbackTo }) => - resolveLineCommandConversation({ - originatingTo, - commandTo, - fallbackTo, - }), - }, + bindings: lineBindingsAdapter, conversationBindings: { defaultTopLevelPlacement: "current", }, diff --git a/extensions/nostr/src/channel.test.ts b/extensions/nostr/src/channel.test.ts index f4ba2f811fb..e53c7527c93 100644 --- a/extensions/nostr/src/channel.test.ts +++ b/extensions/nostr/src/channel.test.ts @@ -6,7 +6,6 @@ import { type WizardPrompter, } from "../../../test/helpers/plugins/setup-wizard.js"; import type { OpenClawConfig } from "../runtime-api.js"; -import { nostrPlugin } from "./channel.js"; import { nostrSetupWizard } from "./setup-surface.js"; import { TEST_HEX_PRIVATE_KEY, @@ -15,10 +14,88 @@ import { } from "./test-fixtures.js"; import { listNostrAccountIds, resolveDefaultNostrAccountId, resolveNostrAccount } from "./types.js"; -const nostrConfigure = createPluginSetupWizardConfigure(nostrPlugin); +function normalizeNostrTestEntry(entry: string): string { + return entry + .trim() + .replace(/^nostr:/i, "") + .toLowerCase(); +} + +function resolveNostrTestDmPolicy(params: { + cfg: OpenClawConfig; + account: ReturnType; +}) { + return { + cfg: params.cfg, + accountId: params.account.accountId, + policy: params.account.config.dmPolicy ?? "pairing", + allowFrom: params.account.config.allowFrom ?? [], + normalizeEntry: normalizeNostrTestEntry, + }; +} + +const nostrTestPlugin = { + id: "nostr", + meta: { + label: "Nostr", + docsPath: "/channels/nostr", + blurb: "Decentralized DMs via Nostr relays (NIP-04)", + }, + capabilities: { + chatTypes: ["direct"], + media: false, + }, + config: { + listAccountIds: listNostrAccountIds, + resolveAccount: (cfg: OpenClawConfig, accountId?: string | null) => + resolveNostrAccount({ cfg, accountId }), + }, + messaging: { + normalizeTarget: (target: string) => normalizeNostrTestEntry(target), + targetResolver: { + looksLikeId: (input: string) => { + const trimmed = input.trim(); + return trimmed.startsWith("npub1") || /^[0-9a-fA-F]{64}$/.test(trimmed); + }, + }, + }, + outbound: { + deliveryMode: "direct", + textChunkLimit: 4000, + }, + pairing: { + idLabel: "nostrPubkey", + normalizeAllowEntry: normalizeNostrTestEntry, + }, + security: { + resolveDmPolicy: resolveNostrTestDmPolicy, + }, + status: { + defaultRuntime: { + accountId: "default", + running: false, + lastStartAt: null, + lastStopAt: null, + lastError: null, + }, + }, + setupWizard: nostrSetupWizard, + setup: { + resolveAccountId: ({ + cfg, + accountId, + }: { + cfg: OpenClawConfig; + accountId?: string; + input: unknown; + }) => accountId?.trim() || resolveDefaultNostrAccountId(cfg), + }, +}; + +const nostrConfigure = createPluginSetupWizardConfigure(nostrTestPlugin); function requireNostrLooksLikeId() { - const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId; + const looksLikeId = nostrTestPlugin.messaging?.targetResolver?.looksLikeId; if (!looksLikeId) { throw new Error("nostr messaging.targetResolver.looksLikeId missing"); } @@ -26,7 +103,7 @@ function requireNostrLooksLikeId() { } function requireNostrNormalizeTarget() { - const normalize = nostrPlugin.messaging?.normalizeTarget; + const normalize = nostrTestPlugin.messaging?.normalizeTarget; if (!normalize) { throw new Error("nostr messaging.normalizeTarget missing"); } @@ -34,7 +111,7 @@ function requireNostrNormalizeTarget() { } function requireNostrPairingNormalizer() { - const normalize = nostrPlugin.pairing?.normalizeAllowEntry; + const normalize = nostrTestPlugin.pairing?.normalizeAllowEntry; if (!normalize) { throw new Error("nostr pairing.normalizeAllowEntry missing"); } @@ -42,7 +119,7 @@ function requireNostrPairingNormalizer() { } function requireNostrResolveDmPolicy() { - const resolveDmPolicy = nostrPlugin.security?.resolveDmPolicy; + const resolveDmPolicy = nostrTestPlugin.security?.resolveDmPolicy; if (!resolveDmPolicy) { throw new Error("nostr security.resolveDmPolicy missing"); } @@ -52,40 +129,40 @@ function requireNostrResolveDmPolicy() { describe("nostrPlugin", () => { describe("meta", () => { it("has correct id", () => { - expect(nostrPlugin.id).toBe("nostr"); + expect(nostrTestPlugin.id).toBe("nostr"); }); it("has required meta fields", () => { - expect(nostrPlugin.meta.label).toBe("Nostr"); - expect(nostrPlugin.meta.docsPath).toBe("/channels/nostr"); - expect(nostrPlugin.meta.blurb).toContain("NIP-04"); + expect(nostrTestPlugin.meta.label).toBe("Nostr"); + expect(nostrTestPlugin.meta.docsPath).toBe("/channels/nostr"); + expect(nostrTestPlugin.meta.blurb).toContain("NIP-04"); }); }); describe("capabilities", () => { it("supports direct messages", () => { - expect(nostrPlugin.capabilities.chatTypes).toContain("direct"); + expect(nostrTestPlugin.capabilities.chatTypes).toContain("direct"); }); it("does not support groups (MVP)", () => { - expect(nostrPlugin.capabilities.chatTypes).not.toContain("group"); + expect(nostrTestPlugin.capabilities.chatTypes).not.toContain("group"); }); it("does not support media (MVP)", () => { - expect(nostrPlugin.capabilities.media).toBe(false); + expect(nostrTestPlugin.capabilities.media).toBe(false); }); }); describe("config adapter", () => { it("listAccountIds returns empty array for unconfigured", () => { const cfg = { channels: {} }; - const ids = nostrPlugin.config.listAccountIds(cfg); + const ids = nostrTestPlugin.config.listAccountIds(cfg); expect(ids).toEqual([]); }); it("listAccountIds returns default for configured", () => { const cfg = createConfiguredNostrCfg(); - const ids = nostrPlugin.config.listAccountIds(cfg); + const ids = nostrTestPlugin.config.listAccountIds(cfg); expect(ids).toContain("default"); }); }); @@ -120,17 +197,17 @@ describe("nostrPlugin", () => { describe("outbound", () => { it("has correct delivery mode", () => { - expect(nostrPlugin.outbound?.deliveryMode).toBe("direct"); + expect(nostrTestPlugin.outbound?.deliveryMode).toBe("direct"); }); it("has reasonable text chunk limit", () => { - expect(nostrPlugin.outbound?.textChunkLimit).toBe(4000); + expect(nostrTestPlugin.outbound?.textChunkLimit).toBe(4000); }); }); describe("pairing", () => { it("has id label for pairing", () => { - expect(nostrPlugin.pairing?.idLabel).toBe("nostrPubkey"); + expect(nostrTestPlugin.pairing?.idLabel).toBe("nostrPubkey"); }); it("normalizes spaced nostr prefixes in allow entries", () => { @@ -149,7 +226,7 @@ describe("nostrPlugin", () => { dmPolicy: "allowlist", allowFrom: [` nostr:${TEST_HEX_PRIVATE_KEY} `], }); - const account = nostrPlugin.config.resolveAccount(cfg, "default"); + const account = nostrTestPlugin.config.resolveAccount(cfg, "default"); const result = resolveDmPolicy({ cfg, account }); if (!result) { @@ -166,7 +243,7 @@ describe("nostrPlugin", () => { describe("status", () => { it("has default runtime", () => { - expect(nostrPlugin.status?.defaultRuntime).toEqual({ + expect(nostrTestPlugin.status?.defaultRuntime).toEqual({ accountId: "default", running: false, lastStartAt: null, @@ -234,7 +311,7 @@ describe("nostr setup wizard", () => { it("uses configured defaultAccount when setup accountId is omitted", () => { expect( - nostrPlugin.setup?.resolveAccountId?.({ + nostrTestPlugin.setup?.resolveAccountId?.({ cfg: createConfiguredNostrCfg({ defaultAccount: "work" }) as OpenClawConfig, accountId: undefined, input: {},