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>
110 lines
3.5 KiB
TypeScript
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/);
|
|
});
|
|
});
|