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

Quelle  setup-surface.test.ts

  Sprache: JAVA
 

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

import { adaptScopedAccountAccessor } from "openclaw/plugin-sdk/channel-config-helpers";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
import { isPrivateNetworkOptInEnabled } from "openclaw/plugin-sdk/ssrf-runtime";
import { describe, expect, it, vi } from "vitest";
import {
  createSetupWizardAdapter,
  createTestWizardPrompter,
  runSetupWizardConfigure,
  type WizardPrompter,
} from "../../../test/helpers/plugins/setup-wizard.js";
import { resolveBlueBubblesAccount } from "./accounts.js";
import { BlueBubblesConfigSchema } from "./config-schema.js";
import {
  resolveBlueBubblesGroupRequireMention,
  resolveBlueBubblesGroupToolPolicy,
} from "./group-policy.js";
import { blueBubblesSetupAdapter, blueBubblesSetupWizard } from "./setup-surface.js";
import {
  inferBlueBubblesTargetChatType,
  isAllowedBlueBubblesSender,
  looksLikeBlueBubblesExplicitTargetId,
  looksLikeBlueBubblesTargetId,
  normalizeBlueBubblesMessagingTarget,
  parseBlueBubblesAllowTarget,
  parseBlueBubblesTarget,
} from "./targets.js";
import { DEFAULT_WEBHOOK_PATH } from "./webhook-shared.js";

async function createBlueBubblesConfigureAdapter() {
  const plugin = {
    id: "bluebubbles",
    meta: {
      id: "bluebubbles",
      label: "BlueBubbles",
      selectionLabel: "BlueBubbles",
      docsPath: "/channels/bluebubbles",
      blurb: "iMessage via BlueBubbles",
    },
    capabilities: {
      chatTypes: ["direct", "group"],
    },
    config: {
      listAccountIds: () => [DEFAULT_ACCOUNT_ID],
      defaultAccountId: () => DEFAULT_ACCOUNT_ID,
      resolveAccount: adaptScopedAccountAccessor(resolveBlueBubblesAccount),
      resolveAllowFrom: ({ cfg, accountId }: { cfg: unknown; accountId: string }) =>
        resolveBlueBubblesAccount({
          cfg: cfg as Parameters<typeof resolveBlueBubblesAccount>[0]["cfg"],
          accountId,
        }).config.allowFrom ?? [],
    },
    setup: blueBubblesSetupAdapter,
  } as Parameters<typeof createSetupWizardAdapter>[0]["plugin"];
  return createSetupWizardAdapter({
    plugin,
    wizard: blueBubblesSetupWizard,
  });
}

async function runBlueBubblesConfigure(params: { cfg: unknown; prompter: WizardPrompter }) {
  const adapter = await createBlueBubblesConfigureAdapter();
  type ConfigureContext = Parameters<NonNullable<typeof adapter.configure>>[0];
  return await runSetupWizardConfigure({
    configure: adapter.configure,
    cfg: params.cfg as ConfigureContext["cfg"],
    runtime: { ...console, exit: vi.fn() } as ConfigureContext["runtime"],
    prompter: params.prompter,
  });
}

