test: speed up line and nostr channel tests

This commit is contained in:
Peter Steinberger
2026-04-07 16:13:48 +01:00
parent a96790fde7
commit 57a3744f16
4 changed files with 183 additions and 80 deletions

View File

@@ -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 }),
};

View File

@@ -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<OpenClawConfig["bindings"]>[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",

View File

@@ -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<ResolvedLineAccount>({
channelKey: "line",
resolveDmPolicy: (account) => account.config.dmPolicy,
@@ -75,8 +49,7 @@ export const linePlugin: ChannelPlugin<ResolvedLineAccount> = 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<ResolvedLineAccount> = 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",
},

View File

@@ -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<typeof resolveNostrAccount>;
}) {
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: {},