import { beforeEach, describe, expect, it } from
"vitest";
import type { PluginRuntime } from
"../runtime-api.js";
import { matrixMessageActions } from
"./actions.js";
import { setMatrixRuntime } from
"./runtime.js";
import type { CoreConfig } from
"./types.js";
const profileAction =
"set-profile" as
const;
const runtimeStub = {
config: {
loadConfig: () => ({}),
},
media: {
loadWebMedia: async () => {
throw new Error(
"not used");
},
mediaKindFromMime: () =>
"image",
isVoiceCompatibleAudio: () =>
false,
getImageMetadata: async () =>
null,
resizeToJpeg: async () => Buffer.from(
""),
},
state: {
resolveStateDir: () =>
"/tmp/openclaw-matrix-test",
},
channel: {
text: {
resolveTextChunkLimit: () =>
4000,
resolveChunkMode: () =>
"length",
chunkMarkdownText: (text: string) => (text ? [text] : []),
chunkMarkdownTextWithMode: (text: string) => (text ? [text] : []),
resolveMarkdownTableMode: () =>
"code",
convertMarkdownTables: (text: string) => text,
},
},
} as unknown as PluginRuntime;
function createConfiguredMatrixConfig(): CoreConfig {
return {
channels: {
matrix: {
enabled:
true,
homeserver:
"https://matrix.example.org",
userId:
"@bot:example.org",
accessToken:
"token",
},
},
} as CoreConfig;
}
describe(
"matrixMessageActions", () => {
beforeEach(() => {
setMatrixRuntime(runtimeStub);
});
it(
"exposes poll create but only handles poll votes inside the plugin", () => {
const describeMessageTool = matrixMessageActions.describeMessageTool;
const supportsAction = matrixMessageActions.supportsAction ?? (() =>
false);
expect(describeMessageTool).toBeTypeOf(
"function");
expect(supportsAction).toBeTypeOf(
"function");
const discovery = describeMessageTool({
cfg: createConfiguredMatrixConfig(),
} as never);
if (!discovery) {
throw new Error(
"describeMessageTool returned null");
}
const actions = discovery.actions;
expect(actions).toContain(
"poll");
expect(actions).toContain(
"poll-vote");
expect(supportsAction({ action:
"poll" } as never)).toBe(
false);
expect(supportsAction({ action:
"poll-vote" } as never)).toBe(
true);
});
it(
"exposes and describes self-profile updates", () => {
const describeMessageTool = matrixMessageActions.describeMessageTool;
const supportsAction = matrixMessageActions.supportsAction ?? (() =>
false);
const discovery = describeMessageTool({
cfg: createConfiguredMatrixConfig(),
senderIsOwner:
true,
} as never);
if (!discovery) {
throw new Error(
"describeMessageTool returned null");
}
const actions = discovery.actions;
const schema = discovery.schema;
if (!schema) {
throw new Error(
"matrix schema missing");
}
const properties = (schema as { properties?: Record<string, unknown> }).properties ?? {};
expect(actions).toContain(profileAction);
expect(supportsAction({ action: profileAction } as never)).toBe(
true);
expect(discovery.mediaSourceParams).toEqual({
"set-profile": [
"avatarUrl",
"avatarPath"],
});
expect(properties.displayName).toBeDefined();
expect(properties.avatarUrl).toBeDefined();
expect(properties.avatarPath).toBeDefined();
});
it(
"hides self-profile updates for non-owner discovery", () => {
const discovery = matrixMessageActions.describeMessageTool({
cfg: createConfiguredMatrixConfig(),
senderIsOwner:
false,
} as never);
if (!discovery) {
throw new Error(
"describeMessageTool returned null");
}
expect(discovery.actions).not.toContain(profileAction);
expect(discovery.schema).toBeNull();
});
it(
"hides self-profile updates when owner status is unknown", () => {
const discovery = matrixMessageActions.describeMessageTool({
cfg: createConfiguredMatrixConfig(),
} as never);
if (!discovery) {
throw new Error(
"describeMessageTool returned null");
}
expect(discovery.actions).not.toContain(profileAction);
expect(discovery.schema).toBeNull();
});
it(
"hides gated actions when the default Matrix account disables them", () => {
const discovery = matrixMessageActions.describeMessageTool({
cfg: {
channels: {
matrix: {
defaultAccount:
"assistant",
actions: {
messages:
true,
reactions:
true,
pins:
true,
profile:
true,
memberInfo:
true,
channelInfo:
true,
verification:
true,
},
accounts: {
assistant: {
homeserver:
"https://matrix.example.org",
userId:
"@bot:example.org",
accessToken:
"token",
encryption:
true,
actions: {
messages:
false,
reactions:
false,
pins:
false,
profile:
false,
memberInfo:
false,
channelInfo:
false,
verification:
false,
},
},
},
},
},
} as CoreConfig,
} as never);
if (!discovery) {
throw new Error(
"describeMessageTool returned null");
}
const actions = discovery.actions;
expect(actions).toEqual([
"poll",
"poll-vote"]);
});
it(
"hides actions until defaultAccount is set for ambiguous multi-account configs", () => {
const discovery = matrixMessageActions.describeMessageTool({
cfg: {
channels: {
matrix: {
accounts: {
assistant: {
homeserver:
"https://matrix.example.org",
accessToken:
"assistant-token",
},
ops: {
homeserver:
"https://matrix.example.org",
accessToken:
"ops-token",
},
},
},
},
} as CoreConfig,
} as never);
if (!discovery) {
throw new Error(
"describeMessageTool returned null");
}
const actions = discovery.actions;
expect(actions).toEqual([]);
});
it(
"honors the selected Matrix account during discovery", () => {
const cfg = {
channels: {
matrix: {
defaultAccount:
"assistant",
accounts: {
assistant: {
homeserver:
"https://matrix.example.org",
userId:
"@assistant:example.org",
accessToken:
"assistant-token",
actions: {
messages:
true,
reactions:
false,
},
},
ops: {
homeserver:
"https://matrix.example.org",
userId:
"@ops:example.org",
accessToken:
"ops-token",
actions: {
messages:
true,
reactions:
true,
},
},
},
},
},
} as CoreConfig;
const describeMessageTool = matrixMessageActions.describeMessageTool;
if (!describeMessageTool) {
throw new Error(
"matrix message action discovery is unavailable");
}
const assistantDiscovery = describeMessageTool({
cfg,
accountId:
"assistant",
} as never);
const opsDiscovery = describeMessageTool({
cfg,
accountId:
"ops",
} as never);
if (!assistantDiscovery || !opsDiscovery) {
throw new Error(
"matrix action discovery returned null");
}
const assistantActions = assistantDiscovery.actions;
const opsActions = opsDiscovery.actions;
expect(assistantActions).not.toContain(
"react");
expect(assistantActions).not.toContain(
"reactions");
expect(opsActions).toContain(
"react");
expect(opsActions).toContain(
"reactions");
});
});