mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 10:41:23 +00:00
test: speed up line and nostr channel tests
This commit is contained in:
65
extensions/line/src/bindings.ts
Normal file
65
extensions/line/src/bindings.ts
Normal 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 }),
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
Reference in New Issue
Block a user