


























@@ -0,0 +1,270 @@
1+import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+import type { CoreConfig } from "./types.js";
4+5+const hoisted = vi.hoisted(() => ({
6+sendReactionNextcloudTalk: vi.fn(),
7+sendMessageNextcloudTalk: vi.fn(),
8+listNextcloudTalkAccountIds: vi.fn(),
9+resolveNextcloudTalkAccount: vi.fn(),
10+}));
11+12+vi.mock("./send.js", () => ({
13+sendReactionNextcloudTalk: hoisted.sendReactionNextcloudTalk,
14+sendMessageNextcloudTalk: hoisted.sendMessageNextcloudTalk,
15+}));
16+17+vi.mock("./accounts.js", () => ({
18+listNextcloudTalkAccountIds: hoisted.listNextcloudTalkAccountIds,
19+resolveNextcloudTalkAccount: hoisted.resolveNextcloudTalkAccount,
20+}));
21+22+const { nextcloudTalkMessageActions } = await import("./message-actions.js");
23+24+const configuredAccount = {
25+accountId: "default",
26+enabled: true,
27+baseUrl: "https://nc.example.com",
28+secret: "bot-secret",
29+} as const;
30+31+const unconfiguredAccount = {
32+accountId: "default",
33+enabled: true,
34+baseUrl: "",
35+secret: null,
36+} as const;
37+38+const disabledAccount = {
39+accountId: "default",
40+enabled: false,
41+baseUrl: "https://nc.example.com",
42+secret: "bot-secret",
43+} as const;
44+45+describe("nextcloudTalkMessageActions", () => {
46+beforeEach(() => {
47+hoisted.sendReactionNextcloudTalk.mockReset();
48+hoisted.sendReactionNextcloudTalk.mockResolvedValue({ ok: true });
49+hoisted.sendMessageNextcloudTalk.mockReset();
50+hoisted.listNextcloudTalkAccountIds.mockReset();
51+hoisted.resolveNextcloudTalkAccount.mockReset();
52+});
53+54+describe("describeMessageTool", () => {
55+it("returns null when no accounts are configured", () => {
56+hoisted.listNextcloudTalkAccountIds.mockReturnValue([]);
57+58+const result = nextcloudTalkMessageActions.describeMessageTool?.({
59+cfg: {} as OpenClawConfig,
60+});
61+62+expect(result).toBeNull();
63+});
64+65+it("returns null when configured account has no secret/baseUrl", () => {
66+hoisted.listNextcloudTalkAccountIds.mockReturnValue([unconfiguredAccount.accountId]);
67+hoisted.resolveNextcloudTalkAccount.mockReturnValue(unconfiguredAccount);
68+69+const result = nextcloudTalkMessageActions.describeMessageTool?.({
70+cfg: {} as OpenClawConfig,
71+});
72+73+expect(result).toBeNull();
74+});
75+76+it("returns null when the only listed account is disabled", () => {
77+hoisted.listNextcloudTalkAccountIds.mockReturnValue([disabledAccount.accountId]);
78+hoisted.resolveNextcloudTalkAccount.mockReturnValue(disabledAccount);
79+80+const result = nextcloudTalkMessageActions.describeMessageTool?.({
81+cfg: {} as OpenClawConfig,
82+});
83+84+expect(result).toBeNull();
85+});
86+87+it("advertises send + react when an account is configured", () => {
88+hoisted.listNextcloudTalkAccountIds.mockReturnValue([configuredAccount.accountId]);
89+hoisted.resolveNextcloudTalkAccount.mockReturnValue(configuredAccount);
90+91+const result = nextcloudTalkMessageActions.describeMessageTool?.({
92+cfg: {} as OpenClawConfig,
93+});
94+95+expect(result?.actions).toEqual(["send", "react"]);
96+});
97+98+it("scopes discovery to a specific accountId when provided", () => {
99+hoisted.resolveNextcloudTalkAccount.mockReturnValue(configuredAccount);
100+101+const result = nextcloudTalkMessageActions.describeMessageTool?.({
102+cfg: {} as OpenClawConfig,
103+accountId: "work",
104+});
105+106+expect(hoisted.resolveNextcloudTalkAccount).toHaveBeenCalledWith({
107+cfg: {},
108+accountId: "work",
109+});
110+expect(hoisted.listNextcloudTalkAccountIds).not.toHaveBeenCalled();
111+expect(result?.actions).toEqual(["send", "react"]);
112+});
113+114+it("returns null when the targeted account is disabled", () => {
115+hoisted.resolveNextcloudTalkAccount.mockReturnValue(disabledAccount);
116+117+const result = nextcloudTalkMessageActions.describeMessageTool?.({
118+cfg: {} as OpenClawConfig,
119+accountId: "work",
120+});
121+122+expect(result).toBeNull();
123+});
124+});
125+126+describe("supportsAction", () => {
127+it("delegates send back to outbound", () => {
128+expect(nextcloudTalkMessageActions.supportsAction?.({ action: "send" })).toBe(false);
129+});
130+131+it("handles react locally", () => {
132+expect(nextcloudTalkMessageActions.supportsAction?.({ action: "react" })).toBe(true);
133+});
134+});
135+136+describe("handleAction", () => {
137+const cfg = {} as CoreConfig;
138+139+it("invokes sendReactionNextcloudTalk with normalized params for the react action", async () => {
140+const result = await nextcloudTalkMessageActions.handleAction?.({
141+channel: "nextcloud-talk",
142+action: "react",
143+params: { to: "room:abc123", messageId: "42", emoji: "👍" },
144+ cfg,
145+accountId: "work",
146+});
147+148+expect(hoisted.sendReactionNextcloudTalk).toHaveBeenCalledTimes(1);
149+expect(hoisted.sendReactionNextcloudTalk).toHaveBeenCalledWith("room:abc123", "42", "👍", {
150+accountId: "work",
151+ cfg,
152+});
153+expect(result).toMatchObject({
154+details: { ok: true, added: "👍" },
155+});
156+});
157+158+it("uses toolContext.currentMessageId when params.messageId is missing", async () => {
159+await nextcloudTalkMessageActions.handleAction?.({
160+channel: "nextcloud-talk",
161+action: "react",
162+params: { to: "room:abc123", emoji: "✅" },
163+ cfg,
164+accountId: null,
165+toolContext: { currentMessageId: 99 },
166+});
167+168+expect(hoisted.sendReactionNextcloudTalk).toHaveBeenCalledWith("room:abc123", "99", "✅", {
169+accountId: undefined,
170+ cfg,
171+});
172+});
173+174+it("requires a target room token", async () => {
175+await expect(
176+nextcloudTalkMessageActions.handleAction?.({
177+channel: "nextcloud-talk",
178+action: "react",
179+params: { messageId: "1", emoji: "👍" },
180+ cfg,
181+}),
182+).rejects.toThrow(/to \(room token\) required/);
183+expect(hoisted.sendReactionNextcloudTalk).not.toHaveBeenCalled();
184+});
185+186+it("requires a messageId (explicit or via toolContext)", async () => {
187+await expect(
188+nextcloudTalkMessageActions.handleAction?.({
189+channel: "nextcloud-talk",
190+action: "react",
191+params: { to: "room:abc123", emoji: "👍" },
192+ cfg,
193+}),
194+).rejects.toThrow(/messageId required/);
195+expect(hoisted.sendReactionNextcloudTalk).not.toHaveBeenCalled();
196+});
197+198+it("requires an emoji", async () => {
199+await expect(
200+nextcloudTalkMessageActions.handleAction?.({
201+channel: "nextcloud-talk",
202+action: "react",
203+params: { to: "room:abc123", messageId: "1" },
204+ cfg,
205+}),
206+).rejects.toThrow(/emoji required/);
207+expect(hoisted.sendReactionNextcloudTalk).not.toHaveBeenCalled();
208+});
209+210+it("rejects send through the action handler (outbound owns send)", async () => {
211+await expect(
212+nextcloudTalkMessageActions.handleAction?.({
213+channel: "nextcloud-talk",
214+action: "send",
215+params: { to: "room:abc123", text: "hi" },
216+ cfg,
217+}),
218+).rejects.toThrow(/handled by outbound/);
219+});
220+221+it("rejects unsupported actions", async () => {
222+await expect(
223+nextcloudTalkMessageActions.handleAction?.({
224+channel: "nextcloud-talk",
225+action: "delete",
226+params: {},
227+ cfg,
228+}),
229+).rejects.toThrow(/Action delete not supported for nextcloud-talk/);
230+});
231+232+it("rejects reaction removal requests without calling the add-reaction sender", async () => {
233+await expect(
234+nextcloudTalkMessageActions.handleAction?.({
235+channel: "nextcloud-talk",
236+action: "react",
237+params: { to: "room:abc123", messageId: "1", emoji: "👍", remove: true },
238+ cfg,
239+}),
240+).rejects.toThrow(/removal is not supported/);
241+expect(hoisted.sendReactionNextcloudTalk).not.toHaveBeenCalled();
242+});
243+244+it("still adds the reaction when remove is explicitly false", async () => {
245+await nextcloudTalkMessageActions.handleAction?.({
246+channel: "nextcloud-talk",
247+action: "react",
248+params: { to: "room:abc123", messageId: "1", emoji: "👍", remove: false },
249+ cfg,
250+});
251+252+expect(hoisted.sendReactionNextcloudTalk).toHaveBeenCalledTimes(1);
253+});
254+255+it("propagates errors from sendReactionNextcloudTalk", async () => {
256+hoisted.sendReactionNextcloudTalk.mockRejectedValueOnce(
257+new Error("Nextcloud Talk reaction failed: 403 forbidden"),
258+);
259+260+await expect(
261+nextcloudTalkMessageActions.handleAction?.({
262+channel: "nextcloud-talk",
263+action: "react",
264+params: { to: "room:abc123", messageId: "1", emoji: "👍" },
265+ cfg,
266+}),
267+).rejects.toThrow(/403 forbidden/);
268+});
269+});
270+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。