describe("bluebubbles setup surface", () => {
  it("preserves existing password SecretRef and keeps default webhook path", async () => {
    const passwordRef = { source: "env", provider: "default", id: "BLUEBUBBLES_PASSWORD" };
    const confirm = vi
      .fn()
      .mockResolvedValueOnce(false)
      .mockResolvedValueOnce(true)
      .mockResolvedValueOnce(true);
    const text = vi.fn();

    const result = await runBlueBubblesConfigure({
      cfg: {
        channels: {
          bluebubbles: {
            enabled: true,
            serverUrl: "http://127.0.0.1:1234",
            password: passwordRef,
          },
        },
      },
      prompter: createTestWizardPrompter({ confirm, text }),
    });

    expect(result.cfg.channels?.bluebubbles?.password).toEqual(passwordRef);
    expect(result.cfg.channels?.bluebubbles?.webhookPath).toBe(DEFAULT_WEBHOOK_PATH);
    expect(text).not.toHaveBeenCalled();
  });

  it("applies a custom webhook path when requested", async () => {
    const confirm = vi
      .fn()
      .mockResolvedValueOnce(true)
      .mockResolvedValueOnce(true)
      .mockResolvedValueOnce(true);
    const text = vi.fn().mockResolvedValueOnce("/custom-bluebubbles");

    const result = await runBlueBubblesConfigure({
      cfg: {
        channels: {
          bluebubbles: {
            enabled: true,
            serverUrl: "http://127.0.0.1:1234",
            password: "secret",
          },
        },
      },
      prompter: createTestWizardPrompter({ confirm, text }),
    });

    expect(result.cfg.channels?.bluebubbles?.webhookPath).toBe("/custom-bluebubbles");
    expect(text).toHaveBeenCalledWith(
      expect.objectContaining({
        message: "Webhook path",
        placeholder: DEFAULT_WEBHOOK_PATH,
      }),
    );
  });

  it("validates server URLs before accepting input", async () => {
    const confirm = vi.fn().mockResolvedValueOnce(false);
    const text = vi.fn().mockResolvedValueOnce("127.0.0.1:1234").mockResolvedValueOnce("secret");

    await runBlueBubblesConfigure({
      cfg: { channels: { bluebubbles: {} } },
      prompter: createTestWizardPrompter({ confirm, text }),
    });

    const serverUrlPrompt = text.mock.calls[0]?.[0] as {
      validate?: (value: string) => string | undefined;
    };
    expect(serverUrlPrompt.validate?.("bad url")).toBe("Invalid URL format");
    expect(serverUrlPrompt.validate?.("127.0.0.1:1234")).toBeUndefined();
  });

  it("disables the channel through the setup wizard", async () => {
    const next = blueBubblesSetupWizard.disable?.({
      channels: {
        bluebubbles: {
          enabled: true,
          serverUrl: "http://127.0.0.1:1234",
        },
      },
    });

    expect(next?.channels?.bluebubbles?.enabled).toBe(false);
  });

  it("reads the named-account DM policy instead of the channel root", async () => {
    expect(
      blueBubblesSetupWizard.dmPolicy?.getCurrent(
        {
          channels: {
            bluebubbles: {
              dmPolicy: "disabled",
              accounts: {
                work: {
                  serverUrl: "http://localhost:1234",
                  password: "secret",
                  dmPolicy: "allowlist",
                },
              },
            },
          },
        },
        "work",
      ),
    ).toBe("allowlist");
  });

  it("reports account-scoped config keys for named accounts", async () => {
    expect(blueBubblesSetupWizard.dmPolicy?.resolveConfigKeys?.({}, "work")).toEqual({
      policyKey: "channels.bluebubbles.accounts.work.dmPolicy",
      allowFromKey: "channels.bluebubbles.accounts.work.allowFrom",
    });
  });

  it("uses configured defaultAccount for omitted DM policy account context", async () => {
    const cfg = {
      channels: {
        bluebubbles: {
          defaultAccount: "work",
          dmPolicy: "disabled",
          allowFrom: ["user@example.com"],
          accounts: {
            work: {
              serverUrl: "http://localhost:1234",
              password: "secret",
              dmPolicy: "allowlist",
            },
          },
        },
      },
    } as OpenClawConfig;

    expect(blueBubblesSetupWizard.dmPolicy?.getCurrent(cfg)).toBe("allowlist");
    expect(blueBubblesSetupWizard.dmPolicy?.resolveConfigKeys?.(cfg)).toEqual({
      policyKey: "channels.bluebubbles.accounts.work.dmPolicy",
      allowFromKey: "channels.bluebubbles.accounts.work.allowFrom",
    });

    const next = blueBubblesSetupWizard.dmPolicy?.setPolicy(cfg, "open");
    const workAccount = next?.channels?.bluebubbles?.accounts?.work as
      | {
          dmPolicy?: string;
        }
      | undefined;
    expect(next?.channels?.bluebubbles?.dmPolicy).toBe("disabled");
    expect(workAccount?.dmPolicy).toBe("open");
  });

  it("uses configured defaultAccount when accountId is omitted in account resolution", async () => {
    const resolved = resolveBlueBubblesAccount({
      cfg: {
        channels: {
          bluebubbles: {
            defaultAccount: "work",
            serverUrl: "http://localhost:3000",
            password: "top-secret",
            accounts: {
              work: {
                serverUrl: "http://localhost:1234",
                password: "secret",
                name: "Work",
              },
            },
          },
        },
      } as OpenClawConfig,
    });

    expect(resolved.accountId).toBe("work");
    expect(resolved.name).toBe("Work");
    expect(resolved.baseUrl).toBe("http://localhost:1234");
    expect(resolved.configured).toBe(true);
  });

  it("uses configured defaultAccount for omitted setup configured state", async () => {
    const configured = await blueBubblesSetupWizard.status.resolveConfigured({
      cfg: {
        channels: {
          bluebubbles: {
            defaultAccount: "work",
            serverUrl: "http://localhost:3000",
            password: "top-secret",
            accounts: {
              alerts: {
                serverUrl: "http://localhost:4000",
                password: "alerts-secret",
              },
              work: {
                serverUrl: "",
                password: "",
              },
            },
          },
        },
      } as OpenClawConfig,
    });

    expect(configured).toBe(false);
  });

  it('writes open policy state to the named account and preserves inherited allowFrom with "*"', async () => {
    const next = blueBubblesSetupWizard.dmPolicy?.setPolicy(
      {
        channels: {
          bluebubbles: {
            allowFrom: ["user@example.com"],
            accounts: {
              work: {
                serverUrl: "http://localhost:1234",
                password: "secret",
              },
            },
          },
        },
      },
      "open",
      "work",
    );

    const workAccount = next?.channels?.bluebubbles?.accounts?.work as
      | {
          dmPolicy?: string;
          allowFrom?: string[];
        }
      | undefined;
    expect(next?.channels?.bluebubbles?.dmPolicy).toBeUndefined();
    expect(workAccount?.dmPolicy).toBe("open");
    expect(workAccount?.allowFrom).toEqual(["user@example.com", "*"]);
  });
});

