Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/Java/Openclaw/extensions/bluebubbles/src/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 21 kB image not shown  

Quelle  actions.test.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { describe, expect, it, vi, beforeEach } from "vitest";
import { sendBlueBubblesAttachment } from "./attachments.js";
import { editBlueBubblesMessage, setGroupIconBlueBubbles } from "./chat.js";
import { resolveBlueBubblesMessageId } from "./monitor-reply-cache.js";
import { getCachedBlueBubblesPrivateApiStatus } from "./probe.js";
import { sendBlueBubblesReaction } from "./reactions.js";
import type { OpenClawConfig } from "./runtime-api.js";
import { resolveChatGuidForTarget, sendMessageBlueBubbles } from "./send.js";

vi.mock("./accounts.js", async () => {
  const { createBlueBubblesAccountsMockModule } = await import("./test-harness.js");
  return createBlueBubblesAccountsMockModule();
});

vi.mock("./reactions.js", () => ({
  sendBlueBubblesReaction: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("./send.js", () => ({
  resolveChatGuidForTarget: vi.fn().mockResolvedValue("iMessage;-;+15551234567"),
  sendMessageBlueBubbles: vi.fn().mockResolvedValue({ messageId: "msg-123" }),
}));

vi.mock("./chat.js", () => ({
  editBlueBubblesMessage: vi.fn().mockResolvedValue(undefined),
  unsendBlueBubblesMessage: vi.fn().mockResolvedValue(undefined),
  renameBlueBubblesChat: vi.fn().mockResolvedValue(undefined),
  setGroupIconBlueBubbles: vi.fn().mockResolvedValue(undefined),
  addBlueBubblesParticipant: vi.fn().mockResolvedValue(undefined),
  removeBlueBubblesParticipant: vi.fn().mockResolvedValue(undefined),
  leaveBlueBubblesChat: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("./attachments.js", () => ({
  sendBlueBubblesAttachment: vi.fn().mockResolvedValue({ messageId: "att-msg-123" }),
}));

vi.mock("./monitor-reply-cache.js", () => ({
  resolveBlueBubblesMessageId: vi.fn((id: string) => id),
}));

vi.mock("./probe.js", () => ({
  isMacOS26OrHigher: vi.fn().mockReturnValue(false),
  getCachedBlueBubblesPrivateApiStatus: vi.fn().mockReturnValue(null),
}));

const freshActionsModulePath = "./actions.js?actions-test";
const { bluebubblesMessageActions } = await import(freshActionsModulePath);

describe("bluebubblesMessageActions", () => {
  const describeMessageTool = bluebubblesMessageActions.describeMessageTool!;
  const supportsAction = bluebubblesMessageActions.supportsAction!;
  const extractToolSend = bluebubblesMessageActions.extractToolSend!;
  const handleAction = bluebubblesMessageActions.handleAction!;
  const callHandleAction = (ctx: Omit<Parameters<typeof handleAction>[0], "channel">) =>
    handleAction({ channel: "bluebubbles", ...ctx });
  const blueBubblesConfig = (): OpenClawConfig => ({
    channels: {
      bluebubbles: {
        serverUrl: "http://localhost:1234",
        password: "test-password",
      },
    },
  });
  const runReactAction = async (params: Record<string, unknown>) => {
    return await callHandleAction({
      action: "react",
      params,
      cfg: blueBubblesConfig(),
      accountId: null,
    });
  };

  beforeEach(() => {
    vi.clearAllMocks();
    vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValue(null);
  });

  describe("describeMessageTool", () => {
    it("returns empty array when account is not enabled", () => {
      const cfg: OpenClawConfig = {
        channels: { bluebubbles: { enabled: false } },
      };
      const actions = describeMessageTool({ cfg })?.actions ?? [];
      expect(actions).toEqual([]);
    });

    it("returns empty array when account is not configured", () => {
      const cfg: OpenClawConfig = {
        channels: { bluebubbles: { enabled: true } },
      };
      const actions = describeMessageTool({ cfg })?.actions ?? [];
      expect(actions).toEqual([]);
    });

    it("returns react action when enabled and configured", () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            enabled: true,
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      const actions = describeMessageTool({ cfg })?.actions ?? [];
      expect(actions).toContain("react");
    });

    it("excludes react action when reactions are gated off", () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            enabled: true,
            serverUrl: "http://localhost:1234",
            password: "test-password",
            actions: { reactions: false },
          },
        },
      };
      const actions = describeMessageTool({ cfg })?.actions ?? [];
      expect(actions).not.toContain("react");
      // Other actions should still be present
      expect(actions).toContain("edit");
      expect(actions).toContain("unsend");
    });

    it("honors account-scoped action gates during discovery", () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
            actions: { reactions: false },
            accounts: {
              work: {
                serverUrl: "http://localhost:5678",
                password: "work-password",
                actions: { reactions: true },
              },
            },
          },
        },
      };

      expect(describeMessageTool({ cfg, accountId: "default" })?.actions).not.toContain("react");
      expect(describeMessageTool({ cfg, accountId: "work" })?.actions).toContain("react");
    });

    it("hides private-api actions when private API is disabled", () => {
      vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            enabled: true,
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      const actions = describeMessageTool({ cfg })?.actions ?? [];
      expect(actions).toContain("upload-file");
      expect(actions).not.toContain("sendAttachment");
      expect(actions).not.toContain("react");
      expect(actions).not.toContain("reply");
      expect(actions).not.toContain("sendWithEffect");
      expect(actions).not.toContain("edit");
      expect(actions).not.toContain("unsend");
      expect(actions).not.toContain("renameGroup");
      expect(actions).not.toContain("setGroupIcon");
      expect(actions).not.toContain("addParticipant");
      expect(actions).not.toContain("removeParticipant");
      expect(actions).not.toContain("leaveGroup");
    });
  });

  describe("supportsAction", () => {
    it("returns true for react action", () => {
      expect(supportsAction({ action: "react" })).toBe(true);
    });

    it("returns true for all supported actions", () => {
      expect(supportsAction({ action: "edit" })).toBe(true);
      expect(supportsAction({ action: "unsend" })).toBe(true);
      expect(supportsAction({ action: "reply" })).toBe(true);
      expect(supportsAction({ action: "sendWithEffect" })).toBe(true);
      expect(supportsAction({ action: "renameGroup" })).toBe(true);
      expect(supportsAction({ action: "setGroupIcon" })).toBe(true);
      expect(supportsAction({ action: "addParticipant" })).toBe(true);
      expect(supportsAction({ action: "removeParticipant" })).toBe(true);
      expect(supportsAction({ action: "leaveGroup" })).toBe(true);
      expect(supportsAction({ action: "sendAttachment" })).toBe(true);
      expect(supportsAction({ action: "upload-file" })).toBe(true);
    });

    it("returns false for unsupported actions", () => {
      expect(supportsAction({ action: "delete" as never })).toBe(false);
      expect(supportsAction({ action: "unknown" as never })).toBe(false);
    });
  });

  describe("extractToolSend", () => {
    it("extracts send params from sendMessage action", () => {
      const result = extractToolSend({
        args: {
          action: "sendMessage",
          to: "+15551234567",
          accountId: "test-account",
        },
      });
      expect(result).toEqual({
        to: "+15551234567",
        accountId: "test-account",
      });
    });

    it("returns null for non-sendMessage action", () => {
      const result = extractToolSend({
        args: { action: "react", to: "+15551234567" },
      });
      expect(result).toBeNull();
    });

    it("returns null when to is missing", () => {
      const result = extractToolSend({
        args: { action: "sendMessage" },
      });
      expect(result).toBeNull();
    });
  });

  describe("handleAction", () => {
    it("maps upload-file to the attachment runtime using canonical naming", async () => {
      const result = await callHandleAction({
        action: "upload-file",
        params: {
          to: "+15551234567",
          filename: "photo.png",
          buffer: Buffer.from("img").toString("base64"),
          message: "caption",
          contentType: "image/png",
        },
        cfg: blueBubblesConfig(),
        accountId: null,
      });

      expect(sendBlueBubblesAttachment).toHaveBeenCalledWith(
        expect.objectContaining({
          to: "+15551234567",
          filename: "photo.png",
          caption: "caption",
          contentType: "image/png",
        }),
      );
      expect(result).toMatchObject({
        details: {
          ok: true,
          messageId: "att-msg-123",
        },
      });
    });

    it("throws for unsupported actions", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      await expect(
        callHandleAction({
          action: "unknownAction" as never,
          params: {},
          cfg,
          accountId: null,
        }),
      ).rejects.toThrow("is not supported");
    });

    it("throws when emoji is missing for react action", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      await expect(
        callHandleAction({
          action: "react",
          params: { messageId: "msg-123" },
          cfg,
          accountId: null,
        }),
      ).rejects.toThrow(/emoji/i);
    });

    it("throws a private-api error for private-only actions when disabled", async () => {
      vi.mocked(getCachedBlueBubblesPrivateApiStatus).mockReturnValueOnce(false);
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      await expect(
        callHandleAction({
          action: "react",
          params: { emoji: "❤️", messageId: "msg-123", chatGuid: "iMessage;-;+15551234567" },
          cfg,
          accountId: null,
        }),
      ).rejects.toThrow("requires Private API");
    });

    it("throws when messageId is missing", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      await expect(
        callHandleAction({
          action: "react",
          params: { emoji: "❤️" },
          cfg,
          accountId: null,
        }),
      ).rejects.toThrow("messageId");
    });

    it("throws when chatGuid cannot be resolved", async () => {
      vi.mocked(resolveChatGuidForTarget).mockResolvedValueOnce(null);

      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      await expect(
        callHandleAction({
          action: "react",
          params: { emoji: "❤️", messageId: "msg-123", to: "+15551234567" },
          cfg,
          accountId: null,
        }),
      ).rejects.toThrow("chatGuid not found");
    });

    it("sends reaction successfully with chatGuid", async () => {
      const result = await runReactAction({
        emoji: "❤️",
        messageId: "msg-123",
        chatGuid: "iMessage;-;+15551234567",
      });

      expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
        expect.objectContaining({
          chatGuid: "iMessage;-;+15551234567",
          messageGuid: "msg-123",
          emoji: "❤️",
        }),
      );
      // jsonResult returns { content: [...], details: payload }
      expect(result).toMatchObject({
        details: { ok: true, added: "❤️" },
      });
    });

    it("sends reaction removal successfully", async () => {
      const result = await runReactAction({
        emoji: "❤️",
        messageId: "msg-123",
        chatGuid: "iMessage;-;+15551234567",
        remove: true,
      });

      expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
        expect.objectContaining({
          remove: true,
        }),
      );
      // jsonResult returns { content: [...], details: payload }
      expect(result).toMatchObject({
        details: { ok: true, removed: true },
      });
    });

    it("resolves chatGuid from to parameter", async () => {
      vi.mocked(resolveChatGuidForTarget).mockResolvedValueOnce("iMessage;-;+15559876543");

      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      await callHandleAction({
        action: "react",
        params: {
          emoji: "��",
          messageId: "msg-456",
          to: "+15559876543",
        },
        cfg,
        accountId: null,
      });

      expect(resolveChatGuidForTarget).toHaveBeenCalled();
      expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
        expect.objectContaining({
          chatGuid: "iMessage;-;+15559876543",
        }),
      );
    });

    it("passes partIndex when provided", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      await callHandleAction({
        action: "react",
        params: {
          emoji: "��",
          messageId: "msg-789",
          chatGuid: "iMessage;-;chat-guid",
          partIndex: 2,
        },
        cfg,
        accountId: null,
      });

      expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
        expect.objectContaining({
          partIndex: 2,
        }),
      );
    });

    it("uses toolContext currentChannelId when no explicit target is provided", async () => {
      vi.mocked(resolveChatGuidForTarget).mockResolvedValueOnce("iMessage;-;+15550001111");

      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };
      await callHandleAction({
        action: "react",
        params: {
          emoji: "��",
          messageId: "msg-456",
        },
        cfg,
        accountId: null,
        toolContext: {
          currentChannelId: "bluebubbles:chat_guid:iMessage;-;+15550001111",
        },
      });

      expect(resolveChatGuidForTarget).toHaveBeenCalledWith(
        expect.objectContaining({
          target: { kind: "chat_guid", chatGuid: "iMessage;-;+15550001111" },
        }),
      );
      expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
        expect.objectContaining({
          chatGuid: "iMessage;-;+15550001111",
        }),
      );
    });

    it("resolves short messageId before reacting", async () => {
      vi.mocked(resolveBlueBubblesMessageId).mockReturnValueOnce("resolved-uuid");

      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };

      await callHandleAction({
        action: "react",
        params: {
          emoji: "❤️",
          messageId: "1",
          chatGuid: "iMessage;-;+15551234567",
        },
        cfg,
        accountId: null,
      });

      expect(resolveBlueBubblesMessageId).toHaveBeenCalledWith("1", { requireKnownShortId: true });
      expect(sendBlueBubblesReaction).toHaveBeenCalledWith(
        expect.objectContaining({
          messageGuid: "resolved-uuid",
        }),
      );
    });

    it("propagates short-id errors from the resolver", async () => {
      vi.mocked(resolveBlueBubblesMessageId).mockImplementationOnce(() => {
        throw new Error("short id expired");
      });

      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };

      await expect(
        callHandleAction({
          action: "react",
          params: {
            emoji: "❤️",
            messageId: "999",
            chatGuid: "iMessage;-;+15551234567",
          },
          cfg,
          accountId: null,
        }),
      ).rejects.toThrow("short id expired");
    });

    it("accepts message param for edit action", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };

      await callHandleAction({
        action: "edit",
        params: { messageId: "msg-123", message: "updated" },
        cfg,
        accountId: null,
      });

      expect(editBlueBubblesMessage).toHaveBeenCalledWith(
        "msg-123",
        "updated",
        expect.objectContaining({ cfg, accountId: undefined }),
      );
    });

    it("accepts message/target aliases for sendWithEffect", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };

      const result = await callHandleAction({
        action: "sendWithEffect",
        params: {
          message: "peekaboo",
          target: "+15551234567",
          effect: "invisible ink",
        },
        cfg,
        accountId: null,
      });

      expect(sendMessageBlueBubbles).toHaveBeenCalledWith(
        "+15551234567",
        "peekaboo",
        expect.objectContaining({ effectId: "invisible ink" }),
      );
      expect(result).toMatchObject({
        details: { ok: true, messageId: "msg-123", effect: "invisible ink" },
      });
    });

    it("passes asVoice through sendAttachment", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };

      const base64Buffer = Buffer.from("voice").toString("base64");

      await callHandleAction({
        action: "sendAttachment",
        params: {
          to: "+15551234567",
          filename: "voice.mp3",
          buffer: base64Buffer,
          contentType: "audio/mpeg",
          asVoice: true,
        },
        cfg,
        accountId: null,
      });

      expect(sendBlueBubblesAttachment).toHaveBeenCalledWith(
        expect.objectContaining({
          filename: "voice.mp3",
          contentType: "audio/mpeg",
          asVoice: true,
        }),
      );
    });

    it("throws when buffer is missing for setGroupIcon", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };

      await expect(
        callHandleAction({
          action: "setGroupIcon",
          params: { chatGuid: "iMessage;-;chat-guid" },
          cfg,
          accountId: null,
        }),
      ).rejects.toThrow(/requires an image/i);
    });

    it("sets group icon successfully with chatGuid and buffer", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };

      // Base64 encode a simple test buffer
      const testBuffer = Buffer.from("fake-image-data");
      const base64Buffer = testBuffer.toString("base64");

      const result = await callHandleAction({
        action: "setGroupIcon",
        params: {
          chatGuid: "iMessage;-;chat-guid",
          buffer: base64Buffer,
          filename: "group-icon.png",
          contentType: "image/png",
        },
        cfg,
        accountId: null,
      });

      expect(setGroupIconBlueBubbles).toHaveBeenCalledWith(
        "iMessage;-;chat-guid",
        expect.any(Uint8Array),
        "group-icon.png",
        expect.objectContaining({ contentType: "image/png" }),
      );
      expect(result).toMatchObject({
        details: { ok: true, chatGuid: "iMessage;-;chat-guid", iconSet: true },
      });
    });

    it("uses default filename when not provided for setGroupIcon", async () => {
      const cfg: OpenClawConfig = {
        channels: {
          bluebubbles: {
            serverUrl: "http://localhost:1234",
            password: "test-password",
          },
        },
      };

      const base64Buffer = Buffer.from("test").toString("base64");

      await callHandleAction({
        action: "setGroupIcon",
        params: {
          chatGuid: "iMessage;-;chat-guid",
          buffer: base64Buffer,
        },
        cfg,
        accountId: null,
      });

      expect(setGroupIconBlueBubbles).toHaveBeenCalledWith(
        "iMessage;-;chat-guid",
        expect.any(Uint8Array),
        "icon.png",
        expect.anything(),
      );
    });
  });
});

¤ Dauer der Verarbeitung: 0.24 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.