Files
Yonexus.Server/tests/fabric-notification.test.ts
hzhang 85034f5de0 feat: selectable pairing-notify provider (discord optional, add fabric)
Pairing-code delivery was hardwired to Discord DM (notifyBotToken +
adminUserId required). Make the provider config-selectable.

- core/config.ts: add notifyProvider ("discord"|"fabric", default
  "discord" for back-compat); discord fields required only for discord;
  add fabric block (centerApiBase/apiKey/guildNodeId/channelId) required
  only for fabric
- notifications/types.ts: neutral PairingNotificationService interface
  (DiscordNotificationService kept as back-compat alias)
- notifications/fabric.ts: post the pairing message to a Fabric channel
  (agent/login -> guild token -> POST messages); self-contained, no
  Fabric plugin dependency
- notifications/factory.ts: select provider from config
- core/runtime.ts: wire via factory
- openclaw.plugin.json: notifyProvider enum + fabric object; drop
  notifyBotToken/adminUserId from required (conditional in code)
- tests: fabric notifier + provider-selection config (80 passing)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:59:02 +01:00

110 lines
3.5 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import {
createFabricNotificationService,
type FabricFetch
} from "../plugin/notifications/fabric.js";
import { validateYonexusServerConfig } from "../plugin/core/config.js";
const request = {
identifier: "client-a",
pairingCode: "PAIR-1234-CODE",
expiresAt: 1_710_000_300,
ttlSeconds: 300,
createdAt: 1_710_000_000
};
const fabricConfig = {
centerApiBase: "http://center.local/api",
apiKey: "fak_test",
guildNodeId: "guild-node-1",
channelId: "chan-1"
};
afterEach(() => {
vi.restoreAllMocks();
});
describe("Fabric notification service", () => {
it("logs in, resolves the guild token, and posts the pairing message", async () => {
const fetcher = vi
.fn<FabricFetch>()
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
user: { id: "u-1" },
guilds: [{ nodeId: "guild-node-1", endpoint: "http://guild.local" }],
guildAccessTokens: [{ guildNodeId: "guild-node-1", token: "gtok" }]
})
})
.mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({}) });
const svc = createFabricNotificationService(fabricConfig, { fetcher });
const ok = await svc.sendPairingNotification(request);
expect(ok).toBe(true);
expect(fetcher).toHaveBeenCalledTimes(2);
expect(fetcher.mock.calls[0][0]).toBe("http://center.local/api/auth/agent/login");
expect(fetcher.mock.calls[1][0]).toBe(
"http://guild.local/api/channels/chan-1/messages"
);
const msgInit = fetcher.mock.calls[1][1]!;
expect(msgInit.headers?.Authorization).toBe("Bearer gtok");
expect(msgInit.body).toContain("PAIR-1234-CODE");
});
it("returns false when the notifier is not a member of the guild", async () => {
const fetcher = vi.fn<FabricFetch>().mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({ user: { id: "u-1" }, guilds: [], guildAccessTokens: [] })
});
const svc = createFabricNotificationService(fabricConfig, { fetcher });
expect(await svc.sendPairingNotification(request)).toBe(false);
});
it("returns false when config is incomplete", async () => {
const fetcher = vi.fn<FabricFetch>();
const svc = createFabricNotificationService(
{ ...fabricConfig, apiKey: "" },
{ fetcher }
);
expect(await svc.sendPairingNotification(request)).toBe(false);
expect(fetcher).not.toHaveBeenCalled();
});
});
describe("notifyProvider config selection", () => {
it("defaults to discord and still requires discord fields", () => {
expect(() =>
validateYonexusServerConfig({ listenPort: 8080 })
).toThrowError(/notifyBotToken is required/);
});
it("does not require discord fields when provider is fabric", () => {
const cfg = validateYonexusServerConfig({
followerIdentifiers: [],
listenPort: 8080,
notifyProvider: "fabric",
fabric: fabricConfig
});
expect(cfg.notifyProvider).toBe("fabric");
expect(cfg.fabric).toEqual(fabricConfig);
expect(cfg.notifyBotToken).toBeUndefined();
});
it("requires fabric fields when provider is fabric", () => {
expect(() =>
validateYonexusServerConfig({ listenPort: 8080, notifyProvider: "fabric" })
).toThrowError(/fabric\.centerApiBase is required/);
});
it("rejects an unknown provider", () => {
expect(() =>
validateYonexusServerConfig({ listenPort: 8080, notifyProvider: "sms" })
).toThrowError(/notifyProvider must be/);
});
});