import { afterEach, beforeEach, describe, expect, it } from "vitest" ;
import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js" ;
import {
__testing,
clearPluginCommands,
executePluginCommand,
getPluginCommandSpecs,
listProviderPluginCommandSpecs,
listPluginCommands,
matchPluginCommand,
registerPluginCommand,
} from "./commands.js" ;
import { setActivePluginRegistry } from "./runtime.js" ;
type CommandsModule = typeof import ("./commands.js" );
const commandsModuleUrl = new URL("./commands.ts" , import .meta.url).href;
async function importCommandsModule(cacheBust: string): Promise<CommandsModule> {
return (await import (`${commandsModuleUrl}?t=${cacheBust}`)) as CommandsModule;
}
function createVoiceCommand(overrides: Partial<Parameters<typeof registerPluginCommand>[1 ]> = {}) {
return {
name: "voice" ,
description: "Voice command" ,
handler: async () => ({ text: "ok" }),
...overrides,
};
}
function registerVoiceCommandForTest(
overrides: Partial<Parameters<typeof registerPluginCommand>[1 ]> = {},
) {
return registerPluginCommand("demo-plugin" , createVoiceCommand(overrides));
}
function resolveBindingConversationFromCommand(
params: Parameters<typeof __testing.resolveBindingConversationFromCommand>[0 ],
) {
return __testing.resolveBindingConversationFromCommand(params);
}
function expectCommandMatch(
commandBody: string,
params: { name: string; pluginId: string; args: string },
) {
expect(matchPluginCommand(commandBody)).toMatchObject({
command: expect.objectContaining({
name: params.name,
pluginId: params.pluginId,
}),
args: params.args,
});
}
function expectProviderCommandSpecs(
provider: Parameters<typeof getPluginCommandSpecs>[0 ],
expectedNames: readonly string[],
) {
expect(getPluginCommandSpecs(provider)).toEqual(
expectedNames.map((name) => ({
name,
description: "Demo command" ,
acceptsArgs: false ,
})),
);
}
function expectProviderCommandSpecCases(
cases: ReadonlyArray<{
provider: Parameters<typeof getPluginCommandSpecs>[0 ];
expectedNames: readonly string[];
}>,
) {
cases.forEach(({ provider, expectedNames }) => {
expectProviderCommandSpecs(provider, expectedNames);
});
}
function expectUnsupportedBindingApiResult(result: { text?: string }) {
expect(result.text).toBe(
JSON.stringify({
requested: {
status: "error" ,
message: "This command cannot bind the current conversation." ,
},
current: null ,
detached: { removed: false },
}),
);
}
function expectBindingConversationCase(
params: Parameters<typeof resolveBindingConversationFromCommand>[0 ],
expected: ReturnType<typeof resolveBindingConversationFromCommand>,
) {
expect(resolveBindingConversationFromCommand(params)).toEqual(expected);
}
beforeEach(() => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "telegram" ,
source: "test" ,
plugin: {
...createChannelTestPluginBase({ id: "telegram" , label: "Telegram" }),
commands: {
nativeCommandsAutoEnabled: true ,
},
bindings: {
selfParentConversationByDefault: true ,
resolveCommandConversation: ({
threadId,
originatingTo,
commandTo,
fallbackTo,
}: {
threadId?: string;
originatingTo?: string;
commandTo?: string;
fallbackTo?: string;
}) => {
const rawTarget = [commandTo, originatingTo, fallbackTo].find(Boolean )?.trim();
if (!rawTarget || rawTarget.startsWith("slash:" )) {
return null ;
}
const normalized = rawTarget.replace(/^telegram:/i, "" );
const topicMatch = /^(.*?):topic:(\d+)$/i.exec(normalized);
if (topicMatch?.[1 ]) {
return {
conversationId: `${topicMatch[1 ]}:topic:${threadId ?? topicMatch[2 ]}`,
parentConversationId: topicMatch[1 ],
};
}
return { conversationId: normalized };
},
},
},
},
{
pluginId: "discord" ,
source: "test" ,
plugin: {
...createChannelTestPluginBase({ id: "discord" , label: "Discord" }),
commands: {
nativeCommandsAutoEnabled: true ,
},
bindings: {
resolveCommandConversation: ({
threadId,
threadParentId,
originatingTo,
commandTo,
fallbackTo,
}: {
threadId?: string;
threadParentId?: string;
originatingTo?: string;
commandTo?: string;
fallbackTo?: string;
}) => {
const rawTarget = [originatingTo, commandTo, fallbackTo].find(Boolean )?.trim();
if (!rawTarget || rawTarget.startsWith("slash:" )) {
return null ;
}
const normalized = rawTarget.replace(/^discord:/i, "" );
if (/^\d+$/.test(normalized)) {
return { conversationId: `user:${normalized}` };
}
if (threadId) {
const baseConversationId =
originatingTo?.trim()?.replace(/^discord:/i, "" ) ||
commandTo?.trim()?.replace(/^discord:/i, "" ) ||
fallbackTo?.trim()?.replace(/^discord:/i, "" );
return {
conversationId: baseConversationId || threadId,
...(threadParentId ? { parentConversationId: threadParentId } : {}),
};
}
if (normalized.startsWith("channel:" ) || normalized.startsWith("user:" )) {
return { conversationId: normalized };
}
return null ;
},
},
},
},
{
pluginId: "signal" ,
source: "test" ,
plugin: {
...createChannelTestPluginBase({ id: "signal" , label: "Signal" }),
commands: {
nativeCommandsAutoEnabled: true ,
},
bindings: {
resolveCommandConversation: ({ senderId }: { senderId?: string }) => {
const normalizedSenderId = senderId?.trim();
return normalizedSenderId ? { conversationId: `dm:${normalizedSenderId}` } : null ;
},
},
},
},
{
pluginId: "slack" ,
source: "test" ,
plugin: {
...createChannelTestPluginBase({
id: "slack" ,
label: "Slack" ,
capabilities: { nativeCommands: true , chatTypes: ["direct" , "group" ] },
}),
},
},
]),
);
});
afterEach(() => {
clearPluginCommands();
});
describe("registerPluginCommand" , () => {
it.each([
{
name: "rejects invalid command names" ,
command: {
// Runtime plugin payloads are untyped; guard at boundary.
name: undefined as unknown as string,
description: "Demo" ,
handler: async () => ({ text: "ok" }),
},
expected: {
ok: false ,
error: "Command name must be a string" ,
},
},
{
name: "rejects invalid command descriptions" ,
command: {
name: "demo" ,
description: undefined as unknown as string,
handler: async () => ({ text: "ok" }),
},
expected: {
ok: false ,
error: "Command description must be a string" ,
},
},
] as const )("$name" , ({ command, expected }) => {
expect(registerPluginCommand("demo-plugin" , command)).toEqual(expected);
});
it("normalizes command metadata for downstream consumers" , () => {
const result = registerPluginCommand("demo-plugin" , {
name: " demo_cmd " ,
description: " Demo command " ,
handler: async () => ({ text: "ok" }),
});
expect(result).toEqual({ ok: true });
expect(listPluginCommands()).toEqual([
{
name: "demo_cmd" ,
description: "Demo command" ,
pluginId: "demo-plugin" ,
acceptsArgs: false ,
},
]);
expect(getPluginCommandSpecs()).toEqual([
{
name: "demo_cmd" ,
description: "Demo command" ,
acceptsArgs: false ,
},
]);
});
it("matches underscore aliases for hyphenated command names" , () => {
registerPluginCommand("demo-plugin" , {
name: "active-memory" ,
description: "Active Memory command" ,
acceptsArgs: true ,
handler: async () => ({ text: "ok" }),
});
expect(matchPluginCommand("/active_memory status" )).toMatchObject({
command: expect.objectContaining({
name: "active-memory" ,
pluginId: "demo-plugin" ,
}),
args: "status" ,
});
});
it("supports provider-specific native command aliases" , () => {
const result = registerVoiceCommandForTest({
nativeNames: {
default : "talkvoice" ,
discord: "discordvoice" ,
},
description: "Demo command" ,
});
expect(result).toEqual({ ok: true });
expectProviderCommandSpecCases([
{ provider: undefined, expectedNames: ["talkvoice" ] },
{ provider: "discord" , expectedNames: ["discordvoice" ] },
{ provider: "telegram" , expectedNames: ["talkvoice" ] },
{ provider: "slack" , expectedNames: [] },
]);
});
it("allows Slack to resolve provider-native plugin specs without changing shared native gating" , () => {
const result = registerVoiceCommandForTest({
nativeNames: {
default : "talkvoice" ,
discord: "discordvoice" ,
},
description: "Demo command" ,
});
expect(result).toEqual({ ok: true });
expect(listProviderPluginCommandSpecs("slack" )).toEqual([
{
name: "talkvoice" ,
description: "Demo command" ,
acceptsArgs: false ,
},
]);
});
it("accepts native progress metadata on plugin commands" , () => {
const result = registerVoiceCommandForTest({
nativeProgressMessages: { telegram: "Running voice command..." },
description: "Demo command" ,
});
expect(result).toEqual({ ok: true });
expect(matchPluginCommand("/voice" )).toMatchObject({
command: expect.objectContaining({
nativeProgressMessages: { telegram: "Running voice command..." },
}),
});
});
it("rejects empty native progress metadata" , () => {
const result = registerVoiceCommandForTest({
nativeProgressMessages: { telegram: " " },
description: "Demo command" ,
});
expect(result).toEqual({
ok: false ,
error: 'Native progress message "telegram" cannot be empty' ,
});
});
it("shares plugin commands across duplicate module instances" , async () => {
const first = await importCommandsModule(`first-${Date.now()}`);
const second = await importCommandsModule(`second-${Date.now()}`);
first.clearPluginCommands();
expect(
first.registerPluginCommand(
"demo-plugin" ,
createVoiceCommand({
nativeNames: {
telegram: "voice" ,
},
}),
),
).toEqual({ ok: true });
expect(second.getPluginCommandSpecs("telegram" )).toEqual([
{
name: "voice" ,
description: "Voice command" ,
acceptsArgs: false ,
},
]);
expect(second.matchPluginCommand("/voice" )).toMatchObject({
command: expect.objectContaining({
name: "voice" ,
pluginId: "demo-plugin" ,
}),
});
second.clearPluginCommands();
});
it.each(["/talkvoice now" , "/discordvoice now" ] as const )(
"matches provider-specific native alias %s back to the canonical command" ,
(commandBody) => {
const result = registerVoiceCommandForTest({
nativeNames: {
default : "talkvoice" ,
discord: "discordvoice" ,
},
description: "Demo command" ,
acceptsArgs: true ,
});
expect(result).toEqual({ ok: true });
expectCommandMatch(commandBody, {
name: "voice" ,
pluginId: "demo-plugin" ,
args: "now" ,
});
},
);
it.each([
{
name: "rejects provider aliases that collide with another registered command" ,
setup: () =>
registerPluginCommand(
"demo-plugin" ,
createVoiceCommand({
nativeNames: {
telegram: "pair_device" ,
},
}),
),
candidate: {
name: "pair" ,
nativeNames: {
telegram: "pair_device" ,
},
description: "Pair command" ,
handler: async () => ({ text: "ok" }),
},
expected: {
ok: false ,
error: 'Command "pair_device" already registered by plugin "demo-plugin"' ,
},
},
{
name: "rejects reserved provider aliases" ,
candidate: createVoiceCommand({
nativeNames: {
telegram: "help" ,
},
}),
expected: {
ok: false ,
error:
'Native command alias "telegram" invalid: Command name "help" is reserved by a built-in command' ,
},
},
] as const )("$name" , ({ setup, candidate, expected }) => {
setup?.();
expect(registerPluginCommand("other-plugin" , candidate)).toEqual(expected);
});
it.each([
{
name: "resolves Discord DM command bindings with the user target prefix intact" ,
params: {
channel: "discord" ,
from: "discord:1177378744822943744" ,
to: "slash:1177378744822943744" ,
accountId: "default" ,
},
expected: {
channel: "discord" ,
accountId: "default" ,
conversationId: "user:1177378744822943744" ,
},
},
{
name: "resolves Discord guild command bindings with the channel target prefix intact" ,
params: {
channel: "discord" ,
from: "discord:channel:1480554272859881494" ,
accountId: "default" ,
},
expected: {
channel: "discord" ,
accountId: "default" ,
conversationId: "channel:1480554272859881494" ,
},
},
{
name: "resolves Discord thread command bindings with parent channel context intact" ,
params: {
channel: "discord" ,
from: "discord:channel:1480554272859881494" ,
accountId: "default" ,
messageThreadId: "thread-42" ,
threadParentId: "channel-parent-7" ,
},
expected: {
channel: "discord" ,
accountId: "default" ,
conversationId: "channel:1480554272859881494" ,
parentConversationId: "channel-parent-7" ,
threadId: "thread-42" ,
},
},
{
name: "does not resolve binding conversations for unsupported command channels" ,
params: {
channel: "slack" ,
from: "slack:U123" ,
to: "C456" ,
accountId: "default" ,
},
expected: null ,
},
{
name: "resolves sender-keyed command bindings when only senderId is available" ,
params: {
channel: "signal" ,
senderId: "signal-user-42" ,
accountId: "default" ,
},
expected: {
channel: "signal" ,
accountId: "default" ,
conversationId: "dm:signal-user-42" ,
},
},
] as const )("$name" , ({ params, expected }) => {
expectBindingConversationCase(params, expected);
});
it("does not expose binding APIs to plugin commands on unsupported channels" , async () => {
const handler = async (ctx: {
requestConversationBinding: (params: { summary: string }) => Promise<unknown>;
getCurrentConversationBinding: () => Promise<unknown>;
detachConversationBinding: () => Promise<unknown>;
}) => {
const requested = await ctx.requestConversationBinding({
summary: "Bind this conversation." ,
});
const current = await ctx.getCurrentConversationBinding();
const detached = await ctx.detachConversationBinding();
return {
text: JSON.stringify({
requested,
current,
detached,
}),
};
};
registerPluginCommand(
"demo-plugin" ,
{
name: "bindcheck" ,
description: "Demo command" ,
acceptsArgs: false ,
handler,
},
{ pluginRoot: "/plugins/demo-plugin" },
);
const result = await executePluginCommand({
command: {
name: "bindcheck" ,
description: "Demo command" ,
acceptsArgs: false ,
handler,
pluginId: "demo-plugin" ,
pluginRoot: "/plugins/demo-plugin" ,
},
channel: "slack" ,
senderId: "U123" ,
isAuthorizedSender: true ,
commandBody: "/bindcheck" ,
config: {} as never,
from: "slack:U123" ,
to: "C456" ,
accountId: "default" ,
});
expectUnsupportedBindingApiResult(result);
});
it("passes host session identity through to the plugin command context" , async () => {
let receivedCtx:
| {
sessionKey?: string;
sessionId?: string;
}
| undefined;
const handler = async (ctx: { sessionKey?: string; sessionId?: string }) => {
receivedCtx = ctx;
return { text: "ok" };
};
const result = await executePluginCommand({
command: {
name: "sessioncheck" ,
description: "Demo command" ,
acceptsArgs: false ,
handler,
pluginId: "demo-plugin" ,
},
channel: "whatsapp" ,
senderId: "U123" ,
isAuthorizedSender: true ,
sessionKey: "agent:main:whatsapp:direct:123" ,
sessionId: "session-123" ,
commandBody: "/sessioncheck" ,
config: {} as never,
});
expect(result).toEqual({ text: "ok" });
expect(receivedCtx).toMatchObject({
sessionKey: "agent:main:whatsapp:direct:123" ,
sessionId: "session-123" ,
});
});
it("passes the effective default account to plugin command handlers when accountId is omitted" , async () => {
setActivePluginRegistry(
createTestRegistry([
{
pluginId: "line" ,
source: "test" ,
plugin: {
...createChannelTestPluginBase({
id: "line" ,
label: "LINE" ,
config: {
listAccountIds: () => ["default" , "work" ],
defaultAccountId: () => "work" ,
resolveAccount: (_cfg, accountId) => ({ accountId: accountId ?? "work" }),
},
}),
bindings: {
resolveCommandConversation: ({
originatingTo,
commandTo,
fallbackTo,
}: {
originatingTo?: string;
commandTo?: string;
fallbackTo?: string;
}) => {
const rawTarget = [originatingTo, commandTo, fallbackTo].find(Boolean )?.trim();
if (!rawTarget) {
return null ;
}
return {
conversationId: rawTarget.replace(/^line:/i, "" ).replace(/^user:/i, "" ),
};
},
},
},
},
]),
);
let receivedCtx:
| {
accountId?: string;
}
| undefined;
const handler = async (ctx: { accountId?: string }) => {
receivedCtx = ctx;
return { text: "ok" };
};
const result = await executePluginCommand({
command: {
name: "accountcheck" ,
description: "Demo command" ,
acceptsArgs: false ,
handler,
pluginId: "demo-plugin" ,
},
channel: "line" ,
senderId: "U123" ,
isAuthorizedSender: true ,
commandBody: "/accountcheck" ,
config: {} as never,
from: "line:user:U1234567890abcdef1234567890abcdef" ,
});
expect(result).toEqual({ text: "ok" });
expect(receivedCtx).toMatchObject({
accountId: "work" ,
});
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-06-09)
¤
*© Formatika GbR, Deutschland