describe("resolveBlueBubblesAccount", () => {
  it("treats SecretRef passwords as configured when serverUrl exists", () => {
    const resolved = resolveBlueBubblesAccount({
      cfg: {
        channels: {
          bluebubbles: {
            enabled: true,
            serverUrl: "http://localhost:1234",
            password: {
              source: "env",
              provider: "default",
              id: "BLUEBUBBLES_PASSWORD",
            },
          },
        },
      },
    });

    expect(resolved.configured).toBe(true);
    expect(resolved.baseUrl).toBe("http://localhost:1234");
  });

  it("strips stale legacy private-network aliases after canonical normalization", () => {
    const resolved = resolveBlueBubblesAccount({
      cfg: {
        channels: {
          bluebubbles: {
            network: {
              allowPrivateNetwork: true,
            },
            accounts: {
              work: {
                serverUrl: "http://localhost:1234",
                password: "secret", // pragma: allowlist secret
                network: {
                  dangerouslyAllowPrivateNetwork: false,
                },
              },
            },
          },
        },
      },
      accountId: "work",
    });

    expect(resolved.config.network).toEqual({
      dangerouslyAllowPrivateNetwork: false,
    });
    expect("allowPrivateNetwork" in resolved.config).toBe(false);
    expect(isPrivateNetworkOptInEnabled(resolved.config)).toBe(false);
  });
});

