mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 10:25:03 +00:00
Mattermost: add slash command coverage and docs
This commit is contained in:
113
extensions/mattermost/src/mattermost/slash-commands.test.ts
Normal file
113
extensions/mattermost/src/mattermost/slash-commands.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { MattermostClient } from "./client.js";
|
||||
import {
|
||||
parseSlashCommandPayload,
|
||||
registerSlashCommands,
|
||||
resolveCallbackUrl,
|
||||
resolveCommandText,
|
||||
resolveSlashCommandConfig,
|
||||
} from "./slash-commands.js";
|
||||
|
||||
describe("slash-commands", () => {
|
||||
it("parses application/x-www-form-urlencoded payloads", () => {
|
||||
const payload = parseSlashCommandPayload(
|
||||
"token=t1&team_id=team&channel_id=ch1&user_id=u1&command=%2Foc_status&text=now",
|
||||
"application/x-www-form-urlencoded",
|
||||
);
|
||||
expect(payload).toMatchObject({
|
||||
token: "t1",
|
||||
team_id: "team",
|
||||
channel_id: "ch1",
|
||||
user_id: "u1",
|
||||
command: "/oc_status",
|
||||
text: "now",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses application/json payloads", () => {
|
||||
const payload = parseSlashCommandPayload(
|
||||
JSON.stringify({
|
||||
token: "t2",
|
||||
team_id: "team",
|
||||
channel_id: "ch2",
|
||||
user_id: "u2",
|
||||
command: "/oc_model",
|
||||
text: "gpt-5",
|
||||
}),
|
||||
"application/json; charset=utf-8",
|
||||
);
|
||||
expect(payload).toMatchObject({
|
||||
token: "t2",
|
||||
command: "/oc_model",
|
||||
text: "gpt-5",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for malformed payloads missing required fields", () => {
|
||||
const payload = parseSlashCommandPayload(
|
||||
JSON.stringify({ token: "t3", command: "/oc_help" }),
|
||||
"application/json",
|
||||
);
|
||||
expect(payload).toBeNull();
|
||||
});
|
||||
|
||||
it("resolves command text with trigger map fallback", () => {
|
||||
const triggerMap = new Map<string, string>([["oc_status", "status"]]);
|
||||
expect(resolveCommandText("oc_status", " ", triggerMap)).toBe("/status");
|
||||
expect(resolveCommandText("oc_status", " now ", triggerMap)).toBe("/status now");
|
||||
expect(resolveCommandText("oc_help", "", undefined)).toBe("/help");
|
||||
});
|
||||
|
||||
it("normalizes callback path in slash config", () => {
|
||||
const config = resolveSlashCommandConfig({ callbackPath: "api/channels/mattermost/command" });
|
||||
expect(config.callbackPath).toBe("/api/channels/mattermost/command");
|
||||
});
|
||||
|
||||
it("falls back to localhost callback URL for wildcard bind hosts", () => {
|
||||
const config = resolveSlashCommandConfig({ callbackPath: "/api/channels/mattermost/command" });
|
||||
const callbackUrl = resolveCallbackUrl({
|
||||
config,
|
||||
gatewayPort: 18789,
|
||||
gatewayHost: "0.0.0.0",
|
||||
});
|
||||
expect(callbackUrl).toBe("http://localhost:18789/api/channels/mattermost/command");
|
||||
});
|
||||
|
||||
it("reuses existing command when trigger already points to callback URL", async () => {
|
||||
const request = vi.fn(async (path: string) => {
|
||||
if (path.startsWith("/commands?team_id=")) {
|
||||
return [
|
||||
{
|
||||
id: "cmd-1",
|
||||
token: "tok-1",
|
||||
team_id: "team-1",
|
||||
trigger: "oc_status",
|
||||
method: "P",
|
||||
url: "http://gateway/callback",
|
||||
auto_complete: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
throw new Error(`unexpected request path: ${path}`);
|
||||
});
|
||||
const client = { request } as unknown as MattermostClient;
|
||||
|
||||
const result = await registerSlashCommands({
|
||||
client,
|
||||
teamId: "team-1",
|
||||
callbackUrl: "http://gateway/callback",
|
||||
commands: [
|
||||
{
|
||||
trigger: "oc_status",
|
||||
description: "status",
|
||||
autoComplete: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]?.managed).toBe(false);
|
||||
expect(result[0]?.id).toBe("cmd-1");
|
||||
expect(request).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
129
extensions/mattermost/src/mattermost/slash-http.test.ts
Normal file
129
extensions/mattermost/src/mattermost/slash-http.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { PassThrough } from "node:stream";
|
||||
import type { OpenClawConfig, RuntimeEnv } from "openclaw/plugin-sdk";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { ResolvedMattermostAccount } from "./accounts.js";
|
||||
import { createSlashCommandHttpHandler } from "./slash-http.js";
|
||||
|
||||
function createRequest(params: {
|
||||
method?: string;
|
||||
body?: string;
|
||||
contentType?: string;
|
||||
}): IncomingMessage {
|
||||
const req = new PassThrough() as IncomingMessage;
|
||||
req.method = params.method ?? "POST";
|
||||
req.headers = {
|
||||
"content-type": params.contentType ?? "application/x-www-form-urlencoded",
|
||||
};
|
||||
process.nextTick(() => {
|
||||
if (params.body) {
|
||||
req.write(params.body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
return req;
|
||||
}
|
||||
|
||||
function createResponse(): {
|
||||
res: ServerResponse;
|
||||
getBody: () => string;
|
||||
getHeaders: () => Map<string, string>;
|
||||
} {
|
||||
let body = "";
|
||||
const headers = new Map<string, string>();
|
||||
const res = {
|
||||
statusCode: 200,
|
||||
setHeader(name: string, value: string) {
|
||||
headers.set(name.toLowerCase(), value);
|
||||
},
|
||||
end(chunk?: string | Buffer) {
|
||||
body = chunk ? String(chunk) : "";
|
||||
},
|
||||
} as unknown as ServerResponse;
|
||||
return {
|
||||
res,
|
||||
getBody: () => body,
|
||||
getHeaders: () => headers,
|
||||
};
|
||||
}
|
||||
|
||||
const accountFixture: ResolvedMattermostAccount = {
|
||||
accountId: "default",
|
||||
enabled: true,
|
||||
botToken: "bot-token",
|
||||
baseUrl: "https://chat.example.com",
|
||||
botTokenSource: "config",
|
||||
baseUrlSource: "config",
|
||||
config: {},
|
||||
};
|
||||
|
||||
describe("slash-http", () => {
|
||||
it("rejects non-POST methods", async () => {
|
||||
const handler = createSlashCommandHttpHandler({
|
||||
account: accountFixture,
|
||||
cfg: {} as OpenClawConfig,
|
||||
runtime: {} as RuntimeEnv,
|
||||
commandTokens: new Set(["valid-token"]),
|
||||
});
|
||||
const req = createRequest({ method: "GET", body: "" });
|
||||
const response = createResponse();
|
||||
|
||||
await handler(req, response.res);
|
||||
|
||||
expect(response.res.statusCode).toBe(405);
|
||||
expect(response.getBody()).toBe("Method Not Allowed");
|
||||
expect(response.getHeaders().get("allow")).toBe("POST");
|
||||
});
|
||||
|
||||
it("rejects malformed payloads", async () => {
|
||||
const handler = createSlashCommandHttpHandler({
|
||||
account: accountFixture,
|
||||
cfg: {} as OpenClawConfig,
|
||||
runtime: {} as RuntimeEnv,
|
||||
commandTokens: new Set(["valid-token"]),
|
||||
});
|
||||
const req = createRequest({ body: "token=abc&command=%2Foc_status" });
|
||||
const response = createResponse();
|
||||
|
||||
await handler(req, response.res);
|
||||
|
||||
expect(response.res.statusCode).toBe(400);
|
||||
expect(response.getBody()).toContain("Invalid slash command payload");
|
||||
});
|
||||
|
||||
it("fails closed when no command tokens are registered", async () => {
|
||||
const handler = createSlashCommandHttpHandler({
|
||||
account: accountFixture,
|
||||
cfg: {} as OpenClawConfig,
|
||||
runtime: {} as RuntimeEnv,
|
||||
commandTokens: new Set<string>(),
|
||||
});
|
||||
const req = createRequest({
|
||||
body: "token=tok1&team_id=t1&channel_id=c1&user_id=u1&command=%2Foc_status&text=",
|
||||
});
|
||||
const response = createResponse();
|
||||
|
||||
await handler(req, response.res);
|
||||
|
||||
expect(response.res.statusCode).toBe(401);
|
||||
expect(response.getBody()).toContain("Unauthorized: invalid command token.");
|
||||
});
|
||||
|
||||
it("rejects unknown command tokens", async () => {
|
||||
const handler = createSlashCommandHttpHandler({
|
||||
account: accountFixture,
|
||||
cfg: {} as OpenClawConfig,
|
||||
runtime: {} as RuntimeEnv,
|
||||
commandTokens: new Set(["known-token"]),
|
||||
});
|
||||
const req = createRequest({
|
||||
body: "token=unknown&team_id=t1&channel_id=c1&user_id=u1&command=%2Foc_status&text=",
|
||||
});
|
||||
const response = createResponse();
|
||||
|
||||
await handler(req, response.res);
|
||||
|
||||
expect(response.res.statusCode).toBe(401);
|
||||
expect(response.getBody()).toContain("Unauthorized: invalid command token.");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user