* Plugins: add inbound claim and Telegram interaction seams * Plugins: add Discord interaction surface * Chore: fix formatting after plugin rebase * fix(hooks): preserve observers after inbound claim * test(hooks): cover claimed inbound observer delivery * fix(plugins): harden typing lease refreshes * fix(discord): pass real auth to plugin interactions * fix(plugins): remove raw session binding runtime exposure * fix(plugins): tighten interactive callback handling * Plugins: gate conversation binding with approvals * Plugins: migrate legacy plugin binding records * Plugins/phone-control: update test command context * Plugins: migrate legacy binding ids * Plugins: migrate legacy codex session bindings * Discord: fix plugin interaction handling * Discord: support direct plugin conversation binds * Plugins: preserve Discord command bind targets * Tests: fix plugin binding and interactive fallout * Discord: stabilize directory lookup tests * Discord: route bound DMs to plugins * Discord: restore plugin bindings after restart * Telegram: persist detached plugin bindings * Plugins: limit binding APIs to Telegram and Discord * Plugins: harden bound conversation routing * Plugins: fix extension target imports * Plugins: fix Telegram runtime extension imports * Plugins: format rebased binding handlers * Discord: bind group DM interactions by channel --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
202 lines
5.4 KiB
TypeScript
202 lines
5.4 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
__testing,
|
|
clearPluginCommands,
|
|
executePluginCommand,
|
|
getPluginCommandSpecs,
|
|
listPluginCommands,
|
|
registerPluginCommand,
|
|
} from "./commands.js";
|
|
|
|
afterEach(() => {
|
|
clearPluginCommands();
|
|
});
|
|
|
|
describe("registerPluginCommand", () => {
|
|
it("rejects malformed runtime command shapes", () => {
|
|
const invalidName = registerPluginCommand(
|
|
"demo-plugin",
|
|
// Runtime plugin payloads are untyped; guard at boundary.
|
|
{
|
|
name: undefined as unknown as string,
|
|
description: "Demo",
|
|
handler: async () => ({ text: "ok" }),
|
|
},
|
|
);
|
|
expect(invalidName).toEqual({
|
|
ok: false,
|
|
error: "Command name must be a string",
|
|
});
|
|
|
|
const invalidDescription = registerPluginCommand("demo-plugin", {
|
|
name: "demo",
|
|
description: undefined as unknown as string,
|
|
handler: async () => ({ text: "ok" }),
|
|
});
|
|
expect(invalidDescription).toEqual({
|
|
ok: false,
|
|
error: "Command description must be a string",
|
|
});
|
|
});
|
|
|
|
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",
|
|
},
|
|
]);
|
|
expect(getPluginCommandSpecs()).toEqual([
|
|
{
|
|
name: "demo_cmd",
|
|
description: "Demo command",
|
|
acceptsArgs: false,
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("supports provider-specific native command aliases", () => {
|
|
const result = registerPluginCommand("demo-plugin", {
|
|
name: "voice",
|
|
nativeNames: {
|
|
default: "talkvoice",
|
|
discord: "discordvoice",
|
|
},
|
|
description: "Demo command",
|
|
handler: async () => ({ text: "ok" }),
|
|
});
|
|
|
|
expect(result).toEqual({ ok: true });
|
|
expect(getPluginCommandSpecs()).toEqual([
|
|
{
|
|
name: "talkvoice",
|
|
description: "Demo command",
|
|
acceptsArgs: false,
|
|
},
|
|
]);
|
|
expect(getPluginCommandSpecs("discord")).toEqual([
|
|
{
|
|
name: "discordvoice",
|
|
description: "Demo command",
|
|
acceptsArgs: false,
|
|
},
|
|
]);
|
|
expect(getPluginCommandSpecs("telegram")).toEqual([
|
|
{
|
|
name: "talkvoice",
|
|
description: "Demo command",
|
|
acceptsArgs: false,
|
|
},
|
|
]);
|
|
expect(getPluginCommandSpecs("slack")).toEqual([]);
|
|
});
|
|
|
|
it("resolves Discord DM command bindings with the user target prefix intact", () => {
|
|
expect(
|
|
__testing.resolveBindingConversationFromCommand({
|
|
channel: "discord",
|
|
from: "discord:1177378744822943744",
|
|
to: "slash:1177378744822943744",
|
|
accountId: "default",
|
|
}),
|
|
).toEqual({
|
|
channel: "discord",
|
|
accountId: "default",
|
|
conversationId: "user:1177378744822943744",
|
|
});
|
|
});
|
|
|
|
it("resolves Discord guild command bindings with the channel target prefix intact", () => {
|
|
expect(
|
|
__testing.resolveBindingConversationFromCommand({
|
|
channel: "discord",
|
|
from: "discord:channel:1480554272859881494",
|
|
accountId: "default",
|
|
}),
|
|
).toEqual({
|
|
channel: "discord",
|
|
accountId: "default",
|
|
conversationId: "channel:1480554272859881494",
|
|
});
|
|
});
|
|
|
|
it("does not resolve binding conversations for unsupported command channels", () => {
|
|
expect(
|
|
__testing.resolveBindingConversationFromCommand({
|
|
channel: "slack",
|
|
from: "slack:U123",
|
|
to: "C456",
|
|
accountId: "default",
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
|
|
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",
|
|
});
|
|
|
|
expect(result.text).toBe(
|
|
JSON.stringify({
|
|
requested: {
|
|
status: "error",
|
|
message: "This command cannot bind the current conversation.",
|
|
},
|
|
current: null,
|
|
detached: { removed: false },
|
|
}),
|
|
);
|
|
});
|
|
});
|