describe("BlueBubblesConfigSchema", () => {
  it("accepts account config when serverUrl and password are both set", () => {
    const parsed = BlueBubblesConfigSchema.safeParse({
      serverUrl: "http://localhost:1234",
      password: "secret", // pragma: allowlist secret
    });
    expect(parsed.success).toBe(true);
  });

  it("accepts SecretRef password when serverUrl is set", () => {
    const parsed = BlueBubblesConfigSchema.safeParse({
      serverUrl: "http://localhost:1234",
      password: {
        source: "env",
        provider: "default",
        id: "BLUEBUBBLES_PASSWORD",
      },
    });
    expect(parsed.success).toBe(true);
  });

  it("requires password when top-level serverUrl is configured", () => {
    const parsed = BlueBubblesConfigSchema.safeParse({
      serverUrl: "http://localhost:1234",
    });
    expect(parsed.success).toBe(false);
    if (parsed.success) {
      return;
    }
    expect(parsed.error.issues[0]?.path).toEqual(["password"]);
    expect(parsed.error.issues[0]?.message).toBe(
      "password is required when serverUrl is configured",
    );
  });

  it("requires password when account serverUrl is configured", () => {
    const parsed = BlueBubblesConfigSchema.safeParse({
      accounts: {
        work: {
          serverUrl: "http://localhost:1234",
        },
      },
    });
    expect(parsed.success).toBe(false);
    if (parsed.success) {
      return;
    }
    expect(parsed.error.issues[0]?.path).toEqual(["accounts", "work", "password"]);
    expect(parsed.error.issues[0]?.message).toBe(
      "password is required when serverUrl is configured",
    );
  });

  it("allows password omission when serverUrl is not configured", () => {
    const parsed = BlueBubblesConfigSchema.safeParse({
      accounts: {
        work: {
          name: "Work iMessage",
        },
      },
    });
    expect(parsed.success).toBe(true);
  });

  it("defaults enrichGroupParticipantsFromContacts to true", () => {
    const parsed = BlueBubblesConfigSchema.safeParse({
      serverUrl: "http://localhost:1234",
      password: "secret", // pragma: allowlist secret
    });
    expect(parsed.success).toBe(true);
    if (!parsed.success) {
      return;
    }
    expect(parsed.data.enrichGroupParticipantsFromContacts).toBe(true);
  });

  it("defaults account enrichGroupParticipantsFromContacts to true", () => {
    const parsed = BlueBubblesConfigSchema.safeParse({
      accounts: {
        work: {
          serverUrl: "http://localhost:1234",
          password: "secret", // pragma: allowlist secret
        },
      },
    });
    expect(parsed.success).toBe(true);
    if (!parsed.success) {
      return;
    }
    const accountConfig = (
      parsed.data as { accounts?: { work?: { enrichGroupParticipantsFromContacts?: boolean } } }
    ).accounts?.work;
    expect(accountConfig?.enrichGroupParticipantsFromContacts).toBe(true);
  });

  it("accepts explicit enrichGroupParticipantsFromContacts at channel and account scope", () => {
    const parsed = BlueBubblesConfigSchema.safeParse({
      enrichGroupParticipantsFromContacts: true,
      accounts: {
        work: {
          enrichGroupParticipantsFromContacts: false,
        },
      },
    });

    expect(parsed.success).toBe(true);
  });
});

describe("bluebubbles group policy", () => {
  it("uses generic channel group policy helpers", () => {
    const cfg = {
      channels: {
        bluebubbles: {
          groups: {
            "chat:primary": {
              requireMention: false,
              tools: { deny: ["exec"] },
            },
            "*": {
              requireMention: true,
              tools: { allow: ["message.send"] },
            },
          },
        },
      },
    } as any;

    expect(resolveBlueBubblesGroupRequireMention({ cfg, groupId: "chat:primary" })).toBe(false);
    expect(resolveBlueBubblesGroupRequireMention({ cfg, groupId: "chat:other" })).toBe(true);
    expect(resolveBlueBubblesGroupToolPolicy({ cfg, groupId: "chat:primary" })).toEqual({
      deny: ["exec"],
    });
    expect(resolveBlueBubblesGroupToolPolicy({ cfg, groupId: "chat:other" })).toEqual({
      allow: ["message.send"],
    });
  });
});

describe("normalizeBlueBubblesMessagingTarget", () => {
  it("normalizes chat_guid targets", () => {
    expect(normalizeBlueBubblesMessagingTarget("chat_guid:ABC-123")).toBe("chat_guid:ABC-123");
  });

  it("normalizes group numeric targets to chat_id", () => {
    expect(normalizeBlueBubblesMessagingTarget("group:123")).toBe("chat_id:123");
  });

  it("strips provider prefix and normalizes handles", () => {
    expect(normalizeBlueBubblesMessagingTarget("bluebubbles:imessage:User@Example.com")).toBe(
      "imessage:user@example.com",
    );
  });

  it("extracts handle from DM chat_guid for cross-context matching", () => {
    expect(normalizeBlueBubblesMessagingTarget("chat_guid:iMessage;-;+19257864429")).toBe(
      "+19257864429",
    );
    expect(normalizeBlueBubblesMessagingTarget("chat_guid:SMS;-;+15551234567")).toBe(
      "+15551234567",
    );
    expect(normalizeBlueBubblesMessagingTarget("chat_guid:iMessage;-;user@example.com")).toBe(
      "user@example.com",
    );
  });

  it("preserves group chat_guid format", () => {
    expect(normalizeBlueBubblesMessagingTarget("chat_guid:iMessage;+;chat123456789")).toBe(
      "chat_guid:iMessage;+;chat123456789",
    );
  });

  it("normalizes raw chat_guid values", () => {
    expect(normalizeBlueBubblesMessagingTarget("iMessage;+;chat660250192681427962")).toBe(
      "chat_guid:iMessage;+;chat660250192681427962",
    );
    expect(normalizeBlueBubblesMessagingTarget("iMessage;-;+19257864429")).toBe("+19257864429");
  });

  it("normalizes chat<digits> pattern to chat_identifier format", () => {
    expect(normalizeBlueBubblesMessagingTarget("chat660250192681427962")).toBe(
      "chat_identifier:chat660250192681427962",
    );
    expect(normalizeBlueBubblesMessagingTarget("chat123")).toBe("chat_identifier:chat123");
    expect(normalizeBlueBubblesMessagingTarget("Chat456789")).toBe("chat_identifier:Chat456789");
  });

  it("normalizes UUID/hex chat identifiers", () => {
    expect(normalizeBlueBubblesMessagingTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toBe(
      "chat_identifier:8b9c1a10536d4d86a336ea03ab7151cc",
    );
    expect(normalizeBlueBubblesMessagingTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe(
      "chat_identifier:1C2D3E4F-1234-5678-9ABC-DEF012345678",
    );
  });
});

describe("looksLikeBlueBubblesTargetId", () => {
  it("accepts chat targets", () => {
    expect(looksLikeBlueBubblesTargetId("chat_guid:ABC-123")).toBe(true);
  });

  it("accepts email handles", () => {
    expect(looksLikeBlueBubblesTargetId("user@example.com")).toBe(true);
  });

  it("accepts phone numbers with punctuation", () => {
    expect(looksLikeBlueBubblesTargetId("+1 (555) 123-4567")).toBe(true);
  });

  it("accepts raw chat_guid values", () => {
    expect(looksLikeBlueBubblesTargetId("iMessage;+;chat660250192681427962")).toBe(true);
  });

  it("accepts chat<digits> pattern as chat_id", () => {
    expect(looksLikeBlueBubblesTargetId("chat660250192681427962")).toBe(true);
    expect(looksLikeBlueBubblesTargetId("chat123")).toBe(true);
    expect(looksLikeBlueBubblesTargetId("Chat456789")).toBe(true);
  });

  it("accepts UUID/hex chat identifiers", () => {
    expect(looksLikeBlueBubblesTargetId("8b9c1a10536d4d86a336ea03ab7151cc")).toBe(true);
    expect(looksLikeBlueBubblesTargetId("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toBe(true);
  });

  it("rejects display names", () => {
    expect(looksLikeBlueBubblesTargetId("Jane Doe")).toBe(false);
  });
});

describe("looksLikeBlueBubblesExplicitTargetId", () => {
  it("treats explicit chat targets as immediate ids", () => {
    expect(looksLikeBlueBubblesExplicitTargetId("chat_guid:ABC-123")).toBe(true);
    expect(looksLikeBlueBubblesExplicitTargetId("imessage:+15551234567")).toBe(true);
  });

  it("prefers directory fallback for bare handles and phone numbers", () => {
    expect(looksLikeBlueBubblesExplicitTargetId("+1 (555) 123-4567")).toBe(false);
    expect(looksLikeBlueBubblesExplicitTargetId("user@example.com")).toBe(false);
  });
});

describe("inferBlueBubblesTargetChatType", () => {
  it("infers direct chat for handles and dm chat_guids", () => {
    expect(inferBlueBubblesTargetChatType("+15551234567")).toBe("direct");
    expect(inferBlueBubblesTargetChatType("chat_guid:iMessage;-;+15551234567")).toBe("direct");
  });

  it("infers group chat for explicit group targets", () => {
    expect(inferBlueBubblesTargetChatType("chat_id:123")).toBe("group");
    expect(inferBlueBubblesTargetChatType("chat_guid:iMessage;+;chat123")).toBe("group");
  });
});

describe("parseBlueBubblesTarget", () => {
  it("parses chat<digits> pattern as chat_identifier", () => {
    expect(parseBlueBubblesTarget("chat660250192681427962")).toEqual({
      kind: "chat_identifier",
      chatIdentifier: "chat660250192681427962",
    });
    expect(parseBlueBubblesTarget("chat123")).toEqual({
      kind: "chat_identifier",
      chatIdentifier: "chat123",
    });
    expect(parseBlueBubblesTarget("Chat456789")).toEqual({
      kind: "chat_identifier",
      chatIdentifier: "Chat456789",
    });
  });

  it("parses UUID/hex chat identifiers as chat_identifier", () => {
    expect(parseBlueBubblesTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({
      kind: "chat_identifier",
      chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc",
    });
    expect(parseBlueBubblesTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({
      kind: "chat_identifier",
      chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678",
    });
  });

  it("parses explicit chat_id: prefix", () => {
    expect(parseBlueBubblesTarget("chat_id:123")).toEqual({ kind: "chat_id", chatId: 123 });
  });

  it("parses phone numbers as handles", () => {
    expect(parseBlueBubblesTarget("+19257864429")).toEqual({
      kind: "handle",
      to: "+19257864429",
      service: "auto",
    });
  });

  it("parses raw chat_guid format", () => {
    expect(parseBlueBubblesTarget("iMessage;+;chat660250192681427962")).toEqual({
      kind: "chat_guid",
      chatGuid: "iMessage;+;chat660250192681427962",
    });
  });
});

describe("parseBlueBubblesAllowTarget", () => {
  it("parses chat<digits> pattern as chat_identifier", () => {
    expect(parseBlueBubblesAllowTarget("chat660250192681427962")).toEqual({
      kind: "chat_identifier",
      chatIdentifier: "chat660250192681427962",
    });
    expect(parseBlueBubblesAllowTarget("chat123")).toEqual({
      kind: "chat_identifier",
      chatIdentifier: "chat123",
    });
  });

  it("parses UUID/hex chat identifiers as chat_identifier", () => {
    expect(parseBlueBubblesAllowTarget("8b9c1a10536d4d86a336ea03ab7151cc")).toEqual({
      kind: "chat_identifier",
      chatIdentifier: "8b9c1a10536d4d86a336ea03ab7151cc",
    });
    expect(parseBlueBubblesAllowTarget("1C2D3E4F-1234-5678-9ABC-DEF012345678")).toEqual({
      kind: "chat_identifier",
      chatIdentifier: "1C2D3E4F-1234-5678-9ABC-DEF012345678",
    });
  });

  it("parses explicit chat_id: prefix", () => {
    expect(parseBlueBubblesAllowTarget("chat_id:456")).toEqual({ kind: "chat_id", chatId: 456 });
  });

  it("parses phone numbers as handles", () => {
    expect(parseBlueBubblesAllowTarget("+19257864429")).toEqual({
      kind: "handle",
      handle: "+19257864429",
    });
  });
});

describe("isAllowedBlueBubblesSender", () => {
  it("denies when allowFrom is empty", () => {
    const allowed = isAllowedBlueBubblesSender({
      allowFrom: [],
      sender: "+15551234567",
    });
    expect(allowed).toBe(false);
  });

  it("allows wildcard entries", () => {
    const allowed = isAllowedBlueBubblesSender({
      allowFrom: ["*"],
      sender: "+15551234567",
    });
    expect(allowed).toBe(true);
  });
});

¤ Dauer der Verarbeitung: 0.23 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.